Skip to content

fix: support native-presented grab surfaces - #11

Merged
V3RON merged 5 commits into
callstackincubator:mainfrom
huytdps13400:fix/native-presented-surfaces
Aug 31, 2026
Merged

fix: support native-presented grab surfaces#11
V3RON merged 5 commits into
callstackincubator:mainfrom
huytdps13400:fix/native-presented-surfaces

Conversation

@huytdps13400

@huytdps13400 huytdps13400 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add ReactNativeGrabSurface for React content hosted in separately presented native sheets and modals
  • resolve selection owners in active-surface → focused-screen → root order, including stacked-surface restoration
  • preserve Root/Screen full-size layout while keeping content-measured surfaces compatible with auto detents
  • add regression coverage, public/web/production exports, usage docs, and a patch changeset

Closes #10.

Why

findNodeAtPoint searches from the resolved owner shadow node. Content presented by libraries such as TrueSheet can live outside the focused screen native subtree, so Grab cannot highlight or map it back to its React Fiber.

ReactNativeGrabSurface active={isPresented} explicitly registers that native subtree only while it is presented. This avoids private global Fabric enumeration and prevents a dismissed sheet from stealing selection.

Verification

  • npx vitest run src/react-native/__tests__/selection-owners.test.ts — passed
  • npm test — 13/13 passed
  • npm run format:check — passed on the submitted commit
  • git diff --check — passed
  • npm run lint — passed with the optional expo-dev-menu declaration available
  • npm run build — ESM and CJS builds passed with the optional declaration available
  • iOS native runtime — passed in the repository Expo example with Fabric, @lodev09/react-native-true-sheet 3.10.1, and this PR branch: the active surface was selected, dragging highlighted the TrueSheet target, the View / Copy / Open menu appeared, and Copy resolved the host element back to the example source file and line
  • dismiss lifecycle — passed; the native sheet closed normally after selection and the surface deactivated through onDidDismiss

On a clean npm 11 install, lint/build otherwise stop at the existing type-only expo-dev-menu optional-peer resolution in dev-menu.ts; no package metadata was changed in this PR.

@huytdps13400

Copy link
Copy Markdown
Contributor Author

Hey @V3RON ! When you have time, could you please review this solution?

My project is currently blocked on supporting content hosted in native-presented sheets/modals (like TrueSheet), so I’d really appreciate your feedback on this approach and whether it looks correct / idiomatic for react-native-grab.

@huytdps13400

Copy link
Copy Markdown
Contributor Author

Native TrueSheet verification is now complete on iOS Simulator using the repository Expo/Fabric example and @lodev09/react-native-true-sheet 3.10.1.

Observed end to end:

  • ReactNativeGrabSurface registered and became the resolved active owner while the sheet was presented.
  • Dragging the target returned a Fabric node at every sampled point and highlighted the exact target bounds.
  • The View / Copy / Open menu rendered inside the native-presented surface.
  • Copy produced <RCTView accessibilityLabel="TrueSheet grab target" ... /> with in View (at .../src/app/(tabs)/index.tsx:82:17), proving component/source resolution crossed the native-surface boundary.
  • Dismissing the sheet completed normally.

The submitted SHA remains 9ae05485e3e442246968be5de07980225b7a3dd1; no code changes were needed after the runtime check.

V3RON added 4 commits August 31, 2026 12:14
Adds a sheet playground screen exercising ReactNativeGrabSurface against a
real native-presented sheet, which is the case ReactNativeGrabSurface exists
for and which the existing modal route does not cover.

Two sheets so surface ordering is testable:
- an `auto` detent sheet, where the surface must stay content-measured
- a fixed-detent sheet stacked on top, where the surface must fill instead

Grab targets render `Text` directly rather than through `ThemedText` so the
selection menu title names the surface the element came from
(`Text (in AutoSheetTarget)`), making it possible to tell which owner
resolved the selection.

Each sheet drives `active` from onDidPresent/onDidDismiss rather than from
the press handlers, so drag-to-dismiss also deactivates the surface.

Claude-Session: https://claude.ai/code/session_01KDiabfZtYE2vxCeybZnA4D
Review follow-ups on the ReactNativeGrabSurface work:

- Drop the `fill` prop from GrabSelectionOwnerView. Root and screen pass the
  fill style themselves, so a surface can now be made to fill its container
  with a plain `style` prop. Without this a surface wrapping full-screen
  content collapsed it in development and laid out correctly in production,
  where the component compiles away to a passthrough.

- Make surface activation symmetric: deactivate on effect cleanup instead of
  relying on the owner being unregistered first.

- Report a failed owner registration instead of returning silently. A surface
  whose ref never attached would otherwise ignore every later activation and
  stay permanently unselectable with no signal.

- Render children from the web shims. `ReactNativeGrabRoot` and
  `ReactNativeGrabScreen` returned null, which drops the entire subtree on web;
  all three shims now preserve layout instead.

- Replace the per-resolve filter and sort of active surfaces with a single
  scan. Resolution runs inside useSyncExternalStore for every mounted overlay.

- Delete the unused selection owners store snapshot API.

Claude-Session: https://claude.ai/code/session_01KDiabfZtYE2vxCeybZnA4D
findNodeAtPoint resolves its point against the shadow node it is given, but
the overlay passed raw page coordinates. The two only coincide when the owner
sits at the window origin, which is the case for ReactNativeGrabRoot — so the
bug stayed invisible until an owner was offset.

Verified on an iOS simulator: on a screen under a native stack header,
tapping an element selected whatever sat one header-height below it. For a
natively presented surface the offset is the sheet's own origin, several
hundred points down, so ReactNativeGrabSurface could not have selected its
content at all.

The owner rect was already measured to make the highlight rect relative; it
now also rebases the incoming point.

Claude-Session: https://claude.ai/code/session_01KDiabfZtYE2vxCeybZnA4D
@V3RON
V3RON merged commit 2a10576 into callstackincubator:main Aug 31, 2026
1 check passed
@huytdps13400

Copy link
Copy Markdown
Contributor Author

@V3RON When you have new version. Let's tag me Please. Thanks

@V3RON

V3RON commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@huytdps13400 Already out and you're tagged in the release. Thanks for contributing!

@huytdps13400

Copy link
Copy Markdown
Contributor Author

Thank you! Great to see the fix released, and I appreciate the tag. Glad to contribute.

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.

Native-presented surfaces such as TrueSheet are not grabbable

2 participants