Skip to content

Commit

Permalink
Return false if gesture recognizers are present but all disabled (#3377)
Browse files Browse the repository at this point in the history
## Description

This PR adjusts `shouldHandleTouch` to check for enabled gesture
recognizers instead of returning true if the count of gesture
recognizers is greater than zero.

The motivation behind this PR is to address a bug where buttons become
unresponsive if, I guess, iOS is inserting disabled accessibility
gesture recognizers into the child views??

Fixes #3376

## Test plan

I was able to reproduce this reliably on a private repo, and used the
debugger to observe `shouldHandleTouch` returning too early from a
descendant of the button meant to be tapped. With this fix, I confirmed
that the correct button returns to handle to touch event and the buttons
all behave as expected

I also was able to reproduce the issue with this sample code:
1. Tap `Courses`
2. Tap `Hello World 1`
3. Tap `All Courses`

The `All Courses` button has an `onPress` which `alert`s, and in this
snippet, I observed no alert occurring

<details>
<summary>Click to expand</summary>

```
import { StyleSheet, View, Text } from 'react-native';
import { BorderlessButton, FlatList } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
import { useNavigation } from 'expo-router';
import { useState } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {useEffect} from 'react';
import {
    FadeInLeft,
    FadeInUp,
    FadeOutLeft,
    FadeOutUp,
} from 'react-native-reanimated';

const AnimatedBorderlessButton = Animated.createAnimatedComponent(BorderlessButton);

const OptionsScreen = ({
    options,
    onSelect
}: {
    options: string[];
    onSelect: (option?: string) => void;
}) => {
    const insets = useSafeAreaInsets();
    const navigation = useNavigation();
    const styles = StyleSheet.create({
        container: {
          paddingTop: insets.top,
            backgroundColor: 'rgba(0,0,0,0.7)',
            flex: 1,
            paddingHorizontal: 24,
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
        },
        text: {
            paddingTop: 16,
            color: 'white',
        },
    });
    useEffect(() => {
        navigation.setOptions({
          tabBarStyle: { display: 'none' },
        });
        return () => {
          navigation.setOptions({
            tabBarStyle: { display: 'flex' },
          });
        };
    }, [navigation]);
    return (
        <Animated.View
            style={styles.container}
            entering={FadeInUp}
            exiting={FadeOutUp}
        >
            <FlatList
                data={options}
                renderItem={({item}) => (
                    <AnimatedBorderlessButton onPress={() => onSelect(item)}>
                      <View accessible accessibilityRole="button">
                        <Text style={styles.text}>{item}</Text>
                      </View>
                    </AnimatedBorderlessButton>
                )}
            />
        </Animated.View>
    );
};

function HomeScreen() {
    const insets = useSafeAreaInsets();
    const [selectedCategory, setSelectedCategory] = useState('');
    const [options, setOptions] = useState<string[]>([]);

    const styles = StyleSheet.create({
        container: {
            flex: 1,
            paddingTop: insets.top,
            backgroundColor: 'white',
        },
        categoryContainer: {
            gap: 16,
            paddingHorizontal: 24,
        },
    });
    const dummyData = {
        categoryOptions: (Array.from({length: 14}, (_, i) => `Hello World ${i + 1}`)),
    };

    return (
        <View style={styles.container}>
            <AnimatedBorderlessButton
              onPress={() => setOptions(dummyData.categoryOptions)}
              entering={FadeInLeft}
              exiting={FadeOutLeft}
            >
                <View accessible accessibilityRole="button">
                    <Text>{selectedCategory.length > 0 ? selectedCategory : 'Courses'}</Text>
                </View>
            </AnimatedBorderlessButton>
            {selectedCategory.length > 0 && (
                <AnimatedBorderlessButton
                    onPress={() => alert('It Worked')}
                    entering={FadeInLeft}
                    exiting={FadeOutLeft}
                >
                  <View accessible accessibilityRole="button">
                      <Text>All Courses</Text>
                  </View>
                </AnimatedBorderlessButton>
            )}
            {options.length > 0 && (
                <OptionsScreen
                    options={options}
                    onSelect={(selectedOption) => {
                        setSelectedCategory(selectedOption ?? '');
                        setOptions([]);
                    }}
                />
            )}
        </View>
    );
}

export default function TabTwoScreen() {
  return (
    <HomeScreen />
  );
}
```

</details>

---------

Co-authored-by: Michał Bert <[email protected]>
  • Loading branch information
jcolicchio and m-bert authored Feb 10, 2025
1 parent 1f9f20b commit 7c3e002
Showing 1 changed file with 12 additions and 2 deletions.
14 changes: 12 additions & 2 deletions apple/RNGestureHandlerButton.mm
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,20 @@ - (BOOL)shouldHandleTouch:(RNGHUIView *)view
return button.userEnabled;
}

// Certain subviews such as RCTViewComponentView have been observed to have disabled
// accessibility gesture recognizers such as _UIAccessibilityHUDGateGestureRecognizer,
// ostensibly set by iOS. Such gesture recognizers cause this function to return YES
// even when the passed view is static text and does not respond to touches. This in
// turn prevents the button from receiving touches, breaking functionality. To handle
// such case, we can count only the enabled gesture recognizers when determining
// whether a view should receive touches.
NSPredicate *isEnabledPredicate = [NSPredicate predicateWithFormat:@"isEnabled == YES"];
NSArray *enabledGestureRecognizers = [view.gestureRecognizers filteredArrayUsingPredicate:isEnabledPredicate];

#if !TARGET_OS_OSX
return [view isKindOfClass:[UIControl class]] || [view.gestureRecognizers count] > 0;
return [view isKindOfClass:[UIControl class]] || [enabledGestureRecognizers count] > 0;
#else
return [view isKindOfClass:[NSControl class]] || [view.gestureRecognizers count] > 0;
return [view isKindOfClass:[NSControl class]] || [enabledGestureRecognizers count] > 0;
#endif
}

Expand Down

0 comments on commit 7c3e002

Please sign in to comment.