mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-15 22:45:41 +08:00
Compare commits
20 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34c907ec0a | ||
|
|
1ae07af796 | ||
|
|
220af93db5 | ||
|
|
1f27e4b1f6 | ||
|
|
9c06a92d09 | ||
|
|
e0e0f79793 | ||
|
|
c7e4bf94e6 | ||
|
|
7024d4bb81 | ||
|
|
21f61d6eeb | ||
|
|
8774ca97e1 | ||
|
|
e653d55479 | ||
|
|
78afbffe97 | ||
|
|
762cc44578 | ||
|
|
c3bd349d77 | ||
|
|
5dcaf903f3 | ||
|
|
2d66ef93ec | ||
|
|
4fe72e3ce7 | ||
|
|
ab1f79c096 | ||
|
|
9305bfa939 | ||
|
|
0c3c450f5f |
6
.github/workflows/expo-preview.yml
vendored
6
.github/workflows/expo-preview.yml
vendored
@@ -27,10 +27,8 @@ jobs:
|
|||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
uses: actions/cache@master
|
uses: actions/cache@master
|
||||||
with:
|
with:
|
||||||
path: |
|
path: '**/node_modules'
|
||||||
node_modules
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
*/*/node_modules
|
|
||||||
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||||
|
|||||||
6
.github/workflows/expo.yml
vendored
6
.github/workflows/expo.yml
vendored
@@ -29,10 +29,8 @@ jobs:
|
|||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
uses: actions/cache@master
|
uses: actions/cache@master
|
||||||
with:
|
with:
|
||||||
path: |
|
path: '**/node_modules'
|
||||||
node_modules
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
*/*/node_modules
|
|
||||||
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ app.use(async (ctx) => {
|
|||||||
>
|
>
|
||||||
${css}
|
${css}
|
||||||
<title>${ref.current?.getCurrentOptions()?.title}</title>
|
<title>${ref.current?.getCurrentOptions()?.title}</title>
|
||||||
<body style="height: 100%">
|
<body style="min-height: 100%">
|
||||||
<div id="root" style="display: flex; height: 100%">
|
<div id="root" style="display: flex; min-height: 100vh">
|
||||||
${html}
|
${html}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -71,13 +71,12 @@ export default function BottomTabsScreen() {
|
|||||||
>
|
>
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
name="Article"
|
name="Article"
|
||||||
|
component={SimpleStackScreen}
|
||||||
options={{
|
options={{
|
||||||
title: 'Article',
|
title: 'Article',
|
||||||
tabBarIcon: getTabBarIcon('file-document-box'),
|
tabBarIcon: getTabBarIcon('file-document-box'),
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
|
|
||||||
</BottomTabs.Screen>
|
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
name="Chat"
|
name="Chat"
|
||||||
component={Chat}
|
component={Chat}
|
||||||
|
|||||||
@@ -22,14 +22,13 @@ export default function MaterialBottomTabsScreen() {
|
|||||||
<MaterialBottomTabs.Navigator barStyle={styles.tabBar}>
|
<MaterialBottomTabs.Navigator barStyle={styles.tabBar}>
|
||||||
<MaterialBottomTabs.Screen
|
<MaterialBottomTabs.Screen
|
||||||
name="Article"
|
name="Article"
|
||||||
|
component={SimpleStackScreen}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Article',
|
tabBarLabel: 'Article',
|
||||||
tabBarIcon: 'file-document-box',
|
tabBarIcon: 'file-document-box',
|
||||||
tabBarColor: '#C9E7F8',
|
tabBarColor: '#C9E7F8',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
|
|
||||||
</MaterialBottomTabs.Screen>
|
|
||||||
<MaterialBottomTabs.Screen
|
<MaterialBottomTabs.Screen
|
||||||
name="Chat"
|
name="Chat"
|
||||||
component={Chat}
|
component={Chat}
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
|
|||||||
return (
|
return (
|
||||||
<ModalPresentationStack.Navigator
|
<ModalPresentationStack.Navigator
|
||||||
mode="modal"
|
mode="modal"
|
||||||
headerMode="screen"
|
|
||||||
screenOptions={({ route, navigation }) => ({
|
screenOptions={({ route, navigation }) => ({
|
||||||
...TransitionPresets.ModalPresentationIOS,
|
...TransitionPresets.ModalPresentationIOS,
|
||||||
cardOverlayEnabled: true,
|
cardOverlayEnabled: true,
|
||||||
|
|||||||
40
example/src/Screens/NotFound.tsx
Normal file
40
example/src/Screens/NotFound.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
|
import { Button } from 'react-native-paper';
|
||||||
|
|
||||||
|
const NotFoundScreen = ({
|
||||||
|
navigation,
|
||||||
|
}: {
|
||||||
|
navigation: StackNavigationProp<{ Home: undefined }>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>404 Not Found</Text>
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
onPress={() => navigation.navigate('Home')}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Go to home
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundScreen;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
title: {
|
||||||
|
fontSize: 36,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
margin: 24,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -111,17 +111,17 @@ const AlbumsScreen = ({
|
|||||||
|
|
||||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||||
|
|
||||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
type Props = {
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
navigation: StackNavigationProp<ParamListBase>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
export default function SimpleStackScreen({ navigation }: Props) {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleStack.Navigator {...rest}>
|
<SimpleStack.Navigator>
|
||||||
<SimpleStack.Screen
|
<SimpleStack.Screen
|
||||||
name="Article"
|
name="Article"
|
||||||
component={ArticleScreen}
|
component={ArticleScreen}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
|
import {
|
||||||
|
Animated,
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
import { Button, Appbar } from 'react-native-paper';
|
import { Button, Appbar } from 'react-native-paper';
|
||||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
import { useTheme, RouteProp, ParamListBase } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackNavigationProp,
|
||||||
HeaderBackground,
|
HeaderBackground,
|
||||||
useHeaderHeight,
|
useHeaderHeight,
|
||||||
|
Header,
|
||||||
|
StackHeaderProps,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import BlurView from '../Shared/BlurView';
|
import BlurView from '../Shared/BlurView';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
@@ -91,11 +100,32 @@ type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
|||||||
navigation: StackNavigationProp<ParamListBase>;
|
navigation: StackNavigationProp<ParamListBase>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function CustomHeader(props: StackHeaderProps) {
|
||||||
|
const { current, next } = props.scene.progress;
|
||||||
|
|
||||||
|
const progress = Animated.add(current, next || 0);
|
||||||
|
const opacity = progress.interpolate({
|
||||||
|
inputRange: [0, 1, 2],
|
||||||
|
outputRange: [0, 1, 0],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header {...props} />
|
||||||
|
<Animated.Text style={[styles.banner, { opacity }]}>
|
||||||
|
Why hello there, pardner!
|
||||||
|
</Animated.Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { colors, dark } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleStack.Navigator {...rest}>
|
<SimpleStack.Navigator {...rest}>
|
||||||
<SimpleStack.Screen
|
<SimpleStack.Screen
|
||||||
@@ -103,6 +133,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
|||||||
component={ArticleScreen}
|
component={ArticleScreen}
|
||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
title: `Article by ${route.params?.author}`,
|
title: `Article by ${route.params?.author}`,
|
||||||
|
header: CustomHeader,
|
||||||
headerTintColor: '#fff',
|
headerTintColor: '#fff',
|
||||||
headerStyle: { backgroundColor: '#ff005d' },
|
headerStyle: { backgroundColor: '#ff005d' },
|
||||||
headerBackTitleVisible: false,
|
headerBackTitleVisible: false,
|
||||||
@@ -138,9 +169,15 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
|||||||
headerBackTitle: 'Back',
|
headerBackTitle: 'Back',
|
||||||
headerTransparent: true,
|
headerTransparent: true,
|
||||||
headerBackground: () => (
|
headerBackground: () => (
|
||||||
<HeaderBackground style={{ backgroundColor: 'transparent' }}>
|
<HeaderBackground
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<BlurView
|
<BlurView
|
||||||
tint="light"
|
tint={dark ? 'dark' : 'light'}
|
||||||
intensity={75}
|
intensity={75}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
/>
|
/>
|
||||||
@@ -160,4 +197,10 @@ const styles = StyleSheet.create({
|
|||||||
button: {
|
button: {
|
||||||
margin: 8,
|
margin: 8,
|
||||||
},
|
},
|
||||||
|
banner: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'tomato',
|
||||||
|
backgroundColor: 'papayawhip',
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
DarkTheme,
|
DarkTheme,
|
||||||
|
PathConfig,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createDrawerNavigator,
|
createDrawerNavigator,
|
||||||
@@ -48,6 +49,7 @@ import StackHeaderCustomization from './Screens/StackHeaderCustomization';
|
|||||||
import BottomTabs from './Screens/BottomTabs';
|
import BottomTabs from './Screens/BottomTabs';
|
||||||
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
||||||
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||||
|
import NotFound from './Screens/NotFound';
|
||||||
import DynamicTabs from './Screens/DynamicTabs';
|
import DynamicTabs from './Screens/DynamicTabs';
|
||||||
import AuthFlow from './Screens/AuthFlow';
|
import AuthFlow from './Screens/AuthFlow';
|
||||||
import CompatAPI from './Screens/CompatAPI';
|
import CompatAPI from './Screens/CompatAPI';
|
||||||
@@ -68,6 +70,7 @@ type RootDrawerParamList = {
|
|||||||
|
|
||||||
type RootStackParamList = {
|
type RootStackParamList = {
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
|
NotFound: undefined;
|
||||||
} & {
|
} & {
|
||||||
[P in keyof typeof SCREENS]: undefined;
|
[P in keyof typeof SCREENS]: undefined;
|
||||||
};
|
};
|
||||||
@@ -221,35 +224,45 @@ export default function App() {
|
|||||||
Root: {
|
Root: {
|
||||||
path: '',
|
path: '',
|
||||||
initialRouteName: 'Home',
|
initialRouteName: 'Home',
|
||||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
screens: Object.keys(SCREENS).reduce<PathConfig>(
|
||||||
(acc, name) => {
|
(acc, name) => {
|
||||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||||
acc[name] = name
|
const path = name
|
||||||
.replace(/([A-Z]+)/g, '-$1')
|
.replace(/([A-Z]+)/g, '-$1')
|
||||||
.replace(/^-/, '')
|
.replace(/^-/, '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
|
acc[name] = {
|
||||||
|
path,
|
||||||
|
screens: {
|
||||||
|
Article: {
|
||||||
|
path: 'article/:author?',
|
||||||
|
parse: {
|
||||||
|
author: (author) =>
|
||||||
|
author.charAt(0).toUpperCase() +
|
||||||
|
author.slice(1).replace(/-/g, ' '),
|
||||||
|
},
|
||||||
|
stringify: {
|
||||||
|
author: (author: string) =>
|
||||||
|
author.toLowerCase().replace(/\s/g, '-'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Albums: 'music',
|
||||||
|
Chat: 'chat',
|
||||||
|
Contacts: 'people',
|
||||||
|
NewsFeed: 'feed',
|
||||||
|
Dialog: 'dialog',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{ Home: '' }
|
{
|
||||||
|
Home: '',
|
||||||
|
NotFound: '*',
|
||||||
|
}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
Article: {
|
|
||||||
path: 'article/:author?',
|
|
||||||
parse: {
|
|
||||||
author: (author) =>
|
|
||||||
author.charAt(0).toUpperCase() +
|
|
||||||
author.slice(1).replace(/-/g, ' '),
|
|
||||||
},
|
|
||||||
stringify: {
|
|
||||||
author: (author: string) =>
|
|
||||||
author.toLowerCase().replace(/\s/g, '-'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Albums: 'music',
|
|
||||||
Chat: 'chat',
|
|
||||||
Contacts: 'people',
|
|
||||||
NewsFeed: 'feed',
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fallback={<Text>Loading…</Text>}
|
fallback={<Text>Loading…</Text>}
|
||||||
@@ -332,6 +345,11 @@ export default function App() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
</Stack.Screen>
|
</Stack.Screen>
|
||||||
|
<Stack.Screen
|
||||||
|
name="NotFound"
|
||||||
|
component={NotFound}
|
||||||
|
options={{ title: 'Oops!' }}
|
||||||
|
/>
|
||||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||||
(name) => (
|
(name) => (
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.5.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.1...@react-navigation/bottom-tabs@5.5.2) (2020-06-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.0...@react-navigation/bottom-tabs@5.5.1) (2020-05-27)
|
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.0...@react-navigation/bottom-tabs@5.5.1) (2020-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/bottom-tabs",
|
"name": "@react-navigation/bottom-tabs",
|
||||||
"description": "Bottom tab navigator following iOS design guidelines",
|
"description": "Bottom tab navigator following iOS design guidelines",
|
||||||
"version": "5.5.1",
|
"version": "5.5.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
"@react-navigation/native": "^5.5.0",
|
"@react-navigation/native": "^5.5.1",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.26](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.25...@react-navigation/compat@5.1.26) (2020-06-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.1.25](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.24...@react-navigation/compat@5.1.25) (2020-05-27)
|
## [5.1.25](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.24...@react-navigation/compat@5.1.25) (2020-05-27)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/compat
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/compat",
|
"name": "@react-navigation/compat",
|
||||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||||
"version": "5.1.25",
|
"version": "5.1.26",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
"@react-navigation/native": "^5.5.0",
|
"@react-navigation/native": "^5.5.1",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
|
|||||||
@@ -3,6 +3,25 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [5.10.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.9.0...@react-navigation/core@5.10.0) (2020-06-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* catch missing params when they are required in navigate ([#8389](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8389)) ([8774ca9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/8774ca97e1da91e97677ecd816c85f66af296b93))
|
||||||
|
* make sure the wildcard pattern catches nested unmatched routes ([c3bd349](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3bd349d77688011c9c55027edd66c6f39de2ade))
|
||||||
|
* only use the query params for focused route in path ([2d66ef9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2d66ef93ec9923a452415c482c40e7c6b769917c))
|
||||||
|
* prevent state change being emitted unnecessarily ([ab1f79c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/ab1f79c096e94475a4da1acf1c850d04fb1bc4cf))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add wildcard patterns for paths ([4fe72e3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/4fe72e3ce7bae9120d04e490401f3bad58ebdf5c)), closes [#8019](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8019)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [5.9.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.2...@react-navigation/core@5.9.0) (2020-05-27)
|
# [5.9.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.2...@react-navigation/core@5.9.0) (2020-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/core",
|
"name": "@react-navigation/core",
|
||||||
"description": "Core utilities for building navigators",
|
"description": "Core utilities for building navigators",
|
||||||
"version": "5.9.0",
|
"version": "5.10.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
[getKey, getState, setKey, setState, state, addOptionsGetter]
|
[getKey, getState, setKey, setState, state, addOptionsGetter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onStateChangeRef = React.useRef(onStateChange);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onStateChangeRef.current = onStateChange;
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
if (
|
if (
|
||||||
@@ -263,12 +269,12 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
trackState(getRootState);
|
trackState(getRootState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFirstMountRef.current && onStateChange) {
|
if (!isFirstMountRef.current && onStateChangeRef.current) {
|
||||||
onStateChange(getRootState());
|
onStateChangeRef.current(getRootState());
|
||||||
}
|
}
|
||||||
|
|
||||||
isFirstMountRef.current = false;
|
isFirstMountRef.current = false;
|
||||||
}, [onStateChange, trackState, getRootState, emitter, state]);
|
}, [trackState, getRootState, emitter, state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
||||||
|
|||||||
@@ -1265,3 +1265,175 @@ it('replaces undefined query params', () => {
|
|||||||
expect(getPathFromState(state, config)).toBe(path);
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches wildcard patterns at root', () => {
|
||||||
|
const path = '/test/bar/42/whatever';
|
||||||
|
const config = {
|
||||||
|
404: '*',
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe('/404');
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe('/404');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches wildcard patterns at nested level', () => {
|
||||||
|
const path = '/bar/42/whatever/baz/initt';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
404: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
params: { id: '42' },
|
||||||
|
state: {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe('/bar/42/404');
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(
|
||||||
|
'/bar/42/404'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches wildcard patterns at nested level with exact', () => {
|
||||||
|
const path = '/whatever';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
404: {
|
||||||
|
path: '*',
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Baz: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe('/404');
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe('/404');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to match wildcard patterns at the end', () => {
|
||||||
|
const path = '/bar/42/test';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
404: '*',
|
||||||
|
Test: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
params: { id: '42' },
|
||||||
|
state: {
|
||||||
|
routes: [{ name: 'Test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses nearest parent wildcard match for unmatched paths', () => {
|
||||||
|
const path = '/bar/42/baz/test';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
Baz: 'baz',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe('/404');
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe('/404');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1883,3 +1883,183 @@ it('ignores extra slashes in the pattern', () => {
|
|||||||
state
|
state
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches wildcard patterns at root', () => {
|
||||||
|
const path = '/test/bar/42/whatever';
|
||||||
|
const config = {
|
||||||
|
404: '*',
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getStateFromPath(path, config)).toEqual(state);
|
||||||
|
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||||
|
state
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches wildcard patterns at nested level', () => {
|
||||||
|
const path = '/bar/42/whatever/baz/initt';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
404: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
params: { id: '42' },
|
||||||
|
state: {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getStateFromPath(path, config)).toEqual(state);
|
||||||
|
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||||
|
state
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches wildcard patterns at nested level with exact', () => {
|
||||||
|
const path = '/whatever';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
404: {
|
||||||
|
path: '*',
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Baz: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getStateFromPath(path, config)).toEqual(state);
|
||||||
|
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||||
|
state
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to match wildcard patterns at the end', () => {
|
||||||
|
const path = '/bar/42/test';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
404: '*',
|
||||||
|
Test: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
params: { id: '42' },
|
||||||
|
state: {
|
||||||
|
routes: [{ name: 'Test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getStateFromPath(path, config)).toEqual(state);
|
||||||
|
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||||
|
state
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses nearest parent wildcard match for unmatched paths', () => {
|
||||||
|
const path = '/bar/42/baz/test';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Bar: {
|
||||||
|
path: '/bar/:id/',
|
||||||
|
screens: {
|
||||||
|
Baz: 'baz',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: '404' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getStateFromPath(path, config)).toEqual(state);
|
||||||
|
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||||
|
state
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ type ConfigItem = {
|
|||||||
screens?: Record<string, ConfigItem>;
|
screens?: Record<string, ConfigItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getActiveRoute = (state: State): { name: string; params?: object } => {
|
||||||
|
const route =
|
||||||
|
typeof state.index === 'number'
|
||||||
|
? state.routes[state.index]
|
||||||
|
: state.routes[state.routes.length - 1];
|
||||||
|
|
||||||
|
if (route.state) {
|
||||||
|
return getActiveRoute(route.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return route;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to serialize a navigation state object to a path string.
|
* Utility to serialize a navigation state object to a path string.
|
||||||
*
|
*
|
||||||
@@ -69,7 +82,8 @@ export default function getPathFromState(
|
|||||||
|
|
||||||
let pattern: string | undefined;
|
let pattern: string | undefined;
|
||||||
|
|
||||||
let currentParams: Record<string, any> = { ...route.params };
|
let focusedParams: Record<string, any> | undefined;
|
||||||
|
let focusedRoute = getActiveRoute(state);
|
||||||
let currentOptions = configs;
|
let currentOptions = configs;
|
||||||
|
|
||||||
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
||||||
@@ -85,7 +99,7 @@ export default function getPathFromState(
|
|||||||
if (route.params) {
|
if (route.params) {
|
||||||
const stringify = currentOptions[route.name]?.stringify;
|
const stringify = currentOptions[route.name]?.stringify;
|
||||||
|
|
||||||
currentParams = fromEntries(
|
const currentParams = fromEntries(
|
||||||
Object.entries(route.params).map(([key, value]) => [
|
Object.entries(route.params).map(([key, value]) => [
|
||||||
key,
|
key,
|
||||||
stringify?.[key] ? stringify[key](value) : String(value),
|
stringify?.[key] ? stringify[key](value) : String(value),
|
||||||
@@ -95,6 +109,26 @@ export default function getPathFromState(
|
|||||||
if (pattern) {
|
if (pattern) {
|
||||||
Object.assign(allParams, currentParams);
|
Object.assign(allParams, currentParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (focusedRoute === route) {
|
||||||
|
// If this is the focused route, keep the params for later use
|
||||||
|
// We save it here since it's been stringified already
|
||||||
|
focusedParams = { ...currentParams };
|
||||||
|
|
||||||
|
pattern
|
||||||
|
?.split('/')
|
||||||
|
.filter((p) => p.startsWith(':'))
|
||||||
|
// eslint-disable-next-line no-loop-func
|
||||||
|
.forEach((p) => {
|
||||||
|
const name = getParamName(p);
|
||||||
|
|
||||||
|
// Remove the params present in the pattern since we'll only use the rest for query string
|
||||||
|
if (focusedParams) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete focusedParams[name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no `screens` property or no nested state, we return pattern
|
// If there is no `screens` property or no nested state, we return pattern
|
||||||
@@ -128,18 +162,19 @@ export default function getPathFromState(
|
|||||||
path += pattern
|
path += pattern
|
||||||
.split('/')
|
.split('/')
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const name = p.replace(/^:/, '').replace(/\?$/, '');
|
const name = getParamName(p);
|
||||||
|
|
||||||
|
// We don't know what to show for wildcard patterns
|
||||||
|
// Showing the route name seems ok, though whatever we show here will be incorrect
|
||||||
|
// Since the page doesn't actually exist
|
||||||
|
if (p === '*') {
|
||||||
|
return route.name;
|
||||||
|
}
|
||||||
|
|
||||||
// If the path has a pattern for a param, put the param in the path
|
// If the path has a pattern for a param, put the param in the path
|
||||||
if (p.startsWith(':')) {
|
if (p.startsWith(':')) {
|
||||||
const value = allParams[name];
|
const value = allParams[name];
|
||||||
|
|
||||||
// Remove the used value from the params object since we'll use the rest for query string
|
|
||||||
if (currentParams) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete currentParams[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === undefined && p.endsWith('?')) {
|
if (value === undefined && p.endsWith('?')) {
|
||||||
// Optional params without value assigned in route.params should be ignored
|
// Optional params without value assigned in route.params should be ignored
|
||||||
return '';
|
return '';
|
||||||
@@ -155,17 +190,21 @@ export default function getPathFromState(
|
|||||||
path += encodeURIComponent(route.name);
|
path += encodeURIComponent(route.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!focusedParams) {
|
||||||
|
focusedParams = focusedRoute.params;
|
||||||
|
}
|
||||||
|
|
||||||
if (route.state) {
|
if (route.state) {
|
||||||
path += '/';
|
path += '/';
|
||||||
} else if (currentParams) {
|
} else if (focusedParams) {
|
||||||
for (let param in currentParams) {
|
for (let param in focusedParams) {
|
||||||
if (currentParams[param] === 'undefined') {
|
if (focusedParams[param] === 'undefined') {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete currentParams[param];
|
delete focusedParams[param];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = queryString.stringify(currentParams);
|
const query = queryString.stringify(focusedParams);
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
path += `?${query}`;
|
path += `?${query}`;
|
||||||
@@ -189,6 +228,9 @@ const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<K, V>);
|
}, {} as Record<K, V>);
|
||||||
|
|
||||||
|
const getParamName = (pattern: string) =>
|
||||||
|
pattern.replace(/^:/, '').replace(/\?$/, '');
|
||||||
|
|
||||||
const joinPaths = (...paths: string[]): string =>
|
const joinPaths = (...paths: string[]): string =>
|
||||||
([] as string[])
|
([] as string[])
|
||||||
.concat(...paths.map((p) => p.split('/')))
|
.concat(...paths.map((p) => p.split('/')))
|
||||||
|
|||||||
@@ -59,11 +59,46 @@ export default function getStateFromPath(
|
|||||||
createNormalizedConfigs(key, options, [], initialRoutes)
|
createNormalizedConfigs(key, options, [], initialRoutes)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.sort(
|
.sort((a, b) => {
|
||||||
(a, b) =>
|
// Sort config so that:
|
||||||
// Sort configs so the most exhaustive is always first to be chosen
|
// - the most exhaustive ones are always at the beginning
|
||||||
b.pattern.split('/').length - a.pattern.split('/').length
|
// - patterns with wildcard are always at the end
|
||||||
);
|
|
||||||
|
// If one of the patterns starts with the other, it's more exhaustive
|
||||||
|
// So move it up
|
||||||
|
if (a.pattern.startsWith(b.pattern)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.pattern.startsWith(a.pattern)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aParts = a.pattern.split('/');
|
||||||
|
const bParts = b.pattern.split('/');
|
||||||
|
|
||||||
|
const aWildcardIndex = aParts.indexOf('*');
|
||||||
|
const bWildcardIndex = bParts.indexOf('*');
|
||||||
|
|
||||||
|
// If only one of the patterns has a wildcard, move it down in the list
|
||||||
|
if (aWildcardIndex === -1 && bWildcardIndex !== -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aWildcardIndex !== -1 && bWildcardIndex === -1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aWildcardIndex === bWildcardIndex) {
|
||||||
|
// If `b` has more `/`, it's more exhaustive
|
||||||
|
// So we move it up in the list
|
||||||
|
return bParts.length - aParts.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the wildcard appears later in the pattern (has higher index), it's more specific
|
||||||
|
// So we move it up in the list
|
||||||
|
return bWildcardIndex - aWildcardIndex;
|
||||||
|
});
|
||||||
|
|
||||||
let remaining = path
|
let remaining = path
|
||||||
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
||||||
@@ -104,41 +139,37 @@ export default function getStateFromPath(
|
|||||||
let result: PartialState<NavigationState> | undefined;
|
let result: PartialState<NavigationState> | undefined;
|
||||||
let current: PartialState<NavigationState> | undefined;
|
let current: PartialState<NavigationState> | undefined;
|
||||||
|
|
||||||
|
// We try to match the paths in 2 passes
|
||||||
|
// In first pass, we match the whole path against the regex instead of segments
|
||||||
|
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
||||||
|
const { routeNames, allParams, remainingPath } = matchAgainstConfigs(
|
||||||
|
remaining,
|
||||||
|
configs.map((c) => ({
|
||||||
|
...c,
|
||||||
|
// Add `$` to the regex to make sure it matches till end of the path and not just beginning
|
||||||
|
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (routeNames !== undefined) {
|
||||||
|
// This will always be empty if full path matched
|
||||||
|
remaining = remainingPath;
|
||||||
|
current = createNestedStateObject(
|
||||||
|
createRouteObjects(configs, routeNames, allParams),
|
||||||
|
initialRoutes
|
||||||
|
);
|
||||||
|
result = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In second pass, we divide the path into segments and match piece by piece
|
||||||
|
// This preserves the old behaviour, but we should remove it in next major
|
||||||
while (remaining) {
|
while (remaining) {
|
||||||
let routeNames: string[] | undefined;
|
let { routeNames, allParams, remainingPath } = matchAgainstConfigs(
|
||||||
let allParams: Record<string, any> | undefined;
|
remaining,
|
||||||
|
configs
|
||||||
|
);
|
||||||
|
|
||||||
// Go through all configs, and see if the next path segment matches our regex
|
remaining = remainingPath;
|
||||||
for (const config of configs) {
|
|
||||||
if (!config.regex) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = remaining.match(config.regex);
|
|
||||||
|
|
||||||
// If our regex matches, we need to extract params from the path
|
|
||||||
if (match) {
|
|
||||||
routeNames = [...config.routeNames];
|
|
||||||
|
|
||||||
const paramPatterns = config.pattern
|
|
||||||
.split('/')
|
|
||||||
.filter((p) => p.startsWith(':'));
|
|
||||||
|
|
||||||
if (paramPatterns.length) {
|
|
||||||
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
|
||||||
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
|
|
||||||
|
|
||||||
acc[p] = value;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining = remaining.replace(match[1], '');
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we hadn't matched any segments earlier, use the path as route name
|
// If we hadn't matched any segments earlier, use the path as route name
|
||||||
if (routeNames === undefined) {
|
if (routeNames === undefined) {
|
||||||
@@ -150,43 +181,7 @@ export default function getStateFromPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = createNestedStateObject(
|
const state = createNestedStateObject(
|
||||||
routeNames.map((name) => {
|
createRouteObjects(configs, routeNames, allParams),
|
||||||
const config = configs.find((c) => c.screen === name);
|
|
||||||
|
|
||||||
let params: object | undefined;
|
|
||||||
|
|
||||||
if (allParams && config?.path) {
|
|
||||||
const pattern = config.path;
|
|
||||||
|
|
||||||
if (pattern) {
|
|
||||||
const paramPatterns = pattern
|
|
||||||
.split('/')
|
|
||||||
.filter((p) => p.startsWith(':'));
|
|
||||||
|
|
||||||
if (paramPatterns.length) {
|
|
||||||
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
|
|
||||||
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
|
||||||
const value = allParams![p];
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
acc[key] =
|
|
||||||
config.parse && config.parse[key]
|
|
||||||
? config.parse[key](value)
|
|
||||||
: value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params && Object.keys(params).length) {
|
|
||||||
return { name, params };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name };
|
|
||||||
}),
|
|
||||||
initialRoutes
|
initialRoutes
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,6 +224,46 @@ const joinPaths = (...paths: string[]): string =>
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('/');
|
.join('/');
|
||||||
|
|
||||||
|
const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
|
||||||
|
let routeNames: string[] | undefined;
|
||||||
|
let allParams: Record<string, any> | undefined;
|
||||||
|
let remainingPath = remaining;
|
||||||
|
|
||||||
|
// Go through all configs, and see if the next path segment matches our regex
|
||||||
|
for (const config of configs) {
|
||||||
|
if (!config.regex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = remainingPath.match(config.regex);
|
||||||
|
|
||||||
|
// If our regex matches, we need to extract params from the path
|
||||||
|
if (match) {
|
||||||
|
routeNames = [...config.routeNames];
|
||||||
|
|
||||||
|
const paramPatterns = config.pattern
|
||||||
|
.split('/')
|
||||||
|
.filter((p) => p.startsWith(':'));
|
||||||
|
|
||||||
|
if (paramPatterns.length) {
|
||||||
|
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||||
|
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
|
||||||
|
|
||||||
|
acc[p] = value;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingPath = remainingPath.replace(match[1], '');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { routeNames, allParams, remainingPath };
|
||||||
|
};
|
||||||
|
|
||||||
const createNormalizedConfigs = (
|
const createNormalizedConfigs = (
|
||||||
screen: string,
|
screen: string,
|
||||||
routeConfig: PathConfig,
|
routeConfig: PathConfig,
|
||||||
@@ -311,7 +346,7 @@ const createConfigItem = (
|
|||||||
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
|
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${escape(it)}\\/`;
|
return `${it === '*' ? '.*' : escape(it)}\\/`;
|
||||||
})
|
})
|
||||||
.join('')})`
|
.join('')})`
|
||||||
)
|
)
|
||||||
@@ -433,6 +468,49 @@ const createNestedStateObject = (
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createRouteObjects = (
|
||||||
|
configs: RouteConfig[],
|
||||||
|
routeNames: string[],
|
||||||
|
allParams?: Record<string, any>
|
||||||
|
) =>
|
||||||
|
routeNames.map((name) => {
|
||||||
|
const config = configs.find((c) => c.screen === name);
|
||||||
|
|
||||||
|
let params: object | undefined;
|
||||||
|
|
||||||
|
if (allParams && config?.path) {
|
||||||
|
const pattern = config.path;
|
||||||
|
|
||||||
|
if (pattern) {
|
||||||
|
const paramPatterns = pattern
|
||||||
|
.split('/')
|
||||||
|
.filter((p) => p.startsWith(':'));
|
||||||
|
|
||||||
|
if (paramPatterns.length) {
|
||||||
|
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
|
||||||
|
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
||||||
|
const value = allParams![p];
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
acc[key] =
|
||||||
|
config.parse && config.parse[key]
|
||||||
|
? config.parse[key](value)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params && Object.keys(params).length) {
|
||||||
|
return { name, params };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name };
|
||||||
|
});
|
||||||
|
|
||||||
const findFocusedRoute = (state: InitialState) => {
|
const findFocusedRoute = (state: InitialState) => {
|
||||||
let current: InitialState | undefined = state;
|
let current: InitialState | undefined = state;
|
||||||
|
|
||||||
|
|||||||
7
packages/core/src/isArrayEqual.tsx
Normal file
7
packages/core/src/isArrayEqual.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Compare two arrays with primitive values as the content.
|
||||||
|
* We need to make sure that both values and order match.
|
||||||
|
*/
|
||||||
|
export default function isArrayEqual(a: any[], b: any[]) {
|
||||||
|
return a.length === b.length && a.every((it, index) => it === b[index]);
|
||||||
|
}
|
||||||
@@ -152,7 +152,7 @@ type NavigationHelpersCommon<
|
|||||||
* @param [params] Params object for the route.
|
* @param [params] Params object for the route.
|
||||||
*/
|
*/
|
||||||
navigate<RouteName extends keyof ParamList>(
|
navigate<RouteName extends keyof ParamList>(
|
||||||
...args: ParamList[RouteName] extends undefined | any
|
...args: undefined extends ParamList[RouteName]
|
||||||
? [RouteName] | [RouteName, ParamList[RouteName]]
|
? [RouteName] | [RouteName, ParamList[RouteName]]
|
||||||
: [RouteName, ParamList[RouteName]]
|
: [RouteName, ParamList[RouteName]]
|
||||||
): void;
|
): void;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import useStateGetters from './useStateGetters';
|
|||||||
import useOnGetState from './useOnGetState';
|
import useOnGetState from './useOnGetState';
|
||||||
import useScheduleUpdate from './useScheduleUpdate';
|
import useScheduleUpdate from './useScheduleUpdate';
|
||||||
import useCurrentRender from './useCurrentRender';
|
import useCurrentRender from './useCurrentRender';
|
||||||
|
import isArrayEqual from './isArrayEqual';
|
||||||
|
|
||||||
// This is to make TypeScript compiler happy
|
// This is to make TypeScript compiler happy
|
||||||
// eslint-disable-next-line babel/no-unused-expressions
|
// eslint-disable-next-line babel/no-unused-expressions
|
||||||
@@ -48,13 +49,6 @@ type NavigatorRoute = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two arrays with primitive values as the content.
|
|
||||||
* We need to make sure that both values and order match.
|
|
||||||
*/
|
|
||||||
const isArrayEqual = (a: any[], b: any[]) =>
|
|
||||||
a.length === b.length && a.every((it, index) => it === b[index]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract route config object from React children elements.
|
* Extract route config object from React children elements.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { NavigationState } from '@react-navigation/routers';
|
import { NavigationState } from '@react-navigation/routers';
|
||||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||||
import NavigationRouteContext from './NavigationRouteContext';
|
import NavigationRouteContext from './NavigationRouteContext';
|
||||||
|
import isArrayEqual from './isArrayEqual';
|
||||||
|
|
||||||
export default function useOnGetState({
|
export default function useOnGetState({
|
||||||
getStateForRoute,
|
getStateForRoute,
|
||||||
@@ -16,13 +17,23 @@ export default function useOnGetState({
|
|||||||
|
|
||||||
const getRehydratedState = React.useCallback(() => {
|
const getRehydratedState = React.useCallback(() => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
return {
|
|
||||||
...state,
|
// Avoid returning new route objects if we don't need to
|
||||||
routes: state.routes.map((route) => ({
|
const routes = state.routes.map((route) => {
|
||||||
...route,
|
const childState = getStateForRoute(route.key);
|
||||||
state: getStateForRoute(route.key),
|
|
||||||
})),
|
if (route.state === childState) {
|
||||||
};
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...route, state: childState };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isArrayEqual(state.routes, routes)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, routes };
|
||||||
}, [getState, getStateForRoute]);
|
}, [getState, getStateForRoute]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.8.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.1...@react-navigation/drawer@5.8.2) (2020-06-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo on drawerPosition default props ([#8357](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8357)) ([762cc44](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/762cc4457842182189eeac84aedbb88169452e1e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.8.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.0...@react-navigation/drawer@5.8.1) (2020-05-27)
|
## [5.8.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.0...@react-navigation/drawer@5.8.1) (2020-05-27)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/drawer
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/drawer",
|
"name": "@react-navigation/drawer",
|
||||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||||
"version": "5.8.1",
|
"version": "5.8.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
"@react-navigation/native": "^5.5.0",
|
"@react-navigation/native": "^5.5.1",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ type Props = {
|
|||||||
|
|
||||||
export default class DrawerView extends React.Component<Props> {
|
export default class DrawerView extends React.Component<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
drawerPostion: I18nManager.isRTL ? 'left' : 'right',
|
drawerPosition: I18nManager.isRTL ? 'left' : 'right',
|
||||||
drawerType: 'front',
|
drawerType: 'front',
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
swipeEnabled: Platform.OS !== 'web',
|
swipeEnabled: Platform.OS !== 'web',
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ export default function DrawerView({
|
|||||||
renderDrawerContent={renderNavigationView}
|
renderDrawerContent={renderNavigationView}
|
||||||
renderSceneContent={renderContent}
|
renderSceneContent={renderContent}
|
||||||
keyboardDismissMode={keyboardDismissMode}
|
keyboardDismissMode={keyboardDismissMode}
|
||||||
drawerPostion={drawerPosition}
|
|
||||||
dimensions={dimensions}
|
dimensions={dimensions}
|
||||||
/>
|
/>
|
||||||
</DrawerOpenContext.Provider>
|
</DrawerOpenContext.Provider>
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.2.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.9...@react-navigation/material-bottom-tabs@5.2.10) (2020-06-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.2.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.8...@react-navigation/material-bottom-tabs@5.2.9) (2020-05-27)
|
## [5.2.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.8...@react-navigation/material-bottom-tabs@5.2.9) (2020-05-27)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/material-bottom-tabs",
|
"name": "@react-navigation/material-bottom-tabs",
|
||||||
"description": "Integration for bottom navigation component from react-native-paper",
|
"description": "Integration for bottom navigation component from react-native-paper",
|
||||||
"version": "5.2.9",
|
"version": "5.2.10",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
"@react-navigation/native": "^5.5.0",
|
"@react-navigation/native": "^5.5.1",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"@types/react-native-vector-icons": "^6.4.5",
|
"@types/react-native-vector-icons": "^6.4.5",
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.2.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.9...@react-navigation/material-top-tabs@5.2.10) (2020-06-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.2.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.8...@react-navigation/material-top-tabs@5.2.9) (2020-05-27)
|
## [5.2.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.8...@react-navigation/material-top-tabs@5.2.9) (2020-05-27)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/material-top-tabs",
|
"name": "@react-navigation/material-top-tabs",
|
||||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||||
"version": "5.2.9",
|
"version": "5.2.10",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
"@react-navigation/native": "^5.5.0",
|
"@react-navigation/native": "^5.5.1",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.5.0...@react-navigation/native@5.5.1) (2020-06-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/native
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.4.3...@react-navigation/native@5.5.0) (2020-05-27)
|
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.4.3...@react-navigation/native@5.5.0) (2020-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/native",
|
"name": "@react-navigation/native",
|
||||||
"description": "React Native integration for React Navigation",
|
"description": "React Native integration for React Navigation",
|
||||||
"version": "5.5.0",
|
"version": "5.5.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native",
|
"react-native",
|
||||||
"react-navigation",
|
"react-navigation",
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^5.9.0"
|
"@react-navigation/core": "^5.10.0",
|
||||||
|
"nanoid": "^3.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
|
|||||||
69
packages/native/src/__mocks__/window.tsx
Normal file
69
packages/native/src/__mocks__/window.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const location = new URL('', 'http://example.com');
|
||||||
|
|
||||||
|
let listeners: (() => void)[] = [];
|
||||||
|
let entries = [{ state: null, href: location.href }];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
let currentState: any = null;
|
||||||
|
|
||||||
|
const history = {
|
||||||
|
get state() {
|
||||||
|
return currentState;
|
||||||
|
},
|
||||||
|
|
||||||
|
pushState(state: any, _: string, path: string) {
|
||||||
|
Object.assign(location, new URL(path, location.origin));
|
||||||
|
|
||||||
|
currentState = state;
|
||||||
|
entries = entries.slice(0, index + 1);
|
||||||
|
entries.push({ state, href: location.href });
|
||||||
|
index = entries.length - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
replaceState(state: any, _: string, path: string) {
|
||||||
|
Object.assign(location, new URL(path, location.origin));
|
||||||
|
|
||||||
|
currentState = state;
|
||||||
|
entries[index] = { state, href: location.href };
|
||||||
|
},
|
||||||
|
|
||||||
|
go(n: number) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (
|
||||||
|
(n > 0 && n < entries.length - index) ||
|
||||||
|
(n < 0 && Math.abs(n) <= index)
|
||||||
|
) {
|
||||||
|
index += n;
|
||||||
|
Object.assign(location, new URL(entries[index].href));
|
||||||
|
listeners.forEach((cb) => cb);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
back() {
|
||||||
|
this.go(-1);
|
||||||
|
},
|
||||||
|
|
||||||
|
forward() {
|
||||||
|
this.go(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEventListener = (type: 'popstate', listener: () => void) => {
|
||||||
|
if (type === 'popstate') {
|
||||||
|
listeners.push(listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeEventListener = (type: 'popstate', listener: () => void) => {
|
||||||
|
if (type === 'popstate') {
|
||||||
|
listeners = listeners.filter((cb) => cb !== listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
location,
|
||||||
|
history,
|
||||||
|
addEventListener,
|
||||||
|
removeEventListener,
|
||||||
|
};
|
||||||
149
packages/native/src/__tests__/NavigationContainer.test.tsx
Normal file
149
packages/native/src/__tests__/NavigationContainer.test.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
useNavigationBuilder,
|
||||||
|
createNavigatorFactory,
|
||||||
|
StackRouter,
|
||||||
|
TabRouter,
|
||||||
|
NavigationHelpersContext,
|
||||||
|
NavigationContainerRef,
|
||||||
|
} from '@react-navigation/core';
|
||||||
|
import { act, render } from 'react-native-testing-library';
|
||||||
|
import NavigationContainer from '../NavigationContainer';
|
||||||
|
import window from '../__mocks__/window';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
global.window = window;
|
||||||
|
|
||||||
|
// We want to use the web version of useLinking
|
||||||
|
jest.mock('../useLinking', () => require('../useLinking.tsx').default);
|
||||||
|
|
||||||
|
it('integrates with the history API', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const createStackNavigator = createNavigatorFactory((props: any) => {
|
||||||
|
const { navigation, state, descriptors } = useNavigationBuilder(
|
||||||
|
StackRouter,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
|
{state.routes.map((route, i) => (
|
||||||
|
<div key={route.key} aria-current={state.index === i || undefined}>
|
||||||
|
{descriptors[route.key].render()}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const createTabNavigator = createNavigatorFactory((props: any) => {
|
||||||
|
const { navigation, state, descriptors } = useNavigationBuilder(
|
||||||
|
TabRouter,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
|
{state.routes.map((route, i) => (
|
||||||
|
<div key={route.key} aria-current={state.index === i || undefined}>
|
||||||
|
{descriptors[route.key].render()}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Stack = createStackNavigator();
|
||||||
|
const Tab = createTabNavigator();
|
||||||
|
|
||||||
|
const TestScreen = ({ route }: any): any =>
|
||||||
|
`${route.name} ${JSON.stringify(route.params)}`;
|
||||||
|
|
||||||
|
const linking = {
|
||||||
|
prefixes: [],
|
||||||
|
config: {
|
||||||
|
Home: {
|
||||||
|
path: '',
|
||||||
|
initialRouteName: 'Feed',
|
||||||
|
screens: {
|
||||||
|
Profile: ':user',
|
||||||
|
Settings: 'edit',
|
||||||
|
Updates: 'updates',
|
||||||
|
Feed: 'feed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Chat: 'chat',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigation = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<NavigationContainer ref={navigation} linking={linking}>
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen name="Home">
|
||||||
|
{() => (
|
||||||
|
<Stack.Navigator initialRouteName="Feed">
|
||||||
|
<Stack.Screen name="Profile" component={TestScreen} />
|
||||||
|
<Stack.Screen name="Settings" component={TestScreen} />
|
||||||
|
<Stack.Screen name="Feed" component={TestScreen} />
|
||||||
|
<Stack.Screen name="Updates" component={TestScreen} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
)}
|
||||||
|
</Tab.Screen>
|
||||||
|
<Tab.Screen name="Chat" component={TestScreen} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/feed');
|
||||||
|
|
||||||
|
act(() => navigation.current?.navigate('Profile', { user: 'jane' }));
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/jane');
|
||||||
|
|
||||||
|
act(() => navigation.current?.navigate('Updates'));
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/updates');
|
||||||
|
|
||||||
|
act(() => navigation.current?.goBack());
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/jane');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
window.history.back();
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/feed');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
window.history.forward();
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/jane');
|
||||||
|
|
||||||
|
act(() => navigation.current?.navigate('Settings'));
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/edit');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
window.history.go(-2);
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/feed');
|
||||||
|
|
||||||
|
act(() => navigation.current?.navigate('Settings'));
|
||||||
|
act(() => navigation.current?.navigate('Chat'));
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/chat');
|
||||||
|
|
||||||
|
act(() => navigation.current?.navigate('Home'));
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe('/edit');
|
||||||
|
});
|
||||||
@@ -6,37 +6,226 @@ import {
|
|||||||
NavigationState,
|
NavigationState,
|
||||||
getActionFromState,
|
getActionFromState,
|
||||||
} from '@react-navigation/core';
|
} from '@react-navigation/core';
|
||||||
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import ServerContext from './ServerContext';
|
import ServerContext from './ServerContext';
|
||||||
import { LinkingOptions } from './types';
|
import { LinkingOptions } from './types';
|
||||||
|
|
||||||
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||||
|
|
||||||
type HistoryState = { index: number };
|
type HistoryRecord = {
|
||||||
|
// Unique identifier for this record to match it with window.history.state
|
||||||
declare const history: {
|
id: string;
|
||||||
state?: HistoryState;
|
// Navigation state object for the history entry
|
||||||
go(delta: number): void;
|
state: NavigationState;
|
||||||
pushState(state: HistoryState, title: string, url: string): void;
|
// Path of the history entry
|
||||||
replaceState(state: HistoryState, title: string, url: string): void;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStateLength = (state: NavigationState) => {
|
const createMemoryHistory = () => {
|
||||||
let length = 0;
|
let index = 0;
|
||||||
|
let items: HistoryRecord[] = [];
|
||||||
|
|
||||||
if (state.history) {
|
// Whether there's a `history.go(n)` pending
|
||||||
length = state.history.length;
|
let pending = false;
|
||||||
} else {
|
|
||||||
length = state.index + 1;
|
const history = {
|
||||||
|
get index(): number {
|
||||||
|
// We store an id in the state instead of an index
|
||||||
|
// Index could get out of sync with in-memory values if page reloads
|
||||||
|
const id = window.history.state?.id;
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
const index = items.findIndex((item) => item.id === id);
|
||||||
|
|
||||||
|
return index > -1 ? index : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
get(index: number) {
|
||||||
|
return items[index]?.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
backIndex({ path }: { path: string }) {
|
||||||
|
// We need to find the index from the element before current to get closest path to go back to
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
const item = items[i];
|
||||||
|
|
||||||
|
if (item.path === path) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
push({ path, state }: { path: string; state: NavigationState }) {
|
||||||
|
const id = nanoid();
|
||||||
|
|
||||||
|
// When a new entry is pushed, all the existing entries after index will be inaccessible
|
||||||
|
// So we remove any existing entries after the current index to clean them up
|
||||||
|
items = items.slice(0, index + 1);
|
||||||
|
|
||||||
|
items.push({ path, state, id });
|
||||||
|
index = items.length - 1;
|
||||||
|
|
||||||
|
// We pass empty string for title because it's ignored in all browsers except safari
|
||||||
|
// We don't store state object in history.state because:
|
||||||
|
// - browsers have limits on how big it can be, and we don't control the size
|
||||||
|
// - while not recommended, there could be non-serializable data in state
|
||||||
|
window.history.pushState({ id }, '', path);
|
||||||
|
},
|
||||||
|
|
||||||
|
replace({ path, state }: { path: string; state: NavigationState }) {
|
||||||
|
const id = window.history.state?.id ?? nanoid();
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
items[index] = { path, state, id };
|
||||||
|
} else {
|
||||||
|
// This is the first time any state modifications are done
|
||||||
|
// So we need to push the entry as there's nothing to replace
|
||||||
|
items.push({ path, state, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.history.replaceState({ id }, '', path);
|
||||||
|
},
|
||||||
|
|
||||||
|
// `history.go(n)` is asynchronous, there are couple of things to keep in mind:
|
||||||
|
// - it won't do anything if we can't go `n` steps, the `popstate` event won't fire.
|
||||||
|
// - each `history.go(n)` call will trigger a separate `popstate` event with correct location.
|
||||||
|
// - the `popstate` event fires before the next frame after calling `history.go(n)`.
|
||||||
|
// This method differs from `history.go(n)` in the sense that it'll go back as many steps it can.
|
||||||
|
go(n: number) {
|
||||||
|
if (n > 0) {
|
||||||
|
// We shouldn't go forward more than available index
|
||||||
|
n = Math.min(n, items.length - 1);
|
||||||
|
} else if (n < 0) {
|
||||||
|
// We shouldn't go back more than the index
|
||||||
|
// Otherwise we'll exit the page
|
||||||
|
n = Math.max(n, -Math.max(index + 1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
index += n;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
pending = true;
|
||||||
|
|
||||||
|
const done = () => {
|
||||||
|
pending = false;
|
||||||
|
|
||||||
|
window.removeEventListener('popstate', done);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the promise in the next frame
|
||||||
|
// If `popstate` hasn't fired by then, then it wasn't handled
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(done));
|
||||||
|
|
||||||
|
window.addEventListener('popstate', done);
|
||||||
|
window.history.go(n);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// The `popstate` event is triggered when history changes, except `pushState` and `replaceState`
|
||||||
|
// If we call `history.go(n)` ourselves, we don't want it to trigger the listener
|
||||||
|
// Here we normalize it so that only external changes (e.g. user pressing back/forward) trigger the listener
|
||||||
|
listen(listener: () => void) {
|
||||||
|
const onPopState = () => {
|
||||||
|
if (pending) {
|
||||||
|
// This was triggered by `history.go(n)`, we shouldn't call the listener
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listener();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', onPopState);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('popstate', onPopState);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return history;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the matching navigation state that changed between 2 navigation states
|
||||||
|
* e.g.: a -> b -> c -> d and a -> b -> c -> e -> f, if history in b changed, b is the matching state
|
||||||
|
*/
|
||||||
|
const findMatchingState = <T extends NavigationState>(
|
||||||
|
a: T | undefined,
|
||||||
|
b: T | undefined
|
||||||
|
): [T | undefined, T | undefined] => {
|
||||||
|
if (a === undefined || b === undefined || a.key !== b.key) {
|
||||||
|
return [undefined, undefined];
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusedState = state.routes[state.index].state;
|
// Tab and drawer will have `history` property, but stack will have history in `routes`
|
||||||
|
const aHistoryLength = a.history ? a.history.length : a.routes.length;
|
||||||
|
const bHistoryLength = b.history ? b.history.length : b.routes.length;
|
||||||
|
|
||||||
if (focusedState && !focusedState.stale) {
|
const aRoute = a.routes[a.index];
|
||||||
// If the focused route has history entries, we need to count them as well
|
const bRoute = b.routes[b.index];
|
||||||
length += getStateLength(focusedState as NavigationState) - 1;
|
|
||||||
|
const aChildState = aRoute.state as T | undefined;
|
||||||
|
const bChildState = bRoute.state as T | undefined;
|
||||||
|
|
||||||
|
// Stop here if this is the state object that changed:
|
||||||
|
// - history length is different
|
||||||
|
// - focused routes are different
|
||||||
|
// - one of them doesn't have child state
|
||||||
|
// - child state keys are different
|
||||||
|
if (
|
||||||
|
aHistoryLength !== bHistoryLength ||
|
||||||
|
aRoute.key !== bRoute.key ||
|
||||||
|
aChildState === undefined ||
|
||||||
|
bChildState === undefined ||
|
||||||
|
aChildState.key !== bChildState.key
|
||||||
|
) {
|
||||||
|
return [a, b];
|
||||||
}
|
}
|
||||||
|
|
||||||
return length;
|
return findMatchingState(aChildState, bChildState);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run async function in series as it's called.
|
||||||
|
*/
|
||||||
|
const series = (cb: () => Promise<void>) => {
|
||||||
|
// Whether we're currently handling a callback
|
||||||
|
let handling = false;
|
||||||
|
let queue: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
const callback = async () => {
|
||||||
|
try {
|
||||||
|
if (handling) {
|
||||||
|
// If we're currently handling a previous event, wait before handling this one
|
||||||
|
// Add the callback to the beginning of the queue
|
||||||
|
queue.unshift(callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handling = true;
|
||||||
|
|
||||||
|
await cb();
|
||||||
|
} finally {
|
||||||
|
handling = false;
|
||||||
|
|
||||||
|
if (queue.length) {
|
||||||
|
// If we have queued items, handle the last one
|
||||||
|
const last = queue.pop();
|
||||||
|
|
||||||
|
last?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
let isUsingLinking = false;
|
let isUsingLinking = false;
|
||||||
@@ -70,6 +259,8 @@ export default function useLinking(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [history] = React.useState(createMemoryHistory);
|
||||||
|
|
||||||
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
||||||
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
||||||
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
||||||
@@ -116,203 +307,143 @@ export default function useLinking(
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
|
const previousStateRef = React.useRef<NavigationState | undefined>(undefined);
|
||||||
const previousHistoryIndexRef = React.useRef(0);
|
const pendingPopStatePathRef = React.useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
const pendingIndexChangeRef = React.useRef<number | undefined>();
|
|
||||||
const pendingStateUpdateRef = React.useRef<boolean>(false);
|
|
||||||
const pendingStateMultiUpdateRef = React.useRef<boolean>(false);
|
|
||||||
|
|
||||||
// If we're navigating ahead >1, we're not restoring whole state,
|
|
||||||
// but just navigate to the selected route not caring about previous routes
|
|
||||||
// therefore if we need to go back, we need to pop screen and navigate to the new one
|
|
||||||
// Possibly, we will need to reuse the same mechanism.
|
|
||||||
// E.g. if we went ahead+4 (numberOfIndicesAhead = 3), and back-2,
|
|
||||||
// actually we need to pop the screen we navigated
|
|
||||||
// and navigate again, setting numberOfIndicesAhead to 1.
|
|
||||||
const numberOfIndicesAhead = React.useRef(0);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onPopState = () => {
|
return history.listen(() => {
|
||||||
const navigation = ref.current;
|
const navigation = ref.current;
|
||||||
|
|
||||||
if (!navigation || !enabled) {
|
if (!navigation || !enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousHistoryIndex = previousHistoryIndexRef.current;
|
const path = location.pathname + location.search;
|
||||||
const historyIndex = history.state?.index ?? 0;
|
|
||||||
|
|
||||||
previousHistoryIndexRef.current = historyIndex;
|
pendingPopStatePathRef.current = path;
|
||||||
|
|
||||||
if (pendingIndexChangeRef.current === historyIndex) {
|
// When browser back/forward is clicked, we first need to check if state object for this index exists
|
||||||
pendingIndexChangeRef.current = undefined;
|
// If it does we'll reset to that state object
|
||||||
|
// Otherwise, we'll handle it like a regular deep link
|
||||||
|
const recordedState = history.get(history.index);
|
||||||
|
|
||||||
|
if (recordedState) {
|
||||||
|
navigation.resetRoot(recordedState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = navigation.getRootState();
|
const state = getStateFromPathRef.current(path, configRef.current);
|
||||||
const path = getPathFromStateRef.current(state, configRef.current);
|
|
||||||
|
|
||||||
let canGoBack = true;
|
if (state) {
|
||||||
let numberOfBacks = 0;
|
const action = getActionFromState(state);
|
||||||
|
|
||||||
if (previousHistoryIndex === historyIndex) {
|
if (action !== undefined) {
|
||||||
if (location.pathname + location.search !== path) {
|
navigation.dispatch(action);
|
||||||
pendingStateUpdateRef.current = true;
|
|
||||||
history.replaceState({ index: historyIndex }, '', path);
|
|
||||||
}
|
|
||||||
} else if (previousHistoryIndex > historyIndex) {
|
|
||||||
numberOfBacks =
|
|
||||||
previousHistoryIndex - historyIndex - numberOfIndicesAhead.current;
|
|
||||||
|
|
||||||
if (numberOfBacks > 0) {
|
|
||||||
pendingStateMultiUpdateRef.current = true;
|
|
||||||
|
|
||||||
if (numberOfBacks > 1) {
|
|
||||||
pendingStateMultiUpdateRef.current = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingStateUpdateRef.current = true;
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfBacks; i++) {
|
|
||||||
navigation.goBack();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
canGoBack = false;
|
navigation.resetRoot(state);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// if current path didn't return any state, we should revert to initial state
|
||||||
|
navigation.resetRoot(state);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (previousHistoryIndex < historyIndex || !canGoBack) {
|
}, [enabled, history, ref]);
|
||||||
if (canGoBack) {
|
|
||||||
numberOfIndicesAhead.current =
|
|
||||||
historyIndex - previousHistoryIndex - 1;
|
|
||||||
} else {
|
|
||||||
navigation.goBack();
|
|
||||||
numberOfIndicesAhead.current -= previousHistoryIndex - historyIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = getStateFromPathRef.current(
|
|
||||||
location.pathname + location.search,
|
|
||||||
configRef.current
|
|
||||||
);
|
|
||||||
|
|
||||||
pendingStateMultiUpdateRef.current = true;
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
const action = getActionFromState(state);
|
|
||||||
|
|
||||||
pendingStateUpdateRef.current = true;
|
|
||||||
|
|
||||||
if (action !== undefined) {
|
|
||||||
navigation.dispatch(action);
|
|
||||||
} else {
|
|
||||||
navigation.resetRoot(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('popstate', onPopState);
|
|
||||||
|
|
||||||
return () => window.removeEventListener('popstate', onPopState);
|
|
||||||
}, [enabled, ref]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref.current && previousStateLengthRef.current === undefined) {
|
if (ref.current) {
|
||||||
previousStateLengthRef.current = getStateLength(
|
// We need to record the current metadata on the first render if they aren't set
|
||||||
ref.current.getRootState()
|
// This will allow the initial state to be in the history entry
|
||||||
);
|
const state = ref.current.getRootState();
|
||||||
}
|
|
||||||
|
|
||||||
if (ref.current && location.pathname + location.search === '/') {
|
|
||||||
history.replaceState(
|
|
||||||
{ index: history.state?.index ?? 0 },
|
|
||||||
'',
|
|
||||||
getPathFromStateRef.current(
|
|
||||||
ref.current.getRootState(),
|
|
||||||
configRef.current
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribe = ref.current?.addListener('state', () => {
|
|
||||||
const navigation = ref.current;
|
|
||||||
|
|
||||||
if (!navigation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = navigation.getRootState();
|
|
||||||
const path = getPathFromStateRef.current(state, configRef.current);
|
const path = getPathFromStateRef.current(state, configRef.current);
|
||||||
|
|
||||||
const previousStateLength = previousStateLengthRef.current ?? 1;
|
if (previousStateRef.current === undefined) {
|
||||||
const stateLength = getStateLength(state);
|
previousStateRef.current = state;
|
||||||
|
|
||||||
if (pendingStateMultiUpdateRef.current) {
|
|
||||||
if (location.pathname + location.search === path) {
|
|
||||||
pendingStateMultiUpdateRef.current = false;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
previousStateLengthRef.current = stateLength;
|
history.replace({ path, state });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
const onStateChange = async () => {
|
||||||
pendingStateUpdateRef.current &&
|
const navigation = ref.current;
|
||||||
location.pathname + location.search === path
|
|
||||||
) {
|
if (!navigation || !enabled) {
|
||||||
pendingStateUpdateRef.current = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = history.state?.index ?? 0;
|
const previousState = previousStateRef.current;
|
||||||
|
const state = navigation.getRootState();
|
||||||
|
|
||||||
if (previousStateLength === stateLength) {
|
const pendingPath = pendingPopStatePathRef.current;
|
||||||
// If no new entries were added to history in our navigation state, we want to replaceState
|
const path = getPathFromStateRef.current(state, configRef.current);
|
||||||
if (location.pathname + location.search !== path) {
|
|
||||||
history.replaceState({ index }, '', path);
|
|
||||||
previousHistoryIndexRef.current = index;
|
|
||||||
}
|
|
||||||
} else if (stateLength > previousStateLength) {
|
|
||||||
// If new entries were added, pushState until we have same length
|
|
||||||
// This won't be accurate if multiple entries were added at once, but that's the best we can do
|
|
||||||
for (let i = 0, l = stateLength - previousStateLength; i < l; i++) {
|
|
||||||
index++;
|
|
||||||
history.pushState({ index }, '', path);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousHistoryIndexRef.current = index;
|
previousStateRef.current = state;
|
||||||
} else if (previousStateLength > stateLength) {
|
pendingPopStatePathRef.current = undefined;
|
||||||
const delta = Math.min(
|
|
||||||
previousStateLength - stateLength,
|
|
||||||
// We need to keep at least one item in the history
|
|
||||||
// Otherwise we'll exit the page
|
|
||||||
previousHistoryIndexRef.current - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
if (delta > 0) {
|
// To detect the kind of state change, we need to:
|
||||||
// We need to set this to ignore the `popstate` event
|
// - Find the common focused navigation state in previous and current state
|
||||||
pendingIndexChangeRef.current = index - delta;
|
// - If only the route keys changed, compare history/routes.length to check if we go back/forward/replace
|
||||||
|
// - If no common focused navigation state found, it's a replace
|
||||||
|
const [previousFocusedState, focusedState] = findMatchingState(
|
||||||
|
previousState,
|
||||||
|
state
|
||||||
|
);
|
||||||
|
|
||||||
// If new entries were removed, go back so that we have same length
|
if (
|
||||||
history.go(-delta);
|
previousFocusedState &&
|
||||||
} else {
|
focusedState &&
|
||||||
// We're not going back in history, but the navigation state changed
|
// We should only handle push/pop if path changed from what was in last `popstate`
|
||||||
// The URL probably also changed, so we need to re-sync the URL
|
// Otherwise it's likely a change triggered by `popstate`
|
||||||
if (location.pathname + location.search !== path) {
|
path !== pendingPath
|
||||||
history.replaceState({ index }, '', path);
|
) {
|
||||||
previousHistoryIndexRef.current = index;
|
const historyDelta =
|
||||||
|
(focusedState.history
|
||||||
|
? focusedState.history.length
|
||||||
|
: focusedState.routes.length) -
|
||||||
|
(previousFocusedState.history
|
||||||
|
? previousFocusedState.history.length
|
||||||
|
: previousFocusedState.routes.length);
|
||||||
|
|
||||||
|
if (historyDelta > 0) {
|
||||||
|
// If history length is increased, we should pushState
|
||||||
|
// Note that path might not actually change here, for example, drawer open should pushState
|
||||||
|
history.push({ path, state });
|
||||||
|
} else if (historyDelta < 0) {
|
||||||
|
// If history length is decreased, i.e. entries were removed, we want to go back
|
||||||
|
|
||||||
|
const nextIndex = history.backIndex({ path });
|
||||||
|
const currentIndex = history.index;
|
||||||
|
|
||||||
|
if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||||
|
// An existing entry for this path exists and it's less than current index, go back to that
|
||||||
|
await history.go(nextIndex - currentIndex);
|
||||||
|
} else {
|
||||||
|
// We couldn't find an existing entry to go back to, so we'll go back by the delta
|
||||||
|
// This won't be correct if multiple routes were pushed in one go before
|
||||||
|
// Usually this shouldn't happen and this is a fallback for that
|
||||||
|
await history.go(historyDelta);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return unsubscribe;
|
// Store the updated state as well as fix the path if incorrect
|
||||||
|
history.replace({ path, state });
|
||||||
|
} else {
|
||||||
|
// If history length is unchanged, we want to replaceState
|
||||||
|
history.replace({ path, state });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no common navigation state was found, assume it's a replace
|
||||||
|
// This would happen if the user did a reset/conditionally changed navigators
|
||||||
|
history.replace({ path, state });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// We debounce onStateChange coz we don't want multiple state changes to be handled at one time
|
||||||
|
// This could happen since `history.go(n)` is asynchronous
|
||||||
|
// If `pushState` or `replaceState` were called before `history.go(n)` completes, it'll mess stuff up
|
||||||
|
return ref.current?.addListener('state', series(onStateChange));
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,46 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.5.0...@react-navigation/stack@5.5.1) (2020-06-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make sure the header is on top of the view ([1ae07af](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/1ae07af79660973f4342a5741a1a826bcc689832))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.4.2...@react-navigation/stack@5.5.0) (2020-06-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix blank screen with animationEnabled: false & headerShown: false ([9c06a92](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/9c06a92d092af150d653c3a2f7fdccd28090bb14)), closes [#8391](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8391)
|
||||||
|
* ignore onOpen from route that wasn't closing ([1f27e4b](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/1f27e4b1f659e59ad15ecbf44b4fb0a80cae302f)), closes [#8257](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8257)
|
||||||
|
* pass gestureRef to PanGestureHandlerNative ([#8394](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8394)) ([c7e4bf9](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/c7e4bf94e664563892cbdafccc108ad519ccec50))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* automatically hide header in nested stacks ([e0e0f79](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/e0e0f79793be552e5532cd0afe9444000d21341e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.4.1...@react-navigation/stack@5.4.2) (2020-06-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* relatively position float Header if !headerTransparent ([#8285](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8285)) ([78afbff](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/78afbffe976b14bb60666a2b1230127db0dc24f6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.4.0...@react-navigation/stack@5.4.1) (2020-05-27)
|
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.4.0...@react-navigation/stack@5.4.1) (2020-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/stack",
|
"name": "@react-navigation/stack",
|
||||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||||
"version": "5.4.1",
|
"version": "5.5.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.14.3",
|
"@react-native-community/bob": "^0.14.3",
|
||||||
"@react-native-community/masked-view": "^0.1.10",
|
"@react-native-community/masked-view": "^0.1.10",
|
||||||
"@react-navigation/native": "^5.5.0",
|
"@react-navigation/native": "^5.5.1",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ export type StackHeaderOptions = {
|
|||||||
*/
|
*/
|
||||||
headerBackAllowFontScaling?: boolean;
|
headerBackAllowFontScaling?: boolean;
|
||||||
/**
|
/**
|
||||||
* Title string used by the back button on iOS, or `null` to disable label. Defaults to the previous scene's `headerTitle`.
|
* Title string used by the back button on iOS. Defaults to the previous scene's `headerTitle`.
|
||||||
|
* Use `headerBackTitleVisible: false` to hide it.
|
||||||
*/
|
*/
|
||||||
headerBackTitle?: string;
|
headerBackTitle?: string;
|
||||||
/**
|
/**
|
||||||
|
|||||||
5
packages/stack/src/utils/HeaderShownContext.tsx
Normal file
5
packages/stack/src/utils/HeaderShownContext.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const HeaderShownContext = React.createContext(false);
|
||||||
|
|
||||||
|
export default HeaderShownContext;
|
||||||
@@ -10,7 +10,7 @@ export function PanGestureHandler(props: PanGestureHandlerProperties) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRefContext.Provider value={gestureRef}>
|
<GestureHandlerRefContext.Provider value={gestureRef}>
|
||||||
<PanGestureHandlerNative {...props} />
|
<PanGestureHandlerNative {...props} ref={gestureRef} />
|
||||||
</GestureHandlerRefContext.Provider>
|
</GestureHandlerRefContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
forNoAnimation,
|
forNoAnimation,
|
||||||
forSlideRight,
|
forSlideRight,
|
||||||
} from '../../TransitionConfigs/HeaderStyleInterpolators';
|
} from '../../TransitionConfigs/HeaderStyleInterpolators';
|
||||||
|
import HeaderShownContext from '../../utils/HeaderShownContext';
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
Scene,
|
Scene,
|
||||||
@@ -54,6 +55,7 @@ export default function HeaderContainer({
|
|||||||
style,
|
style,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const focusedRoute = getFocusedRoute();
|
const focusedRoute = getFocusedRoute();
|
||||||
|
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View pointerEvents="box-none" style={style}>
|
<View pointerEvents="box-none" style={style}>
|
||||||
@@ -62,7 +64,16 @@ export default function HeaderContainer({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { options } = scene.descriptor;
|
const {
|
||||||
|
header,
|
||||||
|
headerShown = isParentHeaderShown === false,
|
||||||
|
headerTransparent,
|
||||||
|
} = scene.descriptor.options || {};
|
||||||
|
|
||||||
|
if (!headerShown) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isFocused = focusedRoute.key === scene.route.key;
|
const isFocused = focusedRoute.key === scene.route.key;
|
||||||
const previousRoute = getPreviousRoute({ route: scene.route });
|
const previousRoute = getPreviousRoute({ route: scene.route });
|
||||||
|
|
||||||
@@ -85,13 +96,20 @@ export default function HeaderContainer({
|
|||||||
// This makes the header look like it's moving with the screen
|
// This makes the header look like it's moving with the screen
|
||||||
const previousScene = self[i - 1];
|
const previousScene = self[i - 1];
|
||||||
const nextScene = self[i + 1];
|
const nextScene = self[i + 1];
|
||||||
|
|
||||||
|
const {
|
||||||
|
headerShown: previousHeaderShown = isParentHeaderShown === false,
|
||||||
|
} = previousScene?.descriptor.options || {};
|
||||||
|
|
||||||
|
const { headerShown: nextHeaderShown = isParentHeaderShown === false } =
|
||||||
|
nextScene?.descriptor.options || {};
|
||||||
|
|
||||||
const isHeaderStatic =
|
const isHeaderStatic =
|
||||||
(previousScene &&
|
(previousHeaderShown === false &&
|
||||||
previousScene.descriptor.options.headerShown === false &&
|
|
||||||
// We still need to animate when coming back from next scene
|
// We still need to animate when coming back from next scene
|
||||||
// A hacky way to check this is if the next scene exists
|
// A hacky way to check this is if the next scene exists
|
||||||
!nextScene) ||
|
!nextScene) ||
|
||||||
(nextScene && nextScene.descriptor.options.headerShown === false);
|
nextHeaderShown === false;
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
mode,
|
mode,
|
||||||
@@ -139,18 +157,12 @@ export default function HeaderContainer({
|
|||||||
style={
|
style={
|
||||||
// Avoid positioning the focused header absolutely
|
// Avoid positioning the focused header absolutely
|
||||||
// Otherwise accessibility tools don't seem to be able to find it
|
// Otherwise accessibility tools don't seem to be able to find it
|
||||||
(mode === 'float' && !isFocused) || options.headerTransparent
|
(mode === 'float' && !isFocused) || headerTransparent
|
||||||
? styles.header
|
? styles.header
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{options.headerShown !== false ? (
|
{header !== undefined ? header(props) : <Header {...props} />}
|
||||||
options.header !== undefined ? (
|
|
||||||
options.header(props)
|
|
||||||
) : (
|
|
||||||
<Header {...props} />
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
</NavigationRouteContext.Provider>
|
</NavigationRouteContext.Provider>
|
||||||
</NavigationContext.Provider>
|
</NavigationContext.Provider>
|
||||||
|
|||||||
@@ -493,6 +493,12 @@ export default class Card extends React.Component<Props> {
|
|||||||
? Color(backgroundColor).alpha() === 0
|
? Color(backgroundColor).alpha() === 0
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
// This is a dummy style that doesn't actually change anything visually.
|
||||||
|
// Animated needs the animated value to be used somewhere, otherwise things don't update properly.
|
||||||
|
// If we disable animations and hide header, it could end up making the value unused.
|
||||||
|
// So we have this dummy style that will always be used regardless of what else changed.
|
||||||
|
const dummyStyle = { opacity: Animated.diffClamp(current, 1, 1) };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardAnimationContext.Provider value={animationContext}>
|
<CardAnimationContext.Provider value={animationContext}>
|
||||||
<View pointerEvents="box-none" {...rest}>
|
<View pointerEvents="box-none" {...rest}>
|
||||||
@@ -502,7 +508,12 @@ export default class Card extends React.Component<Props> {
|
|||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.container, containerStyle, customContainerStyle]}
|
style={[
|
||||||
|
styles.container,
|
||||||
|
dummyStyle,
|
||||||
|
containerStyle,
|
||||||
|
customContainerStyle,
|
||||||
|
]}
|
||||||
pointerEvents="box-none"
|
pointerEvents="box-none"
|
||||||
>
|
>
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Route, useTheme } from '@react-navigation/native';
|
|||||||
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
||||||
import Card from './Card';
|
import Card from './Card';
|
||||||
import HeaderHeightContext from '../../utils/HeaderHeightContext';
|
import HeaderHeightContext from '../../utils/HeaderHeightContext';
|
||||||
|
import HeaderShownContext from '../../utils/HeaderShownContext';
|
||||||
import {
|
import {
|
||||||
Scene,
|
Scene,
|
||||||
Layout,
|
Layout,
|
||||||
@@ -53,8 +54,8 @@ type Props = TransitionPreset & {
|
|||||||
gestureVelocityImpact?: number;
|
gestureVelocityImpact?: number;
|
||||||
mode: StackCardMode;
|
mode: StackCardMode;
|
||||||
headerMode: StackHeaderMode;
|
headerMode: StackHeaderMode;
|
||||||
headerShown?: boolean;
|
headerShown: boolean;
|
||||||
headerTransparent?: boolean;
|
hasAbsoluteHeader: boolean;
|
||||||
headerHeight: number;
|
headerHeight: number;
|
||||||
onHeaderHeightChange: (props: {
|
onHeaderHeightChange: (props: {
|
||||||
route: Route<string>;
|
route: Route<string>;
|
||||||
@@ -84,7 +85,7 @@ function CardContainer({
|
|||||||
headerMode,
|
headerMode,
|
||||||
headerShown,
|
headerShown,
|
||||||
headerStyleInterpolator,
|
headerStyleInterpolator,
|
||||||
headerTransparent,
|
hasAbsoluteHeader,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
onHeaderHeightChange,
|
onHeaderHeightChange,
|
||||||
index,
|
index,
|
||||||
@@ -160,6 +161,9 @@ function CardContainer({
|
|||||||
};
|
};
|
||||||
}, [pointerEvents, scene.progress.next]);
|
}, [pointerEvents, scene.progress.next]);
|
||||||
|
|
||||||
|
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
||||||
|
const isCurrentHeaderShown = headerMode !== 'none' && headerShown !== false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
index={index}
|
index={index}
|
||||||
@@ -187,19 +191,19 @@ function CardContainer({
|
|||||||
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
|
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
|
||||||
pointerEvents={active ? 'box-none' : pointerEvents}
|
pointerEvents={active ? 'box-none' : pointerEvents}
|
||||||
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
|
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
|
||||||
containerStyle={
|
containerStyle={hasAbsoluteHeader ? { marginTop: headerHeight } : null}
|
||||||
headerMode === 'float' && !headerTransparent && headerShown !== false
|
|
||||||
? { marginTop: headerHeight }
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
|
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
>
|
>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.scene}>
|
<View style={styles.scene}>
|
||||||
<HeaderHeightContext.Provider value={headerHeight}>
|
<HeaderShownContext.Provider
|
||||||
{renderScene({ route: scene.route })}
|
value={isParentHeaderShown || isCurrentHeaderShown}
|
||||||
</HeaderHeightContext.Provider>
|
>
|
||||||
|
<HeaderHeightContext.Provider value={headerHeight}>
|
||||||
|
{renderScene({ route: scene.route })}
|
||||||
|
</HeaderHeightContext.Provider>
|
||||||
|
</HeaderShownContext.Provider>
|
||||||
</View>
|
</View>
|
||||||
{headerMode === 'screen'
|
{headerMode === 'screen'
|
||||||
? renderHeader({
|
? renderHeader({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '../../TransitionConfigs/TransitionPresets';
|
} from '../../TransitionConfigs/TransitionPresets';
|
||||||
import { forNoAnimation as forNoAnimationHeader } from '../../TransitionConfigs/HeaderStyleInterpolators';
|
import { forNoAnimation as forNoAnimationHeader } from '../../TransitionConfigs/HeaderStyleInterpolators';
|
||||||
import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators';
|
import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators';
|
||||||
|
import HeaderShownContext from '../../utils/HeaderShownContext';
|
||||||
import getDistanceForDirection from '../../utils/getDistanceForDirection';
|
import getDistanceForDirection from '../../utils/getDistanceForDirection';
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
@@ -385,180 +386,224 @@ export default class CardStack extends React.Component<Props, State> {
|
|||||||
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
|
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<HeaderShownContext.Consumer>
|
||||||
<MaybeScreenContainer
|
{(isParentHeaderShown) => {
|
||||||
enabled={isScreensEnabled}
|
const isFloatHeaderAbsolute =
|
||||||
style={styles.container}
|
headerMode === 'float'
|
||||||
onLayout={this.handleLayout}
|
? this.state.scenes.slice(-2).some((scene) => {
|
||||||
>
|
const { descriptor } = scene;
|
||||||
{routes.map((route, index, self) => {
|
const options = descriptor ? descriptor.options : {};
|
||||||
const focused = focusedRoute.key === route.key;
|
const {
|
||||||
const gesture = gestures[route.key];
|
headerTransparent,
|
||||||
const scene = scenes[index];
|
headerShown = isParentHeaderShown === false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const isScreenActive = scene.progress.next
|
if (headerTransparent || headerShown === false) {
|
||||||
? scene.progress.next.interpolate({
|
return true;
|
||||||
inputRange: [0, 1 - EPSILON, 1],
|
}
|
||||||
outputRange: [1, 1, 0],
|
|
||||||
extrapolate: 'clamp',
|
return false;
|
||||||
})
|
})
|
||||||
: 1;
|
: false;
|
||||||
|
|
||||||
const {
|
const floatingHeader =
|
||||||
safeAreaInsets,
|
headerMode === 'float' ? (
|
||||||
headerShown,
|
<React.Fragment key="header">
|
||||||
headerTransparent,
|
{renderHeader({
|
||||||
cardShadowEnabled,
|
mode: 'float',
|
||||||
cardOverlayEnabled,
|
layout,
|
||||||
cardOverlay,
|
insets: { top, right, bottom, left },
|
||||||
cardStyle,
|
scenes,
|
||||||
animationEnabled,
|
getPreviousRoute,
|
||||||
gestureResponseDistance,
|
getFocusedRoute: this.getFocusedRoute,
|
||||||
gestureVelocityImpact,
|
onContentHeightChange: this.handleHeaderLayout,
|
||||||
gestureDirection = defaultTransitionPreset.gestureDirection,
|
gestureDirection:
|
||||||
transitionSpec = defaultTransitionPreset.transitionSpec,
|
focusedOptions.gestureDirection !== undefined
|
||||||
cardStyleInterpolator = animationEnabled === false
|
? focusedOptions.gestureDirection
|
||||||
? forNoAnimationCard
|
: defaultTransitionPreset.gestureDirection,
|
||||||
: defaultTransitionPreset.cardStyleInterpolator,
|
styleInterpolator:
|
||||||
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
|
focusedOptions.headerStyleInterpolator !== undefined
|
||||||
} = scene.descriptor
|
? focusedOptions.headerStyleInterpolator
|
||||||
? scene.descriptor.options
|
: defaultTransitionPreset.headerStyleInterpolator,
|
||||||
: ({} as StackNavigationOptions);
|
style: [
|
||||||
|
styles.floating,
|
||||||
|
isFloatHeaderAbsolute && styles.absolute,
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
) : null;
|
||||||
|
|
||||||
let transitionConfig = {
|
return (
|
||||||
gestureDirection,
|
<React.Fragment>
|
||||||
transitionSpec,
|
{isFloatHeaderAbsolute ? null : floatingHeader}
|
||||||
cardStyleInterpolator,
|
<MaybeScreenContainer
|
||||||
headerStyleInterpolator,
|
|
||||||
};
|
|
||||||
|
|
||||||
// When a screen is not the last, it should use next screen's transition config
|
|
||||||
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
|
|
||||||
// For example combining a slide and a modal transition would look wrong otherwise
|
|
||||||
// With this approach, combining different transition styles in the same navigator mostly looks right
|
|
||||||
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
|
|
||||||
// but majority of the transitions look alright
|
|
||||||
if (index !== self.length - 1) {
|
|
||||||
const nextScene = scenes[index + 1];
|
|
||||||
|
|
||||||
if (nextScene) {
|
|
||||||
const {
|
|
||||||
animationEnabled,
|
|
||||||
gestureDirection = defaultTransitionPreset.gestureDirection,
|
|
||||||
transitionSpec = defaultTransitionPreset.transitionSpec,
|
|
||||||
cardStyleInterpolator = animationEnabled === false
|
|
||||||
? forNoAnimationCard
|
|
||||||
: defaultTransitionPreset.cardStyleInterpolator,
|
|
||||||
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
|
|
||||||
} = nextScene.descriptor
|
|
||||||
? nextScene.descriptor.options
|
|
||||||
: ({} as StackNavigationOptions);
|
|
||||||
|
|
||||||
transitionConfig = {
|
|
||||||
gestureDirection,
|
|
||||||
transitionSpec,
|
|
||||||
cardStyleInterpolator,
|
|
||||||
headerStyleInterpolator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
top: safeAreaInsetTop = insets.top,
|
|
||||||
right: safeAreaInsetRight = insets.right,
|
|
||||||
bottom: safeAreaInsetBottom = insets.bottom,
|
|
||||||
left: safeAreaInsetLeft = insets.left,
|
|
||||||
} = safeAreaInsets || {};
|
|
||||||
|
|
||||||
const previousRoute = getPreviousRoute({ route: scene.route });
|
|
||||||
|
|
||||||
let previousScene = scenes[index - 1];
|
|
||||||
|
|
||||||
if (previousRoute) {
|
|
||||||
// The previous scene will be shortly before the current scene in the array
|
|
||||||
// So loop back from current index to avoid looping over the full array
|
|
||||||
for (let j = index - 1; j >= 0; j--) {
|
|
||||||
const s = scenes[j];
|
|
||||||
|
|
||||||
if (s && s.route.key === previousRoute.key) {
|
|
||||||
previousScene = s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MaybeScreen
|
|
||||||
key={route.key}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
enabled={isScreensEnabled}
|
enabled={isScreensEnabled}
|
||||||
active={isScreenActive}
|
style={styles.container}
|
||||||
pointerEvents="box-none"
|
onLayout={this.handleLayout}
|
||||||
>
|
>
|
||||||
<CardContainer
|
{routes.map((route, index, self) => {
|
||||||
index={index}
|
const focused = focusedRoute.key === route.key;
|
||||||
active={index === self.length - 1}
|
const gesture = gestures[route.key];
|
||||||
focused={focused}
|
const scene = scenes[index];
|
||||||
closing={closingRouteKeys.includes(route.key)}
|
|
||||||
layout={layout}
|
const isScreenActive = scene.progress.next
|
||||||
gesture={gesture}
|
? scene.progress.next.interpolate({
|
||||||
scene={scene}
|
inputRange: [0, 1 - EPSILON, 1],
|
||||||
previousScene={previousScene}
|
outputRange: [1, 1, 0],
|
||||||
safeAreaInsetTop={safeAreaInsetTop}
|
extrapolate: 'clamp',
|
||||||
safeAreaInsetRight={safeAreaInsetRight}
|
})
|
||||||
safeAreaInsetBottom={safeAreaInsetBottom}
|
: 1;
|
||||||
safeAreaInsetLeft={safeAreaInsetLeft}
|
|
||||||
cardOverlay={cardOverlay}
|
const {
|
||||||
cardOverlayEnabled={cardOverlayEnabled}
|
safeAreaInsets,
|
||||||
cardShadowEnabled={cardShadowEnabled}
|
headerShown = isParentHeaderShown === false,
|
||||||
cardStyle={cardStyle}
|
headerTransparent,
|
||||||
onPageChangeStart={onPageChangeStart}
|
cardShadowEnabled,
|
||||||
onPageChangeConfirm={onPageChangeConfirm}
|
cardOverlayEnabled,
|
||||||
onPageChangeCancel={onPageChangeCancel}
|
cardOverlay,
|
||||||
gestureResponseDistance={gestureResponseDistance}
|
cardStyle,
|
||||||
headerHeight={headerHeights[route.key]}
|
animationEnabled,
|
||||||
onHeaderHeightChange={this.handleHeaderLayout}
|
gestureResponseDistance,
|
||||||
getPreviousRoute={getPreviousRoute}
|
gestureVelocityImpact,
|
||||||
getFocusedRoute={this.getFocusedRoute}
|
gestureDirection = defaultTransitionPreset.gestureDirection,
|
||||||
mode={mode}
|
transitionSpec = defaultTransitionPreset.transitionSpec,
|
||||||
headerMode={headerMode}
|
cardStyleInterpolator = animationEnabled === false
|
||||||
headerShown={headerShown}
|
? forNoAnimationCard
|
||||||
headerTransparent={headerTransparent}
|
: defaultTransitionPreset.cardStyleInterpolator,
|
||||||
renderHeader={renderHeader}
|
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
|
||||||
renderScene={renderScene}
|
} = scene.descriptor
|
||||||
onOpenRoute={onOpenRoute}
|
? scene.descriptor.options
|
||||||
onCloseRoute={onCloseRoute}
|
: ({} as StackNavigationOptions);
|
||||||
onTransitionStart={onTransitionStart}
|
|
||||||
onTransitionEnd={onTransitionEnd}
|
let transitionConfig = {
|
||||||
gestureEnabled={index !== 0 && getGesturesEnabled({ route })}
|
gestureDirection,
|
||||||
gestureVelocityImpact={gestureVelocityImpact}
|
transitionSpec,
|
||||||
{...transitionConfig}
|
cardStyleInterpolator,
|
||||||
/>
|
headerStyleInterpolator,
|
||||||
</MaybeScreen>
|
};
|
||||||
);
|
|
||||||
})}
|
// When a screen is not the last, it should use next screen's transition config
|
||||||
</MaybeScreenContainer>
|
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
|
||||||
{headerMode === 'float'
|
// For example combining a slide and a modal transition would look wrong otherwise
|
||||||
? renderHeader({
|
// With this approach, combining different transition styles in the same navigator mostly looks right
|
||||||
mode: 'float',
|
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
|
||||||
layout,
|
// but majority of the transitions look alright
|
||||||
insets: { top, right, bottom, left },
|
if (index !== self.length - 1) {
|
||||||
scenes,
|
const nextScene = scenes[index + 1];
|
||||||
getPreviousRoute,
|
|
||||||
getFocusedRoute: this.getFocusedRoute,
|
if (nextScene) {
|
||||||
onContentHeightChange: this.handleHeaderLayout,
|
const {
|
||||||
gestureDirection:
|
animationEnabled,
|
||||||
focusedOptions.gestureDirection !== undefined
|
gestureDirection = defaultTransitionPreset.gestureDirection,
|
||||||
? focusedOptions.gestureDirection
|
transitionSpec = defaultTransitionPreset.transitionSpec,
|
||||||
: defaultTransitionPreset.gestureDirection,
|
cardStyleInterpolator = animationEnabled === false
|
||||||
styleInterpolator:
|
? forNoAnimationCard
|
||||||
focusedOptions.headerStyleInterpolator !== undefined
|
: defaultTransitionPreset.cardStyleInterpolator,
|
||||||
? focusedOptions.headerStyleInterpolator
|
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
|
||||||
: defaultTransitionPreset.headerStyleInterpolator,
|
} = nextScene.descriptor
|
||||||
style: styles.floating,
|
? nextScene.descriptor.options
|
||||||
})
|
: ({} as StackNavigationOptions);
|
||||||
: null}
|
|
||||||
</React.Fragment>
|
transitionConfig = {
|
||||||
|
gestureDirection,
|
||||||
|
transitionSpec,
|
||||||
|
cardStyleInterpolator,
|
||||||
|
headerStyleInterpolator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
top: safeAreaInsetTop = insets.top,
|
||||||
|
right: safeAreaInsetRight = insets.right,
|
||||||
|
bottom: safeAreaInsetBottom = insets.bottom,
|
||||||
|
left: safeAreaInsetLeft = insets.left,
|
||||||
|
} = safeAreaInsets || {};
|
||||||
|
|
||||||
|
const previousRoute = getPreviousRoute({
|
||||||
|
route: scene.route,
|
||||||
|
});
|
||||||
|
|
||||||
|
let previousScene = scenes[index - 1];
|
||||||
|
|
||||||
|
if (previousRoute) {
|
||||||
|
// The previous scene will be shortly before the current scene in the array
|
||||||
|
// So loop back from current index to avoid looping over the full array
|
||||||
|
for (let j = index - 1; j >= 0; j--) {
|
||||||
|
const s = scenes[j];
|
||||||
|
|
||||||
|
if (s && s.route.key === previousRoute.key) {
|
||||||
|
previousScene = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerHeight =
|
||||||
|
headerMode !== 'none' && headerShown !== false
|
||||||
|
? headerHeights[route.key]
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaybeScreen
|
||||||
|
key={route.key}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
enabled={isScreensEnabled}
|
||||||
|
active={isScreenActive}
|
||||||
|
pointerEvents="box-none"
|
||||||
|
>
|
||||||
|
<CardContainer
|
||||||
|
index={index}
|
||||||
|
active={index === self.length - 1}
|
||||||
|
focused={focused}
|
||||||
|
closing={closingRouteKeys.includes(route.key)}
|
||||||
|
layout={layout}
|
||||||
|
gesture={gesture}
|
||||||
|
scene={scene}
|
||||||
|
previousScene={previousScene}
|
||||||
|
safeAreaInsetTop={safeAreaInsetTop}
|
||||||
|
safeAreaInsetRight={safeAreaInsetRight}
|
||||||
|
safeAreaInsetBottom={safeAreaInsetBottom}
|
||||||
|
safeAreaInsetLeft={safeAreaInsetLeft}
|
||||||
|
cardOverlay={cardOverlay}
|
||||||
|
cardOverlayEnabled={cardOverlayEnabled}
|
||||||
|
cardShadowEnabled={cardShadowEnabled}
|
||||||
|
cardStyle={cardStyle}
|
||||||
|
onPageChangeStart={onPageChangeStart}
|
||||||
|
onPageChangeConfirm={onPageChangeConfirm}
|
||||||
|
onPageChangeCancel={onPageChangeCancel}
|
||||||
|
gestureResponseDistance={gestureResponseDistance}
|
||||||
|
headerHeight={headerHeight}
|
||||||
|
onHeaderHeightChange={this.handleHeaderLayout}
|
||||||
|
getPreviousRoute={getPreviousRoute}
|
||||||
|
getFocusedRoute={this.getFocusedRoute}
|
||||||
|
mode={mode}
|
||||||
|
headerMode={headerMode}
|
||||||
|
headerShown={headerShown}
|
||||||
|
hasAbsoluteHeader={
|
||||||
|
isFloatHeaderAbsolute && !headerTransparent
|
||||||
|
}
|
||||||
|
renderHeader={renderHeader}
|
||||||
|
renderScene={renderScene}
|
||||||
|
onOpenRoute={onOpenRoute}
|
||||||
|
onCloseRoute={onCloseRoute}
|
||||||
|
onTransitionStart={onTransitionStart}
|
||||||
|
onTransitionEnd={onTransitionEnd}
|
||||||
|
gestureEnabled={
|
||||||
|
index !== 0 && getGesturesEnabled({ route })
|
||||||
|
}
|
||||||
|
gestureVelocityImpact={gestureVelocityImpact}
|
||||||
|
{...transitionConfig}
|
||||||
|
/>
|
||||||
|
</MaybeScreen>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MaybeScreenContainer>
|
||||||
|
{isFloatHeaderAbsolute ? floatingHeader : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</HeaderShownContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,10 +612,13 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
floating: {
|
absolute: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
},
|
},
|
||||||
|
floating: {
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -330,13 +330,15 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
|
|
||||||
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
||||||
const { state, navigation } = this.props;
|
const { state, navigation } = this.props;
|
||||||
|
const { closingRouteKeys, replacingRouteKeys } = this.state;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.replacingRouteKeys.every((key) => key !== route.key) &&
|
closingRouteKeys.some((key) => key === route.key) &&
|
||||||
|
replacingRouteKeys.every((key) => key !== route.key) &&
|
||||||
state.routeNames.includes(route.name) &&
|
state.routeNames.includes(route.name) &&
|
||||||
!state.routes.some((r) => r.key === route.key)
|
!state.routes.some((r) => r.key === route.key)
|
||||||
) {
|
) {
|
||||||
// If route isn't present in current state, assume that a close animation was cancelled
|
// If route isn't present in current state, but was closing, assume that a close animation was cancelled
|
||||||
// So we need to add this route back to the state
|
// So we need to add this route back to the state
|
||||||
navigation.navigate(route);
|
navigation.navigate(route);
|
||||||
} else {
|
} else {
|
||||||
@@ -409,6 +411,9 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
navigation,
|
navigation,
|
||||||
keyboardHandlingEnabled,
|
keyboardHandlingEnabled,
|
||||||
mode = 'card',
|
mode = 'card',
|
||||||
|
headerMode = mode === 'card' && Platform.OS === 'ios'
|
||||||
|
? 'float'
|
||||||
|
: 'screen',
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -419,9 +424,6 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
closingRouteKeys,
|
closingRouteKeys,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const headerMode =
|
|
||||||
mode === 'card' && Platform.OS === 'ios' ? 'float' : 'screen';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationHelpersContext.Provider value={navigation}>
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
<GestureHandlerWrapper style={styles.container}>
|
<GestureHandlerWrapper style={styles.container}>
|
||||||
|
|||||||
@@ -13506,6 +13506,11 @@ nanoid@^3.1.5:
|
|||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.5.tgz#56da1bb76b619391fc61625e8b4e4bff309b9942"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.5.tgz#56da1bb76b619391fc61625e8b4e4bff309b9942"
|
||||||
integrity sha512-77yYm8wPy8igTpUQv9fA0VzEb5Ohxt5naC3zTK1oAb+u1MiyITtx0jpYrYRFfgJlefwJy2SkCaojZvxSYq6toA==
|
integrity sha512-77yYm8wPy8igTpUQv9fA0VzEb5Ohxt5naC3zTK1oAb+u1MiyITtx0jpYrYRFfgJlefwJy2SkCaojZvxSYq6toA==
|
||||||
|
|
||||||
|
nanoid@^3.1.9:
|
||||||
|
version "3.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.9.tgz#1f148669c70bb2072dc5af0666e46edb6cd31fb2"
|
||||||
|
integrity sha512-fFiXlFo4Wkuei3i6w9SQI6yuzGRTGi8Z2zZKZpUxv/bQlBi4jtbVPBSNFZHQA9PNjofWqtIa8p+pnsc0kgZrhQ==
|
||||||
|
|
||||||
nanomatch@^1.2.9:
|
nanomatch@^1.2.9:
|
||||||
version "1.2.13"
|
version "1.2.13"
|
||||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||||
|
|||||||
Reference in New Issue
Block a user