diff --git a/apps/common-app/src/new_api/index.tsx b/apps/common-app/src/new_api/index.tsx
index 9327721c0d..faa2905271 100644
--- a/apps/common-app/src/new_api/index.tsx
+++ b/apps/common-app/src/new_api/index.tsx
@@ -44,6 +44,7 @@ import ReattachingExample from './tests/reattaching';
import NestedRootViewExample from './tests/nestedRootView';
import NestedPressablesExample from './tests/nestedPressables';
import PressableExample from './tests/pressable';
+import RNResponderCancellationExample from './tests/rnResponderCancellation';
import { ExamplesSection } from '../common';
import EmptyExample from '../empty';
@@ -131,6 +132,10 @@ export const NEW_EXAMPLES: ExamplesSection[] = [
{ name: 'Modal with Nested Root View', component: NestedRootViewExample },
{ name: 'Nested pressables', component: NestedPressablesExample },
{ name: 'Pressable', component: PressableExample },
+ {
+ name: 'RN responder cancellation',
+ component: RNResponderCancellationExample,
+ },
],
},
];
diff --git a/apps/common-app/src/new_api/tests/rnResponderCancellation/index.tsx b/apps/common-app/src/new_api/tests/rnResponderCancellation/index.tsx
new file mode 100644
index 0000000000..d3a612c761
--- /dev/null
+++ b/apps/common-app/src/new_api/tests/rnResponderCancellation/index.tsx
@@ -0,0 +1,433 @@
+import React, { useCallback, useRef, useState } from 'react';
+import { ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
+import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
+import {
+ COLORS,
+ Feedback,
+ FeedbackHandle,
+ commonStyles,
+} from '../../../common';
+
+const SECTION_MIN_HEIGHT = 640;
+
+export default function RNResponderCancellationExample() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const scrollStyles = StyleSheet.create({
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ flexGrow: 1,
+ },
+ section: {
+ minHeight: SECTION_MIN_HEIGHT,
+ },
+ divider: {
+ height: 2,
+ marginHorizontal: 16,
+ marginVertical: 4,
+ backgroundColor: '#b6bfd0',
+ },
+});
+
+// ---------- Single handler --------------------------------------------------
+
+const SINGLE_MAX_EVENTS = 8;
+
+function SingleHandlerExample() {
+ const feedbackRef = useRef(null);
+ const sequenceRef = useRef(0);
+ const [events, setEvents] = useState([]);
+ const [cancelsJSResponder, setCancelsJSResponder] = useState(true);
+
+ const pushEvent = useCallback((label: string) => {
+ sequenceRef.current += 1;
+ const event = `${sequenceRef.current}. ${label}`;
+
+ console.log(event);
+ feedbackRef.current?.showMessage(label);
+ setEvents((prev) => [event, ...prev].slice(0, SINGLE_MAX_EVENTS));
+ }, []);
+
+ const panGesture = usePanGesture({
+ minDistance: 12,
+ runOnJS: true,
+ cancelsJSResponder,
+ onActivate: () => {
+ pushEvent('GH pan ACTIVE');
+ },
+ onFinalize: (_event, success) => {
+ pushEvent(`GH pan finalize (${success ? 'success' : 'cancel/fail'})`);
+ },
+ });
+
+ return (
+
+ RN responder cancellation
+
+ Toggle cancelsJSResponder and drag inside the box to compare behavior.
+
+
+ cancelsJSResponder
+
+
+
+
+ {
+ pushEvent('RN onStartShouldSetResponder -> true');
+ return true;
+ }}
+ onMoveShouldSetResponder={() => {
+ pushEvent('RN onMoveShouldSetResponder -> true');
+ return true;
+ }}
+ onResponderGrant={() => {
+ pushEvent('RN onResponderGrant');
+ }}
+ onResponderMove={() => {
+ pushEvent('RN onResponderMove');
+ }}
+ onResponderRelease={() => {
+ pushEvent('RN onResponderRelease');
+ }}
+ onResponderTerminate={() => {
+ pushEvent('RN onResponderTerminate');
+ }}
+ onResponderTerminationRequest={() => {
+ pushEvent('RN onResponderTerminationRequest -> true');
+ return true;
+ }}>
+ Drag me
+
+
+
+
+
+ {events.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ );
+}
+
+const singleStyles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 16,
+ paddingVertical: 24,
+ gap: 12,
+ alignItems: 'center',
+ backgroundColor: COLORS.offWhite,
+ },
+ touchArea: {
+ width: '100%',
+ maxWidth: 340,
+ minHeight: 220,
+ borderRadius: 20,
+ borderWidth: 2,
+ borderColor: COLORS.NAVY,
+ backgroundColor: '#d8ebff',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ touchAreaLabel: {
+ color: COLORS.NAVY,
+ fontWeight: '700',
+ fontSize: 18,
+ },
+ settingsRow: {
+ width: '100%',
+ maxWidth: 340,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ settingsLabel: {
+ color: COLORS.NAVY,
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ logContainer: {
+ width: '100%',
+ maxWidth: 380,
+ minHeight: 170,
+ borderRadius: 12,
+ padding: 12,
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#d5dbe6',
+ gap: 2,
+ },
+ logLine: {
+ fontSize: 13,
+ color: '#2c3a4f',
+ fontFamily: 'Courier',
+ },
+ logLineActive: {
+ color: '#1565c0',
+ fontWeight: 'bold',
+ },
+});
+
+// ---------- Multi handler ---------------------------------------------------
+// Validates that when two Gesture Handler recognizers are active at the same
+// time, both with cancelsJSResponder set to true, finishing ONE of them does
+// NOT unblock React Native JS responders — the block must stay in place until
+// the LAST cancelling recognizer finishes.
+//
+// Expected interaction to reproduce:
+// 1. Finger 1: drag inside "Pan A" → GH_A ACTIVE (RN blocked)
+// 2. Finger 2: drag inside "Pan B" → GH_B ACTIVE
+// 3. Finger 3: tap the "RN responder zone" → grant must NOT fire
+// 4. Release finger 1 → GH_A finalize
+// 5. Finger 3: tap the "RN responder zone" → grant must STILL NOT fire
+// (GH_B is still active)
+// 6. Release finger 2 → GH_B finalize (released)
+// 7. Finger 3: tap the "RN responder zone" → grant SHOULD now fire
+//
+// If step 5 logs "RN zone onResponderGrant" the invariant is broken.
+
+const MULTI_MAX_EVENTS = 14;
+
+function MultiHandlerExample() {
+ const feedbackRef = useRef(null);
+ const sequenceRef = useRef(0);
+ const [events, setEvents] = useState([]);
+ const [cancelsJSResponder, setCancelsJSResponder] = useState(true);
+
+ const pushEvent = useCallback((label: string) => {
+ sequenceRef.current += 1;
+ const event = `${sequenceRef.current}. ${label}`;
+
+ console.log(event);
+ feedbackRef.current?.showMessage(label);
+ setEvents((prev) => [event, ...prev].slice(0, MULTI_MAX_EVENTS));
+ }, []);
+
+ const panA = usePanGesture({
+ minDistance: 8,
+ runOnJS: true,
+ cancelsJSResponder,
+ onActivate: () => pushEvent('GH_A ACTIVE'),
+ onFinalize: (_e, success) =>
+ pushEvent(`GH_A finalize (${success ? 'success' : 'cancel/fail'})`),
+ });
+
+ const panB = usePanGesture({
+ minDistance: 8,
+ runOnJS: true,
+ cancelsJSResponder,
+ onActivate: () => pushEvent('GH_B ACTIVE'),
+ onFinalize: (_e, success) =>
+ pushEvent(`GH_B finalize (${success ? 'success' : 'cancel/fail'})`),
+ });
+
+ const clearLog = useCallback(() => {
+ sequenceRef.current = 0;
+ setEvents([]);
+ }, []);
+
+ return (
+
+ cancelsJSResponder — multi
+
+ Drag A and B with two fingers simultaneously, then tap the RN zone with
+ a third finger. Release one finger at a time and re-tap.
+
+
+
+ cancelsJSResponder
+
+
+ clear
+
+
+
+
+
+
+ Pan A
+
+
+
+
+ Pan B
+
+
+
+
+ {
+ pushEvent('RN zone onStartShouldSetResponder -> true');
+ return true;
+ }}
+ onResponderGrant={() => {
+ pushEvent(
+ 'RN zone onResponderGrant <-- NOT expected while GH active'
+ );
+ }}
+ onResponderRelease={() => pushEvent('RN zone onResponderRelease')}
+ onResponderTerminate={() =>
+ pushEvent('RN zone onResponderTerminate <-- cancelled by GH')
+ }
+ onResponderTerminationRequest={() => {
+ pushEvent('RN zone onResponderTerminationRequest -> true');
+ return true;
+ }}>
+ RN responder zone (tap me)
+
+
+
+
+
+
+ {events.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ );
+}
+
+const multiStyles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ gap: 10,
+ alignItems: 'center',
+ backgroundColor: COLORS.offWhite,
+ },
+ settingsRow: {
+ width: '100%',
+ maxWidth: 380,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ settingsLabel: {
+ color: COLORS.NAVY,
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ clearButton: {
+ color: COLORS.NAVY,
+ fontSize: 13,
+ fontWeight: '600',
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 6,
+ borderWidth: 1,
+ borderColor: COLORS.NAVY,
+ },
+ boxesRow: {
+ width: '100%',
+ maxWidth: 380,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ gap: 10,
+ },
+ panBox: {
+ flex: 1,
+ minHeight: 140,
+ borderRadius: 16,
+ borderWidth: 2,
+ borderColor: COLORS.NAVY,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ panBoxA: { backgroundColor: '#d8ebff' },
+ panBoxB: { backgroundColor: '#ffe0d8' },
+ panLabel: {
+ color: COLORS.NAVY,
+ fontWeight: '700',
+ fontSize: 16,
+ },
+ rnZone: {
+ width: '100%',
+ maxWidth: 380,
+ minHeight: 80,
+ borderRadius: 14,
+ borderWidth: 2,
+ borderStyle: 'dashed',
+ borderColor: '#7a4dff',
+ backgroundColor: '#ece2ff',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ rnZoneLabel: {
+ color: '#3a1f9c',
+ fontWeight: '700',
+ fontSize: 15,
+ },
+ feedbackSlot: {
+ width: '100%',
+ maxWidth: 420,
+ height: 84,
+ paddingHorizontal: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ overflow: 'hidden',
+ },
+ logContainer: {
+ width: '100%',
+ maxWidth: 420,
+ height: 260,
+ borderRadius: 12,
+ padding: 10,
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#d5dbe6',
+ gap: 2,
+ overflow: 'hidden',
+ },
+ logLine: {
+ fontSize: 12,
+ color: '#2c3a4f',
+ fontFamily: 'Courier',
+ },
+ logLineActive: { color: '#1565c0', fontWeight: 'bold' },
+ logLineBad: { color: '#b71c1c', fontWeight: 'bold' },
+ logLineCancel: { color: '#6a1b9a' },
+});
diff --git a/packages/docs-gesture-handler/docs/fundamentals/root-view.mdx b/packages/docs-gesture-handler/docs/fundamentals/root-view.mdx
index 362de2d34f..c82da96006 100644
--- a/packages/docs-gesture-handler/docs/fundamentals/root-view.mdx
+++ b/packages/docs-gesture-handler/docs/fundamentals/root-view.mdx
@@ -33,11 +33,11 @@ If you're using Gesture Handler in your component library, you may want to wrap
`GestureHandlerRootView` can be thought of as a regular `View` component, therefore it accepts all the same props, including [`style`](https://reactnative.dev/docs/0.81/view-style-props).
-If you don't provide anything to the `style` prop, it will default to `{ flex: 1 }`. If you want to customize the styling of the root view, don't forget to also include `flex: 1` in the custom style, otherwise your app won't render anything.
+If you don't provide anything to the `style` prop, it will default to `{ flex: 1 }`. If you want to customize the styling of the root view, don't forget to also include `flex: 1` in the custom style, otherwise your app won't render anything.
## Nesting root views
-In case of nested root views, Gesture Handler will only use the top-most one and ignore the nested ones. If you're unsure if one of your dependencies already renders `GestureHandlerRootView` on its own, don't worry and add one at the root anyway.
+In case of nested root views, Gesture Handler will only use the top-most one and ignore the nested ones. If you're unsure if one of your dependencies already renders `GestureHandlerRootView` on its own, don't worry and add one at the root anyway.
## unstable_forceActive
diff --git a/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx b/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx
index bcb03e4259..fd4fbabdad 100644
--- a/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx
+++ b/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.mdx
@@ -148,3 +148,18 @@ activeCursor: ActiveCursor | SharedValue;
```
This parameter allows specifying which cursor should be used when the gesture activates. Supports all [CSS cursor values](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/cursor#keyword) (e.g. `"grab"`, `"zoom-in"`). Default value is set to `"auto"`.
+
+
+ ### cancelsJSResponder
+
+
+```ts
+cancelsJSResponder?: boolean;
+```
+
+Controls whether activating a Gesture Handler recognizer should cancel React Native JS responders.
+
+This option is configured per gesture handler (for example in `usePanGesture({ cancelsJSResponder: ... })`).
+
+- `true` (default): when this gesture activates, it cancels React Native JS responders in the same root view.
+- `false`: disables that cancellation, so Gesture Handler callbacks and RN responder callbacks can run at the same time.
diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt
index 066a923ea0..526f09cbf5 100644
--- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt
+++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt
@@ -83,6 +83,7 @@ open class GestureHandler {
var needsPointerData = false
var dispatchesAnimatedEvents = false
var dispatchesReanimatedEvents = false
+ var cancelsJSResponder = true
private var hitSlop: FloatArray? = null
var eventCoalescingKey: Short = 0
@@ -137,6 +138,7 @@ open class GestureHandler {
mouseButton = DEFAULT_MOUSE_BUTTON
dispatchesAnimatedEvents = DEFAULT_DISPATCHES_ANIMATED_EVENTS
dispatchesReanimatedEvents = DEFAULT_DISPATCHES_REANIMATED_EVENTS
+ cancelsJSResponder = DEFAULT_CANCELS_JS_RESPONDER
}
fun hasCommonPointers(other: GestureHandler): Boolean {
@@ -961,6 +963,9 @@ open class GestureHandler {
if (config.hasKey(KEY_TEST_ID)) {
handler.testID = config.getString(KEY_TEST_ID)
}
+ if (config.hasKey(KEY_CANCELS_JS_RESPONDER)) {
+ handler.cancelsJSResponder = config.getBoolean(KEY_CANCELS_JS_RESPONDER)
+ }
}
abstract fun createEventBuilder(handler: T): GestureHandlerEventDataBuilder
@@ -983,6 +988,7 @@ open class GestureHandler {
private const val KEY_HIT_SLOP_WIDTH = "width"
private const val KEY_HIT_SLOP_HEIGHT = "height"
private const val KEY_TEST_ID = "testID"
+ private const val KEY_CANCELS_JS_RESPONDER = "cancelsJSResponder"
private fun handleHitSlopProperty(handler: GestureHandler, config: ReadableMap) {
if (config.getType(KEY_HIT_SLOP) == ReadableType.Number) {
@@ -1046,6 +1052,7 @@ open class GestureHandler {
private const val DEFAULT_MOUSE_BUTTON = 0
private const val DEFAULT_DISPATCHES_ANIMATED_EVENTS = false
private const val DEFAULT_DISPATCHES_REANIMATED_EVENTS = false
+ private const val DEFAULT_CANCELS_JS_RESPONDER = true
const val STATE_UNDETERMINED = 0
const val STATE_FAILED = 1
diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
index d1d6e55b4d..6dd8bd4400 100644
--- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
+++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
@@ -42,6 +42,9 @@ class GestureHandlerOrchestrator(
private var finishedHandlersCleanupScheduled = false
private var activationIndex = 0
+ var onCancelJSResponderRequested: ((GestureHandler) -> Unit)? = null
+ var onCancelJSResponderReleased: ((GestureHandler) -> Unit)? = null
+
/**
* Should be called from the view wrapper
*/
@@ -143,6 +146,14 @@ class GestureHandlerOrchestrator(
/*package*/
fun onHandlerStateChange(handler: GestureHandler, newState: Int, prevState: Int) {
handlingChangeSemaphore += 1
+
+ if (isFinished(newState) && handler.isActive && handler.cancelsJSResponder) {
+ // Check if there are any other active handlers that still request the JS responder to be cancelled.
+ if (gestureHandlers.none { it !== handler && it.isActive && it.cancelsJSResponder }) {
+ onCancelJSResponderReleased?.invoke(handler)
+ }
+ }
+
if (isFinished(newState)) {
// We have to loop through copy in order to avoid modifying collection
// while iterating over its elements
@@ -241,6 +252,10 @@ class GestureHandlerOrchestrator(
return
}
+ if (handler.cancelsJSResponder) {
+ onCancelJSResponderRequested?.invoke(handler)
+ }
+
handler.dispatchStateChange(GestureHandler.STATE_ACTIVE, GestureHandler.STATE_BEGAN)
if (currentState != GestureHandler.STATE_ACTIVE) {
diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
index fb327172be..014eed120e 100644
--- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
+++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
@@ -41,6 +41,18 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
rootView,
).apply {
minimumAlphaForTraversal = MIN_ALPHA_FOR_TOUCH
+ onCancelJSResponderRequested = { _ ->
+ shouldIntercept = true
+ val time = SystemClock.uptimeMillis()
+ val event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
+ if (rootView is RootView) {
+ rootView.onChildStartedNativeGesture(rootView, event)
+ }
+ event.recycle()
+ }
+ onCancelJSResponderReleased = { _ ->
+ shouldIntercept = false
+ }
}
jsGestureHandler = RootViewGestureHandler(handlerTag = -wrappedViewTag)
registry.registerHandler(jsGestureHandler)
@@ -92,18 +104,6 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) = handleEvent(event)
override fun onHandleHover(event: MotionEvent, sourceEvent: MotionEvent) = handleEvent(event)
-
- override fun onCancel() {
- shouldIntercept = true
- val time = SystemClock.uptimeMillis()
- val event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0f, 0f, 0).apply {
- action = MotionEvent.ACTION_CANCEL
- }
- if (rootView is RootView) {
- rootView.onChildStartedNativeGesture(rootView, event)
- }
- event.recycle()
- }
}
fun requestDisallowInterceptTouchEvent() {
diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h
index 8598a65fe6..42ff12e2aa 100644
--- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h
+++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h
@@ -81,6 +81,7 @@
@property (nonatomic) BOOL shouldCancelWhenOutside;
@property (nonatomic) BOOL needsPointerData;
@property (nonatomic) BOOL manualActivation;
+@property (nonatomic) BOOL cancelsJSResponder;
@property (nonatomic) BOOL dispatchesAnimatedEvents;
@property (nonatomic) BOOL dispatchesReanimatedEvents;
@property (nonatomic, weak, nullable) RNGHUIView *hostDetectorView;
diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm
index 4987126829..ce69517148 100644
--- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm
+++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm
@@ -105,6 +105,7 @@ - (void)resetConfig
self.testID = nil;
self.manualActivation = NO;
_shouldCancelWhenOutside = NO;
+ _cancelsJSResponder = YES;
_hitSlop = RNGHHitSlopEmpty;
_needsPointerData = NO;
_dispatchesAnimatedEvents = NO;
@@ -164,6 +165,11 @@ - (void)updateConfig:(NSDictionary *)config
self.manualActivation = [RCTConvert BOOL:prop];
}
+ prop = config[@"cancelsJSResponder"];
+ if (prop != nil) {
+ _cancelsJSResponder = [RCTConvert BOOL:prop];
+ }
+
prop = config[@"hitSlop"];
if ([prop isKindOfClass:[NSNumber class]]) {
_hitSlop.left = _hitSlop.right = _hitSlop.top = _hitSlop.bottom = [prop doubleValue];
diff --git a/packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m b/packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m
index 474592d146..b7c1d017ee 100644
--- a/packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m
+++ b/packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m
@@ -53,10 +53,10 @@ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecog
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
// When this method is called it means that one of handlers has activated, in this case we want
- // to send an info to JS so that it cancells all JS responders, as long as the preventing
+ // to send an info to JS so that it cancels all JS responders, as long as the preventing
// recognizer is from Gesture Handler, otherwise we might break some interactions
RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:preventingGestureRecognizer];
- if (handler != nil) {
+ if (handler != nil && handler.cancelsJSResponder) {
[self.delegate gestureRecognizer:preventingGestureRecognizer didActivateInViewWithTouchHandler:self.view];
}
diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts
index 5a802135a2..deccbe6a22 100644
--- a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts
+++ b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts
@@ -26,6 +26,7 @@ import {
export const ALLOWED_PROPS = [
...baseGestureHandlerWithDetectorProps,
+ 'cancelsJSResponder',
...tapGestureHandlerProps,
...panGestureHandlerProps,
...panGestureHandlerCustomNativeProps,
diff --git a/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx
index 37614db708..af8342706a 100644
--- a/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx
+++ b/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx
@@ -28,6 +28,7 @@ export function NativeDetector<
: HostGestureDetector;
ensureNativeDetectorComponent(NativeDetectorComponent);
+
configureRelations(gesture);
const handlerTags = useMemo(() => {
diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
index 77d4ab103c..37833fb296 100644
--- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
+++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
@@ -23,6 +23,7 @@ const CommonConfig = new Set([
'mouseButton',
'testID',
'cancelsTouchesInView',
+ 'cancelsJSResponder',
'manualActivation',
]);
diff --git a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts
index a86315788d..5ebd5069d3 100644
--- a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts
+++ b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts
@@ -79,6 +79,7 @@ export type CommonGestureConfig = {
activeCursor?: ActiveCursor | undefined;
mouseButton?: MouseButton | undefined;
cancelsTouchesInView?: boolean | undefined;
+ cancelsJSResponder?: boolean | undefined;
manualActivation?: boolean | undefined;
},
ActiveCursor | MouseButton