Skip to content

Commit

Permalink
fix(iOS): not working hitslop for headerRight/Left views (#1995)
Browse files Browse the repository at this point in the history
## Description

Since #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 #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
  • Loading branch information
kkafar authored Dec 27, 2023
1 parent c2b68ba commit 5af4b0c
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 3 deletions.
1 change: 1 addition & 0 deletions FabricTestExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import Test1802 from './src/Test1802';
import Test1829 from './src/Test1829';
import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';
import Test1981 from './src/Test1981';

enableFreeze(true);

Expand Down
103 changes: 103 additions & 0 deletions FabricTestExample/src/Test1981.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import { NavigationContainer, NavigationContext, ParamListBase } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { View, StyleSheet, Button, Pressable, Text } from 'react-native';

type NavProp = {
navigation: NativeStackNavigationProp<ParamListBase>;
};

const Stack = createNativeStackNavigator();

function FirstScreen({ navigation }: NavProp) {
const navigateToSecond = () => {
navigation.navigate('Second');
};
return (
<View style={[styles.redbox, styles.centeredView]}>
<Button title="Navigate to Second" onPress={navigateToSecond} />
<PressableWithHitSlop />
</View>
);
}

function SecondScreen({ navigation }: NavProp) {
const navigateToFirst = () => {
navigation.navigate('First');
};

return (
<View style={[styles.greenbox, styles.centeredView]}>
<Button title="Navigate to First" onPress={navigateToFirst} />
</View>
);
}

function HeaderLeft() {
const onPressCallback = () => {
console.log('HeaderLeft onPressCallback invoked');
};

return (
<Pressable style={[styles.bluebox]} hitSlop={12} onPress={onPressCallback}>
<Text style={{ color: 'white' }}>Press me</Text>
</Pressable>
);
}

function PressableWithHitSlop() {
const onPressCallback = () => {
console.log('PressableWithHitSlop onPressCallback invoked');
};

return (
<View
style={{
padding: 12,
margin: -12,
backgroundColor: 'yellow',
}}>
<Pressable
style={[styles.greenbox]}
hitSlop={12}
onPress={onPressCallback}>
<Text style={{ color: 'white' }}>Press me</Text>
</Pressable>
</View>
);
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="First"
component={FirstScreen}
options={{
headerLeft: () => HeaderLeft(),
headerRight: () => PressableWithHitSlop(),
}}
/>
<Stack.Screen name="Second" component={SecondScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
redbox: {
backgroundColor: 'red',
},
greenbox: {
backgroundColor: 'green',
},
bluebox: {
backgroundColor: 'blue',
},
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
1 change: 1 addition & 0 deletions TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import Test1802 from './src/Test1802';
import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';
import Test1829 from './src/Test1829';
import Test1981 from './src/Test1981';

enableFreeze(true);

Expand Down
103 changes: 103 additions & 0 deletions TestsExample/src/Test1981.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import { NavigationContainer, NavigationContext, ParamListBase } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { View, StyleSheet, Button, Pressable, Text } from 'react-native';

type NavProp = {
navigation: NativeStackNavigationProp<ParamListBase>;
};

const Stack = createNativeStackNavigator();

function FirstScreen({ navigation }: NavProp) {
const navigateToSecond = () => {
navigation.navigate('Second');
};
return (
<View style={[styles.redbox, styles.centeredView]}>
<Button title="Navigate to Second" onPress={navigateToSecond} />
<PressableWithHitSlop />
</View>
);
}

function SecondScreen({ navigation }: NavProp) {
const navigateToFirst = () => {
navigation.navigate('First');
};

return (
<View style={[styles.greenbox, styles.centeredView]}>
<Button title="Navigate to First" onPress={navigateToFirst} />
</View>
);
}

function HeaderLeft() {
const onPressCallback = () => {
console.log('HeaderLeft onPressCallback invoked');
};

return (
<Pressable style={[styles.bluebox]} hitSlop={12} onPress={onPressCallback}>
<Text style={{ color: 'white' }}>Press me</Text>
</Pressable>
);
}

function PressableWithHitSlop() {
const onPressCallback = () => {
console.log('PressableWithHitSlop onPressCallback invoked');
};

return (
<View
style={{
padding: 12,
margin: -12,
backgroundColor: 'yellow',
}}>
<Pressable
style={[styles.greenbox]}
hitSlop={12}
onPress={onPressCallback}>
<Text style={{ color: 'white' }}>Press me</Text>
</Pressable>
</View>
);
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="First"
component={FirstScreen}
options={{
headerLeft: () => HeaderLeft(),
headerRight: () => PressableWithHitSlop(),
}}
/>
<Stack.Screen name="Second" component={SecondScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
redbox: {
backgroundColor: 'red',
},
greenbox: {
backgroundColor: 'green',
},
bluebox: {
backgroundColor: 'blue',
},
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
8 changes: 7 additions & 1 deletion ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -560,13 +560,19 @@ - (void)presentationControllerDidDismiss:(UIPresentationController *)presentatio
}
}

- (RNSScreenStackHeaderConfig *_Nullable)findHeaderConfig
- (nullable RNSScreenStackHeaderConfig *)findHeaderConfig
{
// Fast path
if ([self.reactSubviews.lastObject isKindOfClass:RNSScreenStackHeaderConfig.class]) {
return (RNSScreenStackHeaderConfig *)self.reactSubviews.lastObject;
}

for (UIView *view in self.reactSubviews) {
if ([view isKindOfClass:RNSScreenStackHeaderConfig.class]) {
return (RNSScreenStackHeaderConfig *)view;
}
}

return nil;
}

Expand Down
4 changes: 2 additions & 2 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -908,8 +908,8 @@ - (BOOL)isInGestureResponseDistance:(UIGestureRecognizer *)gestureRecognizer top
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(_controller.navigationBar.frame, point)) {
// headerConfig should be the first subview of the topmost screen
UIView *headerConfig = [[_reactSubviews.lastObject reactSubviews] firstObject];
RNSScreenView *topMostScreen = (RNSScreenView *)_reactSubviews.lastObject;
UIView *headerConfig = topMostScreen.findHeaderConfig;
if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event];
if (headerHitTestResult != nil) {
Expand Down

0 comments on commit 5af4b0c

Please sign in to comment.