Skip to content

fix: keep grab controls out of the selection owner's subtree - #19

Merged
V3RON merged 1 commit into
mainfrom
fix/controls-outside-owner-subtree
Aug 31, 2026
Merged

fix: keep grab controls out of the selection owner's subtree#19
V3RON merged 1 commit into
mainfrom
fix/controls-outside-owner-subtree

Conversation

@V3RON

@V3RON V3RON commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Grabbing broke on iOS after #15: every grab resolved to the root, screen or surface itself instead of the touched element, with a full-screen highlight and the menu anchored off the bottom edge.

#15 moved the control bar from a sibling of the registered owner view into it:

<View {...panHandlers} ref={ownerRef}>
  {children}
  <ReactNativeGrabOverlay ... />
  {isResolvedSelectionOwner && <GrabOwnerControls />}
</View>

On iOS GrabOwnerControls is hosted by react-native-screens' FullWindowOverlay, which renders a native node sized to the whole window and accepts only style and accessibilityContainerViewIsModal - there is no way to give it pointerEvents. findNodeAtPoint walks the owner's shadow subtree in reverse child order and skips a node only when its pointerEvents makes it untargetable, which is what keeps ReactNativeGrabOverlay (box-none, with none children) out of the way. The controls have no such protection: the overlay node is last, full-screen and targetable, so it won every hit test, and with nothing targetable beneath it findNodeAtPoint returned the overlay itself - whose component stack reads GrabOwnerControls -> GrabSelectionOwnerView -> ReactNativeGrabRoot/Screen/Surface, hence the owner's name in the menu.

Android was unaffected: FullScreenOverlay is a Fragment there, so no extra node exists and the box-none anchor is skipped as intended.

The controls are now a sibling of the registered view rather than a child of it. They still mount inside the owner component, so #15's behaviour is preserved - a natively presented surface keeps its bar in its own window - but the subtree findNodeAtPoint walks no longer contains grab's own UI.

Testing

Diagnosis and mechanism verified on an iOS simulator (iPhone 17 Pro, example app): on main a tap on the title highlighted the entire screen; with FullWindowOverlay swapped for a Fragment in the built output - the only change - the same tap correctly selected Text (in ThemedText) with a tight highlight, which isolates the overlay node as the cause.

Not yet re-run on device against this branch's build, and not yet checked on the Android emulator (Android is unaffected by construction, but the sibling placement changes which container the control bar measures itself against, so it is worth a look).

The controls moved inside the owner view so they would render in the resolved
owner's window. On iOS that puts `FullWindowOverlay` - a full-screen native node
with no `pointerEvents` prop - into the subtree that `findNodeAtPoint` walks, and
since it skips a node only when `pointerEvents` makes it untargetable, the overlay
swallowed every hit test: each grab resolved to the owner itself, so nothing but
the root, screen or surface could be selected.

Render the controls as a sibling of the registered view instead. They still mount
inside the owner component, so a natively presented surface keeps its bar in its
own window, but the owner's shadow subtree stays clean.
@V3RON
V3RON merged commit d7c9ef5 into main Aug 31, 2026
4 checks passed
@V3RON
V3RON deleted the fix/controls-outside-owner-subtree branch August 31, 2026 15:39
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