Skip to content

Commit 9f24c36

Browse files
committed
changing example
1 parent d8ff02f commit 9f24c36

3 files changed

Lines changed: 457 additions & 401 deletions

File tree

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import React, { useCallback, useRef, useState } from 'react';
2+
import { StyleSheet, Switch, Text, View } from 'react-native';
3+
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
4+
import {
5+
COLORS,
6+
Feedback,
7+
FeedbackHandle,
8+
commonStyles,
9+
} from '../../../common';
10+
11+
// Validates that when two Gesture Handler recognizers are active at the same
12+
// time, both with cancelsJSResponder set to true, finishing ONE of them does
13+
// NOT unblock React Native JS responders — the block must stay in place until
14+
// the LAST cancelling recognizer finishes.
15+
//
16+
// Expected interaction to reproduce:
17+
// 1. Finger 1: drag inside "Pan A" → GH_A ACTIVE (RN blocked)
18+
// 2. Finger 2: drag inside "Pan B" → GH_B ACTIVE
19+
// 3. Finger 3: tap the "RN responder zone" → grant must NOT fire
20+
// 4. Release finger 1 → GH_A finalize
21+
// 5. Finger 3: tap the "RN responder zone" → grant must STILL NOT fire
22+
// (GH_B is still active)
23+
// 6. Release finger 2 → GH_B finalize (released)
24+
// 7. Finger 3: tap the "RN responder zone" → grant SHOULD now fire
25+
//
26+
// If step 5 logs "RN zone onResponderGrant" the invariant is broken.
27+
28+
const MULTI_MAX_EVENTS = 14;
29+
30+
export function MultiHandlerExample() {
31+
const feedbackRef = useRef<FeedbackHandle>(null);
32+
const sequenceRef = useRef(0);
33+
const [events, setEvents] = useState<string[]>([]);
34+
const [cancelsJSResponder, setCancelsJSResponder] = useState(true);
35+
36+
const pushEvent = useCallback((label: string) => {
37+
sequenceRef.current += 1;
38+
const event = `${sequenceRef.current}. ${label}`;
39+
40+
console.log(event);
41+
feedbackRef.current?.showMessage(label);
42+
setEvents((prev) => [event, ...prev].slice(0, MULTI_MAX_EVENTS));
43+
}, []);
44+
45+
const panA = usePanGesture({
46+
minDistance: 8,
47+
runOnJS: true,
48+
cancelsJSResponder,
49+
onActivate: () => pushEvent('GH_A ACTIVE'),
50+
onFinalize: (_e, success) =>
51+
pushEvent(`GH_A finalize (${success ? 'success' : 'cancel/fail'})`),
52+
});
53+
54+
const panB = usePanGesture({
55+
minDistance: 8,
56+
runOnJS: true,
57+
cancelsJSResponder,
58+
onActivate: () => pushEvent('GH_B ACTIVE'),
59+
onFinalize: (_e, success) =>
60+
pushEvent(`GH_B finalize (${success ? 'success' : 'cancel/fail'})`),
61+
});
62+
63+
const clearLog = useCallback(() => {
64+
sequenceRef.current = 0;
65+
setEvents([]);
66+
}, []);
67+
68+
return (
69+
<View style={multiStyles.container}>
70+
<Text style={commonStyles.header}>cancelsJSResponder — multi</Text>
71+
<Text style={commonStyles.instructions}>
72+
Drag A and B with two fingers simultaneously, then tap the RN zone with
73+
a third finger. Release one finger at a time and re-tap.
74+
</Text>
75+
76+
<View style={multiStyles.settingsRow}>
77+
<Text style={multiStyles.settingsLabel}>cancelsJSResponder</Text>
78+
<Switch
79+
value={cancelsJSResponder}
80+
onValueChange={setCancelsJSResponder}
81+
/>
82+
<Text onPress={clearLog} style={multiStyles.clearButton}>
83+
clear
84+
</Text>
85+
</View>
86+
87+
<View style={multiStyles.boxesRow}>
88+
<GestureDetector gesture={panA}>
89+
<View style={[multiStyles.panBox, multiStyles.panBoxA]}>
90+
<Text style={multiStyles.panLabel}>Pan A</Text>
91+
</View>
92+
</GestureDetector>
93+
<GestureDetector gesture={panB}>
94+
<View style={[multiStyles.panBox, multiStyles.panBoxB]}>
95+
<Text style={multiStyles.panLabel}>Pan B</Text>
96+
</View>
97+
</GestureDetector>
98+
</View>
99+
100+
<View
101+
style={multiStyles.rnZone}
102+
onStartShouldSetResponder={() => {
103+
pushEvent('RN zone onStartShouldSetResponder -> true');
104+
return true;
105+
}}
106+
onResponderGrant={() => {
107+
pushEvent(
108+
'RN zone onResponderGrant <-- NOT expected while GH active'
109+
);
110+
}}
111+
onResponderRelease={() => pushEvent('RN zone onResponderRelease')}
112+
onResponderTerminate={() =>
113+
pushEvent('RN zone onResponderTerminate <-- cancelled by GH')
114+
}
115+
onResponderTerminationRequest={() => {
116+
pushEvent('RN zone onResponderTerminationRequest -> true');
117+
return true;
118+
}}>
119+
<Text style={multiStyles.rnZoneLabel}>RN responder zone (tap me)</Text>
120+
</View>
121+
122+
<View style={multiStyles.feedbackSlot}>
123+
<Feedback ref={feedbackRef} duration={1200} />
124+
</View>
125+
<View style={multiStyles.logContainer}>
126+
{events.map((item) => (
127+
<Text
128+
key={item}
129+
style={[
130+
multiStyles.logLine,
131+
item.includes('ACTIVE') && multiStyles.logLineActive,
132+
item.includes('onResponderGrant') && multiStyles.logLineBad,
133+
item.includes('Terminate') && multiStyles.logLineCancel,
134+
]}>
135+
{item}
136+
</Text>
137+
))}
138+
</View>
139+
</View>
140+
);
141+
}
142+
143+
const multiStyles = StyleSheet.create({
144+
container: {
145+
flex: 1,
146+
paddingHorizontal: 16,
147+
paddingVertical: 16,
148+
gap: 10,
149+
alignItems: 'center',
150+
backgroundColor: COLORS.offWhite,
151+
},
152+
settingsRow: {
153+
width: '100%',
154+
maxWidth: 380,
155+
flexDirection: 'row',
156+
alignItems: 'center',
157+
justifyContent: 'space-between',
158+
},
159+
settingsLabel: {
160+
color: COLORS.NAVY,
161+
fontSize: 14,
162+
fontWeight: '600',
163+
},
164+
clearButton: {
165+
color: COLORS.NAVY,
166+
fontSize: 13,
167+
fontWeight: '600',
168+
paddingHorizontal: 10,
169+
paddingVertical: 4,
170+
borderRadius: 6,
171+
borderWidth: 1,
172+
borderColor: COLORS.NAVY,
173+
},
174+
boxesRow: {
175+
width: '100%',
176+
maxWidth: 380,
177+
flexDirection: 'row',
178+
justifyContent: 'space-between',
179+
gap: 10,
180+
},
181+
panBox: {
182+
flex: 1,
183+
minHeight: 140,
184+
borderRadius: 16,
185+
borderWidth: 2,
186+
borderColor: COLORS.NAVY,
187+
justifyContent: 'center',
188+
alignItems: 'center',
189+
},
190+
panBoxA: { backgroundColor: '#d8ebff' },
191+
panBoxB: { backgroundColor: '#ffe0d8' },
192+
panLabel: {
193+
color: COLORS.NAVY,
194+
fontWeight: '700',
195+
fontSize: 16,
196+
},
197+
rnZone: {
198+
width: '100%',
199+
maxWidth: 380,
200+
minHeight: 80,
201+
borderRadius: 14,
202+
borderWidth: 2,
203+
borderStyle: 'dashed',
204+
borderColor: '#7a4dff',
205+
backgroundColor: '#ece2ff',
206+
justifyContent: 'center',
207+
alignItems: 'center',
208+
},
209+
rnZoneLabel: {
210+
color: '#3a1f9c',
211+
fontWeight: '700',
212+
fontSize: 15,
213+
},
214+
feedbackSlot: {
215+
width: '100%',
216+
maxWidth: 420,
217+
height: 84,
218+
paddingHorizontal: 8,
219+
justifyContent: 'center',
220+
alignItems: 'center',
221+
overflow: 'hidden',
222+
},
223+
logContainer: {
224+
width: '100%',
225+
maxWidth: 420,
226+
height: 260,
227+
borderRadius: 12,
228+
padding: 10,
229+
backgroundColor: '#ffffff',
230+
borderWidth: 1,
231+
borderColor: '#d5dbe6',
232+
gap: 2,
233+
overflow: 'hidden',
234+
},
235+
logLine: {
236+
fontSize: 12,
237+
color: '#2c3a4f',
238+
fontFamily: 'Courier',
239+
},
240+
logLineActive: { color: '#1565c0', fontWeight: 'bold' },
241+
logLineBad: { color: '#b71c1c', fontWeight: 'bold' },
242+
logLineCancel: { color: '#6a1b9a' },
243+
});

0 commit comments

Comments
 (0)