[fix] ResponderSystem negotiation logic

This fixes a bug in the negotiation logic that caused a cycle of
terminate->grant events to be sent to the current responder during a pointer
move. The root cause was using an incorrect event path in the calculation of
the lowest common ancestor's index. The fix is to ensure that the event path
stored with the current responder is pruned to begin with the node that is the
current responder (rather than any child responders it may have contained).
This commit is contained in:
Nicolas Gallagher
2020-10-29 15:51:03 -07:00
parent d2e6c29e25
commit 07e578edb8
6 changed files with 335 additions and 21 deletions
@@ -64,3 +64,9 @@ Called when the pointer is released, but not if cancelled (e.g. by a scroll that
<Stories.feedbackEvents />
</Story>
</Preview>
<Preview withSource='none'>
<Story name="panAndPress">
<Stories.panAndPress />
</Story>
</Preview>
@@ -56,13 +56,12 @@ export default function FeedbackEvents() {
}}
>
<Pressable
accessibilityRole="none"
accessibilityRole="button"
onLongPress={handlePress('longPress - inner')}
onPress={handlePress('press - inner')}
onPressIn={handlePress('pressIn - inner')}
onPressOut={handlePress('pressOut - inner')}
style={({ hovered, pressed, focused }) => {
console.log(focused);
let backgroundColor = 'white';
if (hovered) {
backgroundColor = 'lightgray';
@@ -0,0 +1,100 @@
import React, { useRef, useState } from 'react';
import { Animated, View, StyleSheet, PanResponder, Text, TouchableOpacity } from 'react-native';
const App = () => {
const pan = useRef(new Animated.ValueXY()).current;
const [x, setX] = useState(0);
const panResponder = useRef(null);
if (panResponder.current == null) {
panResponder.current = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: e => {
console.log('pan grant');
pan.setOffset({
x: pan.x._value,
y: pan.y._value
});
},
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }]),
onPanResponderRelease: () => {
console.log('pan release');
pan.flattenOffset();
},
onPanResponderTerminate() {
console.log('pan terminate');
pan.flattenOffset();
}
});
}
return (
<View style={styles.container}>
<Text style={styles.titleText}>Pressed: {x}</Text>
<Animated.View
style={{
transform: [{ translateX: pan.x }, { translateY: pan.y }]
}}
{...panResponder.current.panHandlers}
>
<View style={styles.box}>
<TouchableOpacity onPress={() => setX(x + 1)} style={styles.outerTouchable}>
<TouchableOpacity
onPress={() => {
console.log('press inner');
}}
style={styles.innerTouchable}
/>
<TouchableOpacity disabled style={styles.innerTouchable} />
<TouchableOpacity
accessibilityRole="button"
disabled
style={[styles.innerTouchable, styles.disabledButton]}
/>
</TouchableOpacity>
</View>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none'
},
titleText: {
fontSize: 14,
lineHeight: 24,
fontWeight: 'bold'
},
box: {
height: 200,
width: 150,
backgroundColor: 'lightblue',
borderRadius: 5
},
outerTouchable: {
height: 150,
width: 100,
margin: 25,
backgroundColor: 'blue',
borderRadius: 5,
justifyContent: 'center'
},
innerTouchable: {
height: 20,
flex: 1,
marginVertical: 10,
marginHorizontal: 20,
backgroundColor: 'green',
borderRadius: 5
},
disabledButton: {
backgroundColor: 'red'
}
});
export default App;
@@ -1,3 +1,4 @@
export { default as delayEvents } from './DelayEvents';
export { default as disabled } from './Disabled';
export { default as feedbackEvents } from './FeedbackEvents';
export { default as panAndPress } from './PanAndPress';