Skip to content

Commit ca4c6d3

Browse files
committed
better example
1 parent 68c31bb commit ca4c6d3

1 file changed

Lines changed: 286 additions & 12 deletions

File tree

  • apps/common-app/src/new_api/tests/rnResponderCancellation

apps/common-app/src/new_api/tests/rnResponderCancellation/index.tsx

Lines changed: 286 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useRef, useState } from 'react';
2-
import { StyleSheet, Switch, Text, View } from 'react-native';
2+
import { ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
33
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
44
import {
55
COLORS,
@@ -8,9 +8,48 @@ import {
88
commonStyles,
99
} from '../../../common';
1010

11-
const MAX_EVENTS = 8;
11+
const SECTION_MIN_HEIGHT = 640;
1212

1313
export default function RNResponderCancellationExample() {
14+
return (
15+
<ScrollView
16+
style={scrollStyles.scroll}
17+
contentContainerStyle={scrollStyles.content}
18+
showsVerticalScrollIndicator>
19+
<View style={scrollStyles.section}>
20+
<SingleHandlerExample />
21+
</View>
22+
<View style={scrollStyles.divider} />
23+
<View style={scrollStyles.section}>
24+
<MultiHandlerExample />
25+
</View>
26+
</ScrollView>
27+
);
28+
}
29+
30+
const scrollStyles = StyleSheet.create({
31+
scroll: {
32+
flex: 1,
33+
},
34+
content: {
35+
flexGrow: 1,
36+
},
37+
section: {
38+
minHeight: SECTION_MIN_HEIGHT,
39+
},
40+
divider: {
41+
height: 2,
42+
marginHorizontal: 16,
43+
marginVertical: 4,
44+
backgroundColor: '#b6bfd0',
45+
},
46+
});
47+
48+
// ---------- Single handler --------------------------------------------------
49+
50+
const SINGLE_MAX_EVENTS = 8;
51+
52+
function SingleHandlerExample() {
1453
const feedbackRef = useRef<FeedbackHandle>(null);
1554
const sequenceRef = useRef(0);
1655
const [events, setEvents] = useState<string[]>([]);
@@ -22,7 +61,7 @@ export default function RNResponderCancellationExample() {
2261

2362
console.log(event);
2463
feedbackRef.current?.showMessage(label);
25-
setEvents((prev) => [event, ...prev].slice(0, MAX_EVENTS));
64+
setEvents((prev) => [event, ...prev].slice(0, SINGLE_MAX_EVENTS));
2665
}, []);
2766

2867
const panGesture = usePanGesture({
@@ -38,13 +77,13 @@ export default function RNResponderCancellationExample() {
3877
});
3978

4079
return (
41-
<View style={styles.container}>
80+
<View style={singleStyles.container}>
4281
<Text style={commonStyles.header}>RN responder cancellation</Text>
4382
<Text style={commonStyles.instructions}>
4483
Toggle preventRecognizers and drag inside the box to compare behavior.
4584
</Text>
46-
<View style={styles.settingsRow}>
47-
<Text style={styles.settingsLabel}>preventRecognizers</Text>
85+
<View style={singleStyles.settingsRow}>
86+
<Text style={singleStyles.settingsLabel}>preventRecognizers</Text>
4887
<Switch
4988
value={preventRecognizers}
5089
onValueChange={setPreventRecognizers}
@@ -53,7 +92,7 @@ export default function RNResponderCancellationExample() {
5392

5493
<GestureDetector gesture={panGesture}>
5594
<View
56-
style={styles.touchArea}
95+
style={singleStyles.touchArea}
5796
onStartShouldSetResponder={() => {
5897
pushEvent('RN onStartShouldSetResponder -> true');
5998
return true;
@@ -78,18 +117,18 @@ export default function RNResponderCancellationExample() {
78117
pushEvent('RN onResponderTerminationRequest -> true');
79118
return true;
80119
}}>
81-
<Text style={styles.touchAreaLabel}>Drag me</Text>
120+
<Text style={singleStyles.touchAreaLabel}>Drag me</Text>
82121
</View>
83122
</GestureDetector>
84123

85124
<Feedback ref={feedbackRef} duration={1300} />
86-
<View style={styles.logContainer}>
125+
<View style={singleStyles.logContainer}>
87126
{events.map((item) => (
88127
<Text
89128
key={item}
90129
style={[
91-
styles.logLine,
92-
item.includes('GH pan ACTIVE') && styles.logLineActive,
130+
singleStyles.logLine,
131+
item.includes('GH pan ACTIVE') && singleStyles.logLineActive,
93132
]}>
94133
{item}
95134
</Text>
@@ -99,7 +138,7 @@ export default function RNResponderCancellationExample() {
99138
);
100139
}
101140

102-
const styles = StyleSheet.create({
141+
const singleStyles = StyleSheet.create({
103142
container: {
104143
flex: 1,
105144
paddingHorizontal: 16,
@@ -157,3 +196,238 @@ const styles = StyleSheet.create({
157196
fontWeight: 'bold',
158197
},
159198
});
199+
200+
// ---------- Multi handler ---------------------------------------------------
201+
// Validates that when two Gesture Handler recognizers are active at the same
202+
// time, both with preventRecognizers set to true, finishing ONE of them does
203+
// NOT unblock React Native JS responders — the block must stay in place until
204+
// the LAST preventing recognizer finishes.
205+
//
206+
// Expected interaction to reproduce:
207+
// 1. Finger 1: drag inside "Pan A" → GH_A ACTIVE (RN blocked)
208+
// 2. Finger 2: drag inside "Pan B" → GH_B ACTIVE
209+
// 3. Finger 3: tap the "RN responder zone" → grant must NOT fire
210+
// 4. Release finger 1 → GH_A finalize
211+
// 5. Finger 3: tap the "RN responder zone" → grant must STILL NOT fire
212+
// (GH_B is still active)
213+
// 6. Release finger 2 → GH_B finalize (released)
214+
// 7. Finger 3: tap the "RN responder zone" → grant SHOULD now fire
215+
//
216+
// If step 5 logs "RN zone onResponderGrant" the invariant is broken.
217+
218+
const MULTI_MAX_EVENTS = 14;
219+
220+
function MultiHandlerExample() {
221+
const feedbackRef = useRef<FeedbackHandle>(null);
222+
const sequenceRef = useRef(0);
223+
const [events, setEvents] = useState<string[]>([]);
224+
const [preventRecognizers, setPreventRecognizers] = useState(true);
225+
226+
const pushEvent = useCallback((label: string) => {
227+
sequenceRef.current += 1;
228+
const event = `${sequenceRef.current}. ${label}`;
229+
230+
console.log(event);
231+
feedbackRef.current?.showMessage(label);
232+
setEvents((prev) => [event, ...prev].slice(0, MULTI_MAX_EVENTS));
233+
}, []);
234+
235+
const panA = usePanGesture({
236+
minDistance: 8,
237+
runOnJS: true,
238+
preventRecognizers,
239+
onActivate: () => pushEvent('GH_A ACTIVE'),
240+
onFinalize: (_e, success) =>
241+
pushEvent(`GH_A finalize (${success ? 'success' : 'cancel/fail'})`),
242+
});
243+
244+
const panB = usePanGesture({
245+
minDistance: 8,
246+
runOnJS: true,
247+
// preventRecognizers,
248+
onActivate: () => pushEvent('GH_B ACTIVE'),
249+
onFinalize: (_e, success) =>
250+
pushEvent(`GH_B finalize (${success ? 'success' : 'cancel/fail'})`),
251+
});
252+
253+
const clearLog = useCallback(() => {
254+
sequenceRef.current = 0;
255+
setEvents([]);
256+
}, []);
257+
258+
return (
259+
<View style={multiStyles.container}>
260+
<Text style={commonStyles.header}>preventRecognizers — multi</Text>
261+
<Text style={commonStyles.instructions}>
262+
Drag A and B with two fingers simultaneously, then tap the RN zone with
263+
a third finger. Release one finger at a time and re-tap.
264+
</Text>
265+
266+
<View style={multiStyles.settingsRow}>
267+
<Text style={multiStyles.settingsLabel}>preventRecognizers</Text>
268+
<Switch
269+
value={preventRecognizers}
270+
onValueChange={setPreventRecognizers}
271+
/>
272+
<Text onPress={clearLog} style={multiStyles.clearButton}>
273+
clear
274+
</Text>
275+
</View>
276+
277+
<View style={multiStyles.boxesRow}>
278+
<GestureDetector gesture={panA}>
279+
<View style={[multiStyles.panBox, multiStyles.panBoxA]}>
280+
<Text style={multiStyles.panLabel}>Pan A</Text>
281+
</View>
282+
</GestureDetector>
283+
<GestureDetector gesture={panB}>
284+
<View style={[multiStyles.panBox, multiStyles.panBoxB]}>
285+
<Text style={multiStyles.panLabel}>Pan B</Text>
286+
</View>
287+
</GestureDetector>
288+
</View>
289+
290+
<View
291+
style={multiStyles.rnZone}
292+
onStartShouldSetResponder={() => {
293+
pushEvent('RN zone onStartShouldSetResponder -> true');
294+
return true;
295+
}}
296+
onResponderGrant={() => {
297+
pushEvent(
298+
'RN zone onResponderGrant <-- NOT expected while GH active'
299+
);
300+
}}
301+
onResponderRelease={() => pushEvent('RN zone onResponderRelease')}
302+
onResponderTerminate={() =>
303+
pushEvent('RN zone onResponderTerminate <-- cancelled by GH')
304+
}
305+
onResponderTerminationRequest={() => {
306+
pushEvent('RN zone onResponderTerminationRequest -> true');
307+
return true;
308+
}}>
309+
<Text style={multiStyles.rnZoneLabel}>RN responder zone (tap me)</Text>
310+
</View>
311+
312+
<View style={multiStyles.feedbackSlot}>
313+
<Feedback ref={feedbackRef} duration={1200} />
314+
</View>
315+
<View style={multiStyles.logContainer}>
316+
{events.map((item) => (
317+
<Text
318+
key={item}
319+
style={[
320+
multiStyles.logLine,
321+
item.includes('ACTIVE') && multiStyles.logLineActive,
322+
item.includes('onResponderGrant') && multiStyles.logLineBad,
323+
item.includes('Terminate') && multiStyles.logLineCancel,
324+
]}>
325+
{item}
326+
</Text>
327+
))}
328+
</View>
329+
</View>
330+
);
331+
}
332+
333+
const multiStyles = StyleSheet.create({
334+
container: {
335+
flex: 1,
336+
paddingHorizontal: 16,
337+
paddingVertical: 16,
338+
gap: 10,
339+
alignItems: 'center',
340+
backgroundColor: COLORS.offWhite,
341+
},
342+
settingsRow: {
343+
width: '100%',
344+
maxWidth: 380,
345+
flexDirection: 'row',
346+
alignItems: 'center',
347+
justifyContent: 'space-between',
348+
},
349+
settingsLabel: {
350+
color: COLORS.NAVY,
351+
fontSize: 14,
352+
fontWeight: '600',
353+
},
354+
clearButton: {
355+
color: COLORS.NAVY,
356+
fontSize: 13,
357+
fontWeight: '600',
358+
paddingHorizontal: 10,
359+
paddingVertical: 4,
360+
borderRadius: 6,
361+
borderWidth: 1,
362+
borderColor: COLORS.NAVY,
363+
},
364+
boxesRow: {
365+
width: '100%',
366+
maxWidth: 380,
367+
flexDirection: 'row',
368+
justifyContent: 'space-between',
369+
gap: 10,
370+
},
371+
panBox: {
372+
flex: 1,
373+
minHeight: 140,
374+
borderRadius: 16,
375+
borderWidth: 2,
376+
borderColor: COLORS.NAVY,
377+
justifyContent: 'center',
378+
alignItems: 'center',
379+
},
380+
panBoxA: { backgroundColor: '#d8ebff' },
381+
panBoxB: { backgroundColor: '#ffe0d8' },
382+
panLabel: {
383+
color: COLORS.NAVY,
384+
fontWeight: '700',
385+
fontSize: 16,
386+
},
387+
rnZone: {
388+
width: '100%',
389+
maxWidth: 380,
390+
minHeight: 80,
391+
borderRadius: 14,
392+
borderWidth: 2,
393+
borderStyle: 'dashed',
394+
borderColor: '#7a4dff',
395+
backgroundColor: '#ece2ff',
396+
justifyContent: 'center',
397+
alignItems: 'center',
398+
},
399+
rnZoneLabel: {
400+
color: '#3a1f9c',
401+
fontWeight: '700',
402+
fontSize: 15,
403+
},
404+
feedbackSlot: {
405+
width: '100%',
406+
maxWidth: 420,
407+
height: 84,
408+
paddingHorizontal: 8,
409+
justifyContent: 'center',
410+
alignItems: 'center',
411+
overflow: 'hidden',
412+
},
413+
logContainer: {
414+
width: '100%',
415+
maxWidth: 420,
416+
height: 260,
417+
borderRadius: 12,
418+
padding: 10,
419+
backgroundColor: '#ffffff',
420+
borderWidth: 1,
421+
borderColor: '#d5dbe6',
422+
gap: 2,
423+
overflow: 'hidden',
424+
},
425+
logLine: {
426+
fontSize: 12,
427+
color: '#2c3a4f',
428+
fontFamily: 'Courier',
429+
},
430+
logLineActive: { color: '#1565c0', fontWeight: 'bold' },
431+
logLineBad: { color: '#b71c1c', fontWeight: 'bold' },
432+
logLineCancel: { color: '#6a1b9a' },
433+
});

0 commit comments

Comments
 (0)