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