diff --git a/packages/react-grab/e2e/selection.spec.ts b/packages/react-grab/e2e/selection.spec.ts index e9c73246a..938e9d184 100644 --- a/packages/react-grab/e2e/selection.spec.ts +++ b/packages/react-grab/e2e/selection.spec.ts @@ -31,6 +31,23 @@ test.describe("Element Selection", () => { expect(clipboardContent.length).toBeGreaterThan(0); }); + test("should show selection even when page blocks pointermove bubbling", async ({ reactGrab }) => { + await reactGrab.activate(); + + await reactGrab.page.evaluate(() => { + window.addEventListener( + "pointermove", + (event) => { + event.stopImmediatePropagation(); + }, + { capture: false }, + ); + }); + + await reactGrab.hoverElement("li"); + await reactGrab.waitForSelectionBox(); + }); + test("should copy heading element to clipboard", async ({ reactGrab }) => { await reactGrab.activate(); await reactGrab.hoverElement("[data-testid='todo-list'] h1"); diff --git a/packages/react-grab/package.json b/packages/react-grab/package.json index 4ee57e2b1..6dfe305c7 100644 --- a/packages/react-grab/package.json +++ b/packages/react-grab/package.json @@ -102,7 +102,6 @@ "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-typescript": "^7.28.5", - "solid-js": "^1.9.10", "@playwright/test": "^1.40.0", "@tailwindcss/cli": "^4.1.17", "@types/babel__core": "^7.20.5", @@ -111,6 +110,7 @@ "babel-preset-solid": "^1.9.10", "concurrently": "^9.1.2", "expect-sdk": "0.0.0-canary-20260405095424", + "solid-js": "^1.9.10", "tailwindcss": "^4.1.0", "tsx": "^4.21.0" }, diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx index 504c30579..420ed45bf 100644 --- a/packages/react-grab/src/core/index.tsx +++ b/packages/react-grab/src/core/index.tsx @@ -231,7 +231,8 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { const isDragging = createMemo( () => store.current.state === "active" && - (store.current.phase === "dragging-select" || store.current.phase === "dragging-reposition"), + (store.current.phase === "dragging-select" || + store.current.phase === "dragging-reposition"), ); const isDragRepositioning = createMemo( () => store.current.state === "active" && store.current.phase === "dragging-reposition", @@ -2569,7 +2570,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { } handlePointerMove(event.clientX, event.clientY); }, - { passive: true }, + { passive: true, capture: true }, ); eventListenerManager.addWindowListener( diff --git a/packages/react-grab/src/core/store.ts b/packages/react-grab/src/core/store.ts index 825c24f15..7121799ae 100644 --- a/packages/react-grab/src/core/store.ts +++ b/packages/react-grab/src/core/store.ts @@ -12,12 +12,7 @@ interface FrozenDragRect { height: number; } -type GrabPhase = - | "hovering" - | "frozen" - | "dragging-select" - | "dragging-reposition" - | "justDragged"; +type GrabPhase = "hovering" | "frozen" | "dragging-select" | "dragging-reposition" | "justDragged"; type GrabState = | { state: "idle" } diff --git a/packages/react-grab/src/utils/get-element-at-position.ts b/packages/react-grab/src/utils/get-element-at-position.ts index 0fcb34bf8..5009a77c8 100644 --- a/packages/react-grab/src/utils/get-element-at-position.ts +++ b/packages/react-grab/src/utils/get-element-at-position.ts @@ -59,7 +59,7 @@ export const getElementAtPosition = (clientX: number, clientY: number): Element const isPositionClose = isWithinThreshold(clientX, clientY, cache.clientX, cache.clientY); const isWithinThrottle = now - cache.timestamp < ELEMENT_POSITION_THROTTLE_MS; - if (isPositionClose || isWithinThrottle) { + if (isPositionClose && isWithinThrottle) { return cache.element; } }