Summary
On Android, grab selection resolves an element roughly one status-bar height below the finger. It affects every selection owner — ReactNativeGrabRoot included — so it is not specific to ReactNativeGrabScreen, native headers, or native navigators.
iOS is unaffected.
Cause
ReactNativeGrabOverlay feeds touch page coordinates to findNodeAtPoint, rebased onto the owner's origin as reported by nativeFabricUIManager.measureInWindow:
const ownerRect = measureInWindow(owner.shadowNode);
findNodeAtPoint(owner.shadowNode, pageX - ownerRect[0], pageY - ownerRect[1]);
On Android those two are in different coordinate spaces. Touch page coordinates are window-relative (they include the status bar), while measureInWindow reports positions relative to the root view, which sits below it. The difference is the top window inset, so the resolved point lands that much too low.
On iOS both spaces share an origin, so the rebase alone is correct.
Evidence
Instrumented on a Pixel 8 emulator (API 35, density 2.625), tapping the centre of a control at screen y=1057px (402.67dp):
screen owner: page.y = 402.67 measureInWindow.y = 56 (owner's real top in page space is 104.8)
surface owner: page.y = 96.38 measureInWindow.y = 0 (its own window - the two spaces agree)
StatusBar.currentHeight = 48.76
The selection landed on the next control down, 142px (48.8dp) away — exactly StatusBar.currentHeight.
Reproduced with the owner being ReactNativeGrabRoot alone, with no ReactNativeGrabScreen and no header in the tree, which rules out owner nesting as the cause.
Content inside a natively presented surface is correct today, because a sheet or dialog gets its own window whose root starts at the window top, so the two spaces coincide there.
What does not work
- A global
Platform.OS === "android" ? StatusBar.currentHeight : 0 correction. It fixes owners in the main window and breaks surfaces by the same amount, because the offset is a property of the window an owner lives in, not of the platform.
nativeEvent.locationX/locationY. They are relative to the touched child, not to the responder: the run above reported location.y = 14.86 for the same touch.
Suggested direction
Measure the owner in the same space the touch arrives in — the View ref's own measureInWindow, which reports true window coordinates on both platforms — and keep the Fabric measureInWindow for the highlight rect, which is already consistent with getBoundingClientRect. That means retaining the owner's ref alongside its shadow node in containers.ts.
Notes
Pre-existing, and unrelated to #11. It predates selection owners entirely; it was simply never visible on iOS, where the spaces agree.
Summary
On Android, grab selection resolves an element roughly one status-bar height below the finger. It affects every selection owner —
ReactNativeGrabRootincluded — so it is not specific toReactNativeGrabScreen, native headers, or native navigators.iOS is unaffected.
Cause
ReactNativeGrabOverlayfeeds touch page coordinates tofindNodeAtPoint, rebased onto the owner's origin as reported bynativeFabricUIManager.measureInWindow:On Android those two are in different coordinate spaces. Touch page coordinates are window-relative (they include the status bar), while
measureInWindowreports positions relative to the root view, which sits below it. The difference is the top window inset, so the resolved point lands that much too low.On iOS both spaces share an origin, so the rebase alone is correct.
Evidence
Instrumented on a Pixel 8 emulator (API 35, density 2.625), tapping the centre of a control at screen y=1057px (402.67dp):
The selection landed on the next control down, 142px (48.8dp) away — exactly
StatusBar.currentHeight.Reproduced with the owner being
ReactNativeGrabRootalone, with noReactNativeGrabScreenand no header in the tree, which rules out owner nesting as the cause.Content inside a natively presented surface is correct today, because a sheet or dialog gets its own window whose root starts at the window top, so the two spaces coincide there.
What does not work
Platform.OS === "android" ? StatusBar.currentHeight : 0correction. It fixes owners in the main window and breaks surfaces by the same amount, because the offset is a property of the window an owner lives in, not of the platform.nativeEvent.locationX/locationY. They are relative to the touched child, not to the responder: the run above reportedlocation.y = 14.86for the same touch.Suggested direction
Measure the owner in the same space the touch arrives in — the
Viewref's ownmeasureInWindow, which reports true window coordinates on both platforms — and keep the FabricmeasureInWindowfor the highlight rect, which is already consistent withgetBoundingClientRect. That means retaining the owner's ref alongside its shadow node incontainers.ts.Notes
Pre-existing, and unrelated to #11. It predates selection owners entirely; it was simply never visible on iOS, where the spaces agree.