Repository navigation
[iOS] Preserve disabled button targets under box-none parents - #4506
Conversation
Keep RNGestureHandlerButton child hits in the box-none hit-test path so disabled foreground controls continue to block overlapping siblings instead of allowing touch-through.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughThe change updates native Apple button touch handling. It rejects disabled pointer-event states and accepts nested ChangesNested button hit testing
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The iOS hit-testing update preserves disabled button hits while skipping BoxNone and None containers, preventing touch-through without activating disabled controls. No merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🟢 Approval recommended
The change is small, localized to the box-none hit-test branch, and directly aligns behavior with the described iOS reproduction without introducing broader behavioral changes.
Pull request overview
This PR adjusts iOS hit-testing in RNGestureHandlerButton to preserve disabled RNGH button targets when the parent uses pointerEvents="box-none", preventing enabled siblings “underneath” from incorrectly receiving the touch.
Changes:
- Updates the
pointerEvents == BoxNonehit-test loop to returnRNGestureHandlerButtonhits even whenuserEnabledisfalse. - Keeps existing
shouldHandleTouch:filtering behavior for non-button hit targets.
File summaries
| File | Description |
|---|---|
| packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm | Adjusts box-none hit-testing to preserve disabled RNGestureHandlerButton targets so underlying siblings don’t take the touch. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@m-bert I believe this is all good? 🫡 |
|
HI @rileysay! I'm currently investigating one edge case that may be a consequence of this PR, I'll let you know once we decide 😅 |
|
Hi @rileysay! I'm back and I think I have what we need! So the edge case I was talking about looks like this. Given the nested buttons structure, let's say Here you can see example structure:<Touchable onPress={() => log('A')}>
<Touchable pointerEvents="box-none" onPress={() => log('B')}>
<Touchable disabled onPress={() => log('C')}>
<Text>C</Text>
</Touchable>
</Touchable>
</Touchable>Seems like this simple change in if ([view isKindOfClass:[RNGestureHandlerButton class]]) {
RNGestureHandlerButton *button = (RNGestureHandlerButton *)view;
- return button.userEnabled;
+ return button.userEnabled && button.pointerEvents != RNGestureHandlerPointerEventsBoxNone &&
+ button.pointerEvents != RNGestureHandlerPointerEventsNone;
}
I'd be glad if you could confirm that it works 😅 Also remember to run |
|
@m-bert Ah yep, you’re right 😅 I cooked up a little pointer-events matrix and indeed see the edge-case. I’ve applied your suggested change and don’t see any issues with it. |
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.
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.
Description
Follow-up to #4495.
When an RNGH
Touchable/PressableusespointerEvents="box-none", a disabledRNGH button returned from one of its child hit tests is rejected by
shouldHandleTouch:becauseuserEnabledis false.The
box-nonebranch then continues checking lower siblings, which allows anenabled 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-nonepath now preservesRNGestureHandlerButtonchild hits insteadof discarding them based on their enabled state.
Device observations
Reproduced on iOS with both RNGH
Touchableand RNGHPressable.With an RNGH parent using
pointerEvents="box-none":Changing only the parent to a React Native
Viewwith the samepointerEvents="box-none":Test example
Test plan
Test on a physical iOS device with both RNGH
Touchableand RNGHPressable.For each component:
pointerEvents="box-none".onPress.onPress.ViewusingpointerEvents="box-none"and repeat.Expected:
box-noneparentViewbox-noneparentBefore 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.