mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-13 09:30:30 +08:00
Compare commits
8 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ff0531695 | ||
|
|
0149e85a95 | ||
|
|
3c47716826 | ||
|
|
acc9646426 | ||
|
|
6dce0780ed | ||
|
|
dd7cff2016 | ||
|
|
740c6b6706 | ||
|
|
039017bc2a |
@@ -7,6 +7,7 @@ import {
|
||||
I18nManager,
|
||||
Dimensions,
|
||||
ScaledSize,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
@@ -138,18 +139,18 @@ export default function App() {
|
||||
React.useEffect(() => {
|
||||
const restoreState = async () => {
|
||||
try {
|
||||
let state;
|
||||
const initialUrl = await Linking.getInitialURL();
|
||||
|
||||
if (Platform.OS !== 'web' && state === undefined) {
|
||||
if (Platform.OS !== 'web' || initialUrl === null) {
|
||||
const savedState = await AsyncStorage.getItem(
|
||||
NAVIGATION_PERSISTENCE_KEY
|
||||
);
|
||||
|
||||
state = savedState ? JSON.parse(savedState) : undefined;
|
||||
}
|
||||
const state = savedState ? JSON.parse(savedState) : undefined;
|
||||
|
||||
if (state !== undefined) {
|
||||
setInitialState(state);
|
||||
if (state !== undefined) {
|
||||
setInitialState(state);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.4.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.2...@react-navigation/bottom-tabs@5.4.3) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.1...@react-navigation/bottom-tabs@5.4.2) (2020-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.0...@react-navigation/bottom-tabs@5.4.1) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/bottom-tabs",
|
||||
"description": "Bottom tab navigator following iOS design guidelines",
|
||||
"version": "5.4.1",
|
||||
"version": "5.4.3",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
"@react-navigation/native": "^5.2.6",
|
||||
"@react-navigation/native": "^5.3.1",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.19](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.18...@react-navigation/compat@5.1.19) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.18](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.17...@react-navigation/compat@5.1.18) (2020-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.17](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.16...@react-navigation/compat@5.1.17) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.1.17",
|
||||
"version": "5.1.19",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||
"bugs": {
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
"@react-navigation/native": "^5.2.6",
|
||||
"@react-navigation/native": "^5.3.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"react": "~16.9.0",
|
||||
"typescript": "^3.8.3"
|
||||
|
||||
@@ -3,6 +3,23 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.2...@react-navigation/core@5.6.0) (2020-05-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ignore extra slashes in the pattern ([3c47716](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3c47716826d0dfa69dfa6112141c116723372ea1))
|
||||
* ignore state updates when we're not mounted ([0149e85](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/0149e85a95b90c6a9d487fa753ddbf5d01c03e3d)), closes [#8226](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8226)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* merge path patterns for nested screens ([#8253](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8253)) ([acc9646](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/acc9646426fee53558d686dfbe5fd0e35361d8c0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.5.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.1...@react-navigation/core@5.5.2) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "5.5.2",
|
||||
"version": "5.6.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
|
||||
@@ -117,7 +117,8 @@ it("doesn't add query param for empty params", () => {
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens', () => {
|
||||
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const path =
|
||||
'/foo/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
@@ -182,8 +183,77 @@ it('handles state with config with nested screens', () => {
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens and exact', () => {
|
||||
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
id: (id: number) => `x${id}`,
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: '10',
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens and unused configs', () => {
|
||||
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
|
||||
const path = '/foo/foe/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
@@ -239,6 +309,66 @@ it('handles state with config with nested screens and unused configs', () => {
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens and unused configs with exact', () => {
|
||||
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object with stringify in it', () => {
|
||||
const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
@@ -252,7 +382,6 @@ it('handles nested object with stringify in it', () => {
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
@@ -312,8 +441,82 @@ it('handles nested object with stringify in it', () => {
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object with stringify in it with exact', () => {
|
||||
const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
exact: true,
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth', () => {
|
||||
const path = '/baz';
|
||||
const path = '/foo/bar/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
@@ -351,7 +554,95 @@ it('handles nested object for second route depth', () => {
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth and and path and stringify in roots', () => {
|
||||
it('handles nested object for second route depth with exact', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth and path and stringify in roots', () => {
|
||||
const path = '/foo/dathomir/bar/42/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:planet',
|
||||
stringify: {
|
||||
id: (id: number) => `planet=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
params: { planet: 'dathomir' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz', params: { id: 42 } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth and path and stringify in roots with exact', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
@@ -370,7 +661,10 @@ it('handles nested object for second route depth and and path and stringify in r
|
||||
id: Number,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -470,7 +764,7 @@ it('keeps query params if path is empty', () => {
|
||||
});
|
||||
|
||||
it('cuts nested configs too', () => {
|
||||
const path = '/baz';
|
||||
const path = '/foo/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
@@ -478,7 +772,48 @@ it('cuts nested configs too', () => {
|
||||
Bar: '',
|
||||
},
|
||||
},
|
||||
Baz: { path: 'baz' },
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('cuts nested configs too with exact', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Bar: {
|
||||
path: '',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -504,7 +839,7 @@ it('cuts nested configs too', () => {
|
||||
});
|
||||
|
||||
it('handles empty path at the end', () => {
|
||||
const path = '/bar';
|
||||
const path = '/foo/bar';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
@@ -641,7 +976,6 @@ it('strips undefined query params', () => {
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
@@ -681,7 +1015,79 @@ it('strips undefined query params', () => {
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: undefined,
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('strips undefined query params with exact', () => {
|
||||
const path = '/bar/sweet/apple/foo/bis/jane?count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
exact: true,
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
@@ -714,7 +1120,6 @@ it('handles stripping all query params', () => {
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
@@ -753,9 +1158,6 @@ it('handles stripping all query params', () => {
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: undefined,
|
||||
answer: undefined,
|
||||
valid: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -773,3 +1175,93 @@ it('handles stripping all query params', () => {
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles stripping all query params with exact', () => {
|
||||
const path = '/bar/sweet/apple/foo/bis/jane';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
exact: true,
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('replaces undefined query params', () => {
|
||||
const path = '/bar/undefined/apple';
|
||||
const config = {
|
||||
Bar: 'bar/:type/:fruit',
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
@@ -147,7 +147,10 @@ it('converts path string to initial state with config with nested screens', () =
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
@@ -213,7 +216,10 @@ it('converts path string to initial state with config with nested screens and un
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
@@ -268,16 +274,23 @@ it('handles nested object with unused configs and with parse in it', () => {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bos: {
|
||||
path: 'bos',
|
||||
exact: true,
|
||||
},
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
exact: true,
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
@@ -348,11 +361,18 @@ it('handles parse in nested object for second route depth', () => {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
exact: true,
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -519,16 +539,23 @@ it('handles two initialRouteNames', () => {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
initialRouteName: 'Bos',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bos: {
|
||||
path: 'bos',
|
||||
exact: true,
|
||||
},
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
exact: true,
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
@@ -601,16 +628,23 @@ it('accepts initialRouteName without config for it', () => {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
initialRouteName: 'Bas',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bos: {
|
||||
path: 'bos',
|
||||
exact: true,
|
||||
},
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
exact: true,
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
@@ -1777,3 +1811,75 @@ it('handle optional params in the beginning v2', () => {
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('merges parent patterns if needed', () => {
|
||||
const path = 'foo/42/baz/babel';
|
||||
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:bar',
|
||||
parse: {
|
||||
bar: Number,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz/:qux',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
params: { bar: 42 },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: { qux: 'babel' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores extra slashes in the pattern', () => {
|
||||
const path = '/bar/42';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Bar: {
|
||||
path: '/bar//:id/',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { id: '42' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,16 +9,15 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
||||
|
||||
type StringifyConfig = Record<string, (value: any) => string>;
|
||||
|
||||
type Options = {
|
||||
[routeName: string]:
|
||||
| string
|
||||
| {
|
||||
path?: string;
|
||||
stringify?: StringifyConfig;
|
||||
screens?: Options;
|
||||
};
|
||||
type OptionsItem = {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
stringify?: StringifyConfig;
|
||||
screens?: Options;
|
||||
};
|
||||
|
||||
type Options = Record<string, string | OptionsItem>;
|
||||
|
||||
/**
|
||||
* Utility to serialize a navigation state object to a path string.
|
||||
*
|
||||
@@ -53,84 +52,78 @@ export default function getPathFromState(
|
||||
if (state === undefined) {
|
||||
throw Error('NavigationState not passed');
|
||||
}
|
||||
let path = '/';
|
||||
|
||||
// Create a normalized configs array which will be easier to use
|
||||
const configs = createNormalizedConfigs(options);
|
||||
|
||||
let path = '/';
|
||||
let current: State | undefined = state;
|
||||
|
||||
const allParams: Record<string, any> = {};
|
||||
|
||||
while (current) {
|
||||
let index = typeof current.index === 'number' ? current.index : 0;
|
||||
let route = current.routes[index] as Route<string> & {
|
||||
state?: State;
|
||||
};
|
||||
let currentOptions = options;
|
||||
let pattern = route.name;
|
||||
// we keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
||||
let nestedRouteNames = '';
|
||||
|
||||
while (route.name in currentOptions) {
|
||||
if (typeof currentOptions[route.name] === 'string') {
|
||||
pattern = currentOptions[route.name] as string;
|
||||
break;
|
||||
} else if (typeof currentOptions[route.name] === 'object') {
|
||||
// if there is no `screens` property, we return pattern
|
||||
if (
|
||||
!(currentOptions[route.name] as {
|
||||
screens: Options;
|
||||
}).screens
|
||||
) {
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
break;
|
||||
let pattern: string | undefined;
|
||||
|
||||
let currentParams: Record<string, any> = { ...route.params };
|
||||
let currentOptions = configs;
|
||||
|
||||
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
||||
let nestedRouteNames = [];
|
||||
|
||||
let hasNext = true;
|
||||
|
||||
while (route.name in currentOptions && hasNext) {
|
||||
pattern = currentOptions[route.name].pattern;
|
||||
|
||||
nestedRouteNames.push(route.name);
|
||||
|
||||
if (route.params) {
|
||||
const stringify = currentOptions[route.name]?.stringify;
|
||||
|
||||
currentParams = Object.fromEntries(
|
||||
Object.entries(route.params).map(([key, value]) => [
|
||||
key,
|
||||
stringify?.[key] ? stringify[key](value) : String(value),
|
||||
])
|
||||
);
|
||||
|
||||
if (pattern) {
|
||||
Object.assign(allParams, currentParams);
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no `screens` property or no nested state, we return pattern
|
||||
if (!currentOptions[route.name].screens || route.state === undefined) {
|
||||
hasNext = false;
|
||||
} else {
|
||||
index =
|
||||
typeof route.state.index === 'number'
|
||||
? route.state.index
|
||||
: route.state.routes.length - 1;
|
||||
|
||||
const nextRoute = route.state.routes[index];
|
||||
const nestedConfig = currentOptions[route.name].screens;
|
||||
|
||||
// if there is config for next route name, we go deeper
|
||||
if (nestedConfig && nextRoute.name in nestedConfig) {
|
||||
route = nextRoute as Route<string> & { state?: State };
|
||||
currentOptions = nestedConfig;
|
||||
} else {
|
||||
// if it is the end of state, we return pattern
|
||||
if (route.state === undefined) {
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
break;
|
||||
} else {
|
||||
index =
|
||||
typeof route.state.index === 'number' ? route.state.index : 0;
|
||||
const nextRoute = route.state.routes[index];
|
||||
const deeperConfig = (currentOptions[route.name] as {
|
||||
screens: Options;
|
||||
}).screens;
|
||||
// if there is config for next route name, we go deeper
|
||||
if (nextRoute.name in deeperConfig) {
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
route = nextRoute as Route<string> & { state?: State };
|
||||
currentOptions = deeperConfig;
|
||||
} else {
|
||||
// if not, there is no sense in going deeper in config
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If not, there is no sense in going deeper in config
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pattern === undefined) {
|
||||
// cut the first `/`
|
||||
pattern = nestedRouteNames.substring(1);
|
||||
pattern = nestedRouteNames.join('/');
|
||||
}
|
||||
|
||||
const config =
|
||||
currentOptions[route.name] !== undefined
|
||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
||||
.stringify
|
||||
: undefined;
|
||||
|
||||
const params = route.params
|
||||
? // Stringify all of the param values before we use them
|
||||
Object.entries(route.params).reduce<{
|
||||
[key: string]: string;
|
||||
}>((acc, [key, value]) => {
|
||||
acc[key] = config?.[key] ? config[key](value) : String(value);
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
if (currentOptions[route.name] !== undefined) {
|
||||
path += pattern
|
||||
.split('/')
|
||||
@@ -138,16 +131,23 @@ export default function getPathFromState(
|
||||
const name = p.replace(/^:/, '').replace(/\?$/, '');
|
||||
|
||||
// If the path has a pattern for a param, put the param in the path
|
||||
if (params && name in params && p.startsWith(':')) {
|
||||
const value = params[name];
|
||||
if (p.startsWith(':')) {
|
||||
const value = allParams[name];
|
||||
|
||||
// Remove the used value from the params object since we'll use the rest for query string
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[name];
|
||||
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 '';
|
||||
}
|
||||
|
||||
return encodeURIComponent(value);
|
||||
} else if (p.endsWith('?')) {
|
||||
// optional params without value assigned in route.params should be ignored
|
||||
return '';
|
||||
}
|
||||
|
||||
return encodeURIComponent(p);
|
||||
})
|
||||
.join('/');
|
||||
@@ -157,14 +157,15 @@ export default function getPathFromState(
|
||||
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (params) {
|
||||
for (let param in params) {
|
||||
if (params[param] === 'undefined') {
|
||||
} else if (currentParams) {
|
||||
for (let param in currentParams) {
|
||||
if (currentParams[param] === 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[param];
|
||||
delete currentParams[param];
|
||||
}
|
||||
}
|
||||
const query = queryString.stringify(params);
|
||||
|
||||
const query = queryString.stringify(currentParams);
|
||||
|
||||
if (query) {
|
||||
path += `?${query}`;
|
||||
@@ -180,3 +181,60 @@ export default function getPathFromState(
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
type ConfigItem = {
|
||||
pattern?: string;
|
||||
stringify?: StringifyConfig;
|
||||
screens?: Record<string, ConfigItem>;
|
||||
};
|
||||
|
||||
function joinPaths(...paths: string[]): string {
|
||||
return paths
|
||||
.map((p) => p.split('/'))
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
function createConfigItem(
|
||||
config: OptionsItem | string,
|
||||
parentPattern?: string
|
||||
): ConfigItem {
|
||||
if (typeof config === 'string') {
|
||||
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
|
||||
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
|
||||
|
||||
return { pattern };
|
||||
}
|
||||
|
||||
// If an object is specified as the value (e.g. Foo: { ... }),
|
||||
// It can have `path` property and `screens` prop which has nested configs
|
||||
const pattern =
|
||||
config.exact !== true && parentPattern && config.path
|
||||
? joinPaths(parentPattern, config.path)
|
||||
: config.path;
|
||||
|
||||
const screens = config.screens
|
||||
? createNormalizedConfigs(config.screens, pattern)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
|
||||
pattern: pattern?.split('/').filter(Boolean).join('/'),
|
||||
stringify: config.stringify,
|
||||
screens,
|
||||
};
|
||||
}
|
||||
|
||||
function createNormalizedConfigs(
|
||||
options: Options,
|
||||
pattern?: string
|
||||
): Record<string, ConfigItem> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).map(([name, c]) => {
|
||||
const result = createConfigItem(c, pattern);
|
||||
|
||||
return [name, result];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Options = {
|
||||
| string
|
||||
| {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
parse?: ParseConfig;
|
||||
screens?: Options;
|
||||
initialRouteName?: string;
|
||||
@@ -21,10 +22,11 @@ type Options = {
|
||||
|
||||
type RouteConfig = {
|
||||
screen: string;
|
||||
match: RegExp | null;
|
||||
regex?: RegExp;
|
||||
path: string;
|
||||
pattern: string;
|
||||
routeNames: string[];
|
||||
parse: ParseConfig | undefined;
|
||||
parse?: ParseConfig;
|
||||
};
|
||||
|
||||
type InitialRouteConfig = {
|
||||
@@ -62,17 +64,14 @@ export default function getStateFromPath(
|
||||
let initialRoutes: InitialRouteConfig[] = [];
|
||||
|
||||
// Create a normalized configs array which will be easier to use
|
||||
const configs = ([] as RouteConfig[]).concat(
|
||||
...Object.keys(options).map((key) =>
|
||||
createNormalizedConfigs(key, options, [], initialRoutes)
|
||||
)
|
||||
);
|
||||
|
||||
// sort configs so the most exhaustive is always first to be chosen
|
||||
configs.sort(
|
||||
(config1, config2) =>
|
||||
config2.pattern.split('/').length - config1.pattern.split('/').length
|
||||
);
|
||||
const configs = Object.keys(options)
|
||||
.map((key) => createNormalizedConfigs(key, options, [], initialRoutes))
|
||||
.flat()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
// Sort configs so the most exhaustive is always first to be chosen
|
||||
b.pattern.split('/').length - a.pattern.split('/').length
|
||||
);
|
||||
|
||||
let remaining = path
|
||||
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
||||
@@ -87,18 +86,23 @@ export default function getStateFromPath(
|
||||
// When handling empty path, we should only look at the root level config
|
||||
const match = configs.find(
|
||||
(config) =>
|
||||
config.pattern === '' &&
|
||||
config.path === '' &&
|
||||
config.routeNames.every(
|
||||
// make sure that none of the parent configs have a non-empty path defined
|
||||
(name) => !configs.find((c) => c.screen === name)?.pattern
|
||||
// Make sure that none of the parent configs have a non-empty path defined
|
||||
(name) => !configs.find((c) => c.screen === name)?.path
|
||||
)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
return createNestedStateObject(
|
||||
match.routeNames,
|
||||
initialRoutes,
|
||||
parseQueryParams(path, match.parse)
|
||||
match.routeNames.map((name, i, self) => {
|
||||
if (i === self.length - 1) {
|
||||
return { name, params: parseQueryParams(path, match.parse) };
|
||||
}
|
||||
|
||||
return { name };
|
||||
}),
|
||||
initialRoutes
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,15 +114,15 @@ export default function getStateFromPath(
|
||||
|
||||
while (remaining) {
|
||||
let routeNames: string[] | undefined;
|
||||
let params: Record<string, any> | undefined;
|
||||
let allParams: Record<string, any> | undefined;
|
||||
|
||||
// Go through all configs, and see if the next path segment matches our regex
|
||||
for (const config of configs) {
|
||||
if (!config.match) {
|
||||
if (!config.regex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = remaining.match(config.match);
|
||||
const match = remaining.match(config.regex);
|
||||
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
@@ -129,16 +133,10 @@ export default function getStateFromPath(
|
||||
.filter((p) => p.startsWith(':'));
|
||||
|
||||
if (paramPatterns.length) {
|
||||
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
||||
const value = match[(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
|
||||
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
|
||||
|
||||
if (value) {
|
||||
acc[key] =
|
||||
config.parse && config.parse[key]
|
||||
? config.parse[key](value)
|
||||
: value;
|
||||
}
|
||||
acc[p] = value;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -159,7 +157,46 @@ export default function getStateFromPath(
|
||||
remaining = segments.join('/');
|
||||
}
|
||||
|
||||
const state = createNestedStateObject(routeNames, initialRoutes, params);
|
||||
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 };
|
||||
}),
|
||||
initialRoutes
|
||||
);
|
||||
|
||||
if (current) {
|
||||
// The state should be nested inside the deepest route we parsed before
|
||||
@@ -194,44 +231,67 @@ export default function getStateFromPath(
|
||||
return result;
|
||||
}
|
||||
|
||||
function joinPaths(...paths: string[]): string {
|
||||
return paths
|
||||
.map((p) => p.split('/'))
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
function createNormalizedConfigs(
|
||||
key: string,
|
||||
screen: string,
|
||||
routeConfig: Options,
|
||||
routeNames: string[] = [],
|
||||
initials: InitialRouteConfig[]
|
||||
initials: InitialRouteConfig[],
|
||||
parentPattern?: string
|
||||
): RouteConfig[] {
|
||||
const configs: RouteConfig[] = [];
|
||||
|
||||
routeNames.push(key);
|
||||
routeNames.push(screen);
|
||||
|
||||
const value = routeConfig[key];
|
||||
const config = routeConfig[screen];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (typeof config === 'string') {
|
||||
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
|
||||
configs.push(createConfigItem(key, routeNames, value));
|
||||
} else if (typeof value === 'object') {
|
||||
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
|
||||
|
||||
configs.push(createConfigItem(screen, routeNames, pattern, config));
|
||||
} else if (typeof config === 'object') {
|
||||
let pattern: string | undefined;
|
||||
|
||||
// if an object is specified as the value (e.g. Foo: { ... }),
|
||||
// it can have `path` property and
|
||||
// it could have `screens` prop which has nested configs
|
||||
if (typeof value.path === 'string') {
|
||||
configs.push(createConfigItem(key, routeNames, value.path, value.parse));
|
||||
if (typeof config.path === 'string') {
|
||||
pattern =
|
||||
config.exact !== true && parentPattern
|
||||
? joinPaths(parentPattern, config.path)
|
||||
: config.path;
|
||||
|
||||
configs.push(
|
||||
createConfigItem(screen, routeNames, pattern, config.path, config.parse)
|
||||
);
|
||||
}
|
||||
|
||||
if (value.screens) {
|
||||
if (config.screens) {
|
||||
// property `initialRouteName` without `screens` has no purpose
|
||||
if (value.initialRouteName) {
|
||||
if (config.initialRouteName) {
|
||||
initials.push({
|
||||
initialRouteName: value.initialRouteName,
|
||||
connectedRoutes: Object.keys(value.screens),
|
||||
initialRouteName: config.initialRouteName,
|
||||
connectedRoutes: Object.keys(config.screens),
|
||||
});
|
||||
}
|
||||
Object.keys(value.screens).forEach((nestedConfig) => {
|
||||
|
||||
Object.keys(config.screens).forEach((nestedConfig) => {
|
||||
const result = createNormalizedConfigs(
|
||||
nestedConfig,
|
||||
value.screens as Options,
|
||||
config.screens as Options,
|
||||
routeNames,
|
||||
initials
|
||||
initials,
|
||||
pattern
|
||||
);
|
||||
|
||||
configs.push(...result);
|
||||
});
|
||||
}
|
||||
@@ -246,9 +306,13 @@ function createConfigItem(
|
||||
screen: string,
|
||||
routeNames: string[],
|
||||
pattern: string,
|
||||
path: string,
|
||||
parse?: ParseConfig
|
||||
): RouteConfig {
|
||||
const match = pattern
|
||||
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
|
||||
pattern = pattern.split('/').filter(Boolean).join('/');
|
||||
|
||||
const regex = pattern
|
||||
? new RegExp(
|
||||
`^(${pattern
|
||||
.split('/')
|
||||
@@ -261,12 +325,13 @@ function createConfigItem(
|
||||
})
|
||||
.join('')})`
|
||||
)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
screen,
|
||||
match,
|
||||
regex,
|
||||
pattern,
|
||||
path,
|
||||
// The routeNames array is mutated, so copy it to keep the current state
|
||||
routeNames: [...routeNames],
|
||||
parse,
|
||||
@@ -305,21 +370,18 @@ function findInitialRoute(
|
||||
function createStateObject(
|
||||
initialRoute: string | undefined,
|
||||
routeName: string,
|
||||
isEmpty: boolean,
|
||||
params?: Record<string, any> | undefined
|
||||
params: Record<string, any> | undefined,
|
||||
isEmpty: boolean
|
||||
): InitialState {
|
||||
if (isEmpty) {
|
||||
if (initialRoute) {
|
||||
return {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: initialRoute },
|
||||
{ name: routeName as string, ...(params && { params }) },
|
||||
],
|
||||
routes: [{ name: initialRoute }, { name: routeName as string, params }],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
routes: [{ name: routeName as string, ...(params && { params }) }],
|
||||
routes: [{ name: routeName as string, params }],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -328,44 +390,50 @@ function createStateObject(
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: initialRoute },
|
||||
{ name: routeName as string, state: { routes: [] } },
|
||||
{ name: routeName as string, params, state: { routes: [] } },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return { routes: [{ name: routeName as string, state: { routes: [] } }] };
|
||||
return {
|
||||
routes: [{ name: routeName as string, params, state: { routes: [] } }],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createNestedStateObject(
|
||||
routeNames: string[],
|
||||
initialRoutes: InitialRouteConfig[],
|
||||
params: object | undefined
|
||||
routes: { name: string; params?: object }[],
|
||||
initialRoutes: InitialRouteConfig[]
|
||||
) {
|
||||
let state: InitialState;
|
||||
let routeName = routeNames.shift() as string;
|
||||
let initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
let route = routes.shift() as { name: string; params?: object };
|
||||
let initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||
|
||||
state = createStateObject(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
route.name,
|
||||
route.params,
|
||||
routes.length === 0
|
||||
);
|
||||
|
||||
if (routeNames.length > 0) {
|
||||
if (routes.length > 0) {
|
||||
let nestedState = state;
|
||||
|
||||
while ((routeName = routeNames.shift() as string)) {
|
||||
initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
nestedState.routes[nestedState.index || 0].state = createStateObject(
|
||||
while ((route = routes.shift() as { name: string; params?: object })) {
|
||||
initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||
|
||||
const nestedStateIndex =
|
||||
nestedState.index || nestedState.routes.length - 1;
|
||||
|
||||
nestedState.routes[nestedStateIndex].state = createStateObject(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
route.name,
|
||||
route.params,
|
||||
routes.length === 0
|
||||
);
|
||||
if (routeNames.length > 0) {
|
||||
nestedState = nestedState.routes[nestedState.index || 0]
|
||||
|
||||
if (routes.length > 0) {
|
||||
nestedState = nestedState.routes[nestedStateIndex]
|
||||
.state as InitialState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ const UNINTIALIZED_STATE = {};
|
||||
export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||
const stateRef = React.useRef<T>(UNINTIALIZED_STATE as any);
|
||||
const isSchedulingRef = React.useRef(false);
|
||||
const isMountedRef = React.useRef(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (stateRef.current === UNINTIALIZED_STATE) {
|
||||
stateRef.current =
|
||||
@@ -20,7 +29,7 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||
const getState = React.useCallback(() => stateRef.current, []);
|
||||
|
||||
const setState = React.useCallback((state: T) => {
|
||||
if (state === stateRef.current) {
|
||||
if (state === stateRef.current || !isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,6 +51,10 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||
}, []);
|
||||
|
||||
const flushUpdates = React.useCallback(() => {
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that the tracking state is up-to-date.
|
||||
// We call it unconditionally, but React should skip the update if state is unchanged.
|
||||
setTrackingState(stateRef.current);
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.7.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.2...@react-navigation/drawer@5.7.3) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.7.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.1...@react-navigation/drawer@5.7.2) (2020-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.7.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.0...@react-navigation/drawer@5.7.1) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/drawer",
|
||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||
"version": "5.7.1",
|
||||
"version": "5.7.3",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
"@react-navigation/native": "^5.2.6",
|
||||
"@react-navigation/native": "^5.3.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.2...@react-navigation/material-bottom-tabs@5.2.3) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.1...@react-navigation/material-bottom-tabs@5.2.2) (2020-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.0...@react-navigation/material-bottom-tabs@5.2.1) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-bottom-tabs",
|
||||
"description": "Integration for bottom navigation component from react-native-paper",
|
||||
"version": "5.2.1",
|
||||
"version": "5.2.3",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
"@react-navigation/native": "^5.2.6",
|
||||
"@react-navigation/native": "^5.3.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"@types/react-native-vector-icons": "^6.4.5",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.2...@react-navigation/material-top-tabs@5.2.3) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.1...@react-navigation/material-top-tabs@5.2.2) (2020-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.0...@react-navigation/material-top-tabs@5.2.1) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -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.1",
|
||||
"version": "5.2.3",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
"@react-navigation/native": "^5.2.6",
|
||||
"@react-navigation/native": "^5.3.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -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.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.3.0...@react-navigation/native@5.3.1) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.6...@react-navigation/native@5.3.0) (2020-05-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* initialState should take priority over deep link ([039017b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/039017bc2af69120d2d10e8f2c8a62919c37eb65))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.5...@react-navigation/native@5.2.6) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/native",
|
||||
"description": "React Native integration for React Navigation",
|
||||
"version": "5.2.6",
|
||||
"version": "5.3.1",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
@@ -32,7 +32,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.5.2"
|
||||
"@react-navigation/core": "^5.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
|
||||
@@ -22,7 +22,7 @@ type Props = NavigationContainerProps & {
|
||||
* Container component which holds the navigation state designed for React Native apps.
|
||||
* This should be rendered at the root wrapping the whole app.
|
||||
*
|
||||
* @param props.initialState Initial state object for the navigation tree. When deep link handling is enabled, this will be ignored if there's an incoming link.
|
||||
* @param props.initialState Initial state object for the navigation tree. When deep link handling is enabled, this will override deep links when specified. Make sure that you don't specify an `initialState` when there's a deep link (`Linking.getInitialURL()`).
|
||||
* @param props.onStateChange Callback which is called with the latest navigation state when it changes.
|
||||
* @param props.theme Theme object for the navigators.
|
||||
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
|
||||
@@ -46,15 +46,13 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||
...linking,
|
||||
});
|
||||
|
||||
const [isReady, initialState = rest.initialState] = useThenable(
|
||||
getInitialState
|
||||
);
|
||||
const [isReady, initialState] = useThenable(getInitialState);
|
||||
|
||||
React.useImperativeHandle(ref, () => refContainer.current);
|
||||
|
||||
const linkingContext = React.useMemo(() => ({ options: linking }), [linking]);
|
||||
|
||||
if (isLinkingEnabled && !isReady) {
|
||||
if (rest.initialState == null && isLinkingEnabled && !isReady) {
|
||||
// This is temporary until we have Suspense for data-fetching
|
||||
// Then the fallback will be handled by a parent `Suspense` component
|
||||
return fallback as React.ReactElement;
|
||||
@@ -65,7 +63,9 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||
<ThemeProvider value={theme}>
|
||||
<BaseNavigationContainer
|
||||
{...rest}
|
||||
initialState={initialState}
|
||||
initialState={
|
||||
rest.initialState == null ? initialState : rest.initialState
|
||||
}
|
||||
ref={refContainer}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.3...@react-navigation/stack@5.3.4) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.2...@react-navigation/stack@5.3.3) (2020-05-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix ios transitionspec settle time ([#8028](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8028)) ([dd7cff2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/dd7cff201608365a80f1d50a006df3e0d18e94a1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.1...@react-navigation/stack@5.3.2) (2020-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.0...@react-navigation/stack@5.3.1) (2020-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/stack",
|
||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||
"version": "5.3.1",
|
||||
"version": "5.3.4",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -41,7 +41,7 @@
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.13.1",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"@react-navigation/native": "^5.2.6",
|
||||
"@react-navigation/native": "^5.3.1",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-native": "^0.62.7",
|
||||
|
||||
@@ -11,8 +11,8 @@ export const TransitionIOSSpec: TransitionSpec = {
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
overshootClamping: true,
|
||||
restDisplacementThreshold: 0.01,
|
||||
restSpeedThreshold: 0.01,
|
||||
restDisplacementThreshold: 10,
|
||||
restSpeedThreshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user