Commit 35afdc7
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
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| |||
0 commit comments