Skip to content

Commit 9703a2a

Browse files
authored
[iOS] Preserve disabled button targets under box-none parents (#4506)
## Description 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. ## Device observations 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 plan 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. ## AI assistance 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 1c8af36 commit 9703a2a

1 file changed

Lines changed: 5 additions & 2 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,8 @@ - (BOOL)shouldHandleTouch:(RNGHUIView *)view atPoint:(CGPoint)point
14541454
{
14551455
if ([view isKindOfClass:[RNGestureHandlerButton class]]) {
14561456
RNGestureHandlerButton *button = (RNGestureHandlerButton *)view;
1457-
return button.userEnabled;
1457+
return button.userEnabled && button.pointerEvents != RNGestureHandlerPointerEventsBoxNone &&
1458+
button.pointerEvents != RNGestureHandlerPointerEventsNone;
14581459
}
14591460

14601461
// Certain subviews such as RCTViewComponentView have been observed to have disabled
@@ -1501,7 +1502,9 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
15011502
if (!subview.isHidden && subview.alpha > 0) {
15021503
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
15031504
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
1504-
if (hitView != nil && [self shouldHandleTouch:hitView atPoint:point]) {
1505+
if (hitView != nil &&
1506+
([hitView isKindOfClass:[RNGestureHandlerButton class]] ||
1507+
[self shouldHandleTouch:hitView atPoint:point])) {
15051508
return hitView;
15061509
}
15071510
}

0 commit comments

Comments
 (0)