From 4611771f04bcf8bae2421bce12e84265cbc879f6 Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 22 Jul 2026 12:48:37 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20restore=20pr?= =?UTF-8?q?esenter=20focus=20trapping=20after=20share=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore FocusScope and aria-disabled for access presenter nav after share links --- CHANGELOG.md | 1 + .../components/PresenterFloatingBar.tsx | 21 ++++++++++++++----- .../components/PresenterOverlay.tsx | 2 +- .../hooks/usePresenterShortcuts.ts | 6 ++++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac490d64ab..42f4c7c9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to ### Changed - ♿️(frontend) use semantic `
` structure in document info card #2379 +- ♿️(frontend) restore presenter focus trapping after share links #2533 ## [v5.4.1] - 2026-07-09 diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx index b4421eff96..d0b1db739e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx @@ -43,6 +43,11 @@ const barCss = css` border: 1px solid var(--c--contextuals--border--surface--primary); background: var(--c--contextuals--background--surface--primary); box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); + + button[aria-disabled='true'] { + opacity: 0.4; + cursor: default; + } `; const separatorCss = css` @@ -71,6 +76,12 @@ const PresenterDropdownLayerStyle = createGlobalStyle` box-sizing: border-box; height: 32px; } + + .react-aria-Popover .c__dropdown-menu--tiny .c__dropdown-menu-item:focus-visible { + outline: 2px solid var(--c--theme--colors--primary-400, #3b82f6); + outline-offset: -2px; + border-radius: 4px; + } `; export const PresenterFloatingBar = ({ @@ -97,7 +108,7 @@ export const PresenterFloatingBar = ({ useEffect(() => { const id = requestAnimationFrame(() => { barRef.current - ?.querySelector('button:not([disabled])') + ?.querySelector('button:not([aria-disabled="true"])') ?.focus(); }); return () => cancelAnimationFrame(id); @@ -137,8 +148,8 @@ export const PresenterFloatingBar = ({ size="nano" color="neutral" variant="tertiary" - disabled={isFirst} - onClick={onPrev} + aria-disabled={isFirst} + onClick={isFirst ? undefined : onPrev} aria-label={t('Previous slide')} icon={} /> @@ -149,8 +160,8 @@ export const PresenterFloatingBar = ({ size="nano" color="neutral" variant="tertiary" - disabled={isLast} - onClick={onNext} + aria-disabled={isLast} + onClick={isLast ? undefined : onNext} aria-label={t('Next slide')} icon={} /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx index f55da03c01..27f7027bdc 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx @@ -173,7 +173,7 @@ export const PresenterOverlay = ({ } return createPortal( - + Date: Wed, 22 Jul 2026 13:09:56 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fixup!=20=E2=99=BF=EF=B8=8F(frontend)=20res?= =?UTF-8?q?tore=20presenter=20focus=20trapping=20after=20share=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/PresenterFloatingBar.tsx | 15 +++++++++++++-- .../doc-presenter/components/PresenterOverlay.tsx | 6 ++++++ .../doc-presenter/hooks/usePresenterShortcuts.ts | 9 ++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx index d0b1db739e..948d5e0197 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx @@ -22,6 +22,7 @@ interface PresenterFloatingBarProps { onPrev: () => void; onNext: () => void; onCopyLink: () => void; + onActionsOpenChange?: (isOpen: boolean) => void; onToggleFullscreen: () => void; onClose: () => void; } @@ -91,6 +92,7 @@ export const PresenterFloatingBar = ({ onPrev, onNext, onCopyLink, + onActionsOpenChange, onToggleFullscreen, onClose, }: PresenterFloatingBarProps) => { @@ -119,7 +121,16 @@ export const PresenterFloatingBar = ({ const toggleActions = (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); - setIsActionsOpen((isOpen) => !isOpen); + setIsActionsOpen((prev) => { + const next = !prev; + onActionsOpenChange?.(next); + return next; + }); + }; + + const handleActionsOpenChange = (isOpen: boolean) => { + setIsActionsOpen(isOpen); + onActionsOpenChange?.(isOpen); }; const actionOptions = useMemo( @@ -169,7 +180,7 @@ export const PresenterFloatingBar = ({ true} variant="tiny" > diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx index 27f7027bdc..9537014f89 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx @@ -56,6 +56,10 @@ export const PresenterOverlay = ({ const editor = useEditorStore((state) => state.editor); const copyPresenterLink = useCopyPresenterLink(doc.id); + // Track the floating bar's actions popover state so keyboard shortcuts + // can avoid closing the presenter while the popover is open. + const [isActionsOpen, setIsActionsOpen] = useState(false); + // Snapshot the editor's blocks once at mount. Subsequent collaborator // edits do not affect the ongoing presentation (by design). const snapshotRef = useRef(null); @@ -138,6 +142,7 @@ export const PresenterOverlay = ({ onToggleFullscreen: () => void toggle(), onClose, isFullscreen, + isPopoverOpen: isActionsOpen, }); const mountedIndices = useMemo(() => { @@ -202,6 +207,7 @@ export const PresenterOverlay = ({ onPrev={goPrev} onNext={goNext} onCopyLink={() => copyPresenterLink(currentIndex)} + onActionsOpenChange={setIsActionsOpen} onToggleFullscreen={() => void toggle()} onClose={onClose} /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts b/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts index 90cb1b4773..69f00514c2 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts @@ -8,6 +8,7 @@ interface ShortcutHandlers { onToggleFullscreen: () => void; onClose: () => void; isFullscreen: boolean; + isPopoverOpen: boolean; } const ARROW_CODES = new Set(['ArrowLeft', 'ArrowRight']); @@ -20,6 +21,7 @@ export const usePresenterShortcuts = ({ onToggleFullscreen, onClose, isFullscreen, + isPopoverOpen, }: ShortcutHandlers) => { useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -74,10 +76,10 @@ export const usePresenterShortcuts = ({ case 'Escape': // While fullscreen, the browser handles Esc natively (exits // fullscreen) and we deliberately stay open. Once out of - // fullscreen, Esc closes the presenter, unless a popover (e.g. - // the share dropdown) is open, in which case Esc should only + // fullscreen, Esc closes the presenter — unless the presenter's + // own actions popover is open, in which case Esc should only // dismiss the popover. - if (!isFullscreen && !document.querySelector('.react-aria-Popover')) { + if (!isFullscreen && !isPopoverOpen) { event.preventDefault(); onClose(); } @@ -97,5 +99,6 @@ export const usePresenterShortcuts = ({ onToggleFullscreen, onClose, isFullscreen, + isPopoverOpen, ]); }; From 0edf3ac5e8765e74324856d0a0d4b47d45e15f17 Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 22 Jul 2026 13:38:42 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fixup!=20=E2=99=BF=EF=B8=8F(frontend)=20res?= =?UTF-8?q?tore=20presenter=20focus=20trapping=20after=20share=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/docs/doc-presenter/components/PresenterOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx index 9537014f89..53a633e628 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx @@ -178,7 +178,7 @@ export const PresenterOverlay = ({ } return createPortal( - + Date: Wed, 22 Jul 2026 13:46:04 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fixup!=20fixup!=20=E2=99=BF=EF=B8=8F(fronte?= =?UTF-8?q?nd)=20restore=20presenter=20focus=20trapping=20after=20share=20?= =?UTF-8?q?links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts b/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts index 4d63695492..ee6976bbb9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts @@ -14,6 +14,7 @@ const renderShortcuts = ( onToggleFullscreen: vi.fn(), onClose: vi.fn(), isFullscreen: false, + isPopoverOpen: false, ...overrides, }; renderHook(() => usePresenterShortcuts(handlers));