diff --git a/.changeset/tidy-hoops-fold.md b/.changeset/tidy-hoops-fold.md new file mode 100644 index 0000000..4bac3b1 --- /dev/null +++ b/.changeset/tidy-hoops-fold.md @@ -0,0 +1,5 @@ +--- +"react-native-grab": patch +--- + +Keep the grab controls out of the selection owner's shadow subtree, so elements can be grabbed again on iOS. The controls moved inside the owner view when they started rendering in the resolved owner's window, and on iOS they are hosted by `FullWindowOverlay` - a full-screen native node that takes no `pointerEvents` prop. `findNodeAtPoint` walks the owner's subtree and skips a node only when its `pointerEvents` makes it untargetable, so that overlay swallowed every hit test and resolved each grab to the owner itself. The controls are now a sibling of the registered view, which keeps them in the owner's window without shadowing its content. diff --git a/src/react-native/grab-controls.tsx b/src/react-native/grab-controls.tsx index 75c19c1..49fcf8f 100644 --- a/src/react-native/grab-controls.tsx +++ b/src/react-native/grab-controls.tsx @@ -134,9 +134,10 @@ export const GrabOwnerControls = () => { ); return ( - // The anchor is absolutely positioned so that mounting the controls inside an - // owner cannot disturb its layout: an owner is free to be a `gap`-spaced flex - // container, and absolutely positioned children are not flex items. + // The anchor is absolutely positioned so that mounting the controls alongside + // an owner cannot disturb the surrounding layout: that parent is free to be a + // `gap`-spaced flex container, and absolutely positioned children are not flex + // items. {/* Measured rather than the anchor, because `FullScreenOverlay` lifts this diff --git a/src/react-native/grab-selection-owner.tsx b/src/react-native/grab-selection-owner.tsx index 833f1b7..5f52113 100644 --- a/src/react-native/grab-selection-owner.tsx +++ b/src/react-native/grab-selection-owner.tsx @@ -53,10 +53,21 @@ export const GrabSelectionOwnerView = ({ const isResolvedSelectionOwner = useIsResolvedGrabSelectionOwner(ownerId); return ( - - {children} - + <> + + {children} + + + + {/* + A sibling of the registered view rather than a child of it: `findNodeAtPoint` + walks the owner's shadow subtree and skips a node only when its `pointerEvents` + makes it untargetable. The controls cannot rely on that - on iOS they are hosted + by `FullWindowOverlay`, a full-screen native node that takes no `pointerEvents` + prop - so a controls subtree inside the owner swallows every hit test and + resolves each grab to the owner itself. + */} {isResolvedSelectionOwner && } - + ); };