From e104d47dc11e057bfa6cedd77a6cc8852dfc73de Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:01:22 +0100 Subject: [PATCH] fix(viewer): always restore text selection when pan is toggled off --- .../core/components/viewer/PanAPIBridge.tsx | 39 +++++++++++++------ .../viewer/useViewerWorkbenchBarButtons.tsx | 8 ++-- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/frontend/editor/src/core/components/viewer/PanAPIBridge.tsx b/frontend/editor/src/core/components/viewer/PanAPIBridge.tsx index 772af6d5ac..9d9b09fb61 100644 --- a/frontend/editor/src/core/components/viewer/PanAPIBridge.tsx +++ b/frontend/editor/src/core/components/viewer/PanAPIBridge.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { usePan } from "@embedpdf/plugin-pan/react"; +import { useInteractionManagerCapability } from "@embedpdf/plugin-interaction-manager/react"; import { useViewer } from "@app/contexts/ViewerContext"; import { useActiveDocumentId } from "@app/components/viewer/useActiveDocumentId"; import { useDocumentReady } from "@app/components/viewer/hooks/useDocumentReady"; @@ -21,13 +22,18 @@ export function PanAPIBridge() { function PanAPIBridgeInner({ documentId }: { documentId: string }) { const { provides: pan, isPanning } = usePan(documentId); + const { provides: imCapability } = useInteractionManagerCapability(); const { registerBridge, triggerImmediatePanUpdate } = useViewer(); - // Keep pan ref updated to avoid re-running effect when object reference changes + // Keep refs updated to avoid re-running effect when object references change const panRef = useRef(pan); useEffect(() => { panRef.current = pan; }, [pan]); + const imRef = useRef(imCapability); + useEffect(() => { + imRef.current = imCapability; + }, [imCapability]); // Track previous isPanning value to detect changes const prevIsPanningRef = useRef(isPanning); @@ -39,6 +45,17 @@ function PanAPIBridgeInner({ documentId }: { documentId: string }) { isPanning, }; + // Pan off must always land in selection (pointerMode), not the default mode - + // if pan ever became the default, disablePan/togglePan couldn't escape it (#5175). + const goToPointerMode = () => { + const im = imRef.current; + if (im) { + im.forDocument(documentId).activate("pointerMode"); + } else { + currentPan.disablePan(); + } + }; + // Register this bridge with ViewerContext registerBridge("pan", { state: newState, @@ -47,22 +64,20 @@ function PanAPIBridgeInner({ documentId }: { documentId: string }) { currentPan.enablePan(); }, disable: () => { - currentPan.disablePan(); + goToPointerMode(); }, toggle: () => { - currentPan.togglePan(); - }, - makePanDefault: () => { - // v2.5.0: makePanDefault may not exist, enable pan as fallback - if ( - "makePanDefault" in currentPan && - typeof (currentPan as any).makePanDefault === "function" - ) { - (currentPan as any).makePanDefault(); + if (isPanning) { + goToPointerMode(); } else { currentPan.enablePan(); } }, + makePanDefault: () => { + // Never make pan the default mode (that is what locks the viewer in + // #5175). Just enable pan for the current interaction. + currentPan.enablePan(); + }, }, }); @@ -75,7 +90,7 @@ function PanAPIBridgeInner({ documentId }: { documentId: string }) { return () => { registerBridge("pan", null); }; - }, [isPanning, registerBridge, triggerImmediatePanUpdate]); + }, [isPanning, registerBridge, triggerImmediatePanUpdate, documentId]); return null; } diff --git a/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx b/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx index 4960672dd9..53669424f0 100644 --- a/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx +++ b/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx @@ -211,12 +211,10 @@ export function useViewerWorkbenchBarButtons( disabled: !isPanning && pendingCount > 0 && redactionActiveType !== null, onClick: () => { + // Don't optimistically flip isPanning - it must reflect the real EmbedPDF + // mode so the button can't show "off" while still stuck in pan (#5175). viewer.panActions.togglePan(); - setIsPanning((prev) => { - const next = !prev; - if (next && isRulerActive) setIsRulerActive?.(false); - return next; - }); + if (!isPanning && isRulerActive) setIsRulerActive?.(false); }, }, {