Skip to content

Commit 08f030f

Browse files
rileysaym-bert
authored andcommitted
[iOS] Preserve disabled button targets under box-none parents (#4506)
Follow-up to #4495. When an RNGH `Touchable`/`Pressable` uses `pointerEvents="box-none"`, a disabled RNGH button returned from one of its child hit tests is rejected by `shouldHandleTouch:` because `userEnabled` is false. The `box-none` branch then continues checking lower siblings, which allows an enabled sibling underneath the disabled control to receive the touch. This is inconsistent with the behavior preserved in #4495, where a disabled button can still remain the hit target so controls underneath don't take over the touch, while the disabled button itself doesn't activate. The `box-none` path now preserves `RNGestureHandlerButton` child hits instead of discarding them based on their enabled state. Reproduced on iOS with both RNGH `Touchable` and RNGH `Pressable`. With an RNGH parent using `pointerEvents="box-none"`: - tapping the disabled foreground triggers the enabled sibling underneath - tapping the exposed background also triggers the background, as expected Changing only the parent to a React Native `View` with the same `pointerEvents="box-none"`: - tapping the disabled foreground does nothing - tapping the exposed background triggers the background <details> <summary>Test example</summary> ```tsx import React, { useState } from 'react'; import { Pressable as RNPressable, StyleSheet, Text, View, } from 'react-native'; import { Pressable as RNGHPressable, Touchable, } from 'react-native-gesture-handler'; type ButtonType = 'touchable' | 'pressable'; export default function BoxNoneDisabledTargetExample() { const [buttonType, setButtonType] = useState<ButtonType>('touchable'); const [useRNParent, setUseRNParent] = useState(false); const [backgroundPresses, setBackgroundPresses] = useState(0); const Button = buttonType === 'touchable' ? Touchable : RNGHPressable; const children = ( <> <Button onPress={() => setBackgroundPresses((count) => count + 1)} style={[StyleSheet.absoluteFillObject, styles.background]}> <Text>Enabled background</Text> </Button> <Button disabled style={styles.disabledForeground}> <Text>Disabled foreground</Text> </Button> </> ); return ( <View style={styles.container}> <RNPressable onPress={() => setButtonType((current) => current === 'touchable' ? 'pressable' : 'touchable' ) }> <Text>Button type: {buttonType}</Text> </RNPressable> <RNPressable onPress={() => setUseRNParent((value) => !value)}> <Text> Parent: {useRNParent ? 'React Native View' : 'RNGH button'} </Text> </RNPressable> {useRNParent ? ( <View pointerEvents="box-none" style={styles.frame}> {children} </View> ) : ( <Touchable pointerEvents="box-none" style={styles.frame}> {children} </Touchable> )} <Text>Background presses: {backgroundPresses}</Text> </View> ); } const styles = StyleSheet.create({ container: { padding: 16, gap: 12, }, frame: { height: 150, }, background: { backgroundColor: '#9fd8a8', justifyContent: 'flex-end', padding: 8, }, disabledForeground: { position: 'absolute', left: 40, right: 24, top: 20, height: 85, backgroundColor: '#9a9a9a', alignItems: 'center', justifyContent: 'center', }, }); ``` </details> Test on a physical iOS device with both RNGH `Touchable` and RNGH `Pressable`. For each component: 1. Use an RNGH parent with `pointerEvents="box-none"`. 2. Tap well inside the disabled foreground. 3. Verify the enabled background does **not** receive `onPress`. 4. Tap the exposed background area. 5. Verify the background does receive `onPress`. 6. Replace only the parent with a React Native `View` using `pointerEvents="box-none"` and repeat. Expected: | Parent | Disabled foreground | Exposed background | | --- | --- | --- | | RNGH `box-none` parent | no press | background press | | RN `View` `box-none` parent | no press | background press | Before this change, the RNGH-parent case incorrectly sends the disabled foreground tap to the background sibling. Used AI to help inspect the surrounding hit-test paths and prepare the test case and patch. I personally reproduced the issue on device.
1 parent 2219eca commit 08f030f

1 file changed

Lines changed: 4 additions & 2 deletions

File tree

‎packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ - (BOOL)shouldHandleTouch:(RNGHUIView *)view
6262
{
6363
if ([view isKindOfClass:[RNGestureHandlerButton class]]) {
6464
RNGestureHandlerButton *button = (RNGestureHandlerButton *)view;
65-
return button.userEnabled;
65+
return button.userEnabled && button.pointerEvents != RNGestureHandlerPointerEventsBoxNone &&
66+
button.pointerEvents != RNGestureHandlerPointerEventsNone;
6667
}
6768

6869
// Certain subviews such as RCTViewComponentView have been observed to have disabled
@@ -105,7 +106,8 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
105106
if (!subview.isHidden && subview.alpha > 0) {
106107
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
107108
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
108-
if (hitView != nil && [self shouldHandleTouch:hitView]) {
109+
if (hitView != nil &&
110+
([hitView isKindOfClass:[RNGestureHandlerButton class]] || [self shouldHandleTouch:hitView])) {
109111
return hitView;
110112
}
111113
}

0 commit comments

Comments
 (0)