Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4897ae0
initial
coado Apr 8, 2026
8fd0c11
Add on Android
coado Apr 9, 2026
1f123b1
pass preventRecognizers per Android root view
coado Apr 10, 2026
b939be1
preventRecognizers per gesture detector
coado Apr 10, 2026
dc9270f
prevent recognizers Android
coado Apr 14, 2026
aae857b
rename
coado Apr 14, 2026
986a56a
remove smth
coado Apr 14, 2026
1197ee7
testing v3
coado Apr 15, 2026
da47e89
better example
coado Apr 15, 2026
a97742e
set preventRecognizers on gesture handlers
coado Apr 15, 2026
fca5a98
fix docx
coado Apr 15, 2026
f9b2835
fix docs again
coado Apr 16, 2026
ea95543
fix
coado Apr 16, 2026
2e97530
block recognizers after the state is checked
coado Apr 16, 2026
c4b49ec
fix typo
coado Apr 16, 2026
68c31bb
docs
coado Apr 16, 2026
ca4c6d3
better example
coado Apr 17, 2026
fd57422
rename to cancelsJsResponder
coado Apr 17, 2026
fff3181
merge
coado Apr 24, 2026
758cea7
Revert "merge"
coado Apr 24, 2026
803ee7c
Update packages/react-native-gesture-handler/src/v3/detectors/NativeD…
coado Apr 24, 2026
92fecd7
removed from allowed props list
coado Apr 24, 2026
770b410
better comment
coado Apr 24, 2026
eea0192
better docs
coado Apr 24, 2026
d8ff02f
pass onJSResponderCancelListener to the orchestrator, move shouldInte…
coado Apr 24, 2026
9f24c36
changing example
coado Apr 24, 2026
4d509d3
Merge branch 'main' into prevent-recognizers
coado Apr 24, 2026
0c13b44
empty
coado Apr 24, 2026
bc8f207
merge
coado Apr 24, 2026
7795b1a
Merge branch 'prevent-recognizers' of github.com:software-mansion/rea…
coado Apr 24, 2026
403a750
use badges
coado Apr 24, 2026
3f7c16e
fix types
coado Apr 24, 2026
d419464
change description
coado Apr 24, 2026
6eac6c7
pass non-null callback
coado Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/common-app/src/new_api/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import PointerTypeExample from './tests/pointerType';
import PressableExample from './tests/pressable';
import ReattachingExample from './tests/reattaching';
import RectButtonExample from './tests/rectButton';
import RNResponderCancellationExample from './tests/rnResponderCancellation';
import TwoFingerPanExample from './tests/twoFingerPan';
import WebStylesResetExample from './tests/webStylesReset';

Expand Down Expand Up @@ -127,6 +128,10 @@ export const NEW_EXAMPLES: ExamplesSection[] = [
{ name: 'Nested pressables', component: NestedPressablesExample },
{ name: 'Nested touchables', component: NestedTouchablesExample },
{ name: 'Pressable', component: PressableExample },
{
name: 'RN responder cancellation',
component: RNResponderCancellationExample,
},
],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import React, { useCallback, useRef, useState } from 'react';
import { StyleSheet, Switch, Text, View } from 'react-native';
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';

import type { FeedbackHandle } from '../../../common';
import { COLORS, commonStyles, Feedback } from '../../../common';

// 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;

export function MultiHandlerExample() {
const feedbackRef = useRef<FeedbackHandle>(null);
const sequenceRef = useRef(0);
const [events, setEvents] = useState<string[]>([]);
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) =>
pushEvent(`GH_A finalize (${e.canceled ? 'cancel/fail' : 'success'})`),
});

const panB = usePanGesture({
minDistance: 8,
runOnJS: true,
cancelsJSResponder,
onActivate: () => pushEvent('GH_B ACTIVE'),
onFinalize: (e) =>
pushEvent(`GH_B finalize (${e.canceled ? 'cancel/fail' : 'success'})`),
});

const clearLog = useCallback(() => {
sequenceRef.current = 0;
setEvents([]);
}, []);

return (
<View style={multiStyles.container}>
<Text style={commonStyles.header}>cancelsJSResponder — multi</Text>
<Text style={commonStyles.instructions}>
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.
</Text>

<View style={multiStyles.settingsRow}>
<Text style={multiStyles.settingsLabel}>cancelsJSResponder</Text>
<Switch
value={cancelsJSResponder}
onValueChange={setCancelsJSResponder}
/>
<Text onPress={clearLog} style={multiStyles.clearButton}>
clear
</Text>
</View>

<View style={multiStyles.boxesRow}>
<GestureDetector gesture={panA}>
<View style={[multiStyles.panBox, multiStyles.panBoxA]}>
<Text style={multiStyles.panLabel}>Pan A</Text>
</View>
</GestureDetector>
<GestureDetector gesture={panB}>
<View style={[multiStyles.panBox, multiStyles.panBoxB]}>
<Text style={multiStyles.panLabel}>Pan B</Text>
</View>
</GestureDetector>
</View>

<View
style={multiStyles.rnZone}
onStartShouldSetResponder={() => {
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;
}}>
<Text style={multiStyles.rnZoneLabel}>RN responder zone (tap me)</Text>
</View>

<View style={multiStyles.feedbackSlot}>
<Feedback ref={feedbackRef} duration={1200} />
</View>
<View style={multiStyles.logContainer}>
{events.map((item) => (
<Text
key={item}
style={[
multiStyles.logLine,
item.includes('ACTIVE') && multiStyles.logLineActive,
item.includes('onResponderGrant') && multiStyles.logLineBad,
item.includes('Terminate') && multiStyles.logLineCancel,
]}>
{item}
</Text>
))}
</View>
</View>
);
}

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' },
});
Loading
Loading