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,
]);
};