Skip to content

Commit 35afdc7

Browse files
authored
Don't block JS responder in RootHelper (#4297)
## Description Fixes #4295 On Android, 3.x cancels the React Native JS responder whenever any native view calls `requestDisallowInterceptTouchEvent` during a touch. Two common scenarios break because of this (neither involves any RNGH gesture): - **RN `Pressable`/`Touchable*` wrapping a `TextInput`** — `ReactEditText` unconditionally calls `parent.requestDisallowInterceptTouchEvent(true)` on every `ACTION_DOWN`, even when `editable={false}` and `pointerEvents="none"` (`pointerEvents` isn't enforced in native Android dispatch for `ReactEditText`, only in RN's JS hit test). Taps landing on the input dispatch `topTouchCancel` and the press never completes (#4295 — this is what breaks Gluestack UI's Select). - **Any default-config `PanResponder`** — on grant, RN's `JSResponderHandler.setJSResponder` blocks native parents via `requestDisallowInterceptTouchEvent(true)` (`onShouldBlockNativeResponder` defaults to `true` on Android). That call bounces back through the RNGH root view, which then cancels the responder that was just granted: every drag logs `onPanResponderGrant` → `onPanResponderTerminate`. **Root cause:** #4094 moved the JS-responder cancellation from `RootViewGestureHandler.onCancel` into `GestureHandlerOrchestrator.makeActive`, gated on the new `cancelsJSResponder` flag — which defaults to `true` for every handler, **including the internal `RootViewGestureHandler`**. That handler activates inside `tryCancelAllHandlers()` (the `requestDisallowInterceptTouchEvent` path), so a mechanism that previously only cancelled RNGH's own handlers now also kills the JS responder. The fix opts the internal root handler out of JS-responder cancellation. This restores: - **v2 semantics** — root-handler activation cancelled other RNGH handlers but never dispatched a JS-responder cancel; handlers activating still cancel it through their own flag (incidentally, this also restores the behavior of v2's removed `handleSetJSResponder`: a JS responder taking over with `blockNativeResponder` cancels RNGH handlers, not itself), - **stock RN semantics** — `ReactSurfaceView.requestDisallowInterceptTouchEvent` deliberately keeps the JS touch stream alive; RN core only cancels the responder via explicit `onChildStartedNativeGesture` calls (e.g. `ScrollView`, `SwipeRefreshLayout` — those paths are unaffected by this change), - **iOS parity** — `RNRootViewGestureRecognizer` only notifies the delegate when the *preventing* recognizer maps to an RNGH handler with `cancelsJSResponder` set; the root recognizer itself never triggers the cancellation, so iOS never had this bug. The flag cannot be reverted accidentally: `resetConfig()` only runs through `Factory.setConfig` (the JS config path), and the root handler is constructed directly with a negative tag and never configured from JS. The "block release" timing is keyed on `gestureHandlers.isEmpty()`, not on the flag, so it is also unaffected. ## Test plan <details><summary>Repro 1 — #4295 (TextInput inside RN Pressable)</summary> <p> ```jsx <GestureHandlerRootView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Pressable onPress={() => setCount((c) => c + 1)} style={{ borderWidth: 1, padding: 10 }}> <TextInput value="Tap me" editable={false} pointerEvents="none" /> <Text>Icon area (works)</Text> </Pressable> <Text>Press count: {count}</Text> </GestureHandlerRootView> ``` </p> </details> <details><summary>Repro 2 — default PanResponder terminated on grant</summary> <p> ```jsx const responder = PanResponder.create({ // default onShouldBlockNativeResponder (true on Android) — do not override onStartShouldSetPanResponder: () => true, onPanResponderGrant: () => log('grant'), onPanResponderRelease: () => log('release'), onPanResponderTerminate: () => log('TERMINATE'), }); <GestureHandlerRootView style={{ flex: 1 }}> <View {...responder.panHandlers} style={{ width: 250, height: 180 }} /> </GestureHandlerRootView> ``` </p> </details> Verified on Android emulator (Pixel 7 Pro, API 34) with `basic-example` (RN 0.86, Fabric): - Repro 1 — before: taps on the `TextInput` area never fire `onPress` (count stays 0); taps on the sibling `Text` work. After: all taps fire (3/3 on the input area, sibling still works). - Repro 2 — before: every drag logs `grant` → `TERMINATE`. After: `grant` → `release`. - iOS (iPhone 17 Pro simulator, same RN version): Repro 1 works correctly with and without this change — confirms iOS is unaffected and this stays Android-only. - `RN responder cancellation` test example (added in #4094), Single handler tab, both toggle states — behavior unchanged by this PR: - `cancelsJSResponder: true`: `onResponderGrant` → moves → `onResponderTerminate` → `GH pan ACTIVE` → finalize (success), - `cancelsJSResponder: false`: responder keeps receiving moves, ends with `onResponderRelease`, pan still activates and finalizes with success.
1 parent 12cb193 commit 35afdc7

1 file changed

Lines changed: 1 addition & 0 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
8383

8484
init {
8585
this.tag = handlerTag
86+
this.cancelsJSResponder = false
8687
}
8788

8889
private fun handleEvent(event: MotionEvent) {

0 commit comments

Comments
 (0)