Skip to content

Commit 7c3e002

Browse files
jcolicchiom-bert
andauthored
Return false if gesture recognizers are present but all disabled (#3377)
## 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]>
1 parent 1f9f20b commit 7c3e002

File tree

1 file changed

+12
-2
lines changed

1 file changed

+12
-2
lines changed

apple/RNGestureHandlerButton.mm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,20 @@ - (BOOL)shouldHandleTouch:(RNGHUIView *)view
6464
return button.userEnabled;
6565
}
6666

67+
// Certain subviews such as RCTViewComponentView have been observed to have disabled
68+
// accessibility gesture recognizers such as _UIAccessibilityHUDGateGestureRecognizer,
69+
// ostensibly set by iOS. Such gesture recognizers cause this function to return YES
70+
// even when the passed view is static text and does not respond to touches. This in
71+
// turn prevents the button from receiving touches, breaking functionality. To handle
72+
// such case, we can count only the enabled gesture recognizers when determining
73+
// whether a view should receive touches.
74+
NSPredicate *isEnabledPredicate = [NSPredicate predicateWithFormat:@"isEnabled == YES"];
75+
NSArray *enabledGestureRecognizers = [view.gestureRecognizers filteredArrayUsingPredicate:isEnabledPredicate];
76+
6777
#if !TARGET_OS_OSX
68-
return [view isKindOfClass:[UIControl class]] || [view.gestureRecognizers count] > 0;
78+
return [view isKindOfClass:[UIControl class]] || [enabledGestureRecognizers count] > 0;
6979
#else
70-
return [view isKindOfClass:[NSControl class]] || [view.gestureRecognizers count] > 0;
80+
return [view isKindOfClass:[NSControl class]] || [enabledGestureRecognizers count] > 0;
7181
#endif
7282
}
7383

0 commit comments

Comments
 (0)