mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-13 22:42:25 +08:00
Compare commits
12 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
uses: actions/cache@master
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
uses: actions/cache@master
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -40,8 +40,8 @@ app.use(async (ctx) => {
|
||||
>
|
||||
${css}
|
||||
<title>${ref.current?.getCurrentOptions()?.title}</title>
|
||||
<body style="height: 100%">
|
||||
<div id="root" style="display: flex; height: 100%">
|
||||
<body style="min-height: 100%">
|
||||
<div id="root" style="display: flex; min-height: 100vh">
|
||||
${html}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,12 @@
|
||||
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 MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
@@ -8,6 +15,8 @@ import {
|
||||
StackNavigationProp,
|
||||
HeaderBackground,
|
||||
useHeaderHeight,
|
||||
Header,
|
||||
StackHeaderProps,
|
||||
} from '@react-navigation/stack';
|
||||
import BlurView from '../Shared/BlurView';
|
||||
import Article from '../Shared/Article';
|
||||
@@ -91,6 +100,25 @@ type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
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) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
@@ -103,6 +131,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author}`,
|
||||
header: CustomHeader,
|
||||
headerTintColor: '#fff',
|
||||
headerStyle: { backgroundColor: '#ff005d' },
|
||||
headerBackTitleVisible: false,
|
||||
@@ -160,4 +189,10 @@ const styles = StyleSheet.create({
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
banner: {
|
||||
textAlign: 'center',
|
||||
color: 'tomato',
|
||||
backgroundColor: 'papayawhip',
|
||||
padding: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
DarkTheme,
|
||||
PathConfig,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createDrawerNavigator,
|
||||
@@ -48,6 +49,7 @@ import StackHeaderCustomization from './Screens/StackHeaderCustomization';
|
||||
import BottomTabs from './Screens/BottomTabs';
|
||||
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
||||
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||
import NotFound from './Screens/NotFound';
|
||||
import DynamicTabs from './Screens/DynamicTabs';
|
||||
import AuthFlow from './Screens/AuthFlow';
|
||||
import CompatAPI from './Screens/CompatAPI';
|
||||
@@ -68,6 +70,7 @@ type RootDrawerParamList = {
|
||||
|
||||
type RootStackParamList = {
|
||||
Home: undefined;
|
||||
NotFound: undefined;
|
||||
} & {
|
||||
[P in keyof typeof SCREENS]: undefined;
|
||||
};
|
||||
@@ -221,35 +224,45 @@ export default function App() {
|
||||
Root: {
|
||||
path: '',
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
||||
screens: Object.keys(SCREENS).reduce<PathConfig>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
acc[name] = name
|
||||
const path = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.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;
|
||||
},
|
||||
{ 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>}
|
||||
@@ -332,6 +345,11 @@ export default function App() {
|
||||
</ScrollView>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name="NotFound"
|
||||
component={NotFound}
|
||||
options={{ title: 'Oops!' }}
|
||||
/>
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
(name) => (
|
||||
<Stack.Screen
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/bottom-tabs",
|
||||
"description": "Bottom tab navigator following iOS design guidelines",
|
||||
"version": "5.5.1",
|
||||
"version": "5.5.2",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.1.25",
|
||||
"version": "5.1.26",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||
"bugs": {
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.14.3",
|
||||
"@react-navigation/native": "^5.5.0",
|
||||
"@react-navigation/native": "^5.5.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"react": "~16.9.0",
|
||||
"typescript": "^3.8.3"
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "5.9.0",
|
||||
"version": "5.10.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
|
||||
@@ -237,6 +237,12 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
[getKey, getState, setKey, setState, state, addOptionsGetter]
|
||||
);
|
||||
|
||||
const onStateChangeRef = React.useRef(onStateChange);
|
||||
|
||||
React.useEffect(() => {
|
||||
onStateChangeRef.current = onStateChange;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (
|
||||
@@ -263,12 +269,12 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
trackState(getRootState);
|
||||
}
|
||||
|
||||
if (!isFirstMountRef.current && onStateChange) {
|
||||
onStateChange(getRootState());
|
||||
if (!isFirstMountRef.current && onStateChangeRef.current) {
|
||||
onStateChangeRef.current(getRootState());
|
||||
}
|
||||
|
||||
isFirstMountRef.current = false;
|
||||
}, [onStateChange, trackState, getRootState, emitter, state]);
|
||||
}, [trackState, getRootState, emitter, state]);
|
||||
|
||||
return (
|
||||
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
||||
|
||||
@@ -1265,3 +1265,175 @@ it('replaces undefined query params', () => {
|
||||
expect(getPathFromState(state, 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
|
||||
);
|
||||
});
|
||||
|
||||
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>;
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -69,7 +82,8 @@ export default function getPathFromState(
|
||||
|
||||
let pattern: string | undefined;
|
||||
|
||||
let currentParams: Record<string, any> = { ...route.params };
|
||||
let focusedParams: Record<string, any> | undefined;
|
||||
let focusedRoute = getActiveRoute(state);
|
||||
let currentOptions = configs;
|
||||
|
||||
// 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) {
|
||||
const stringify = currentOptions[route.name]?.stringify;
|
||||
|
||||
currentParams = fromEntries(
|
||||
const currentParams = fromEntries(
|
||||
Object.entries(route.params).map(([key, value]) => [
|
||||
key,
|
||||
stringify?.[key] ? stringify[key](value) : String(value),
|
||||
@@ -95,6 +109,26 @@ export default function getPathFromState(
|
||||
if (pattern) {
|
||||
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
|
||||
@@ -128,18 +162,19 @@ export default function getPathFromState(
|
||||
path += pattern
|
||||
.split('/')
|
||||
.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 (p.startsWith(':')) {
|
||||
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('?')) {
|
||||
// Optional params without value assigned in route.params should be ignored
|
||||
return '';
|
||||
@@ -155,17 +190,21 @@ export default function getPathFromState(
|
||||
path += encodeURIComponent(route.name);
|
||||
}
|
||||
|
||||
if (!focusedParams) {
|
||||
focusedParams = focusedRoute.params;
|
||||
}
|
||||
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (currentParams) {
|
||||
for (let param in currentParams) {
|
||||
if (currentParams[param] === 'undefined') {
|
||||
} else if (focusedParams) {
|
||||
for (let param in focusedParams) {
|
||||
if (focusedParams[param] === 'undefined') {
|
||||
// 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) {
|
||||
path += `?${query}`;
|
||||
@@ -189,6 +228,9 @@ const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
|
||||
return acc;
|
||||
}, {} as Record<K, V>);
|
||||
|
||||
const getParamName = (pattern: string) =>
|
||||
pattern.replace(/^:/, '').replace(/\?$/, '');
|
||||
|
||||
const joinPaths = (...paths: string[]): string =>
|
||||
([] as string[])
|
||||
.concat(...paths.map((p) => p.split('/')))
|
||||
|
||||
@@ -59,11 +59,46 @@ export default function getStateFromPath(
|
||||
createNormalizedConfigs(key, options, [], initialRoutes)
|
||||
)
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
// Sort configs so the most exhaustive is always first to be chosen
|
||||
b.pattern.split('/').length - a.pattern.split('/').length
|
||||
);
|
||||
.sort((a, b) => {
|
||||
// Sort config so that:
|
||||
// - the most exhaustive ones are always at the beginning
|
||||
// - 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
|
||||
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
||||
@@ -104,41 +139,37 @@ export default function getStateFromPath(
|
||||
let result: 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) {
|
||||
let routeNames: string[] | undefined;
|
||||
let allParams: Record<string, any> | undefined;
|
||||
let { routeNames, allParams, remainingPath } = matchAgainstConfigs(
|
||||
remaining,
|
||||
configs
|
||||
);
|
||||
|
||||
// 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 = 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;
|
||||
}
|
||||
}
|
||||
remaining = remainingPath;
|
||||
|
||||
// If we hadn't matched any segments earlier, use the path as route name
|
||||
if (routeNames === undefined) {
|
||||
@@ -150,43 +181,7 @@ export default function getStateFromPath(
|
||||
}
|
||||
|
||||
const state = createNestedStateObject(
|
||||
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 };
|
||||
}),
|
||||
createRouteObjects(configs, routeNames, allParams),
|
||||
initialRoutes
|
||||
);
|
||||
|
||||
@@ -229,6 +224,46 @@ const joinPaths = (...paths: string[]): string =>
|
||||
.filter(Boolean)
|
||||
.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 = (
|
||||
screen: string,
|
||||
routeConfig: PathConfig,
|
||||
@@ -311,7 +346,7 @@ const createConfigItem = (
|
||||
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
|
||||
}
|
||||
|
||||
return `${escape(it)}\\/`;
|
||||
return `${it === '*' ? '.*' : escape(it)}\\/`;
|
||||
})
|
||||
.join('')})`
|
||||
)
|
||||
@@ -433,6 +468,49 @@ const createNestedStateObject = (
|
||||
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) => {
|
||||
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.
|
||||
*/
|
||||
navigate<RouteName extends keyof ParamList>(
|
||||
...args: ParamList[RouteName] extends undefined | any
|
||||
...args: undefined extends ParamList[RouteName]
|
||||
? [RouteName] | [RouteName, ParamList[RouteName]]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
|
||||
@@ -34,6 +34,7 @@ import useStateGetters from './useStateGetters';
|
||||
import useOnGetState from './useOnGetState';
|
||||
import useScheduleUpdate from './useScheduleUpdate';
|
||||
import useCurrentRender from './useCurrentRender';
|
||||
import isArrayEqual from './isArrayEqual';
|
||||
|
||||
// This is to make TypeScript compiler happy
|
||||
// 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.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { NavigationState } from '@react-navigation/routers';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import isArrayEqual from './isArrayEqual';
|
||||
|
||||
export default function useOnGetState({
|
||||
getStateForRoute,
|
||||
@@ -16,13 +17,23 @@ export default function useOnGetState({
|
||||
|
||||
const getRehydratedState = React.useCallback(() => {
|
||||
const state = getState();
|
||||
return {
|
||||
...state,
|
||||
routes: state.routes.map((route) => ({
|
||||
...route,
|
||||
state: getStateForRoute(route.key),
|
||||
})),
|
||||
};
|
||||
|
||||
// Avoid returning new route objects if we don't need to
|
||||
const routes = state.routes.map((route) => {
|
||||
const childState = 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]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/drawer",
|
||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||
"version": "5.8.1",
|
||||
"version": "5.8.2",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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-native": "^0.62.7",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -100,7 +100,7 @@ type Props = {
|
||||
|
||||
export default class DrawerView extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
drawerPostion: I18nManager.isRTL ? 'left' : 'right',
|
||||
drawerPosition: I18nManager.isRTL ? 'left' : 'right',
|
||||
drawerType: 'front',
|
||||
gestureEnabled: true,
|
||||
swipeEnabled: Platform.OS !== 'web',
|
||||
|
||||
@@ -238,7 +238,6 @@ export default function DrawerView({
|
||||
renderDrawerContent={renderNavigationView}
|
||||
renderSceneContent={renderContent}
|
||||
keyboardDismissMode={keyboardDismissMode}
|
||||
drawerPostion={drawerPosition}
|
||||
dimensions={dimensions}
|
||||
/>
|
||||
</DrawerOpenContext.Provider>
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-bottom-tabs",
|
||||
"description": "Integration for bottom navigation component from react-native-paper",
|
||||
"version": "5.2.9",
|
||||
"version": "5.2.10",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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-native": "^0.62.7",
|
||||
"@types/react-native-vector-icons": "^6.4.5",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-top-tabs",
|
||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||
"version": "5.2.9",
|
||||
"version": "5.2.10",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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-native": "^0.62.7",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/native",
|
||||
"description": "React Native integration for React Navigation",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
@@ -33,7 +33,8 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.9.0"
|
||||
"@react-navigation/core": "^5.10.0",
|
||||
"nanoid": "^3.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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,
|
||||
getActionFromState,
|
||||
} from '@react-navigation/core';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import ServerContext from './ServerContext';
|
||||
import { LinkingOptions } from './types';
|
||||
|
||||
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||
|
||||
type HistoryState = { index: number };
|
||||
|
||||
declare const history: {
|
||||
state?: HistoryState;
|
||||
go(delta: number): void;
|
||||
pushState(state: HistoryState, title: string, url: string): void;
|
||||
replaceState(state: HistoryState, title: string, url: string): void;
|
||||
type HistoryRecord = {
|
||||
// Unique identifier for this record to match it with window.history.state
|
||||
id: string;
|
||||
// Navigation state object for the history entry
|
||||
state: NavigationState;
|
||||
// Path of the history entry
|
||||
path: string;
|
||||
};
|
||||
|
||||
const getStateLength = (state: NavigationState) => {
|
||||
let length = 0;
|
||||
const createMemoryHistory = () => {
|
||||
let index = 0;
|
||||
let items: HistoryRecord[] = [];
|
||||
|
||||
if (state.history) {
|
||||
length = state.history.length;
|
||||
} else {
|
||||
length = state.index + 1;
|
||||
// Whether there's a `history.go(n)` pending
|
||||
let pending = false;
|
||||
|
||||
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) {
|
||||
// If the focused route has history entries, we need to count them as well
|
||||
length += getStateLength(focusedState as NavigationState) - 1;
|
||||
const aRoute = a.routes[a.index];
|
||||
const bRoute = b.routes[b.index];
|
||||
|
||||
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;
|
||||
@@ -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
|
||||
// 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
|
||||
@@ -116,203 +307,143 @@ export default function useLinking(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
|
||||
const previousHistoryIndexRef = React.useRef(0);
|
||||
|
||||
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);
|
||||
const previousStateRef = React.useRef<NavigationState | undefined>(undefined);
|
||||
const pendingPopStatePathRef = React.useRef<string | undefined>(undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPopState = () => {
|
||||
return history.listen(() => {
|
||||
const navigation = ref.current;
|
||||
|
||||
if (!navigation || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousHistoryIndex = previousHistoryIndexRef.current;
|
||||
const historyIndex = history.state?.index ?? 0;
|
||||
const path = location.pathname + location.search;
|
||||
|
||||
previousHistoryIndexRef.current = historyIndex;
|
||||
pendingPopStatePathRef.current = path;
|
||||
|
||||
if (pendingIndexChangeRef.current === historyIndex) {
|
||||
pendingIndexChangeRef.current = undefined;
|
||||
// When browser back/forward is clicked, we first need to check if state object for this index exists
|
||||
// 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;
|
||||
}
|
||||
|
||||
const state = navigation.getRootState();
|
||||
const path = getPathFromStateRef.current(state, configRef.current);
|
||||
const state = getStateFromPathRef.current(path, configRef.current);
|
||||
|
||||
let canGoBack = true;
|
||||
let numberOfBacks = 0;
|
||||
if (state) {
|
||||
const action = getActionFromState(state);
|
||||
|
||||
if (previousHistoryIndex === historyIndex) {
|
||||
if (location.pathname + location.search !== path) {
|
||||
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();
|
||||
}
|
||||
if (action !== undefined) {
|
||||
navigation.dispatch(action);
|
||||
} 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) {
|
||||
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]);
|
||||
});
|
||||
}, [enabled, history, ref]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.current && previousStateLengthRef.current === undefined) {
|
||||
previousStateLengthRef.current = getStateLength(
|
||||
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();
|
||||
if (ref.current) {
|
||||
// We need to record the current metadata on the first render if they aren't set
|
||||
// This will allow the initial state to be in the history entry
|
||||
const state = ref.current.getRootState();
|
||||
const path = getPathFromStateRef.current(state, configRef.current);
|
||||
|
||||
const previousStateLength = previousStateLengthRef.current ?? 1;
|
||||
const stateLength = getStateLength(state);
|
||||
|
||||
if (pendingStateMultiUpdateRef.current) {
|
||||
if (location.pathname + location.search === path) {
|
||||
pendingStateMultiUpdateRef.current = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (previousStateRef.current === undefined) {
|
||||
previousStateRef.current = state;
|
||||
}
|
||||
|
||||
previousStateLengthRef.current = stateLength;
|
||||
history.replace({ path, state });
|
||||
}
|
||||
|
||||
if (
|
||||
pendingStateUpdateRef.current &&
|
||||
location.pathname + location.search === path
|
||||
) {
|
||||
pendingStateUpdateRef.current = false;
|
||||
const onStateChange = async () => {
|
||||
const navigation = ref.current;
|
||||
|
||||
if (!navigation || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = history.state?.index ?? 0;
|
||||
const previousState = previousStateRef.current;
|
||||
const state = navigation.getRootState();
|
||||
|
||||
if (previousStateLength === stateLength) {
|
||||
// If no new entries were added to history in our navigation state, we want to replaceState
|
||||
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);
|
||||
}
|
||||
const pendingPath = pendingPopStatePathRef.current;
|
||||
const path = getPathFromStateRef.current(state, configRef.current);
|
||||
|
||||
previousHistoryIndexRef.current = index;
|
||||
} else if (previousStateLength > stateLength) {
|
||||
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
|
||||
);
|
||||
previousStateRef.current = state;
|
||||
pendingPopStatePathRef.current = undefined;
|
||||
|
||||
if (delta > 0) {
|
||||
// We need to set this to ignore the `popstate` event
|
||||
pendingIndexChangeRef.current = index - delta;
|
||||
// To detect the kind of state change, we need to:
|
||||
// - Find the common focused navigation state in previous and current state
|
||||
// - 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
|
||||
history.go(-delta);
|
||||
} else {
|
||||
// We're not going back in history, but the navigation state changed
|
||||
// The URL probably also changed, so we need to re-sync the URL
|
||||
if (location.pathname + location.search !== path) {
|
||||
history.replaceState({ index }, '', path);
|
||||
previousHistoryIndexRef.current = index;
|
||||
if (
|
||||
previousFocusedState &&
|
||||
focusedState &&
|
||||
// We should only handle push/pop if path changed from what was in last `popstate`
|
||||
// Otherwise it's likely a change triggered by `popstate`
|
||||
path !== pendingPath
|
||||
) {
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/stack",
|
||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||
"version": "5.4.1",
|
||||
"version": "5.4.2",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -42,7 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.14.3",
|
||||
"@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/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
|
||||
@@ -53,8 +53,7 @@ type Props = TransitionPreset & {
|
||||
gestureVelocityImpact?: number;
|
||||
mode: StackCardMode;
|
||||
headerMode: StackHeaderMode;
|
||||
headerShown?: boolean;
|
||||
headerTransparent?: boolean;
|
||||
hasAbsoluteHeader: boolean;
|
||||
headerHeight: number;
|
||||
onHeaderHeightChange: (props: {
|
||||
route: Route<string>;
|
||||
@@ -82,9 +81,8 @@ function CardContainer({
|
||||
getFocusedRoute,
|
||||
mode,
|
||||
headerMode,
|
||||
headerShown,
|
||||
headerStyleInterpolator,
|
||||
headerTransparent,
|
||||
hasAbsoluteHeader,
|
||||
headerHeight,
|
||||
onHeaderHeightChange,
|
||||
index,
|
||||
@@ -187,11 +185,7 @@ function CardContainer({
|
||||
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
|
||||
pointerEvents={active ? 'box-none' : pointerEvents}
|
||||
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
|
||||
containerStyle={
|
||||
headerMode === 'float' && !headerTransparent && headerShown !== false
|
||||
? { marginTop: headerHeight }
|
||||
: null
|
||||
}
|
||||
containerStyle={hasAbsoluteHeader ? { marginTop: headerHeight } : null}
|
||||
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
|
||||
style={StyleSheet.absoluteFill}
|
||||
>
|
||||
|
||||
@@ -335,6 +335,24 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
return state.routes[state.index];
|
||||
};
|
||||
|
||||
private doesFloatHeaderNeedAbsolutePositioning = () => {
|
||||
if (this.props.headerMode !== 'float') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.state.scenes.slice(-2).some((scene) => {
|
||||
const { descriptor } = scene;
|
||||
const options = descriptor ? descriptor.options : {};
|
||||
const { headerTransparent, headerShown } = options;
|
||||
|
||||
if (headerTransparent || headerShown === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
mode,
|
||||
@@ -363,6 +381,8 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
const focusedDescriptor = descriptors[focusedRoute.key];
|
||||
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
|
||||
|
||||
const isFloatHeaderAbsolute = this.doesFloatHeaderNeedAbsolutePositioning();
|
||||
|
||||
let defaultTransitionPreset =
|
||||
mode === 'modal' ? ModalTransition : DefaultTransition;
|
||||
|
||||
@@ -384,8 +404,36 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
// For modals, usually we want the screen underneath to be visible, so also disable it there
|
||||
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
|
||||
|
||||
let floatingHeader;
|
||||
|
||||
if (headerMode === 'float') {
|
||||
floatingHeader = (
|
||||
<React.Fragment key="header">
|
||||
{renderHeader({
|
||||
mode: 'float',
|
||||
layout,
|
||||
insets: { top, right, bottom, left },
|
||||
scenes,
|
||||
getPreviousRoute,
|
||||
getFocusedRoute: this.getFocusedRoute,
|
||||
onContentHeightChange: this.handleHeaderLayout,
|
||||
gestureDirection:
|
||||
focusedOptions.gestureDirection !== undefined
|
||||
? focusedOptions.gestureDirection
|
||||
: defaultTransitionPreset.gestureDirection,
|
||||
styleInterpolator:
|
||||
focusedOptions.headerStyleInterpolator !== undefined
|
||||
? focusedOptions.headerStyleInterpolator
|
||||
: defaultTransitionPreset.headerStyleInterpolator,
|
||||
style: isFloatHeaderAbsolute ? styles.floating : undefined,
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isFloatHeaderAbsolute ? null : floatingHeader}
|
||||
<MaybeScreenContainer
|
||||
enabled={isScreensEnabled}
|
||||
style={styles.container}
|
||||
@@ -522,8 +570,11 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
getFocusedRoute={this.getFocusedRoute}
|
||||
mode={mode}
|
||||
headerMode={headerMode}
|
||||
headerShown={headerShown}
|
||||
headerTransparent={headerTransparent}
|
||||
hasAbsoluteHeader={
|
||||
isFloatHeaderAbsolute &&
|
||||
headerShown !== false &&
|
||||
!headerTransparent
|
||||
}
|
||||
renderHeader={renderHeader}
|
||||
renderScene={renderScene}
|
||||
onOpenRoute={onOpenRoute}
|
||||
@@ -538,26 +589,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
);
|
||||
})}
|
||||
</MaybeScreenContainer>
|
||||
{headerMode === 'float'
|
||||
? renderHeader({
|
||||
mode: 'float',
|
||||
layout,
|
||||
insets: { top, right, bottom, left },
|
||||
scenes,
|
||||
getPreviousRoute,
|
||||
getFocusedRoute: this.getFocusedRoute,
|
||||
onContentHeightChange: this.handleHeaderLayout,
|
||||
gestureDirection:
|
||||
focusedOptions.gestureDirection !== undefined
|
||||
? focusedOptions.gestureDirection
|
||||
: defaultTransitionPreset.gestureDirection,
|
||||
styleInterpolator:
|
||||
focusedOptions.headerStyleInterpolator !== undefined
|
||||
? focusedOptions.headerStyleInterpolator
|
||||
: defaultTransitionPreset.headerStyleInterpolator,
|
||||
style: styles.floating,
|
||||
})
|
||||
: null}
|
||||
{isFloatHeaderAbsolute ? floatingHeader : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13506,6 +13506,11 @@ nanoid@^3.1.5:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.5.tgz#56da1bb76b619391fc61625e8b4e4bff309b9942"
|
||||
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:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
|
||||
Reference in New Issue
Block a user