From baf3e88dd500e76e91829da71dd65cfa692503a5 Mon Sep 17 00:00:00 2001 From: Noah Jadallah Date: Mon, 2 Nov 2026 12:24:01 +0000 Subject: [PATCH] fix(desktop): annotating inside a page's dialog no longer closes it The annotation overlay lives in a closed shadow root under , so a page's document listeners see its pointer and focus events as coming from outside any open dialog. Radix popovers closed as soon as the comment field took focus, modal dialogs pulled focus back out of the comment field and closed on toolbar clicks, and react-remove-scroll cancelled wheel events over the editor. Stop pointerdown, focus and wheel/touch events at the overlay host in the bubble phase, after the overlay's own handlers, and stop focusout at when focus moves into the overlay. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/preview/PickPreload.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/desktop/src/preview/PickPreload.ts b/apps/desktop/src/preview/PickPreload.ts index 4cb5987cdeb85..044226e15ec69 100644 --- a/apps/desktop/src/preview/PickPreload.ts +++ b/apps/desktop/src/preview/PickPreload.ts @@ -43,6 +43,16 @@ const MAX_MARQUEE_ELEMENTS = 20; const ELEMENT_CONTEXT_TIMEOUT_MS = 5_000; const CONTENT_LAYER_Z_INDEX = 1; const CHROME_LAYER_Z_INDEX = 10; +// Page dialogs (Radix, react-remove-scroll) dismiss, refocus or block scrolling on document events +// that shadow retargeting attributes to the overlay host. +const PAGE_ISOLATED_EVENTS = [ + "pointerdown", + "focusin", + "focusout", + "wheel", + "touchstart", + "touchmove", +] as const; let recordingCursor: ReturnType | null = null; ipcRenderer.on( @@ -1314,6 +1324,13 @@ function startAnnotation(sendEnabled: boolean): void { event.stopPropagation(); }; + const stopAtOverlay = (event: Event): void => event.stopPropagation(); + // Focus moving from the page into the overlay targets the page element, so the host never sees it. + const onFocusOutToOverlay = (event: FocusEvent): void => { + if (event.relatedTarget instanceof Element && isAnnotationNode(event.relatedTarget)) + event.stopPropagation(); + }; + const onPointerOut = (event: PointerEvent): void => { if (event.relatedTarget === null) clearHoverOutline(); }; @@ -1351,6 +1368,8 @@ function startAnnotation(sendEnabled: boolean): void { dragHandle.removeEventListener("pointermove", onEditorPointerMove); dragHandle.removeEventListener("pointerup", onEditorPointerUp); dragHandle.removeEventListener("pointercancel", onEditorPointerUp); + for (const type of PAGE_ISOLATED_EVENTS) host.removeEventListener(type, stopAtOverlay); + document.documentElement.removeEventListener("focusout", onFocusOutToOverlay); if (editorLayoutFrame !== null) window.cancelAnimationFrame(editorLayoutFrame); ipcRenderer.off(CANCEL_PICK_CHANNEL, onCancel); ipcRenderer.off(ANNOTATION_CAPTURED_CHANNEL, onCaptured); @@ -1463,6 +1482,10 @@ function startAnnotation(sendEnabled: boolean): void { window.addEventListener("keydown", onKeyDown, { capture: true }); window.addEventListener("scroll", repaint, { capture: true, passive: true }); window.addEventListener("resize", repaint, { passive: true }); + // Bubble phase, so the overlay's own handlers still see these events. + for (const type of PAGE_ISOLATED_EVENTS) + host.addEventListener(type, stopAtOverlay, { passive: true }); + document.documentElement.addEventListener("focusout", onFocusOutToOverlay); ipcRenderer.on(CANCEL_PICK_CHANNEL, onCancel); ipcRenderer.on(ANNOTATION_CAPTURED_CHANNEL, onCaptured); if (inputLayer) {