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/__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)); 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..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; } @@ -43,6 +44,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 +77,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 = ({ @@ -80,6 +92,7 @@ export const PresenterFloatingBar = ({ onPrev, onNext, onCopyLink, + onActionsOpenChange, onToggleFullscreen, onClose, }: PresenterFloatingBarProps) => { @@ -97,7 +110,7 @@ export const PresenterFloatingBar = ({ useEffect(() => { const id = requestAnimationFrame(() => { barRef.current - ?.querySelector('button:not([disabled])') + ?.querySelector('button:not([aria-disabled="true"])') ?.focus(); }); return () => cancelAnimationFrame(id); @@ -108,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( @@ -137,8 +159,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 +171,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={} /> @@ -158,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 f55da03c01..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 @@ -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(() => { @@ -173,7 +178,7 @@ export const PresenterOverlay = ({ } return createPortal( - + 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 02ef7cc760..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,8 +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. - if (!isFullscreen) { + // 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 && !isPopoverOpen) { event.preventDefault(); onClose(); } @@ -95,5 +99,6 @@ export const usePresenterShortcuts = ({ onToggleFullscreen, onClose, isFullscreen, + isPopoverOpen, ]); };