Skip to content

Commit 99444cb

Browse files
kkafarja1ns
authored andcommitted
fix(iOS): not working hitslop for headerRight/Left views (software-mansion#1995)
## Description Since software-mansion#1825 header config is no longer first child of a screen & `hitTest:withEvent:` method assumed this invariant to be true. Fixed that by using appropriate screen method instead of blind assumption. Fixes software-mansion#1981 ## Changes * Fixed `hitTest:withEvent:` method by using `findHeaderConfig` `RNSScreenView`'s method * Improved `findHeaderConfig` method itself ## Test code and steps to reproduce `Test1981` ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
1 parent a87a399 commit 99444cb

File tree

6 files changed

+217
-3
lines changed

6 files changed

+217
-3
lines changed

FabricTestExample/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import Test1802 from './src/Test1802';
9090
import Test1829 from './src/Test1829';
9191
import Test1844 from './src/Test1844';
9292
import Test1864 from './src/Test1864';
93+
import Test1981 from './src/Test1981';
9394

9495
enableFreeze(true);
9596

FabricTestExample/src/Test1981.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from 'react';
2+
import { NavigationContainer, NavigationContext, ParamListBase } from '@react-navigation/native';
3+
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
4+
import { View, StyleSheet, Button, Pressable, Text } from 'react-native';
5+
6+
type NavProp = {
7+
navigation: NativeStackNavigationProp<ParamListBase>;
8+
};
9+
10+
const Stack = createNativeStackNavigator();
11+
12+
function FirstScreen({ navigation }: NavProp) {
13+
const navigateToSecond = () => {
14+
navigation.navigate('Second');
15+
};
16+
return (
17+
<View style={[styles.redbox, styles.centeredView]}>
18+
<Button title="Navigate to Second" onPress={navigateToSecond} />
19+
<PressableWithHitSlop />
20+
</View>
21+
);
22+
}
23+
24+
function SecondScreen({ navigation }: NavProp) {
25+
const navigateToFirst = () => {
26+
navigation.navigate('First');
27+
};
28+
29+
return (
30+
<View style={[styles.greenbox, styles.centeredView]}>
31+
<Button title="Navigate to First" onPress={navigateToFirst} />
32+
</View>
33+
);
34+
}
35+
36+
function HeaderLeft() {
37+
const onPressCallback = () => {
38+
console.log('HeaderLeft onPressCallback invoked');
39+
};
40+
41+
return (
42+
<Pressable style={[styles.bluebox]} hitSlop={12} onPress={onPressCallback}>
43+
<Text style={{ color: 'white' }}>Press me</Text>
44+
</Pressable>
45+
);
46+
}
47+
48+
function PressableWithHitSlop() {
49+
const onPressCallback = () => {
50+
console.log('PressableWithHitSlop onPressCallback invoked');
51+
};
52+
53+
return (
54+
<View
55+
style={{
56+
padding: 12,
57+
margin: -12,
58+
backgroundColor: 'yellow',
59+
}}>
60+
<Pressable
61+
style={[styles.greenbox]}
62+
hitSlop={12}
63+
onPress={onPressCallback}>
64+
<Text style={{ color: 'white' }}>Press me</Text>
65+
</Pressable>
66+
</View>
67+
);
68+
}
69+
70+
export default function App() {
71+
return (
72+
<NavigationContainer>
73+
<Stack.Navigator>
74+
<Stack.Screen
75+
name="First"
76+
component={FirstScreen}
77+
options={{
78+
headerLeft: () => HeaderLeft(),
79+
headerRight: () => PressableWithHitSlop(),
80+
}}
81+
/>
82+
<Stack.Screen name="Second" component={SecondScreen} />
83+
</Stack.Navigator>
84+
</NavigationContainer>
85+
);
86+
}
87+
88+
const styles = StyleSheet.create({
89+
redbox: {
90+
backgroundColor: 'red',
91+
},
92+
greenbox: {
93+
backgroundColor: 'green',
94+
},
95+
bluebox: {
96+
backgroundColor: 'blue',
97+
},
98+
centeredView: {
99+
flex: 1,
100+
justifyContent: 'center',
101+
alignItems: 'center',
102+
},
103+
});

TestsExample/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import Test1802 from './src/Test1802';
9393
import Test1844 from './src/Test1844';
9494
import Test1864 from './src/Test1864';
9595
import Test1829 from './src/Test1829';
96+
import Test1981 from './src/Test1981';
9697

9798
enableFreeze(true);
9899

TestsExample/src/Test1981.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from 'react';
2+
import { NavigationContainer, NavigationContext, ParamListBase } from '@react-navigation/native';
3+
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
4+
import { View, StyleSheet, Button, Pressable, Text } from 'react-native';
5+
6+
type NavProp = {
7+
navigation: NativeStackNavigationProp<ParamListBase>;
8+
};
9+
10+
const Stack = createNativeStackNavigator();
11+
12+
function FirstScreen({ navigation }: NavProp) {
13+
const navigateToSecond = () => {
14+
navigation.navigate('Second');
15+
};
16+
return (
17+
<View style={[styles.redbox, styles.centeredView]}>
18+
<Button title="Navigate to Second" onPress={navigateToSecond} />
19+
<PressableWithHitSlop />
20+
</View>
21+
);
22+
}
23+
24+
function SecondScreen({ navigation }: NavProp) {
25+
const navigateToFirst = () => {
26+
navigation.navigate('First');
27+
};
28+
29+
return (
30+
<View style={[styles.greenbox, styles.centeredView]}>
31+
<Button title="Navigate to First" onPress={navigateToFirst} />
32+
</View>
33+
);
34+
}
35+
36+
function HeaderLeft() {
37+
const onPressCallback = () => {
38+
console.log('HeaderLeft onPressCallback invoked');
39+
};
40+
41+
return (
42+
<Pressable style={[styles.bluebox]} hitSlop={12} onPress={onPressCallback}>
43+
<Text style={{ color: 'white' }}>Press me</Text>
44+
</Pressable>
45+
);
46+
}
47+
48+
function PressableWithHitSlop() {
49+
const onPressCallback = () => {
50+
console.log('PressableWithHitSlop onPressCallback invoked');
51+
};
52+
53+
return (
54+
<View
55+
style={{
56+
padding: 12,
57+
margin: -12,
58+
backgroundColor: 'yellow',
59+
}}>
60+
<Pressable
61+
style={[styles.greenbox]}
62+
hitSlop={12}
63+
onPress={onPressCallback}>
64+
<Text style={{ color: 'white' }}>Press me</Text>
65+
</Pressable>
66+
</View>
67+
);
68+
}
69+
70+
export default function App() {
71+
return (
72+
<NavigationContainer>
73+
<Stack.Navigator>
74+
<Stack.Screen
75+
name="First"
76+
component={FirstScreen}
77+
options={{
78+
headerLeft: () => HeaderLeft(),
79+
headerRight: () => PressableWithHitSlop(),
80+
}}
81+
/>
82+
<Stack.Screen name="Second" component={SecondScreen} />
83+
</Stack.Navigator>
84+
</NavigationContainer>
85+
);
86+
}
87+
88+
const styles = StyleSheet.create({
89+
redbox: {
90+
backgroundColor: 'red',
91+
},
92+
greenbox: {
93+
backgroundColor: 'green',
94+
},
95+
bluebox: {
96+
backgroundColor: 'blue',
97+
},
98+
centeredView: {
99+
flex: 1,
100+
justifyContent: 'center',
101+
alignItems: 'center',
102+
},
103+
});

ios/RNSScreen.mm

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,13 +560,19 @@ - (void)presentationControllerDidDismiss:(UIPresentationController *)presentatio
560560
}
561561
}
562562

563-
- (RNSScreenStackHeaderConfig *_Nullable)findHeaderConfig
563+
- (nullable RNSScreenStackHeaderConfig *)findHeaderConfig
564564
{
565+
// Fast path
566+
if ([self.reactSubviews.lastObject isKindOfClass:RNSScreenStackHeaderConfig.class]) {
567+
return (RNSScreenStackHeaderConfig *)self.reactSubviews.lastObject;
568+
}
569+
565570
for (UIView *view in self.reactSubviews) {
566571
if ([view isKindOfClass:RNSScreenStackHeaderConfig.class]) {
567572
return (RNSScreenStackHeaderConfig *)view;
568573
}
569574
}
575+
570576
return nil;
571577
}
572578

ios/RNSScreenStack.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,8 @@ - (BOOL)isInGestureResponseDistance:(UIGestureRecognizer *)gestureRecognizer top
908908
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
909909
{
910910
if (CGRectContainsPoint(_controller.navigationBar.frame, point)) {
911-
// headerConfig should be the first subview of the topmost screen
912-
UIView *headerConfig = [[_reactSubviews.lastObject reactSubviews] firstObject];
911+
RNSScreenView *topMostScreen = (RNSScreenView *)_reactSubviews.lastObject;
912+
UIView *headerConfig = topMostScreen.findHeaderConfig;
913913
if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
914914
UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event];
915915
if (headerHitTestResult != nil) {

0 commit comments

Comments
 (0)