Skip to content

feat: add prehit spatial index as safe fallback accelerator#292

Open
aidenybai wants to merge 1 commit intomainfrom
feat/prehit-spatial-index
Open

feat: add prehit spatial index as safe fallback accelerator#292
aidenybai wants to merge 1 commit intomainfrom
feat/prehit-spatial-index

Conversation

@aidenybai
Copy link
Copy Markdown
Owner

@aidenybai aidenybai commented Apr 11, 2026

Summary

  • Adds a pre-indexed spatial hit-testing system (Hilbert R-tree) that accelerates element detection when the browser's elementFromPoint returns a non-grabbable element (decorative overlays, dev tool canvases, etc.)
  • Safety model: elementFromPoint is always tried first — the browser handles all CSS edge cases (clip-path, pointer-events, stacking contexts). The spatial index only runs as a tier-2 fallback, replacing the expensive elementsFromPoint O(n) scan with an O(log n) R-tree query
  • Moves isDecorativeOverlay into isValidGrabbableElement so empty positioned divs are filtered universally, not just by the index
  • 57 new e2e tests covering overflow clipping, CSS containment, stacking order, inline elements, decorative overlays, fixed position, transforms, dynamic DOM changes, and fixed-only pages

Detection pipeline

cache hit? → elementFromPoint (browser-native) → prehit index (R-tree) → elementsFromPoint (last resort)

Test plan

  • 622 tests pass (0 failures)
  • Build, typecheck, lint clean
  • No behavioral change when prehit agrees with browser (majority of cases)
  • Prehit only activates when elementFromPoint returns overlay/non-grabbable
  • Manual testing on nisarg.io, github.com, vercel.com

Note

Medium Risk
Changes core element hit-testing logic by adding a new spatial index fallback and new overlay filtering, which could affect what element is selected in edge CSS/stacking cases despite keeping elementFromPoint as the primary path.

Overview
Adds a new activation-scoped tier-2 hit-testing path: when elementFromPoint returns a non-grabbable element, getElementAtPosition now queries a prebuilt element-at-point spatial index (Hilbert R-tree) before falling back to elementsFromPoint.

The index is built on activation and destroyed on deactivation/cleanup, accounts for overflow/contain clipping, separately tracks position: fixed elements, and resolves overlaps via a new compareStackingOrder helper. Empty positioned “decorative overlay” elements are now filtered universally via isValidGrabbableElement.

Expands the e2e app with new sections (overflow clipping, CSS containment, stacking order, overlays, inline elements, transform/opacity) and adds a large new Playwright suite (element-detection.spec.ts) covering these and additional dynamic/edge scenarios.

Reviewed by Cursor Bugbot for commit 37c17a0. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds a pre-indexed spatial hit-testing fallback in react-grab to speed up element detection when elementFromPoint hits a non‑grabbable overlay, cutting worst‑case scans from O(n) to O(log n) without changing normal behavior. The index builds on activation and is torn down on deactivation and cleanup.

  • New Features
    • Builds a Hilbert R‑tree via IntersectionObserver (10,000px root margin), caches position: fixed elements with viewport rects/z-index, and ranks candidates with compareStackingOrder (z-index, transforms, opacity, containment, clip-path) plus clip‑chain visibility checks.
    • Detection order: elementFromPoint → spatial index (only if top hit isn’t grabbable and index is ready) → elementsFromPoint; index rebuilds each activation and is destroyed on deactivation and cleanup.
    • Filters empty decorative overlays in isValidGrabbableElement across all paths; adds 57 E2E tests covering overflow/containment clipping, stacking, inline elements, overlays, transforms, fixed‑only pages, modals, zero‑size/invisible elements, animations, dynamic DOM, and reactivation rebuilds.

Written for commit 37c17a0. Summary will update on new commits.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gym Ready Ready Preview, Comment Apr 11, 2026 4:24am
react-grab-website Ready Ready Preview, Comment Apr 11, 2026 4:24am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@292
npm i https://pkg.pr.new/aidenybai/react-grab/grab@292
npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/mcp@292
npm i https://pkg.pr.new/aidenybai/react-grab@292

commit: 37c17a0

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 14 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/utils/get-element-at-position.ts">

<violation number="1" location="packages/react-grab/src/utils/get-element-at-position.ts:77">
P1: The new spatial-index fallback can return stale hits because the index is never rebuilt after viewport/layout changes, so this branch may bypass `elementsFromPoint()` with outdated geometry.</violation>
</file>

<file name="packages/react-grab/src/utils/is-valid-grabbable-element.ts">

<violation number="1" location="packages/react-grab/src/utils/is-valid-grabbable-element.ts:94">
P1: This overlay heuristic now rejects legitimate empty absolute/sticky controls that `elementFromPoint()` correctly hit, so selection can fall through to the wrong element underneath.</violation>
</file>

<file name="packages/react-grab/src/utils/element-at-point-index.ts">

<violation number="1" location="packages/react-grab/src/utils/element-at-point-index.ts:59">
P2: Spatial index geometry is snapshotted once and never invalidated, so hits can become stale after layout or DOM changes.</violation>

<violation number="2" location="packages/react-grab/src/utils/element-at-point-index.ts:221">
P1: Recheck the candidate's current bounds before accepting a cached tree hit. Otherwise layout changes can return an element from its old location and skip `elementsFromPoint`.</violation>

<violation number="3" location="packages/react-grab/src/utils/element-at-point-index.ts:227">
P1: Don't return the fixed-element shortcut before comparing overlapping fixed candidates by full stacking order.</violation>
</file>

<file name="packages/react-grab/src/utils/is-decorative-overlay.ts">

<violation number="1" location="packages/react-grab/src/utils/is-decorative-overlay.ts:15">
P2: Include `position: fixed` here; otherwise small empty fixed overlays can still block fallback selection and get treated as grabbable.</violation>

<violation number="2" location="packages/react-grab/src/utils/is-decorative-overlay.ts:16">
P2: Raw `tagName` comparison can miss SVG/non-HTML elements and misclassify them as decorative overlays.</violation>
</file>

<file name="packages/react-grab/src/utils/compare-stacking-order.ts">

<violation number="1" location="packages/react-grab/src/utils/compare-stacking-order.ts:87">
P2: Tie-breaking equal `z-index` branches by DOM order misorders positioned `z-index:auto` elements.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

result = topElement;
} else {
} else if (isElementAtPointIndexReady()) {
const spatialResult = queryElementAtPointIndex(clientX, clientY);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new spatial-index fallback can return stale hits because the index is never rebuilt after viewport/layout changes, so this branch may bypass elementsFromPoint() with outdated geometry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/utils/get-element-at-position.ts, line 77:

<comment>The new spatial-index fallback can return stale hits because the index is never rebuilt after viewport/layout changes, so this branch may bypass `elementsFromPoint()` with outdated geometry.</comment>

<file context>
@@ -64,30 +65,22 @@ export const getElementAtPosition = (clientX: number, clientY: number): Element
     result = topElement;
-  } else {
+  } else if (isElementAtPointIndexReady()) {
+    const spatialResult = queryElementAtPointIndex(clientX, clientY);
+    if (spatialResult && isValidGrabbableElement(spatialResult)) {
+      result = spatialResult;
</file context>
Fix with Cubic

Comment thread packages/react-grab/src/utils/is-valid-grabbable-element.ts
Comment thread packages/react-grab/src/utils/element-at-point-index.ts
Comment thread packages/react-grab/src/utils/element-at-point-index.ts
Comment thread packages/react-grab/src/utils/is-decorative-overlay.ts Outdated
Comment thread packages/react-grab/src/utils/is-decorative-overlay.ts
@@ -0,0 +1,252 @@
import { HilbertRTree } from "./hilbert-r-tree.js";
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Spatial index geometry is snapshotted once and never invalidated, so hits can become stale after layout or DOM changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/utils/element-at-point-index.ts, line 59:

<comment>Spatial index geometry is snapshotted once and never invalidated, so hits can become stale after layout or DOM changes.</comment>

<file context>
@@ -0,0 +1,252 @@
+        observer.unobserve(entry.target);
+
+        const targetElement = entry.target as HTMLElement;
+        const boundingRect = entry.boundingClientRect;
+        if (boundingRect.width === 0 || boundingRect.height === 0) continue;
+        if (!isValidGrabbableElement(targetElement)) continue;
</file context>
Fix with Cubic

Comment thread packages/react-grab/src/utils/compare-stacking-order.ts
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eb6950a. Configure here.

Comment thread packages/react-grab/src/utils/is-decorative-overlay.ts Outdated
if (PAINT_CONTAIN_VALUES.has(keyword)) return true;
}
return false;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate hasPaintContainment in two new files

Low Severity

hasPaintContainment is independently implemented in both compare-stacking-order.ts and element-at-point-index.ts with different logic. One uses String.includes("paint") while the other splits by space and checks a Set. They produce the same results for valid CSS contain values but diverge on edge cases, creating a maintenance risk where a fix to one copy could be missed in the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eb6950a. Configure here.

Comment thread packages/react-grab/src/utils/is-decorative-overlay.ts
Comment thread packages/react-grab/src/utils/compare-stacking-order.ts
Comment thread packages/react-grab/src/utils/is-valid-grabbable-element.ts
Comment thread packages/react-grab/src/utils/is-decorative-overlay.ts Outdated
if (fixedHit) {
if (visibleCandidates.length === 0) return fixedHit;
visibleCandidates.push(fixedHit);
}
Copy link
Copy Markdown
Contributor

@vercel vercel bot Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple fixed elements at the same point are not compared using proper CSS stacking order rules, causing incorrect element selection

Fix on Vercel

const candidate = currentIndex.elements[hitIndex];
if (!candidate.isConnected) continue;
if (!isVisibleAtPoint(candidate, clientX, clientY)) continue;
visibleCandidates.push(candidate);
Copy link
Copy Markdown
Contributor

@vercel vercel bot Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spatial index returns candidates with stale bounding box coordinates, causing incorrect element hits when element bounds change after index creation

Fix on Vercel

Add a pre-indexed element hit-testing system that replaces the expensive
elementsFromPoint scan when elementFromPoint returns a non-grabbable
element (decorative overlays, dev tools, etc.).

Safety model: browser-native elementFromPoint is always tried first
and trusted for all CSS edge cases. The spatial index only runs as a
fallback, replacing the O(n) elementsFromPoint call with an O(log n)
R-tree query + stacking order sort.

Infrastructure:
- HilbertRTree: packed R-tree with Hilbert curve ordering for spatial queries
- compareStackingOrder: CSS stacking context comparator (z-index, opacity,
  transform, contain, backdrop-filter, perspective, clip-path)
- isVisibleAtPoint: ancestor clip-chain walker with WeakMap cache
- isDecorativeOverlay: filters empty positioned elements (now also used
  in isValidGrabbableElement for all detection paths)
- Fixed elements cached with viewport rects and z-indexes at build time

Includes 57 new e2e tests covering overflow clipping, CSS containment,
stacking order, inline elements, decorative overlays, fixed position,
transforms, dynamic DOM, and fixed-only pages.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant