Repository navigation
[iOS] Tap that dismisses a native UIMenu also fires onPress on the Pressable underneath #58983
Description
Description
On iOS 26 and 27, a tap that dismisses a native UIMenu also activates the Pressable under the tap. UIKit menus presented from a UIBarButtonItem (and other menu sources) are passthrough: a tap outside the menu dismisses it and is not delivered to native views underneath. In a React Native app the same tap reaches JS as a complete press, so onPress fires on whatever card or button sits under the finger.
Native apps do not behave this way. In Photos and Reminders on the same simulator, a tap outside an open navigation-bar menu only closes the menu (2 of 2 tries each). In our app the card under the tap opened 3 of 3 times.
Cause
When the menu is open, UIKit adds _UIPassthroughGateGestureRecognizer to the touch. It has delaysTouchesBegan = YES, so UIKit holds the touch back from views, and it never fails while the tap dismisses the menu. Logged from touchesEnded: in RCTSurfaceTouchHandler, the recognizers on the touch were:
with the menu open
_UISystemGestureGateGestureRecognizer state=0 external=1 cancels=1 delaysBegan=0
_UIPassthroughScrollGestureRecognizer state=3 external=1 cancels=0 delaysBegan=0
_UIPassthroughGateGestureRecognizer state=0 external=1 cancels=1 delaysBegan=1
RCTSurfaceTouchHandler state=1 ...
ordinary tap, no menu
_UISystemGestureGateGestureRecognizer state=0 external=1 cancels=1 delaysBegan=0
RCTSurfaceTouchHandler state=1 ...
RCTSurfaceTouchHandler is itself a gesture recognizer, so delaysTouchesBegan on another recognizer does not hold back its touchesBegan:/touchesEnded:. It dispatches touchStart and touchEnd to JS unconditionally. The existing cancelsTouchesInView handling (#54755) does not apply, because the gate recognizer never recognizes; it only delays. shouldRequireFailureOfGestureRecognizer: defers the handler's own state but not the JS events.
Proposed fix
In touchesEnded:, dispatch touchCancel instead of touchEnd for a touch that an external recognizer (one canBePreventedByGestureRecognizer: already treats as external) is still holding back: delaysTouchesBegan == YES and not failed.
- (BOOL)_isTouchHeldBackByExternalGestureRecognizer:(UITouch *)touch
{
for (UIGestureRecognizer *recognizer in touch.gestureRecognizers) {
if (recognizer == self || !recognizer.delaysTouchesBegan ||
recognizer.state == UIGestureRecognizerStateFailed) {
continue;
}
if ([self canBePreventedByGestureRecognizer:recognizer]) {
return YES;
}
}
return NO;
}We run this as a patch on 0.85.3. With it, the dismissing tap no longer opens the card, and ordinary taps, scrolling, and taps in the native tab bar and header are unchanged. Happy to open a PR.
Steps to reproduce
- iOS 26 or 27, New Architecture.
- Show a native menu from a navigation bar button, for example
@react-navigation/native-stackunstable_headerRightItemswithtype: "menu"(react-native-screens 4.26), over a screen with aPressablefilling the content. - Open the menu.
- Tap the
Pressableoutside the menu.
Expected: the menu closes and nothing else happens, as in native apps.
Actual: the menu closes and the Pressable's onPress fires.
Minimal screen:
// native-stack screen options
options={{
unstable_headerRightItems: () => [
{ type: "menu", label: "Menu", menu: { items: [{ type: "action", label: "One", onPress: () => {} }] } },
],
}}
// screen content
<Pressable style={{ flex: 1 }} onPress={() => Alert.alert("pressed")} />React Native Version
0.85.3 (the touch handler on main has the same logic)
Affected Platforms
Runtime - iOS
Output of npx @react-native-community/cli info
System: macOS 27.0.1, Xcode 27.0 (27A266a)
react-native: 0.85.3, expo 56.0.21, react-native-screens 4.26.2
newArchEnabled: true, hermes: true
Stacktrace or Logs
No crash. Recognizer log above.
MANDATORY Reproducer
The minimal screen above in a fresh Expo SDK 56 / RN 0.85 app with @react-navigation/native-stack. A standalone repository can follow if needed.
Screenshots and Videos
No response
Activity
react-native-bot commentedon Oct 11, 2026
Warning
Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:
You can read more about about it on our website: How to report a bug.
anderskraken commentedon Oct 11, 2026
Reproducer: Expo Snack
Expected: the menu closes and the
onPress countstays the same. Actual: the menu closes and the count goes up. In Expo Go 55.0.34 on an iOS 27.0 simulator, the count went up on 3 of 3 tries.The Snack uses SDK 55 (React Native 0.83). On SDK 56, Expo Go 56.0.4 crashes before running any Snack code, even an empty one, with
Tried to register two views with the same name RNSScreen. ThetouchesEnded:path inRCTSurfaceTouchHandleris the same in 0.83, 0.85.3 andmain.If Expo Go's floating tools button covers Menu, drag it aside first.