software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
6.13k stars 982 forks source link

Fix `Pressables`'s interference with other gestures when nested #3015

Closed latekvo closed 3 months ago

latekvo commented 3 months ago

Description

LongPress was found to be using excessively high value for it's minDuration and maxDistance configs.

While maxDistance config caused no issues, minDuration config caused an instantaneous activation. This behaviour stems from setTimeout handling values up to 2^31-1 while MAX_SAFE_INTEGER is much higher than that. This unintended activation blocked some nested gestures underneath the Pressable from activating on web.

By replacing Number.MAX_SAFE_INTEGER with 2 ** 31 - 1 which represents the largest possible 32-bit integer, those issues were resolved.

Found while investigating this issue.

Both issues - the one reported by @milan-digiuseppe-level and the one described in this PR might be related, but it's not immidiately clear as I couldn't replicate the former one.

closes #2863

Test plan

Before After

Attached code

/* eslint-disable no-alert */
import React from 'react';
import { Text, Animated, StyleSheet, View, Pressable } from 'react-native';

import {
  Pressable as GHPressable,
  Swipeable,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import ReanimatedSwipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
import Reanimated, {
  SharedValue,
  useAnimatedStyle,
} from 'react-native-reanimated';

function LeftAction(prog: SharedValue<number>, drag: SharedValue<number>) {
  const styleAnimation = useAnimatedStyle(() => {
    console.log('[R] showLeftProgress:', prog.value);
    console.log('[R] appliedTranslation:', drag.value);

    return {
      transform: [{ translateX: drag.value - 50 }],
    };
  });

  return (
    <Reanimated.View style={styleAnimation}>
      <Text style={styles.leftAction}>Text</Text>
    </Reanimated.View>
  );
}

function RightAction(prog: SharedValue<number>, drag: SharedValue<number>) {
  const styleAnimation = useAnimatedStyle(() => {
    console.log('[R] showRightProgress:', prog.value);
    console.log('[R] appliedTranslation:', drag.value);

    return {
      transform: [{ translateX: drag.value + 50 }],
    };
  });

  return (
    <Reanimated.View style={styleAnimation}>
      <Text style={styles.rightAction}>Text</Text>
    </Reanimated.View>
  );
}

function LegacyLeftAction(prog: any, drag: any) {
  prog.addListener((value: any) => {
    console.log('[L] showLeftProgress:', value.value);
  });
  drag.addListener((value: any) => {
    console.log('[L] appliedTranslation:', value.value);
  });

  const trans = Animated.subtract(drag, 50);

  return (
    <Animated.Text
      style={[
        styles.leftAction,
        {
          transform: [{ translateX: trans }],
        },
      ]}>
      Text
    </Animated.Text>
  );
}

function LegacyRightAction(prog: any, drag: any) {
  prog.addListener((value: any) => {
    console.log('[L] showRightProgress:', value.value);
  });
  drag.addListener((value: any) => {
    console.log('[L] appliedTranslation:', value.value);
  });

  const trans = Animated.add(drag, 50);

  return (
    <Animated.Text
      style={[
        styles.rightAction,
        {
          transform: [{ translateX: trans }],
        },
      ]}>
      Text
    </Animated.Text>
  );
}

export default function Example() {
  return (
    <GestureHandlerRootView>
      <View style={styles.separator} />

      <ReanimatedSwipeable
        containerStyle={styles.swipeable}
        friction={2}
        leftThreshold={80}
        enableTrackpadTwoFingerGesture
        rightThreshold={56}
        renderLeftActions={LeftAction}
        renderRightActions={RightAction}>
        <Pressable onPress={() => alert('pressed!')} style={styles.pressable}>
          <Text>[new] with RN pressable</Text>
        </Pressable>
      </ReanimatedSwipeable>

      <View style={styles.separator} />

      <ReanimatedSwipeable
        containerStyle={styles.swipeable}
        friction={2}
        leftThreshold={80}
        enableTrackpadTwoFingerGesture
        rightThreshold={56}
        renderLeftActions={LeftAction}
        renderRightActions={RightAction}>
        <GHPressable onPress={() => alert('pressed!')} style={styles.pressable}>
          <Text>[new] with GH pressable</Text>
        </GHPressable>
      </ReanimatedSwipeable>

      <View style={styles.separator} />

      <Swipeable
        containerStyle={styles.swipeable}
        friction={2}
        leftThreshold={80}
        enableTrackpadTwoFingerGesture
        rightThreshold={56}
        renderLeftActions={LegacyLeftAction}
        renderRightActions={LegacyRightAction}>
        <Pressable onPress={() => alert('pressed!')} style={styles.pressable}>
          <Text>[Legacy] with RN pressable</Text>
        </Pressable>
      </Swipeable>

      <View style={styles.separator} />

      <Swipeable
        containerStyle={styles.swipeable}
        friction={2}
        leftThreshold={80}
        enableTrackpadTwoFingerGesture
        rightThreshold={56}
        renderLeftActions={LegacyLeftAction}
        renderRightActions={LegacyRightAction}>
        <GHPressable onPress={() => alert('pressed!')} style={styles.pressable}>
          <Text>[Legacy] with GH pressable</Text>
        </GHPressable>
      </Swipeable>

      <View style={styles.separator} />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  leftAction: { width: 46, height: 56, backgroundColor: 'crimson' },
  rightAction: { width: 46, height: 56, backgroundColor: 'purple' },
  separator: {
    width: '100%',
    borderTopWidth: 1,
  },
  swipeable: {
    height: 56,
    backgroundColor: 'papayawhip',
    alignItems: 'center',
  },
  pressable: {
    padding: 20,
    width: 200,
    height: 56,
    backgroundColor: 'pink',
    alignItems: 'center',
  },
});
latekvo commented 3 months ago

After further testing, it appears that on iOS, when Pressable is nested inside a native Scroll, only every second press is registered. I'll address this issue asap.

j-piasecki commented 3 months ago

Doesn't changing LongPress to Manual change the behavior on native? I believe we were talking about it and Manual was causing other gestures to misbehave.

j-piasecki commented 3 months ago

Also, the title is a bit confusing as it suggests changes to Swipeable component.

latekvo commented 3 months ago

Doesn't changing LongPress to Manual change the behavior on native? I believe we were talking about it and Manual was causing other gestures to misbehave. - @j-piasecki

It does! But I found that by adding .manualActivation(true) to the Manual gesture, most of those issues can be alleviated. The only one that I have to fix after this change is the one I mentioned in the comment above.

Also, the title is a bit confusing as it suggests changes to Swipeable component.

I'll fix the title.

m-bert commented 3 months ago

Also, please update PR description 😅