Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: onPress callbacks are invoked for all nested Pressables #3295

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

coado
Copy link
Contributor

@coado coado commented Dec 17, 2024

Description

Fixes #3282

The onStart and onBegin callbacks are invoked on the deepest responder on iOS and Android respectively. Inside those methods the isTouchPropagationAllowed ref is set to true. Base on that we can assume which Pressable should be the responder of the touch on the JS side. Yet there is nothing that would stop invoking onPress callbacks if the isTouchPropagationAllowed is set to false in pressOutHandler. The fix introduces early return in the pressOutHandler if clicked Pressable is not the deepest one.

On the old architecture the measure method is called asynchronously, usually after the onStart. In nested pressables, if the deepest one is clicked it still calls onTouchesDown and onTouchesUp, which sets shouldPreventNativeEffects to true, omitting the onStart, which sets it back to false. Because of that, when now we tried to click on the outer Pressable it would be stopped in onStart shouldPreventNativeEvents check without setting the isTouchPropagationAllowed to true and this is the exact same state that led to setting the shouldPreventNativeEffects to true so it is a cycle. The simplest solution for this is to check if the onStart is called before the measure in shouldPreventNativeEffects check and early return if it's not.

Test plan

Click on the red box and notice that callbacks on its parent are not invoked. Click on the text input to see that the callbacks on its Pressable parent are also not invoked.

Code
import * as React from 'react';
import {
  View,
  Text,
  Pressable as RNPressable,
  SafeAreaView,
  StyleSheet,
  TextInput as RNTextInput,
} from 'react-native';
import {
  GestureHandlerRootView,
  Pressable as RNGHPressable,
  TextInput as RNGHTextInput,
} from 'react-native-gesture-handler';

export default function HomeScreen() {
  return (
    <GestureHandlerRootView>
      <SafeAreaView style={{ flex: 1 }}>
        <View style={styles.container}>
          <View style={styles.wrapper}>
            <RNGHPressable
              style={[styles.pressableStyles, styles.outerPressableStyles]}
              onPress={() => console.log(JSON.stringify('RNGH: Parent Press'))}
              onPressIn={() => console.log('RNGH: Parent PressIn')}
              onPressOut={() => console.log('RNGH: Parent PressOut')}>
              <RNGHPressable
                style={[styles.pressableStyles, styles.innerPressableStyles]}
                onPress={() => console.log(JSON.stringify('RNGH: Child Press'))}
                onPressIn={() => console.log('RNGH: Child PressIn')}
                onPressOut={() => console.log('RNGH: Child PressOut')}>
                <Text>RNGH</Text>
              </RNGHPressable>
            </RNGHPressable>

            <RNPressable
              style={[styles.pressableStyles, styles.outerPressableStyles]}
              onPress={() => console.log(JSON.stringify('RN: Parent Press'))}
              onPressIn={() => console.log('RN: Parent PressIn')}
              onPressOut={() => console.log('RN: Parent PressOut')}>
              <RNPressable
                style={[styles.pressableStyles, styles.innerPressableStyles]}
                onPress={() => console.log(JSON.stringify('RN: Child Press'))}
                onPressIn={() => console.log('RN: Child PressIn')}
                onPressOut={() => console.log('RN: Child PressOut')}>
                <Text>RN</Text>
              </RNPressable>
            </RNPressable>
          </View>

          <View style={styles.wrapper}>
            <RNGHPressable
              style={styles.textInputPressable}
              onPress={() =>
                console.log(JSON.stringify('RNGH: TextInput Press'))
              }
              onPressIn={() => console.log('RNGH: TextInput PressIn')}
              onPressOut={() => console.log('RNGH: TextInput PressOut')}>
              <RNGHTextInput
                placeholder="RNGH TextInput"
                style={styles.textInputStyles}
                onChangeText={(text) => console.log(text)}
              />
            </RNGHPressable>

            <RNPressable
              style={styles.textInputPressable}
              onPress={() => console.log(JSON.stringify('RN: TextInput Press'))}
              onPressIn={() => console.log('RN: TextInput PressIn')}
              onPressOut={() => console.log('RN: TextInput PressOut')}>
              <RNTextInput
                placeholder="RN TextInput"
                style={styles.textInputStyles}
                onChangeText={(text) => console.log(text)}
              />
            </RNPressable>
          </View>
        </View>
      </SafeAreaView>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  wrapper: {
    alignItems: 'center',
    justifyContent: 'center',
    gap: 10,
    flexDirection: 'row',
    marginTop: 10,
  },
  pressableStyles: {
    paddingVertical: 15,
    paddingHorizontal: 20,
    alignItems: 'center',
    justifyContent: 'center',
  },
  innerPressableStyles: {
    backgroundColor: 'red',
    width: 120,
    height: 50,
  },
  outerPressableStyles: {
    backgroundColor: '#3BE7BB',
  },
  textInputPressable: {
    backgroundColor: 'gray',
    padding: 10,
    borderRadius: 5,
    width: 160,
    // height: 50,
    alignItems: 'center',
    justifyContent: 'center',
  },
  textInputStyles: {
    backgroundColor: 'lightgray',
    paddingHorizontal: 20,
    paddingVertical: 10,
    height: 40
  },
  pressableText: {
    color: '#000',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

@j-piasecki
Copy link
Member

Could you add a test case for the nested pressables in the example app? Ideally with a few different levels of nesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

onPress action on Pressable component inside another triggers both actions
2 participants