From 197d8b301a414c10c2fa5a98d8b791cff8fb59f4 Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Sun, 8 Mar 2026 17:00:37 +0100 Subject: [PATCH 1/2] fix(extension): keep host dialogs open during annotation --- .../components/App.modal-overlay.test.tsx | 266 ++++++++++++++++++ .../extension/src/content/components/App.tsx | 92 +++--- .../src/content/components/OnUIDialog.tsx | 8 +- .../components/OnUIRegionDialog.test.tsx | 4 +- .../content/components/OnUIRegionDialog.tsx | 9 +- .../content/components/OnUIToolbar.test.tsx | 13 - .../src/content/components/OnUIToolbar.tsx | 21 +- .../src/content/hooks/useElementHover.ts | 54 ++-- .../src/content/utils/overlay-events.ts | 35 +++ 9 files changed, 391 insertions(+), 111 deletions(-) create mode 100644 packages/extension/src/content/components/App.modal-overlay.test.tsx create mode 100644 packages/extension/src/content/utils/overlay-events.ts diff --git a/packages/extension/src/content/components/App.modal-overlay.test.tsx b/packages/extension/src/content/components/App.modal-overlay.test.tsx new file mode 100644 index 0000000..7503d63 --- /dev/null +++ b/packages/extension/src/content/components/App.modal-overlay.test.tsx @@ -0,0 +1,266 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import { App } from './App'; + +const runtimeState = vi.hoisted(() => ({ + annotateMode: false, +})); + +vi.mock('../hooks/useTabRuntimeState', async () => { + const hooks = await vi.importActual('preact/hooks'); + + return { + useTabRuntimeState: () => { + const [annotateMode, setAnnotateModeState] = hooks.useState(runtimeState.annotateMode); + + hooks.useEffect(() => { + runtimeState.annotateMode = annotateMode; + }, [annotateMode]); + + return { + enabled: true, + annotateMode, + isContextInvalid: false, + setEnabled: vi.fn(async () => {}), + setAnnotateMode: vi.fn(async (nextAnnotateMode: boolean) => { + runtimeState.annotateMode = nextAnnotateMode; + setAnnotateModeState(nextAnnotateMode); + }), + toggleEnabled: vi.fn(async () => {}), + toggleAnnotateMode: vi.fn(async () => { + setAnnotateModeState((previous) => { + const next = !previous; + runtimeState.annotateMode = next; + return next; + }); + }), + }; + }, + }; +}); + +vi.mock('../hooks/useAnnotations', () => ({ + useAnnotations: () => ({ + annotations: [], + addAnnotation: vi.fn(async () => ({ id: 'created-id' })), + addAnnotationsBulk: vi.fn(async () => []), + updateAnnotation: vi.fn(async () => true), + deleteAnnotation: vi.fn(async () => true), + clearAnnotations: vi.fn(async () => true), + refreshAnnotations: vi.fn(async () => {}), + isLoading: false, + error: null, + isContextInvalid: false, + }), +})); + +vi.mock('../messaging', () => ({ + getSettings: vi.fn(async () => ({ success: true, data: { clearOnCopy: false } })), + updateSettings: vi.fn(async () => ({ success: true, data: { clearOnCopy: false } })), +})); + +vi.mock('./ElementHighlight', () => ({ + ElementHighlight: () => null, +})); + +vi.mock('./AnnotationMarkers', () => ({ + AnnotationMarkers: () => null, +})); + +vi.mock('./RegionOutline', () => ({ + RegionOutline: () => null, +})); + +vi.mock('./RegionTransformHandles', () => ({ + RegionTransformHandles: () => null, +})); + +vi.mock('./OnUIDialog', () => ({ + OnUIDialog: ({ onCancel }: { onCancel: () => void }) => ( +
+
Annotation dialog mock
+ +
+ ), +})); + +vi.mock('./OnUIRegionDialog', () => ({ + OnUIRegionDialog: () => null, +})); + +vi.mock('./RegionDrawOverlay', () => ({ + RegionDrawOverlay: () => null, +})); + +vi.mock('./ErrorToast', () => ({ + ErrorToast: () => null, +})); + +function installPointerEventShim() { + if (typeof window.PointerEvent === 'function') { + return; + } + + class PointerEventShim extends MouseEvent {} + + Object.defineProperty(window, 'PointerEvent', { + configurable: true, + writable: true, + value: PointerEventShim, + }); +} + +function installElementsFromPointMock() { + const elementsFromPoint = vi.fn(() => [] as Element[]); + Object.defineProperty(document, 'elementsFromPoint', { + configurable: true, + writable: true, + value: elementsFromPoint, + }); + return elementsFromPoint; +} + +function installHostDismissListener(type: 'pointerdown' | 'keydown') { + const hostDismiss = vi.fn(); + document.addEventListener(type, hostDismiss); + + return { + hostDismiss, + cleanup: () => document.removeEventListener(type, hostDismiss), + }; +} + +function renderInOnUiHost() { + document.body.innerHTML = ''; + + const host = document.createElement('div'); + host.id = 'onui-shadow-host'; + + const appContainer = document.createElement('div'); + host.appendChild(appContainer); + document.body.appendChild(host); + + const modal = document.createElement('div'); + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-label', 'Host dialog'); + document.body.appendChild(modal); + + render(, { container: appContainer }); + + return { host, modal }; +} + +async function flushEffects() { + await Promise.resolve(); + await Promise.resolve(); +} + +function dispatchPointerDown(target: Element, coords = { clientX: 24, clientY: 24 }) { + target.dispatchEvent( + new window.PointerEvent('pointerdown', { + bubbles: true, + cancelable: true, + button: 0, + ...coords, + }) + ); +} + +describe('App modal-safe overlay behavior', () => { + beforeAll(() => { + installPointerEventShim(); + }); + + beforeEach(() => { + runtimeState.annotateMode = false; + vi.clearAllMocks(); + installElementsFromPointMock(); + }); + + it('does not trigger host pointer dismissal when toggling annotate mode from the onUI toolbar', async () => { + const { modal } = renderInOnUiHost(); + const user = userEvent.setup(); + const { hostDismiss, cleanup } = installHostDismissListener('pointerdown'); + + await user.click(screen.getByRole('button', { name: 'Toggle onUI panel' })); + await user.click(screen.getByRole('button', { name: 'Toggle annotate mode' })); + + const annotateButton = screen.getByRole('button', { name: 'Toggle annotate mode' }); + await waitFor(() => { + expect(annotateButton.className).toContain('is-active'); + }); + + expect(hostDismiss).not.toHaveBeenCalled(); + expect(modal.getAttribute('role')).toBe('dialog'); + cleanup(); + }); + + it('selects modal content in annotate mode without invoking the host pointer dismissal handler', async () => { + runtimeState.annotateMode = true; + const elementsFromPoint = installElementsFromPointMock(); + const { modal } = renderInOnUiHost(); + const { hostDismiss, cleanup } = installHostDismissListener('pointerdown'); + + const target = document.createElement('button'); + target.textContent = 'Dialog action'; + Object.defineProperty(target, 'getBoundingClientRect', { + configurable: true, + value: () => ({ + top: 20, + left: 20, + bottom: 60, + right: 180, + width: 160, + height: 40, + x: 20, + y: 20, + toJSON: () => ({}), + }), + }); + modal.appendChild(target); + elementsFromPoint.mockReturnValue([target]); + await flushEffects(); + + dispatchPointerDown(target); + + expect(hostDismiss).not.toHaveBeenCalled(); + expect(await screen.findByText('Annotation dialog mock')).toBeTruthy(); + cleanup(); + }); + + it('owns Escape while annotate mode is active and keeps the host dialog dismissal handler from firing', async () => { + runtimeState.annotateMode = true; + renderInOnUiHost(); + const user = userEvent.setup(); + const { hostDismiss, cleanup } = installHostDismissListener('keydown'); + + await user.click(screen.getByRole('button', { name: 'Toggle onUI panel' })); + const annotateButton = screen.getByRole('button', { name: 'Toggle annotate mode' }); + expect(annotateButton.className).toContain('is-active'); + + await user.keyboard('{Escape}'); + + await waitFor(() => { + expect(annotateButton.className).not.toContain('is-active'); + }); + expect(hostDismiss).not.toHaveBeenCalled(); + cleanup(); + }); + + it('still allows host pointer dismissal when annotate mode is inactive and the click is outside onUI', () => { + renderInOnUiHost(); + const { hostDismiss, cleanup } = installHostDismissListener('pointerdown'); + + const outsideButton = document.createElement('button'); + outsideButton.textContent = 'Outside target'; + document.body.appendChild(outsideButton); + + dispatchPointerDown(outsideButton); + + expect(hostDismiss).toHaveBeenCalledTimes(1); + cleanup(); + }); +}); diff --git a/packages/extension/src/content/components/App.tsx b/packages/extension/src/content/components/App.tsx index 99def6c..6937bd7 100644 --- a/packages/extension/src/content/components/App.tsx +++ b/packages/extension/src/content/components/App.tsx @@ -18,6 +18,7 @@ import { useAnnotations } from '../hooks/useAnnotations'; import { useTabRuntimeState } from '../hooks/useTabRuntimeState'; import { createAnnotationFromElement } from '../utils/create-annotation'; import { createAnnotationFromRegion } from '../utils/create-region-annotation'; +import { ONUI_SHADOW_HOST_ID, stopEventPropagation } from '../utils/overlay-events'; import { OnUIRegionDialog } from './OnUIRegionDialog'; import { RegionDrawOverlay } from './RegionDrawOverlay'; import { RegionOutline } from './RegionOutline'; @@ -33,6 +34,8 @@ function createBatchId(): string { return `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } +type SelectionEvent = MouseEvent | PointerEvent; + interface SaveDialogData { comment: string; intent?: AnnotationIntent | undefined; @@ -163,7 +166,7 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { }, [isDrawMode]); // Handle element click in annotation mode - const handleElementClick = useCallback((element: Element, event: MouseEvent) => { + const handleElementClick = useCallback((element: Element, event: SelectionEvent) => { setEditingAnnotation(null); setPendingRegion(null); @@ -192,6 +195,27 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { setSelectedElement(element); }, []); + useEffect(() => { + const host = document.getElementById(ONUI_SHADOW_HOST_ID); + if (!host) { + return; + } + + const stopOverlayPointerEvent = (event: Event) => { + stopEventPropagation(event); + }; + + host.addEventListener('pointerdown', stopOverlayPointerEvent); + host.addEventListener('mousedown', stopOverlayPointerEvent); + host.addEventListener('click', stopOverlayPointerEvent); + + return () => { + host.removeEventListener('pointerdown', stopOverlayPointerEvent); + host.removeEventListener('mousedown', stopOverlayPointerEvent); + host.removeEventListener('click', stopOverlayPointerEvent); + }; + }, []); + // Open multi-target dialog when shift is released useEffect(() => { const handleShiftRelease = (event: KeyboardEvent) => { @@ -221,30 +245,6 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { multiDialogTargets.length, ]); - // Esc while selecting clears pending multi targets before toolbar exits annotate mode - useEffect(() => { - const handleEscapeClearSelection = (event: KeyboardEvent) => { - if (event.key !== 'Escape') return; - if (!annotateMode) return; - if (pendingMultiSelection.length === 0) return; - if (selectedElement || editingAnnotation || multiDialogTargets.length > 0) return; - - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - setPendingMultiSelection([]); - }; - - document.addEventListener('keydown', handleEscapeClearSelection, true); - return () => document.removeEventListener('keydown', handleEscapeClearSelection, true); - }, [ - annotateMode, - pendingMultiSelection.length, - selectedElement, - editingAnnotation, - multiDialogTargets.length, - ]); - const { hoveredElement } = useElementHover({ enabled: annotateMode && @@ -496,8 +496,8 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { return; } - if (pendingRegion) { - setPendingRegion(null); + if (pendingRegion || selectedElement || editingAnnotation || multiDialogTargets.length > 0) { + handleCancelPopup(); return; } @@ -507,11 +507,6 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { return; } - if (editingAnnotation && editingRegionGeometry) { - setEditingRegionGeometry(null); - return; - } - if (pendingMultiSelection.length > 0) { setPendingMultiSelection([]); return; @@ -523,14 +518,44 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { }, [ annotateMode, editingAnnotation, - editingRegionGeometry, + handleCancelPopup, isDrawMode, isDrawingDraft, + multiDialogTargets.length, onToggleAnnotateMode, pendingMultiSelection.length, pendingRegion, + selectedElement, ]); + const shouldOwnEscape = + annotateMode || + isDrawMode || + isDrawingDraft || + pendingRegion !== null || + selectedElement !== null || + editingAnnotation !== null || + multiDialogTargets.length > 0 || + pendingMultiSelection.length > 0; + + useEffect(() => { + if (!shouldOwnEscape) { + return; + } + + const handleEscapeKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape') { + return; + } + + stopEventPropagation(event, true); + handleEscape(); + }; + + window.addEventListener('keydown', handleEscapeKeyDown, true); + return () => window.removeEventListener('keydown', handleEscapeKeyDown, true); + }, [handleEscape, shouldOwnEscape]); + // Handle clear all annotations const handleClearAnnotations = useCallback(async () => { await clearAnnotations(); @@ -595,7 +620,6 @@ function EnabledApp({ annotateMode, onToggleAnnotateMode }: EnabledAppProps) { onToggleAnnotateMode={handleToggleAnnotateExclusive} onToggleDrawMode={handleToggleDrawMode} onSelectDrawShape={setDrawShape} - onEscape={handleEscape} annotations={annotations} outputLevel={outputLevel} onOutputLevelChange={setOutputLevel} diff --git a/packages/extension/src/content/components/OnUIDialog.tsx b/packages/extension/src/content/components/OnUIDialog.tsx index ef372c8..da02782 100644 --- a/packages/extension/src/content/components/OnUIDialog.tsx +++ b/packages/extension/src/content/components/OnUIDialog.tsx @@ -186,11 +186,7 @@ export function OnUIDialog({ // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - e.stopPropagation(); - onCancel(); - } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { const shouldSave = Boolean(commentRef.current.trim()) && (!isMultiCreate || hasTargets); if (!shouldSave || isSavingRef.current) { return; @@ -202,7 +198,7 @@ export function OnUIDialog({ document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [onCancel, runSave, isMultiCreate, hasTargets]); + }, [runSave, isMultiCreate, hasTargets]); const elementPath = getElementPath(element); const selector = getCssSelector(element); diff --git a/packages/extension/src/content/components/OnUIRegionDialog.test.tsx b/packages/extension/src/content/components/OnUIRegionDialog.test.tsx index 0ae6029..71d2fb8 100644 --- a/packages/extension/src/content/components/OnUIRegionDialog.test.tsx +++ b/packages/extension/src/content/components/OnUIRegionDialog.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { OnUIRegionDialog } from './OnUIRegionDialog'; describe('OnUIRegionDialog', () => { - it('closes on Escape', async () => { + it('closes when the dialog close button is clicked', async () => { const onCancel = vi.fn(); const { container } = render( @@ -20,7 +20,7 @@ describe('OnUIRegionDialog', () => { expect(dialog).toBeTruthy(); const user = userEvent.setup(); - await user.keyboard('{Escape}'); + await user.click(screen.getByRole('button', { name: 'Close' })); expect(onCancel).toHaveBeenCalledTimes(1); }); diff --git a/packages/extension/src/content/components/OnUIRegionDialog.tsx b/packages/extension/src/content/components/OnUIRegionDialog.tsx index f6a95a2..e84c574 100644 --- a/packages/extension/src/content/components/OnUIRegionDialog.tsx +++ b/packages/extension/src/content/components/OnUIRegionDialog.tsx @@ -94,13 +94,6 @@ export function OnUIRegionDialog({ return; } - if (event.key === 'Escape') { - event.preventDefault(); - event.stopPropagation(); - onCancel(); - return; - } - if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); event.stopPropagation(); @@ -110,7 +103,7 @@ export function OnUIRegionDialog({ document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [handleSave, onCancel]); + }, [handleSave]); const position = getDialogPosition(geometry); diff --git a/packages/extension/src/content/components/OnUIToolbar.test.tsx b/packages/extension/src/content/components/OnUIToolbar.test.tsx index d42411d..ff84ffd 100644 --- a/packages/extension/src/content/components/OnUIToolbar.test.tsx +++ b/packages/extension/src/content/components/OnUIToolbar.test.tsx @@ -42,7 +42,6 @@ function renderToolbar({ onToggleAnnotateMode = vi.fn(), onToggleDrawMode = vi.fn(), onSelectDrawShape = vi.fn(), - onEscape = vi.fn(), clearOnCopy = false, onClearOnCopyChange = vi.fn(), onClearAnnotations = vi.fn(), @@ -55,7 +54,6 @@ function renderToolbar({ onToggleAnnotateMode?: () => void; onToggleDrawMode?: () => void; onSelectDrawShape?: (shape: 'rectangle' | 'ellipse') => void; - onEscape?: () => void; clearOnCopy?: boolean; onClearOnCopyChange?: (enabled: boolean) => void; onClearAnnotations?: () => void | Promise; @@ -70,7 +68,6 @@ function renderToolbar({ onToggleAnnotateMode={onToggleAnnotateMode} onToggleDrawMode={onToggleDrawMode} onSelectDrawShape={onSelectDrawShape} - onEscape={onEscape} annotations={annotations} outputLevel="standard" onOutputLevelChange={vi.fn()} @@ -150,16 +147,6 @@ describe('OnUIToolbar', () => { expect(screen.queryByText('Shift multi-select: 4 selected. Release Shift to annotate all.')).toBeNull(); }); - it('delegates Escape handling to app-level callback once per key press', async () => { - const onEscape = vi.fn(); - renderToolbar({ isAnnotateMode: true, onEscape }); - const user = userEvent.setup(); - - await user.keyboard('{Escape}'); - - expect(onEscape).toHaveBeenCalledTimes(1); - }); - it('clears annotations after successful copy when clear-on-copy is enabled', async () => { const onClearAnnotations = vi.fn(); renderToolbar({ diff --git a/packages/extension/src/content/components/OnUIToolbar.tsx b/packages/extension/src/content/components/OnUIToolbar.tsx index 22abcf5..592612f 100644 --- a/packages/extension/src/content/components/OnUIToolbar.tsx +++ b/packages/extension/src/content/components/OnUIToolbar.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'preact/hooks'; +import { useState, useCallback } from 'preact/hooks'; import type { Annotation, OutputLevel, RegionShape } from '@/types'; import { generateOutput } from '../utils/output-generation'; import { copyToClipboard } from '../utils/clipboard'; @@ -76,7 +76,6 @@ interface OnUIToolbarProps { onToggleAnnotateMode: () => void; onToggleDrawMode: () => void; onSelectDrawShape: (shape: RegionShape) => void; - onEscape: () => void; annotations: Annotation[]; outputLevel: OutputLevel; onOutputLevelChange: (level: OutputLevel) => void; @@ -93,7 +92,6 @@ export function OnUIToolbar({ onToggleAnnotateMode, onToggleDrawMode, onSelectDrawShape, - onEscape, annotations, outputLevel, onOutputLevelChange, @@ -142,23 +140,6 @@ export function OnUIToolbar({ console.log(`${LOG_PREFIX} annotations cleared`); }, [annotations.length, onClearAnnotations, LOG_PREFIX]); - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.defaultPrevented) { - return; - } - - if (event.key !== 'Escape') { - return; - } - - onEscape(); - }; - - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [onEscape]); - const activeHintMode = isDrawMode ? 'draw' : isAnnotateMode ? 'annotate' : null; const activeHint = activeHintMode === 'annotate' diff --git a/packages/extension/src/content/hooks/useElementHover.ts b/packages/extension/src/content/hooks/useElementHover.ts index 89db2b0..93088c7 100644 --- a/packages/extension/src/content/hooks/useElementHover.ts +++ b/packages/extension/src/content/hooks/useElementHover.ts @@ -1,34 +1,20 @@ import { useState, useEffect, useCallback, useRef } from 'preact/hooks'; import { getElementAtPoint } from '../utils/element-detection'; +import { isOnUiOverlayEvent, stopEventPropagation } from '../utils/overlay-events'; import { throttle } from '../utils/throttle'; interface UseElementHoverOptions { enabled: boolean; throttleMs?: number; - onElementClick?: (element: Element, event: MouseEvent) => void; + onElementClick?: (element: Element, event: MouseEvent | PointerEvent) => void; } interface UseElementHoverReturn { hoveredElement: Element | null; } -const ONUI_SHADOW_HOST_ID = 'onui-shadow-host'; - -function isOnUiOverlayEvent(event: MouseEvent): boolean { - const target = event.target; - if (target instanceof Element) { - if (target.id === ONUI_SHADOW_HOST_ID) { - return true; - } - - if (target.closest(`#${ONUI_SHADOW_HOST_ID}`)) { - return true; - } - } - - return event.composedPath().some((node) => { - return node instanceof Element && node.id === ONUI_SHADOW_HOST_ID; - }); +function isPrimarySelectionEvent(event: MouseEvent | PointerEvent): boolean { + return event.button === 0; } /** @@ -89,16 +75,16 @@ export function useElementHover( // Stable reference to the throttled handler const handleMouseMove = throttleRef.current.throttled; - const handleClick = useCallback( - (e: MouseEvent) => { + const handleSelect = useCallback( + (event: MouseEvent | PointerEvent) => { if (!enabledRef.current || !onElementClickRef.current) return; - if (isOnUiOverlayEvent(e)) return; + if (!isPrimarySelectionEvent(event)) return; + if (isOnUiOverlayEvent(event)) return; - const element = getElementAtPoint(e.clientX, e.clientY); + const element = getElementAtPoint(event.clientX, event.clientY); if (element) { - e.preventDefault(); - e.stopPropagation(); - onElementClickRef.current(element, e); + stopEventPropagation(event, true); + onElementClickRef.current(element, event); } }, [] // Stable - uses refs internally @@ -112,15 +98,27 @@ export function useElementHover( } document.addEventListener('mousemove', handleMouseMove, { passive: true }); - document.addEventListener('click', handleClick, { capture: true }); + if (typeof window.PointerEvent === 'function') { + window.addEventListener('pointerdown', handleSelect as EventListener, true); + document.addEventListener('pointerdown', handleSelect as EventListener, true); + } else { + window.addEventListener('mousedown', handleSelect as EventListener, true); + document.addEventListener('mousedown', handleSelect as EventListener, true); + } return () => { document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('click', handleClick, { capture: true }); + if (typeof window.PointerEvent === 'function') { + window.removeEventListener('pointerdown', handleSelect as EventListener, true); + document.removeEventListener('pointerdown', handleSelect as EventListener, true); + } else { + window.removeEventListener('mousedown', handleSelect as EventListener, true); + document.removeEventListener('mousedown', handleSelect as EventListener, true); + } // Cancel any pending throttled calls on cleanup throttleRef.current?.cancel(); }; - }, [enabled, handleMouseMove, handleClick]); + }, [enabled, handleMouseMove, handleSelect]); return { hoveredElement, diff --git a/packages/extension/src/content/utils/overlay-events.ts b/packages/extension/src/content/utils/overlay-events.ts new file mode 100644 index 0000000..d368b69 --- /dev/null +++ b/packages/extension/src/content/utils/overlay-events.ts @@ -0,0 +1,35 @@ +export const ONUI_SHADOW_HOST_ID = 'onui-shadow-host'; + +function getComposedPath(event: Event): EventTarget[] { + if (typeof event.composedPath === 'function') { + return event.composedPath(); + } + + return []; +} + +export function isOnUiOverlayEvent(event: Event): boolean { + const target = event.target; + if (target instanceof Element) { + if (target.id === ONUI_SHADOW_HOST_ID) { + return true; + } + + if (target.closest(`#${ONUI_SHADOW_HOST_ID}`)) { + return true; + } + } + + return getComposedPath(event).some((node) => { + return node instanceof Element && node.id === ONUI_SHADOW_HOST_ID; + }); +} + +export function stopEventPropagation(event: Event, preventDefault = false): void { + if (preventDefault && event.cancelable) { + event.preventDefault(); + } + + event.stopPropagation(); + event.stopImmediatePropagation(); +} From d6249da1021dd713331d77bda2d6fe179c65c796 Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Mon, 9 Mar 2026 07:31:21 +0100 Subject: [PATCH 2/2] fix(extension): restore typecheck for annotation form --- .../content/components/OnUIAnnotationForm.tsx | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/extension/src/content/components/OnUIAnnotationForm.tsx b/packages/extension/src/content/components/OnUIAnnotationForm.tsx index fb552a0..e2121d5 100644 --- a/packages/extension/src/content/components/OnUIAnnotationForm.tsx +++ b/packages/extension/src/content/components/OnUIAnnotationForm.tsx @@ -81,56 +81,56 @@ const SEVERITY_OPTIONS: Option[] = [ interface OnUIAnnotationDialogProps { title: string; - subtitle?: string; - subtitleTitle?: string; + subtitle?: string | undefined; + subtitleTitle?: string | undefined; onCancel: () => void; - position?: JSX.CSSProperties; - className?: string; - width?: string; - showBackdrop?: boolean; - dialogRef?: Ref; - children: ComponentChildren; + position?: JSX.CSSProperties | undefined; + className?: string | undefined; + width?: string | undefined; + showBackdrop?: boolean | undefined; + dialogRef?: Ref | undefined; + children?: ComponentChildren; } type OnUIAnnotationDialogShellProps = OnUIAnnotationDialogProps; interface OnUIAnnotationFormProps { comment: string; - intent?: AnnotationIntent; - severity?: AnnotationSeverity; + intent?: AnnotationIntent | undefined; + severity?: AnnotationSeverity | undefined; onCommentChange: (value: string) => void; onIntentChange: (value: AnnotationIntent | undefined) => void; onSeverityChange: (value: AnnotationSeverity | undefined) => void; onSave: () => void; onCancel: () => void; - onDelete?: () => void; - commentLabel?: string; - commentPlaceholder?: string; - intentLabel?: string; - severityLabel?: string; - cancelLabel?: string; - saveLabel?: string; - deleteLabel?: string; - saveDisabled?: boolean; - isSaving?: boolean; - textareaRef?: Ref; + onDelete?: (() => void) | undefined; + commentLabel?: string | undefined; + commentPlaceholder?: string | undefined; + intentLabel?: string | undefined; + severityLabel?: string | undefined; + cancelLabel?: string | undefined; + saveLabel?: string | undefined; + deleteLabel?: string | undefined; + saveDisabled?: boolean | undefined; + isSaving?: boolean | undefined; + textareaRef?: Ref | undefined; children?: ComponentChildren; } interface OnUIAnnotationFormDialogProps extends OnUIAnnotationDialogProps { comment: string; - intent?: AnnotationIntent; - severity?: AnnotationSeverity; + intent?: AnnotationIntent | undefined; + severity?: AnnotationSeverity | undefined; onCommentChange: (value: string) => void; onIntentChange: (value: AnnotationIntent | undefined) => void; onSeverityChange: (value: AnnotationSeverity | undefined) => void; onSave: () => void; - onDelete?: () => void; - commentPlaceholder?: string; + onDelete?: (() => void) | undefined; + commentPlaceholder?: string | undefined; saveLabel: string; - saveDisabled?: boolean; - isSaving?: boolean; - textareaRef?: Ref; + saveDisabled?: boolean | undefined; + isSaving?: boolean | undefined; + textareaRef?: Ref | undefined; children?: ComponentChildren; } @@ -169,7 +169,7 @@ export function OnUIAnnotationDialogShell({ return ( <> {showBackdrop &&
} -
+
{title}
@@ -256,7 +256,7 @@ export function OnUIAnnotationForm({ {commentLabel}