Skip to content

Add cancelsJSResponder prop to the gesture handlers - #4094

Merged
coado merged 34 commits into
mainfrom
prevent-recognizers
May 7, 2026
Merged

Add cancelsJSResponder prop to the gesture handlers#4094
coado merged 34 commits into
mainfrom
prevent-recognizers

Conversation

@coado

@coado coado commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a per-gesture cancelsJSResponder?: boolean prop on both platforms to the v3 handler hooks. It defaults to true to preserve the behavior of cancelling RN JS responder system.

On Android it moves the root-view dispatch off onCancel onto new orchestrator callbacks onCancelJSResponderRequested and onCancelJSResponderReleased. The orchestrator only files the "released" callback when the last active handler with cancelsJSResponder: true finishes.

on iOS the RNRootViewGestureRecognizer checks the handler prop before notifying the delegate.

Test plan

Added new test example with multiple handlers that might or might not cancel the JS responder.

Simulator Screenshot - iPhone 16 Pro Max - 2026-04-17 at 12 59 42

Copilot AI review requested due to automatic review settings April 16, 2026 09:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new preventRecognizers configuration option to gesture handlers to control whether GH activation cancels React Native JS responders (with native iOS/Android plumbing + an example to demonstrate behavior).

Changes:

  • Added preventRecognizers to shared gesture config types/whitelists so it can flow from JS to native.
  • Implemented native behavior gates on iOS (root recognizer) and Android (orchestrator → root helper interception) with default true.
  • Added docs + a new common-app example to showcase responder cancellation differences.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts Adds preventRecognizers to common gesture config typing.
packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts Allows the new config prop to pass through native config filtering.
packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx Minor formatting-only change near detector initialization.
packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts Adds preventRecognizers to allowed prop list for detector plumbing.
packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m iOS: only trigger RN responder cancellation when preventRecognizers is enabled.
packages/react-native-gesture-handler/apple/RNGestureHandler.mm iOS: default + config parsing for preventRecognizers.
packages/react-native-gesture-handler/apple/RNGestureHandler.h iOS: exposes preventRecognizers on the handler.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt Android: intercept/cancel RN touch stream when requested by orchestrator.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt Android: emits request/release callbacks based on handler activation + finish.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt Android: stores/reads preventRecognizers from config with default true.
packages/docs-gesture-handler/docs/fundamentals/root-view.mdx Doc formatting cleanup (indentation/trailing whitespace).
packages/docs-gesture-handler/docs/fundamentals/gesture-detector.mdx Adds docs for preventRecognizers (but currently includes a broken tag and placement concerns).
apps/common-app/src/new_api/tests/rnResponderCancellation/index.tsx New example screen demonstrating RN responder cancellation vs. concurrent handling.
apps/common-app/src/new_api/index.tsx Registers the new example in the app’s examples list.
Comments suppressed due to low confidence (1)

packages/docs-gesture-handler/docs/fundamentals/gesture-detector.mdx:206

  • The closing tag for HeaderWithBadges was changed to </BadHeaderWithBadgesges>, which is not a valid component/tag and will break MDX rendering/build. Replace it with the correct </HeaderWithBadges> closing tag.
<HeaderWithBadges platforms={['web']}>
### touchAction
</HeaderWithBadges>


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m Outdated
Comment thread packages/docs-gesture-handler/docs/fundamentals/gesture-detector.mdx Outdated
@coado

coado commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author

@copilot review?

Copilot AI commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@copilot review?

Here's the review of the current state of the PR:

Overall: The implementation is solid. All 3 previously flagged issues are resolved. Here's a brief summary per area:

Android:

  • onCancelJSResponderRequested is correctly placed after the FAILED/CANCELLED early-return guard in makeActive — no stale shouldIntercept=true risk for already-finished handlers.
  • onCancelJSResponderReleased checks that no other active handler still has cancelsJSResponder=true before clearing shouldIntercept. ✓

iOS:

  • cancelsJSResponder property defaults to YES in resetConfig, read from config in updateConfig. ✓
  • canBePreventedByGestureRecognizer: gates the delegate call on handler.cancelsJSResponder. ✓

TypeScript:

  • Prop flows correctly: CommonGestureConfig (v3 types) → CommonConfig whitelist (v3) → ALLOWED_PROPS (v2 utils). ✓

Docs: cancelsJSResponder is documented in base-gesture-config.mdx (not under GestureDetector props). ✓

No issues found in the current state.

@coado coado changed the title [DRAFT] Add preventRecognizers prop to the gesture handlers Add preventRecognizers prop to the gesture handlers Apr 23, 2026
@coado
coado marked this pull request as ready for review April 23, 2026 14:10

@j-piasecki j-piasecki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the title and description with the final prop name?

Also, I don't have an iOS device on hand, so can you check the following scenario?

There are two handlers and js recognizer on the same view. Gesture A with cancelsJSRecognizer: false, and a config that prevents it from activating (like minimumDistance: 100000 on pan). Gesture B has cancelsJSRecognizer: false and requires A to fail before it activates (useExclusiveGestures(A, B)), but should try to activate almost immediately (like pan with minimumDistance: 10). Now, in that scenario, does B trying to activate but getting blocked on A, cancel the JS recognizer, or not?

I've checked Android and there it works as expected (until A fails, the JS recognizer is not interrupted).

Comment thread packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx Outdated
Comment thread packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx Outdated
Comment thread packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m Outdated
@coado coado changed the title Add preventRecognizers prop to the gesture handlers Add cancelsJSResponder prop to the gesture handlers Apr 24, 2026
coado and others added 5 commits April 24, 2026 11:40

@m-bert m-bert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like @j-piasecki caught everything

Comment thread apps/common-app/src/new_api/tests/rnResponderCancellation/index.tsx Outdated
Comment thread packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx Outdated
Comment thread packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx Outdated
Comment thread packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx Outdated
@coado
coado force-pushed the prevent-recognizers branch from 3a3d592 to 9f24c36 Compare April 24, 2026 14:30
@coado

coado commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Can you update the title and description with the final prop name?

Also, I don't have an iOS device on hand, so can you check the following scenario?

There are two handlers and js recognizer on the same view. Gesture A with cancelsJSRecognizer: false, and a config that prevents it from activating (like minimumDistance: 100000 on pan). Gesture B has cancelsJSRecognizer: false and requires A to fail before it activates (useExclusiveGestures(A, B)), but should try to activate almost immediately (like pan with minimumDistance: 10). Now, in that scenario, does B trying to activate but getting blocked on A, cancel the JS recognizer, or not?

I've checked Android and there it works as expected (until A fails, the JS recognizer is not interrupted).

Not sure if you meant cancelsJSRecognizer: true on B, but in both cases seems like it works as expected.

@coado
coado merged commit 5505329 into main May 7, 2026
12 checks passed
@coado
coado deleted the prevent-recognizers branch May 7, 2026 13:05
Copilot AI added a commit that referenced this pull request May 27, 2026
…t incorrect JS responder cancellation

PR #4094 moved the ACTION_CANCEL dispatch from RootViewGestureHandler.onCancel()
into the orchestrator's makeActive() method, gated by handler.cancelsJSResponder.
However, when requestDisallowInterceptTouchEvent triggers tryCancelAllHandlers(),
the RootViewGestureHandler itself activates through makeActive(), and since
cancelsJSResponder defaults to true, it incorrectly dispatches ACTION_CANCEL
to the root view — cancelling the very native view (e.g. ScrollView) that
requested the touch lock.

Setting cancelsJSResponder=false on the RootViewGestureHandler prevents this
spurious cancellation while preserving the correct behavior for user gesture
handlers.
m-bert added a commit that referenced this pull request Jul 6, 2026
## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants