mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-14 22:41:55 +08:00
Compare commits
148 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dddaff45c | ||
|
|
21b397f0d6 | ||
|
|
2ff0531695 | ||
|
|
0149e85a95 | ||
|
|
3c47716826 | ||
|
|
acc9646426 | ||
|
|
6dce0780ed | ||
|
|
dd7cff2016 | ||
|
|
740c6b6706 | ||
|
|
039017bc2a | ||
|
|
b85a1c3055 | ||
|
|
18f8188dc8 | ||
|
|
47a1229837 | ||
|
|
00b11e303e | ||
|
|
f384706741 | ||
|
|
d1a6f3e30e | ||
|
|
fd6636a8cd | ||
|
|
eb24fea8b9 | ||
|
|
85ae378d8c | ||
|
|
bea14aa26f | ||
|
|
4d1e102f8c | ||
|
|
f07cd13561 | ||
|
|
f6d06768d3 | ||
|
|
3381d680d7 | ||
|
|
fcd1cc64c1 | ||
|
|
3999fc2836 | ||
|
|
9fd2635756 | ||
|
|
6bec620a3f | ||
|
|
c7b8e2e966 | ||
|
|
719e1a7b46 | ||
|
|
10eca8b92e | ||
|
|
b66e3436a7 | ||
|
|
1c075ffb16 | ||
|
|
1ee3038a4d | ||
|
|
961b519fb1 | ||
|
|
0a19e94b23 | ||
|
|
1e73fed6de | ||
|
|
3193a30da6 | ||
|
|
499c50cd43 | ||
|
|
420f6926e1 | ||
|
|
70be3f6d86 | ||
|
|
bd35b4fc20 | ||
|
|
c511bc0b2b | ||
|
|
b4834ce703 | ||
|
|
ae5442ebe8 | ||
|
|
6dd52d35cf | ||
|
|
d6fa279d93 | ||
|
|
c3fa83efe0 | ||
|
|
f2291d110f | ||
|
|
942d2be2c7 | ||
|
|
b747e527a4 | ||
|
|
38020de80b | ||
|
|
67404f4999 | ||
|
|
2792f438fe | ||
|
|
2573b5beaa | ||
|
|
2697355ab2 | ||
|
|
a695cf9c05 | ||
|
|
c9c825bee6 | ||
|
|
b172b51f17 | ||
|
|
9c05af50b4 | ||
|
|
24febf6ea9 | ||
|
|
8cbb201f1a | ||
|
|
2467ce4ff7 | ||
|
|
5683bebfd6 | ||
|
|
78485cea69 | ||
|
|
1613915669 | ||
|
|
335a04edc1 | ||
|
|
5e0069a896 | ||
|
|
249248e741 | ||
|
|
821343fed3 | ||
|
|
82edb2581b | ||
|
|
cb67530dc5 | ||
|
|
36689e24c2 | ||
|
|
6e51f596fa | ||
|
|
402df73aa2 | ||
|
|
187aefe9c4 | ||
|
|
2613a62874 | ||
|
|
6bdf6ae4ed | ||
|
|
e2bcf5168c | ||
|
|
dfdba8d741 | ||
|
|
a3f7a5feba | ||
|
|
004c7d7ab1 | ||
|
|
49f658fbc0 | ||
|
|
cb2f157a56 | ||
|
|
c4acdaa703 | ||
|
|
f1a8bceba5 | ||
|
|
44081172d4 | ||
|
|
de5d985f3b | ||
|
|
b71de6cc79 | ||
|
|
303f0b78a5 | ||
|
|
ce3994c82c | ||
|
|
ba1f405129 | ||
|
|
d4fd906915 | ||
|
|
b7fa90bf8d | ||
|
|
9556aa9eff | ||
|
|
9a8fea8f2c | ||
|
|
9973db86f0 | ||
|
|
8432e5ab25 | ||
|
|
9bb5cfded3 | ||
|
|
4ac40b5c5d | ||
|
|
cd47915861 | ||
|
|
d649fbc669 | ||
|
|
105da6ab2f | ||
|
|
ac7f972e92 | ||
|
|
babb5027f9 | ||
|
|
78d7a66b2b | ||
|
|
a248c453ba | ||
|
|
e097df880a | ||
|
|
856449b200 | ||
|
|
d94e43c3c8 | ||
|
|
3096de6286 | ||
|
|
1c001424b5 | ||
|
|
0f2368965c | ||
|
|
61f16d3f25 | ||
|
|
853740bfaf | ||
|
|
179b6312fe | ||
|
|
043924ca48 | ||
|
|
813a5903b5 | ||
|
|
3709e652f4 | ||
|
|
5b15c7164f | ||
|
|
e030932497 | ||
|
|
adbfedcd58 | ||
|
|
bc9b044fb3 | ||
|
|
f24d3a3461 | ||
|
|
3df65e2819 | ||
|
|
5c4afc5cb4 | ||
|
|
d5bb357053 | ||
|
|
b1fe73097f | ||
|
|
49f6fed6d3 | ||
|
|
b1a65fc73e | ||
|
|
3ea8eec432 | ||
|
|
00e0f05190 | ||
|
|
193c344ba5 | ||
|
|
358d9e9feb | ||
|
|
6a5d0a035a | ||
|
|
b75744abd5 | ||
|
|
6dbda1a0c2 | ||
|
|
70029d6c13 | ||
|
|
469d0542c7 | ||
|
|
0dcaea3242 | ||
|
|
646cbfb28e | ||
|
|
660cac3557 | ||
|
|
e637250a7e | ||
|
|
82af7bed71 | ||
|
|
cb46d0bca4 | ||
|
|
b3665a325d | ||
|
|
0cc7a12b9c | ||
|
|
90e417248d |
@@ -1,58 +1,96 @@
|
|||||||
version: 2
|
version: 2.1
|
||||||
|
|
||||||
defaults: &defaults
|
executors:
|
||||||
docker:
|
default:
|
||||||
- image: circleci/node:10
|
docker:
|
||||||
working_directory: ~/project
|
- image: circleci/node:10
|
||||||
|
working_directory: ~/project
|
||||||
|
environment:
|
||||||
|
YARN_CACHE_FOLDER: "~/.cache/yarn"
|
||||||
|
|
||||||
|
commands:
|
||||||
|
attach_project:
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: ~/project
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
install-dependencies:
|
install-dependencies:
|
||||||
<<: *defaults
|
executor: default
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_project
|
||||||
at: ~/project
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- v1-dependencies-{{ checksum "yarn.lock" }}
|
- v2-dependencies-{{ checksum "yarn.lock" }}
|
||||||
- v1-dependencies-
|
- v2-dependencies-
|
||||||
- run: yarn install --frozen-lockfile
|
- run:
|
||||||
|
name: Install project dependencies
|
||||||
|
command: yarn install --frozen-lockfile
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: v1-dependencies-{{ checksum "yarn.lock" }}
|
key: v2-dependencies-{{ checksum "yarn.lock" }}
|
||||||
paths: node_modules
|
paths: ~/.cache/yarn
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: .
|
root: .
|
||||||
paths: .
|
paths: .
|
||||||
|
|
||||||
lint-and-typecheck:
|
lint-and-typecheck:
|
||||||
<<: *defaults
|
executor: default
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_project
|
||||||
at: ~/project
|
- run:
|
||||||
- run: |
|
name: Lint files
|
||||||
yarn lint
|
command: yarn lint
|
||||||
yarn typescript
|
- run:
|
||||||
|
name: Typecheck files
|
||||||
|
command: yarn typescript
|
||||||
|
|
||||||
unit-tests:
|
unit-tests:
|
||||||
<<: *defaults
|
executor: default
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_project
|
||||||
at: ~/project
|
- run:
|
||||||
- run: |
|
name: Run unit tests
|
||||||
yarn test --coverage
|
command: yarn test --coverage
|
||||||
cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
- run:
|
||||||
- store_artifacts:
|
name: Upload test coverage
|
||||||
path: coverage
|
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
||||||
destination: coverage
|
- store_artifacts:
|
||||||
|
path: coverage
|
||||||
|
destination: coverage
|
||||||
|
|
||||||
|
integration-tests:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- attach_project
|
||||||
|
- run:
|
||||||
|
name: Install Headless Chrome dependencies
|
||||||
|
command: |
|
||||||
|
sudo apt-get install -yq \
|
||||||
|
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
||||||
|
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
|
||||||
|
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
|
||||||
|
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
|
||||||
|
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||||
|
- run:
|
||||||
|
name: Build example for web
|
||||||
|
command: yarn example expo build:web --no-pwa
|
||||||
|
- run:
|
||||||
|
name: Run integration tests
|
||||||
|
command: yarn example test --maxWorkers=2
|
||||||
|
|
||||||
build-packages:
|
build-packages:
|
||||||
<<: *defaults
|
executor: default
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_project
|
||||||
at: ~/project
|
- run:
|
||||||
- run: |
|
name: Build packages in the monorepo
|
||||||
yarn lerna run prepare
|
command: yarn lerna run prepare
|
||||||
node scripts/check-types-path.js
|
- run:
|
||||||
|
name: Verify paths for types
|
||||||
|
command: node scripts/check-types-path.js
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
|
||||||
build-and-test:
|
build-and-test:
|
||||||
jobs:
|
jobs:
|
||||||
- install-dependencies
|
- install-dependencies
|
||||||
@@ -62,6 +100,9 @@ workflows:
|
|||||||
- unit-tests:
|
- unit-tests:
|
||||||
requires:
|
requires:
|
||||||
- install-dependencies
|
- install-dependencies
|
||||||
|
- integration-tests:
|
||||||
|
requires:
|
||||||
|
- install-dependencies
|
||||||
- build-packages:
|
- build-packages:
|
||||||
requires:
|
requires:
|
||||||
- install-dependencies
|
- install-dependencies
|
||||||
|
|||||||
2
.github/workflows/expo-preview.yml
vendored
2
.github/workflows/expo-preview.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get expo link
|
- name: Get expo link
|
||||||
id: expo
|
id: expo
|
||||||
run: echo "::set-output name=path::@react-navigation/react-react-navigationample?release-channel=pr-${{ github.event.number }}"
|
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
|
||||||
|
|
||||||
- name: Comment on PR
|
- name: Comment on PR
|
||||||
uses: unsplash/comment-on-pr@master
|
uses: unsplash/comment-on-pr@master
|
||||||
|
|||||||
20
.github/workflows/stale.yml
vendored
20
.github/workflows/stale.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: "Close stale issues and pull requests"
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v1
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
stale-issue-message: 'Hello 👋, this issue has been open for more than 3 months with no activity on it. If the issue is still present in the latest version, please leave a comment within 7 days to keep it open, otherwise it will be closed automatically. If you found a solution on workaround for the issue, please comment here for others to find. If this issue is critical for you, please consider sending a pull request to fix the issue.'
|
|
||||||
stale-pr-message: 'Hello 👋, this pull request has been open for more than 3 months with no activity on it. If you think this is still necessary with the latest version, please comment and ping a maintainer to get this reviewed, otherwise it will be closed automatically in 7 days.'
|
|
||||||
days-before-stale: 90
|
|
||||||
days-before-close: 7
|
|
||||||
stale-issue-label: 'stale'
|
|
||||||
stale-pr-label: 'stale'
|
|
||||||
exempt-issue-label: 'keep open'
|
|
||||||
exempt-pr-label: 'keep open'
|
|
||||||
15
.github/workflows/triage.yml
vendored
15
.github/workflows/triage.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to reproduce the issue with minimal code (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
|
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
|
||||||
|
|
||||||
needs-repro:
|
needs-repro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
args: comment "Hey! Thanks for opening the issue. Can you provide a minimal repro which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
|
args: comment "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
|
||||||
|
|
||||||
question:
|
question:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -36,3 +36,14 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. This helps us prioritize fixing bugs in the library. Seems you have a usage question. Please ask the question on [StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation) instead using the `react-navigation` label. You can also chat with other community members on [Reactiflux Discord server](https://www.reactiflux.com/) in the `#react-navigation` channel."
|
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. This helps us prioritize fixing bugs in the library. Seems you have a usage question. Please ask the question on [StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation) instead using the `react-navigation` label. You can also chat with other community members on [Reactiflux Discord server](https://www.reactiflux.com/) in the `#react-navigation` channel."
|
||||||
|
|
||||||
|
feature-request:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.label.name == 'feature-request'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- uses: actions/github@v1.0.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. Seems you have a feature request. Please post the feature request on [Canny](https://react-navigation.canny.io/feature-requests). This lets other users upvote your feature request and helps us prioritize the most requested features."
|
||||||
|
|||||||
27
.github/workflows/versions.yml
vendored
Normal file
27
.github/workflows/versions.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Check versions
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: react-navigation/check-versions-action@master
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
packages: |
|
||||||
|
@react-navigation/bottom-tabs
|
||||||
|
@react-navigation/compat
|
||||||
|
@react-navigation/core
|
||||||
|
@react-navigation/drawer
|
||||||
|
@react-navigation/material-bottom-tabs
|
||||||
|
@react-navigation/material-top-tabs
|
||||||
|
@react-navigation/native
|
||||||
|
@react-navigation/routers
|
||||||
|
@react-navigation/stack
|
||||||
|
react-navigation-animated-switch
|
||||||
|
react-navigation-drawer
|
||||||
|
react-navigation-material-bottom-tabs
|
||||||
|
react-navigation-stack
|
||||||
|
react-navigation-tabs
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
.gradle
|
.gradle
|
||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
|
.history
|
||||||
|
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"slug": "react-navigation-example",
|
"slug": "react-navigation-example",
|
||||||
"description": "Demo app to showcase various functionality of React Navigation",
|
"description": "Demo app to showcase various functionality of React Navigation",
|
||||||
"privacy": "public",
|
"privacy": "public",
|
||||||
"sdkVersion": "36.0.0",
|
"sdkVersion": "37.0.0",
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"ios",
|
"ios",
|
||||||
"android",
|
"android",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = function(api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"settings": {
|
|
||||||
"import/core-modules": [
|
|
||||||
"detox",
|
|
||||||
"detox/runners/jest/adapter",
|
|
||||||
"detox/runners/jest/specReporter"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"env": { "jest": true, "jasmine": true }
|
|
||||||
}
|
|
||||||
44
example/e2e/__integration_tests__/Link.test.tsx
Normal file
44
example/e2e/__integration_tests__/Link.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { page } from '../config/setup-playwright';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await page.click('[data-testid=LinkComponent]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the article page', async () => {
|
||||||
|
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||||
|
'/link-component/Article?author=Gandalf'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||||
|
(it: any) => it.role === 'heading'
|
||||||
|
)?.name
|
||||||
|
).toBe('Article by Gandalf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes to the album page and goes back', async () => {
|
||||||
|
await page.click('[href="/link-component/Album"]');
|
||||||
|
|
||||||
|
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||||
|
'/link-component/Album'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||||
|
(it: any) => it.role === 'heading'
|
||||||
|
)?.name
|
||||||
|
).toBe('Album');
|
||||||
|
|
||||||
|
await page.click('[aria-label="Article by Gandalf, back"]');
|
||||||
|
|
||||||
|
await page.waitForNavigation();
|
||||||
|
|
||||||
|
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||||
|
'/link-component/Article?author=Gandalf'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||||
|
(it: any) => it.role === 'heading'
|
||||||
|
)?.name
|
||||||
|
).toBe('Article by Gandalf');
|
||||||
|
});
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { by, element, expect, device } from 'detox';
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await device.reloadReactNative();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has dark theme toggle', async () => {
|
|
||||||
await expect(element(by.text('Dark theme'))).toBeVisible();
|
|
||||||
});
|
|
||||||
13
example/e2e/__integration_tests__/index.test.tsx
Normal file
13
example/e2e/__integration_tests__/index.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { page } from '../config/setup-playwright';
|
||||||
|
|
||||||
|
it('loads the example app', async () => {
|
||||||
|
const snapshot = await page.accessibility.snapshot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(snapshot?.children?.find((it) => it.role === 'heading')?.name).toBe(
|
||||||
|
'Examples'
|
||||||
|
);
|
||||||
|
const title = await page.$eval('[role=heading]', (el) => el.textContent);
|
||||||
|
|
||||||
|
expect(title).toBe('Examples');
|
||||||
|
});
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"setupFilesAfterEnv": ["./init.js"],
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"reporters": ["detox/runners/jest/streamlineReporter"],
|
|
||||||
"verbose": true
|
|
||||||
}
|
|
||||||
24
example/e2e/config/setup-playwright.tsx
Normal file
24
example/e2e/config/setup-playwright.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-env jest */
|
||||||
|
|
||||||
|
import { chromium, Browser, BrowserContext, Page } from 'playwright';
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
let context: BrowserContext;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
browser = await chromium.launch();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3579');
|
||||||
|
});
|
||||||
|
|
||||||
|
export { browser, context, page };
|
||||||
8
example/e2e/config/setup-server.tsx
Normal file
8
example/e2e/config/setup-server.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { setup } from 'jest-dev-server';
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
await setup({
|
||||||
|
command: 'yarn serve -l 3579 web-build',
|
||||||
|
port: 3579,
|
||||||
|
});
|
||||||
|
}
|
||||||
5
example/e2e/config/teardown-server.tsx
Normal file
5
example/e2e/config/teardown-server.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { teardown } from 'jest-dev-server';
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
await teardown();
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/* eslint-disable jest/no-jasmine-globals, import/no-commonjs */
|
|
||||||
|
|
||||||
const detox = require('detox');
|
|
||||||
const config = require('../../package.json').detox;
|
|
||||||
const adapter = require('detox/runners/jest/adapter');
|
|
||||||
const specReporter = require('detox/runners/jest/specReporter');
|
|
||||||
|
|
||||||
// Set the default timeout
|
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
jasmine.getEnv().addReporter(adapter);
|
|
||||||
|
|
||||||
// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
|
|
||||||
// This is strictly optional.
|
|
||||||
jasmine.getEnv().addReporter(specReporter);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await detox.init(config);
|
|
||||||
}, 300000);
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await adapter.beforeEach();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await adapter.afterAll();
|
|
||||||
await detox.cleanup();
|
|
||||||
});
|
|
||||||
6
example/jest.config.js
Normal file
6
example/jest.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
testRegex: '/__integration_tests__/.*\\.(test|spec)\\.(js|tsx?)$',
|
||||||
|
globalSetup: './e2e/config/setup-server.tsx',
|
||||||
|
globalTeardown: './e2e/config/teardown-server.tsx',
|
||||||
|
setupFilesAfterEnv: ['./e2e/config/setup-playwright.tsx'],
|
||||||
|
};
|
||||||
@@ -15,8 +15,8 @@ const modules = ['@expo/vector-icons']
|
|||||||
// List all packages under `packages/`
|
// List all packages under `packages/`
|
||||||
.readdirSync(packages)
|
.readdirSync(packages)
|
||||||
// Ignore hidden files such as .DS_Store
|
// Ignore hidden files such as .DS_Store
|
||||||
.filter(p => !p.startsWith('.'))
|
.filter((p) => !p.startsWith('.'))
|
||||||
.map(p => {
|
.map((p) => {
|
||||||
const pak = JSON.parse(
|
const pak = JSON.parse(
|
||||||
fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8')
|
fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8')
|
||||||
);
|
);
|
||||||
@@ -50,9 +50,9 @@ module.exports = {
|
|||||||
blacklistRE: blacklist(
|
blacklistRE: blacklist(
|
||||||
fs
|
fs
|
||||||
.readdirSync(packages)
|
.readdirSync(packages)
|
||||||
.map(p => path.join(packages, p))
|
.map((p) => path.join(packages, p))
|
||||||
.map(
|
.map(
|
||||||
it => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
|
(it) => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
enhanceMiddleware: middleware => {
|
enhanceMiddleware: (middleware) => {
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
// When an asset is imported outside the project root, it has wrong path on Android
|
// When an asset is imported outside the project root, it has wrong path on Android
|
||||||
// This happens for the back button in stack, so we fix the path to correct one
|
// This happens for the back button in stack, so we fix the path to correct one
|
||||||
|
|||||||
@@ -8,35 +8,40 @@
|
|||||||
"web": "expo start:web",
|
"web": "expo start:web",
|
||||||
"native": "react-native start",
|
"native": "react-native start",
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
"ios": "react-native run-ios"
|
"ios": "react-native run-ios",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^10.0.0",
|
"@expo/vector-icons": "^10.2.0",
|
||||||
"@react-native-community/masked-view": "0.1.6",
|
"@react-native-community/masked-view": "^0.1.10",
|
||||||
"@types/react-native-restart": "^0.0.0",
|
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"expo": "^36.0.2",
|
"expo": "^37.0.8",
|
||||||
"expo-asset": "~8.0.0",
|
"expo-asset": "~8.1.3",
|
||||||
"expo-blur": "^8.0.0",
|
"expo-blur": "~8.1.0",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"react-dom": "~16.9.0",
|
"react-dom": "~16.9.0",
|
||||||
"react-native": "~0.61.5",
|
"react-native": "~0.61.5",
|
||||||
"react-native-gesture-handler": "^1.5.6",
|
"react-native-gesture-handler": "^1.6.0",
|
||||||
"react-native-paper": "^3.6.0",
|
"react-native-paper": "^3.10.1",
|
||||||
"react-native-reanimated": "^1.7.0",
|
"react-native-reanimated": "^1.8.0",
|
||||||
"react-native-restart": "^0.0.13",
|
"react-native-restart": "^0.0.15",
|
||||||
"react-native-safe-area-context": "^0.7.2",
|
"react-native-safe-area-context": "^0.7.3",
|
||||||
"react-native-screens": "^2.0.0-beta.2",
|
"react-native-screens": "^2.7.0",
|
||||||
"react-native-tab-view": "2.13.0",
|
"react-native-tab-view": "2.14.0",
|
||||||
"react-native-unimodules": "^0.7.0",
|
"react-native-unimodules": "~0.9.1",
|
||||||
"react-native-web": "^0.11.7"
|
"react-native-web": "^0.11.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/webpack-config": "^0.10.12",
|
"@expo/webpack-config": "^0.11.19",
|
||||||
"@types/react": "^16.9.19",
|
"@types/jest-dev-server": "^4.2.0",
|
||||||
"@types/react-native": "^0.60.30",
|
"@types/react": "^16.9.34",
|
||||||
"babel-preset-expo": "^8.0.0",
|
"@types/react-native": "^0.62.7",
|
||||||
"expo-cli": "^3.11.9",
|
"babel-preset-expo": "^8.1.0",
|
||||||
"typescript": "^3.7.5"
|
"expo-cli": "^3.20.1",
|
||||||
|
"jest": "^26.0.1",
|
||||||
|
"jest-dev-server": "^4.4.0",
|
||||||
|
"playwright": "^0.14.0",
|
||||||
|
"serve": "^11.3.0",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
example/src/AsyncStorage.native.tsx
Normal file
3
example/src/AsyncStorage.native.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { AsyncStorage } from 'react-native';
|
||||||
|
|
||||||
|
export default AsyncStorage;
|
||||||
14
example/src/AsyncStorage.tsx
Normal file
14
example/src/AsyncStorage.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default {
|
||||||
|
getItem(key: string) {
|
||||||
|
return Promise.resolve(localStorage.getItem(key));
|
||||||
|
},
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
return Promise.resolve(localStorage.setItem(key, value));
|
||||||
|
},
|
||||||
|
removeItem(key: string) {
|
||||||
|
return Promise.resolve(localStorage.removeItem(key));
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
return Promise.resolve(localStorage.clear());
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import TouchableBounce from '../Shared/TouchableBounce';
|
import TouchableBounce from '../Shared/TouchableBounce';
|
||||||
@@ -28,7 +29,10 @@ export default function BottomTabsScreen() {
|
|||||||
return (
|
return (
|
||||||
<BottomTabs.Navigator
|
<BottomTabs.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarButton: props => <TouchableBounce {...props} />,
|
tabBarButton:
|
||||||
|
Platform.OS === 'web'
|
||||||
|
? undefined
|
||||||
|
: (props) => <TouchableBounce {...props} />,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
@@ -38,7 +42,7 @@ export default function BottomTabsScreen() {
|
|||||||
tabBarIcon: getTabBarIcon('file-document-box'),
|
tabBarIcon: getTabBarIcon('file-document-box'),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props => <SimpleStackScreen {...props} headerMode="none" />}
|
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
|
||||||
</BottomTabs.Screen>
|
</BottomTabs.Screen>
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
name="Chat"
|
name="Chat"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import {
|
import {
|
||||||
createCompatNavigatorFactory,
|
createCompatNavigatorFactory,
|
||||||
@@ -11,25 +11,32 @@ import {
|
|||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
|
import NewsFeed from '../Shared/NewsFeed';
|
||||||
|
|
||||||
type CompatStackParams = {
|
type CompatStackParams = {
|
||||||
Article: { author: string };
|
Albums: undefined;
|
||||||
Album: undefined;
|
Nested: { author: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArticleScreen: CompatScreenType<StackNavigationProp<
|
type NestedStackParams = {
|
||||||
CompatStackParams,
|
Feed: undefined;
|
||||||
'Article'
|
Article: { author: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
|
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||||
|
CompatStackParams
|
||||||
>> = ({ navigation }) => {
|
>> = ({ navigation }) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => navigation.push('Album')}
|
onPress={() => navigation.push('Nested', { author: 'Babel fish' })}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
Push album
|
Push nested
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
@@ -39,24 +46,20 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article author={{ name: navigation.getParam('author') }} />
|
<Albums scrollEnabled={scrollEnabled} />
|
||||||
</React.Fragment>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ArticleScreen.navigationOptions = ({ navigation }) => ({
|
const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
|
||||||
title: `Article by ${navigation.getParam('author')}`,
|
navigation,
|
||||||
});
|
}) => {
|
||||||
|
|
||||||
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
|
||||||
CompatStackParams
|
|
||||||
>> = ({ navigation }) => {
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
onPress={() => navigation.push('Article')}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
Push article
|
Push article
|
||||||
@@ -69,22 +72,69 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Albums />
|
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||||
</React.Fragment>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CompatStack = createCompatNavigatorFactory(createStackNavigator)<
|
const ArticleScreen: CompatScreenType<StackNavigationProp<
|
||||||
|
NestedStackParams,
|
||||||
|
'Article'
|
||||||
|
>> = ({ navigation }) => {
|
||||||
|
navigation.dangerouslyGetParent();
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
onPress={() => navigation.push('Albums')}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Push albums
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<Article
|
||||||
|
author={{ name: navigation.getParam('author') }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ArticleScreen.navigationOptions = ({ navigation }) => ({
|
||||||
|
title: `Article by ${navigation.getParam('author')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createCompatStackNavigator = createCompatNavigatorFactory(
|
||||||
|
createStackNavigator
|
||||||
|
);
|
||||||
|
|
||||||
|
const CompatStack = createCompatStackNavigator<
|
||||||
StackNavigationProp<CompatStackParams>
|
StackNavigationProp<CompatStackParams>
|
||||||
>(
|
>(
|
||||||
{
|
{
|
||||||
Article: {
|
Albums: AlbumsScreen,
|
||||||
screen: ArticleScreen,
|
Nested: {
|
||||||
|
screen: createCompatStackNavigator<
|
||||||
|
StackNavigationProp<NestedStackParams>
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
Feed: FeedScreen,
|
||||||
|
Article: ArticleScreen,
|
||||||
|
},
|
||||||
|
{ navigationOptions: { headerShown: false } }
|
||||||
|
),
|
||||||
params: {
|
params: {
|
||||||
author: 'Gandalf',
|
author: 'Gandalf',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Album: AlbumsScreen,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: 'modal',
|
mode: 'modal',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function BottomTabsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomTabs.Navigator>
|
<BottomTabs.Navigator>
|
||||||
{tabs.map(i => (
|
{tabs.map((i) => (
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
key={i}
|
key={i}
|
||||||
name={`tab-${i}`}
|
name={`tab-${i}`}
|
||||||
@@ -29,12 +29,14 @@ export default function BottomTabsScreen() {
|
|||||||
{() => (
|
{() => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Title>Tab {i}</Title>
|
<Title>Tab {i}</Title>
|
||||||
<Button onPress={() => setTabs(tabs => [...tabs, tabs.length])}>
|
<Button onPress={() => setTabs((tabs) => [...tabs, tabs.length])}>
|
||||||
Add a tab
|
Add a tab
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
setTabs(tabs => (tabs.length > 1 ? tabs.slice(0, -1) : tabs))
|
setTabs((tabs) =>
|
||||||
|
tabs.length > 1 ? tabs.slice(0, -1) : tabs
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Remove a tab
|
Remove a tab
|
||||||
|
|||||||
162
example/src/Screens/LinkComponent.tsx
Normal file
162
example/src/Screens/LinkComponent.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||||
|
import { Button } from 'react-native-paper';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
StackActions,
|
||||||
|
RouteProp,
|
||||||
|
ParamListBase,
|
||||||
|
useLinkProps,
|
||||||
|
} from '@react-navigation/native';
|
||||||
|
import {
|
||||||
|
createStackNavigator,
|
||||||
|
StackNavigationProp,
|
||||||
|
} from '@react-navigation/stack';
|
||||||
|
import Article from '../Shared/Article';
|
||||||
|
import Albums from '../Shared/Albums';
|
||||||
|
|
||||||
|
type SimpleStackParams = {
|
||||||
|
Article: { author: string };
|
||||||
|
Album: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
|
const LinkButton = ({
|
||||||
|
to,
|
||||||
|
...rest
|
||||||
|
}: React.ComponentProps<typeof Button> & { to: string }) => {
|
||||||
|
const { onPress, ...props } = useLinkProps({ to });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
{...rest}
|
||||||
|
{...Platform.select({
|
||||||
|
web: { onClick: onPress } as any,
|
||||||
|
default: { onPress },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArticleScreen = ({
|
||||||
|
navigation,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
navigation: SimpleStackNavigation;
|
||||||
|
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
to="/link-component/Album"
|
||||||
|
style={[styles.button, { padding: 8 }]}
|
||||||
|
>
|
||||||
|
Go to /link-component/Album
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/link-component/Album"
|
||||||
|
action={StackActions.replace('Album')}
|
||||||
|
style={[styles.button, { padding: 8 }]}
|
||||||
|
>
|
||||||
|
Replace with /link-component/Album
|
||||||
|
</Link>
|
||||||
|
<LinkButton
|
||||||
|
to="/link-component/Album"
|
||||||
|
mode="contained"
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Go to /link-component/Album
|
||||||
|
</LinkButton>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<Article
|
||||||
|
author={{ name: route.params.author }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlbumsScreen = ({
|
||||||
|
navigation,
|
||||||
|
}: {
|
||||||
|
navigation: SimpleStackNavigation;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
to="/link-component/Article?author=Babel"
|
||||||
|
style={[styles.button, { padding: 8 }]}
|
||||||
|
>
|
||||||
|
Go to /link-component/Article
|
||||||
|
</Link>
|
||||||
|
<LinkButton
|
||||||
|
to="/link-component/Article?author=Babel"
|
||||||
|
mode="contained"
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Go to /link-component/Article
|
||||||
|
</LinkButton>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<Albums scrollEnabled={scrollEnabled} />
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||||
|
|
||||||
|
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||||
|
navigation: StackNavigationProp<ParamListBase>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerShown: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleStack.Navigator {...rest}>
|
||||||
|
<SimpleStack.Screen
|
||||||
|
name="Article"
|
||||||
|
component={ArticleScreen}
|
||||||
|
options={({ route }) => ({
|
||||||
|
title: `Article by ${route.params.author}`,
|
||||||
|
})}
|
||||||
|
initialParams={{ author: 'Gandalf' }}
|
||||||
|
/>
|
||||||
|
<SimpleStack.Screen
|
||||||
|
name="Album"
|
||||||
|
component={AlbumsScreen}
|
||||||
|
options={{ title: 'Album' }}
|
||||||
|
/>
|
||||||
|
</SimpleStack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
buttons: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
margin: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
127
example/src/Screens/MasterDetail.tsx
Normal file
127
example/src/Screens/MasterDetail.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Dimensions, ScaledSize } from 'react-native';
|
||||||
|
import { Appbar } from 'react-native-paper';
|
||||||
|
import { ParamListBase } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import {
|
||||||
|
createDrawerNavigator,
|
||||||
|
DrawerNavigationProp,
|
||||||
|
DrawerContent,
|
||||||
|
} from '@react-navigation/drawer';
|
||||||
|
import Article from '../Shared/Article';
|
||||||
|
import Albums from '../Shared/Albums';
|
||||||
|
import NewsFeed from '../Shared/NewsFeed';
|
||||||
|
|
||||||
|
type DrawerParams = {
|
||||||
|
Article: undefined;
|
||||||
|
NewsFeed: undefined;
|
||||||
|
Album: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
|
||||||
|
|
||||||
|
const useIsLargeScreen = () => {
|
||||||
|
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
|
||||||
|
setDimensions(window);
|
||||||
|
};
|
||||||
|
|
||||||
|
Dimensions.addEventListener('change', onDimensionsChange);
|
||||||
|
|
||||||
|
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return dimensions.width > 414;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
onGoBack,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
onGoBack: () => void;
|
||||||
|
title: string;
|
||||||
|
}) => {
|
||||||
|
const isLargeScreen = useIsLargeScreen();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Appbar.Header>
|
||||||
|
{isLargeScreen ? null : <Appbar.BackAction onPress={onGoBack} />}
|
||||||
|
<Appbar.Content title={title} />
|
||||||
|
</Appbar.Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
|
||||||
|
<Article />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
|
||||||
|
<NewsFeed />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
|
||||||
|
<Albums />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Drawer = createDrawerNavigator<DrawerParams>();
|
||||||
|
|
||||||
|
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
|
||||||
|
navigation: StackNavigationProp<ParamListBase>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DrawerScreen({ navigation, ...rest }: Props) {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerShown: false,
|
||||||
|
gestureEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLargeScreen = useIsLargeScreen();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer.Navigator
|
||||||
|
openByDefault
|
||||||
|
drawerType={isLargeScreen ? 'permanent' : 'back'}
|
||||||
|
drawerStyle={isLargeScreen ? null : { width: '100%' }}
|
||||||
|
overlayColor="transparent"
|
||||||
|
drawerContent={(props) => (
|
||||||
|
<>
|
||||||
|
<Appbar.Header>
|
||||||
|
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
|
||||||
|
<Appbar.Content title="Pages" />
|
||||||
|
</Appbar.Header>
|
||||||
|
<DrawerContent {...props} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Drawer.Screen name="Article" component={ArticleScreen} />
|
||||||
|
<Drawer.Screen
|
||||||
|
name="NewsFeed"
|
||||||
|
component={NewsFeedScreen}
|
||||||
|
options={{ title: 'Feed' }}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Album"
|
||||||
|
component={AlbumsScreen}
|
||||||
|
options={{ title: 'Album' }}
|
||||||
|
/>
|
||||||
|
</Drawer.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ export default function MaterialBottomTabsScreen() {
|
|||||||
tabBarColor: '#C9E7F8',
|
tabBarColor: '#C9E7F8',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props => <SimpleStackScreen {...props} headerMode="none" />}
|
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
|
||||||
</MaterialBottomTabs.Screen>
|
</MaterialBottomTabs.Screen>
|
||||||
<MaterialBottomTabs.Screen
|
<MaterialBottomTabs.Screen
|
||||||
name="Chat"
|
name="Chat"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +17,8 @@ type ModalStackParams = {
|
|||||||
|
|
||||||
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
|
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
@@ -42,7 +44,10 @@ const ArticleScreen = ({
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
<Article
|
||||||
|
author={{ name: route.params.author }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -66,7 +71,7 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Albums scrollEnabled={false} />
|
<Albums scrollEnabled={scrollEnabled} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,8 @@ type SimpleStackParams = {
|
|||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
@@ -43,7 +45,10 @@ const ArticleScreen = ({
|
|||||||
Pop screen
|
Pop screen
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
<Article
|
||||||
|
author={{ name: route.params.author }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -71,7 +76,7 @@ const NewsFeedScreen = ({
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<NewsFeed scrollEnabled={false} />
|
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -99,7 +104,7 @@ const AlbumsScreen = ({
|
|||||||
Pop by 2
|
Pop by 2
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Albums scrollEnabled={false} />
|
<Albums scrollEnabled={scrollEnabled} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type SimpleStackParams = {
|
|||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
@@ -45,7 +47,10 @@ const ArticleScreen = ({
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
<Article
|
||||||
|
author={{ name: route.params.author }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -75,7 +80,7 @@ const AlbumsScreen = ({
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Albums scrollEnabled={false} />
|
<Albums scrollEnabled={scrollEnabled} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||||
import { Button, Paragraph } from 'react-native-paper';
|
import { Button, Paragraph } from 'react-native-paper';
|
||||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,8 @@ type SimpleStackParams = {
|
|||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
@@ -40,7 +42,10 @@ const ArticleScreen = ({
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
<Article
|
||||||
|
author={{ name: route.params.author }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ScrollViewProps,
|
ScrollViewProps,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Platform,
|
Platform,
|
||||||
|
ScaledSize,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useScrollToTop } from '@react-navigation/native';
|
import { useScrollToTop } from '@react-navigation/native';
|
||||||
|
|
||||||
@@ -40,15 +41,38 @@ const COVERS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function Albums(props: Partial<ScrollViewProps>) {
|
export default function Albums(props: Partial<ScrollViewProps>) {
|
||||||
|
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
|
||||||
|
setDimensions(window);
|
||||||
|
};
|
||||||
|
|
||||||
|
Dimensions.addEventListener('change', onDimensionsChange);
|
||||||
|
|
||||||
|
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const ref = React.useRef<ScrollView>(null);
|
const ref = React.useRef<ScrollView>(null);
|
||||||
|
|
||||||
useScrollToTop(ref);
|
useScrollToTop(ref);
|
||||||
|
|
||||||
|
const itemSize = dimensions.width / Math.floor(dimensions.width / 150);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView ref={ref} contentContainerStyle={styles.content} {...props}>
|
<ScrollView ref={ref} contentContainerStyle={styles.content} {...props}>
|
||||||
{COVERS.map((source, i) => (
|
{COVERS.map((source, i) => (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
<View
|
||||||
<View key={i} style={styles.item}>
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
styles.item,
|
||||||
|
Platform.OS !== 'web' && {
|
||||||
|
height: itemSize,
|
||||||
|
width: itemSize,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Image source={source} style={styles.photo} />
|
<Image source={source} style={styles.photo} />
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -76,10 +100,6 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
item: {
|
|
||||||
height: Dimensions.get('window').width / 2,
|
|
||||||
width: '50%',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
photo: {
|
photo: {
|
||||||
|
|||||||
@@ -68,10 +68,7 @@ export default function Chat(props: Partial<ScrollViewProps>) {
|
|||||||
styles.input,
|
styles.input,
|
||||||
{ backgroundColor: colors.card, color: colors.text },
|
{ backgroundColor: colors.card, color: colors.text },
|
||||||
]}
|
]}
|
||||||
placeholderTextColor={Color(colors.text)
|
placeholderTextColor={Color(colors.text).alpha(0.5).rgb().string()}
|
||||||
.alpha(0.5)
|
|
||||||
.rgb()
|
|
||||||
.string()}
|
|
||||||
placeholder="Write a message"
|
placeholder="Write a message"
|
||||||
underlineColorAndroid="transparent"
|
underlineColorAndroid="transparent"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -51,10 +51,7 @@ export default function NewsFeed(props: Props) {
|
|||||||
<Card style={styles.card}>
|
<Card style={styles.card}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="What's on your mind?"
|
placeholder="What's on your mind?"
|
||||||
placeholderTextColor={Color(colors.text)
|
placeholderTextColor={Color(colors.text).alpha(0.5).rgb().string()}
|
||||||
.alpha(0.5)
|
|
||||||
.rgb()
|
|
||||||
.string()}
|
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
AsyncStorage,
|
|
||||||
YellowBox,
|
YellowBox,
|
||||||
Platform,
|
Platform,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
I18nManager,
|
I18nManager,
|
||||||
|
Dimensions,
|
||||||
|
ScaledSize,
|
||||||
|
Linking,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { enableScreens } from 'react-native-screens';
|
import { enableScreens } from 'react-native-screens';
|
||||||
@@ -20,11 +22,10 @@ import {
|
|||||||
Appbar,
|
Appbar,
|
||||||
List,
|
List,
|
||||||
Divider,
|
Divider,
|
||||||
|
Text,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import {
|
import {
|
||||||
InitialState,
|
InitialState,
|
||||||
useLinking,
|
|
||||||
NavigationContainerRef,
|
|
||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
DarkTheme,
|
DarkTheme,
|
||||||
@@ -40,7 +41,9 @@ import {
|
|||||||
HeaderStyleInterpolators,
|
HeaderStyleInterpolators,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
|
|
||||||
|
import AsyncStorage from './AsyncStorage';
|
||||||
import LinkingPrefixes from './LinkingPrefixes';
|
import LinkingPrefixes from './LinkingPrefixes';
|
||||||
|
import SettingsItem from './Shared/SettingsItem';
|
||||||
import SimpleStack from './Screens/SimpleStack';
|
import SimpleStack from './Screens/SimpleStack';
|
||||||
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
||||||
import StackTransparent from './Screens/StackTransparent';
|
import StackTransparent from './Screens/StackTransparent';
|
||||||
@@ -51,12 +54,16 @@ import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
|||||||
import DynamicTabs from './Screens/DynamicTabs';
|
import DynamicTabs from './Screens/DynamicTabs';
|
||||||
import AuthFlow from './Screens/AuthFlow';
|
import AuthFlow from './Screens/AuthFlow';
|
||||||
import CompatAPI from './Screens/CompatAPI';
|
import CompatAPI from './Screens/CompatAPI';
|
||||||
import SettingsItem from './Shared/SettingsItem';
|
import MasterDetail from './Screens/MasterDetail';
|
||||||
|
import LinkComponent from './Screens/LinkComponent';
|
||||||
|
|
||||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||||
|
|
||||||
enableScreens();
|
enableScreens();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
global.REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED = true;
|
||||||
|
|
||||||
type RootDrawerParamList = {
|
type RootDrawerParamList = {
|
||||||
Root: undefined;
|
Root: undefined;
|
||||||
Another: undefined;
|
Another: undefined;
|
||||||
@@ -95,6 +102,10 @@ const SCREENS = {
|
|||||||
title: 'Dynamic Tabs',
|
title: 'Dynamic Tabs',
|
||||||
component: DynamicTabs,
|
component: DynamicTabs,
|
||||||
},
|
},
|
||||||
|
MasterDetail: {
|
||||||
|
title: 'Master Detail',
|
||||||
|
component: MasterDetail,
|
||||||
|
},
|
||||||
AuthFlow: {
|
AuthFlow: {
|
||||||
title: 'Auth Flow',
|
title: 'Auth Flow',
|
||||||
component: AuthFlow,
|
component: AuthFlow,
|
||||||
@@ -103,6 +114,10 @@ const SCREENS = {
|
|||||||
title: 'Compat Layer',
|
title: 'Compat Layer',
|
||||||
component: CompatAPI,
|
component: CompatAPI,
|
||||||
},
|
},
|
||||||
|
LinkComponent: {
|
||||||
|
title: '<Link />',
|
||||||
|
component: LinkComponent,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Drawer = createDrawerNavigator<RootDrawerParamList>();
|
const Drawer = createDrawerNavigator<RootDrawerParamList>();
|
||||||
@@ -114,36 +129,6 @@ const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
|
|||||||
Asset.loadAsync(StackAssets);
|
Asset.loadAsync(StackAssets);
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const containerRef = React.useRef<NavigationContainerRef>();
|
|
||||||
|
|
||||||
// To test deep linking on, run the following in the Terminal:
|
|
||||||
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
|
|
||||||
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
|
|
||||||
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
|
|
||||||
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
|
|
||||||
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
|
||||||
const { getInitialState } = useLinking(containerRef, {
|
|
||||||
prefixes: LinkingPrefixes,
|
|
||||||
config: {
|
|
||||||
Root: {
|
|
||||||
path: '',
|
|
||||||
initialRouteName: 'Home',
|
|
||||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
|
||||||
(acc, name) => {
|
|
||||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
|
||||||
acc[name] = name
|
|
||||||
.replace(/([A-Z]+)/g, '-$1')
|
|
||||||
.replace(/^-/, '')
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ Home: '' }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [theme, setTheme] = React.useState(DefaultTheme);
|
const [theme, setTheme] = React.useState(DefaultTheme);
|
||||||
|
|
||||||
const [isReady, setIsReady] = React.useState(false);
|
const [isReady, setIsReady] = React.useState(false);
|
||||||
@@ -154,17 +139,18 @@ export default function App() {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const restoreState = async () => {
|
const restoreState = async () => {
|
||||||
try {
|
try {
|
||||||
let state = await getInitialState();
|
const initialUrl = await Linking.getInitialURL();
|
||||||
|
|
||||||
if (Platform.OS !== 'web' && state === undefined) {
|
if (Platform.OS !== 'web' || initialUrl === null) {
|
||||||
const savedState = await AsyncStorage.getItem(
|
const savedState = await AsyncStorage.getItem(
|
||||||
NAVIGATION_PERSISTENCE_KEY
|
NAVIGATION_PERSISTENCE_KEY
|
||||||
);
|
);
|
||||||
state = savedState ? JSON.parse(savedState) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state !== undefined) {
|
const state = savedState ? JSON.parse(savedState) : undefined;
|
||||||
setInitialState(state);
|
|
||||||
|
if (state !== undefined) {
|
||||||
|
setInitialState(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
@@ -180,7 +166,7 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
restoreState();
|
restoreState();
|
||||||
}, [getInitialState]);
|
}, []);
|
||||||
|
|
||||||
const paperTheme = React.useMemo(() => {
|
const paperTheme = React.useMemo(() => {
|
||||||
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
|
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
|
||||||
@@ -196,27 +182,68 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
}, [theme.colors, theme.dark]);
|
}, [theme.colors, theme.dark]);
|
||||||
|
|
||||||
|
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
|
||||||
|
setDimensions(window);
|
||||||
|
};
|
||||||
|
|
||||||
|
Dimensions.addEventListener('change', onDimensionsChange);
|
||||||
|
|
||||||
|
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLargeScreen = dimensions.width >= 1024;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaperProvider theme={paperTheme}>
|
<PaperProvider theme={paperTheme}>
|
||||||
{Platform.OS === 'ios' && (
|
{Platform.OS === 'ios' && (
|
||||||
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
|
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
|
||||||
)}
|
)}
|
||||||
<NavigationContainer
|
<NavigationContainer
|
||||||
ref={containerRef}
|
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
onStateChange={state =>
|
onStateChange={(state) =>
|
||||||
AsyncStorage.setItem(
|
AsyncStorage.setItem(
|
||||||
NAVIGATION_PERSISTENCE_KEY,
|
NAVIGATION_PERSISTENCE_KEY,
|
||||||
JSON.stringify(state)
|
JSON.stringify(state)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
linking={{
|
||||||
|
// To test deep linking on, run the following in the Terminal:
|
||||||
|
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
|
||||||
|
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
|
||||||
|
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
|
||||||
|
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
|
||||||
|
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
||||||
|
prefixes: LinkingPrefixes,
|
||||||
|
config: {
|
||||||
|
Root: {
|
||||||
|
path: '',
|
||||||
|
initialRouteName: 'Home',
|
||||||
|
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
||||||
|
(acc, name) => {
|
||||||
|
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||||
|
acc[name] = name
|
||||||
|
.replace(/([A-Z]+)/g, '-$1')
|
||||||
|
.replace(/^-/, '')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ Home: '' }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fallback={<Text>Loading…</Text>}
|
||||||
>
|
>
|
||||||
<Drawer.Navigator>
|
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="Root"
|
name="Root"
|
||||||
options={{
|
options={{
|
||||||
@@ -240,13 +267,15 @@ export default function App() {
|
|||||||
name="Home"
|
name="Home"
|
||||||
options={{
|
options={{
|
||||||
title: 'Examples',
|
title: 'Examples',
|
||||||
headerLeft: () => (
|
headerLeft: isLargeScreen
|
||||||
<Appbar.Action
|
? undefined
|
||||||
color={theme.colors.text}
|
: () => (
|
||||||
icon="menu"
|
<Appbar.Action
|
||||||
onPress={() => navigation.toggleDrawer()}
|
color={theme.colors.text}
|
||||||
/>
|
icon="menu"
|
||||||
),
|
onPress={() => navigation.toggleDrawer()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
@@ -280,14 +309,15 @@ export default function App() {
|
|||||||
theme.dark ? 'light' : 'dark'
|
theme.dark ? 'light' : 'dark'
|
||||||
);
|
);
|
||||||
|
|
||||||
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
|
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||||
name => (
|
(name) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={name}
|
key={name}
|
||||||
|
testID={name}
|
||||||
title={SCREENS[name].title}
|
title={SCREENS[name].title}
|
||||||
onPress={() => navigation.navigate(name)}
|
onPress={() => navigation.navigate(name)}
|
||||||
/>
|
/>
|
||||||
@@ -297,7 +327,7 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</Stack.Screen>
|
</Stack.Screen>
|
||||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||||
name => (
|
(name) => (
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
key={name}
|
key={name}
|
||||||
name={name}
|
name={name}
|
||||||
|
|||||||
1
example/web/_redirects
Normal file
1
example/web/_redirects
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* /index.html 200
|
||||||
@@ -7,7 +7,7 @@ const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
|||||||
const node_modules = path.resolve(__dirname, '..', 'node_modules');
|
const node_modules = path.resolve(__dirname, '..', 'node_modules');
|
||||||
const packages = path.resolve(__dirname, '..', 'packages');
|
const packages = path.resolve(__dirname, '..', 'packages');
|
||||||
|
|
||||||
module.exports = async function(env, argv) {
|
module.exports = async function (env, argv) {
|
||||||
const config = await createExpoWebpackConfigAsync(env, argv);
|
const config = await createExpoWebpackConfigAsync(env, argv);
|
||||||
|
|
||||||
config.context = path.resolve(__dirname, '..');
|
config.context = path.resolve(__dirname, '..');
|
||||||
@@ -20,23 +20,25 @@ module.exports = async function(env, argv) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
config.resolve.plugins = config.resolve.plugins.filter(
|
config.resolve.plugins = config.resolve.plugins.filter(
|
||||||
p => !(p instanceof ModuleScopePlugin)
|
(p) => !(p instanceof ModuleScopePlugin)
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(config.resolve.alias, {
|
Object.assign(config.resolve.alias, {
|
||||||
react: path.resolve(node_modules, 'react'),
|
'react': path.resolve(node_modules, 'react'),
|
||||||
'react-native': path.resolve(node_modules, 'react-native-web'),
|
'react-native': path.resolve(node_modules, 'react-native-web'),
|
||||||
'react-native-web': path.resolve(node_modules, 'react-native-web'),
|
'react-native-web': path.resolve(node_modules, 'react-native-web'),
|
||||||
'@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'),
|
'@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'),
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.readdirSync(packages).forEach(name => {
|
fs.readdirSync(packages)
|
||||||
config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
|
.filter((name) => !name.startsWith('.'))
|
||||||
packages,
|
.forEach((name) => {
|
||||||
name,
|
config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
|
||||||
'src'
|
packages,
|
||||||
);
|
name,
|
||||||
});
|
require(`../packages/${name}/package.json`).source
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const error = console.error;
|
const error = console.error;
|
||||||
|
|
||||||
console.error = (...args) =>
|
console.error = (...args) =>
|
||||||
// Supress error messages regarding error boundary in tests
|
// Suppress error messages regarding error boundary in tests
|
||||||
/(Consider adding an error boundary to your tree to customize error handling behavior|React will try to recreate this component tree from scratch using the error boundary you provided|Error boundaries should implement getDerivedStateFromError)/m.test(
|
/(Consider adding an error boundary to your tree to customize error handling behavior|React will try to recreate this component tree from scratch using the error boundary you provided|Error boundaries should implement getDerivedStateFromError)/m.test(
|
||||||
args[0]
|
args[0]
|
||||||
)
|
)
|
||||||
|
|||||||
5
netlify.toml
Normal file
5
netlify.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
[build]
|
||||||
|
base = "/"
|
||||||
|
publish = "example/web-build"
|
||||||
|
command = "yarn example expo build:web"
|
||||||
30
package.json
30
package.json
@@ -18,7 +18,7 @@
|
|||||||
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
|
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext '.js,.ts,.tsx' .",
|
"lint": "eslint --ext '.js,.ts,.tsx' .",
|
||||||
"typescript": "tsc --noEmit",
|
"typescript": "tsc --noEmit --composite false",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prerelease": "lerna run clean",
|
"prerelease": "lerna run clean",
|
||||||
"release": "lerna publish",
|
"release": "lerna publish",
|
||||||
@@ -26,25 +26,25 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.8.4",
|
"@babel/preset-env": "^7.9.6",
|
||||||
"@babel/preset-flow": "^7.8.3",
|
"@babel/preset-flow": "^7.9.0",
|
||||||
"@babel/preset-react": "^7.8.3",
|
"@babel/preset-react": "^7.9.4",
|
||||||
"@babel/preset-typescript": "^7.8.3",
|
"@babel/preset-typescript": "^7.9.0",
|
||||||
"@babel/runtime": "^7.8.4",
|
"@babel/runtime": "^7.9.6",
|
||||||
"@commitlint/config-conventional": "^8.3.4",
|
"@commitlint/config-conventional": "^8.3.4",
|
||||||
"@types/jest": "^25.1.2",
|
"@types/jest": "^25.2.1",
|
||||||
|
"babel-jest": "^26.0.1",
|
||||||
"codecov": "^3.6.5",
|
"codecov": "^3.6.5",
|
||||||
"commitlint": "^8.3.5",
|
"commitlint": "^8.3.5",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.5",
|
||||||
"detox": "^15.1.4",
|
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-satya164": "^3.1.5",
|
"eslint-config-satya164": "^3.1.7",
|
||||||
"husky": "^4.2.1",
|
"husky": "^4.2.5",
|
||||||
"jest": "^25.1.0",
|
"jest": "^26.0.1",
|
||||||
"lerna": "^3.20.2",
|
"lerna": "^3.20.2",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^2.0.5",
|
||||||
"typescript": "^3.7.5"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
|
|||||||
@@ -3,6 +3,178 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.4.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.3...@react-navigation/bottom-tabs@5.4.4) (2020-05-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.4...@react-navigation/bottom-tabs@5.4.0) (2020-05-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/7971)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.3...@react-navigation/bottom-tabs@5.3.4) (2020-05-05)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.2...@react-navigation/bottom-tabs@5.3.3) (2020-05-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.1...@react-navigation/bottom-tabs@5.3.2) (2020-05-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.0...@react-navigation/bottom-tabs@5.3.1) (2020-04-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.8...@react-navigation/bottom-tabs@5.3.0) (2020-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||||
|
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/942d2be2c72720469475ce12ec8df23825994dbf))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.6...@react-navigation/bottom-tabs@5.2.7) (2020-04-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.5...@react-navigation/bottom-tabs@5.2.6) (2020-04-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.5](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.4...@react-navigation/bottom-tabs@5.2.5) (2020-03-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.3...@react-navigation/bottom-tabs@5.2.4) (2020-03-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.2...@react-navigation/bottom-tabs@5.2.3) (2020-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.1...@react-navigation/bottom-tabs@5.2.2) (2020-03-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't use react-native-screens on web ([b1a65fc](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b1a65fc73e8603ae2c06ef101a74df31e80bb9b2)), closes [#7485](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/7485)
|
||||||
|
* initialize height and width to zero if undefined ([3df65e2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/3df65e28197db3bb8371059146546d57661c5ba3)), closes [#6789](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6789)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.0...@react-navigation/bottom-tabs@5.2.1) (2020-03-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.1...@react-navigation/bottom-tabs@5.2.0) (2020-03-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add safeAreaInsets to bottom tabs ([82af7be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/82af7bed7135e42e24693b48cf7f1c6f9f5a6981))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.0...@react-navigation/bottom-tabs@5.1.1) (2020-03-03)
|
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.0...@react-navigation/bottom-tabs@5.1.1) (2020-03-03)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/bottom-tabs",
|
"name": "@react-navigation/bottom-tabs",
|
||||||
"description": "Bottom tab navigator following iOS design guidelines",
|
"description": "Bottom tab navigator following iOS design guidelines",
|
||||||
"version": "5.1.1",
|
"version": "5.4.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs",
|
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs",
|
||||||
"main": "lib/commonjs/index.js",
|
"main": "lib/commonjs/index.js",
|
||||||
"react-native": "src/index.tsx",
|
"react-native": "src/index.tsx",
|
||||||
|
"source": "src/index.tsx",
|
||||||
"module": "lib/module/index.js",
|
"module": "lib/module/index.js",
|
||||||
"types": "lib/typescript/src/index.d.ts",
|
"types": "lib/typescript/src/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -34,22 +35,24 @@
|
|||||||
"react-native-iphone-x-helper": "^1.2.1"
|
"react-native-iphone-x-helper": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.9.3",
|
"@react-native-community/bob": "^0.13.1",
|
||||||
"@react-navigation/native": "^5.0.9",
|
"@react-navigation/native": "^5.3.2",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.19",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.60.30",
|
"@types/react-native": "^0.62.7",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"react-native": "~0.61.5",
|
"react-native": "~0.61.5",
|
||||||
"react-native-safe-area-context": "^0.7.2",
|
"react-native-safe-area-context": "^0.7.3",
|
||||||
"typescript": "^3.7.5"
|
"react-native-screens": "^2.7.0",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-navigation/native": "^5.0.5",
|
"@react-navigation/native": "^5.0.5",
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*",
|
"react-native": "*",
|
||||||
"react-native-safe-area-context": ">= 0.6.0"
|
"react-native-safe-area-context": ">= 0.6.0",
|
||||||
|
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
|
||||||
},
|
},
|
||||||
"@react-native-community/bob": {
|
"@react-native-community/bob": {
|
||||||
"source": "src",
|
"source": "src",
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ export { default as BottomTabBar } from './views/BottomTabBar';
|
|||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
*/
|
*/
|
||||||
export {
|
export type {
|
||||||
BottomTabNavigationOptions,
|
BottomTabNavigationOptions,
|
||||||
BottomTabNavigationProp,
|
BottomTabNavigationProp,
|
||||||
|
BottomTabScreenProps,
|
||||||
BottomTabBarProps,
|
BottomTabBarProps,
|
||||||
BottomTabBarOptions,
|
BottomTabBarOptions,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
StyleProp,
|
StyleProp,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
|
GestureResponderEvent,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import {
|
import {
|
||||||
NavigationHelpers,
|
NavigationHelpers,
|
||||||
@@ -11,6 +12,8 @@ import {
|
|||||||
ParamListBase,
|
ParamListBase,
|
||||||
Descriptor,
|
Descriptor,
|
||||||
TabNavigationState,
|
TabNavigationState,
|
||||||
|
TabActionHelpers,
|
||||||
|
RouteProp,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
|
|
||||||
export type BottomTabNavigationEventMap = {
|
export type BottomTabNavigationEventMap = {
|
||||||
@@ -40,18 +43,15 @@ export type BottomTabNavigationProp<
|
|||||||
TabNavigationState,
|
TabNavigationState,
|
||||||
BottomTabNavigationOptions,
|
BottomTabNavigationOptions,
|
||||||
BottomTabNavigationEventMap
|
BottomTabNavigationEventMap
|
||||||
> & {
|
> &
|
||||||
/**
|
TabActionHelpers<ParamList>;
|
||||||
* Jump to an existing tab.
|
|
||||||
*
|
export type BottomTabScreenProps<
|
||||||
* @param name Name of the route for the tab.
|
ParamList extends ParamListBase,
|
||||||
* @param [params] Params object for the route.
|
RouteName extends keyof ParamList = string
|
||||||
*/
|
> = {
|
||||||
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
|
navigation: BottomTabNavigationProp<ParamList, RouteName>;
|
||||||
...args: ParamList[RouteName] extends undefined | any
|
route: RouteProp<ParamList, RouteName>;
|
||||||
? [RouteName] | [RouteName, ParamList[RouteName]]
|
|
||||||
: [RouteName, ParamList[RouteName]]
|
|
||||||
): void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BottomTabNavigationOptions = {
|
export type BottomTabNavigationOptions = {
|
||||||
@@ -148,7 +148,7 @@ export type BottomTabBarOptions = {
|
|||||||
*/
|
*/
|
||||||
inactiveTintColor?: string;
|
inactiveTintColor?: string;
|
||||||
/**
|
/**
|
||||||
* Background olor for the active tab.
|
* Background color for the active tab.
|
||||||
*/
|
*/
|
||||||
activeBackgroundColor?: string;
|
activeBackgroundColor?: string;
|
||||||
/**
|
/**
|
||||||
@@ -184,6 +184,16 @@ export type BottomTabBarOptions = {
|
|||||||
* Whether the label position should adapt to the orientation.
|
* Whether the label position should adapt to the orientation.
|
||||||
*/
|
*/
|
||||||
adaptive?: boolean;
|
adaptive?: boolean;
|
||||||
|
/**
|
||||||
|
* Safe area insets for the tab bar. This is used to avoid elements like the navigation bar on Android and bottom safe area on iOS.
|
||||||
|
* By default, the device's safe area insets are automatically detected. You can override the behavior with this option.
|
||||||
|
*/
|
||||||
|
safeAreaInsets?: {
|
||||||
|
top?: number;
|
||||||
|
right?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Style object for the tab bar container.
|
* Style object for the tab bar container.
|
||||||
*/
|
*/
|
||||||
@@ -196,6 +206,13 @@ export type BottomTabBarProps = BottomTabBarOptions & {
|
|||||||
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
|
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BottomTabBarButtonProps = TouchableWithoutFeedbackProps & {
|
export type BottomTabBarButtonProps = Omit<
|
||||||
|
TouchableWithoutFeedbackProps,
|
||||||
|
'onPress'
|
||||||
|
> & {
|
||||||
|
to?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
onPress?: (
|
||||||
|
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import {
|
|||||||
NavigationRouteContext,
|
NavigationRouteContext,
|
||||||
CommonActions,
|
CommonActions,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
useLinkBuilder,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import { SafeAreaConsumer } from 'react-native-safe-area-context';
|
import { useSafeArea } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import BottomTabItem from './BottomTabItem';
|
import BottomTabItem from './BottomTabItem';
|
||||||
import { BottomTabBarProps } from '../types';
|
import { BottomTabBarProps } from '../types';
|
||||||
@@ -43,14 +44,21 @@ export default function BottomTabBar({
|
|||||||
keyboardHidesTabBar = false,
|
keyboardHidesTabBar = false,
|
||||||
labelPosition,
|
labelPosition,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
|
safeAreaInsets,
|
||||||
showIcon,
|
showIcon,
|
||||||
showLabel,
|
showLabel,
|
||||||
style,
|
style,
|
||||||
tabStyle,
|
tabStyle,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
const buildLink = useLinkBuilder();
|
||||||
|
|
||||||
|
const [dimensions, setDimensions] = React.useState(() => {
|
||||||
|
const { height = 0, width = 0 } = Dimensions.get('window');
|
||||||
|
|
||||||
|
return { height, width };
|
||||||
|
});
|
||||||
|
|
||||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
|
||||||
const [layout, setLayout] = React.useState({
|
const [layout, setLayout] = React.useState({
|
||||||
height: 0,
|
height: 0,
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
@@ -115,7 +123,7 @@ export default function BottomTabBar({
|
|||||||
const handleLayout = (e: LayoutChangeEvent) => {
|
const handleLayout = (e: LayoutChangeEvent) => {
|
||||||
const { height, width } = e.nativeEvent.layout;
|
const { height, width } = e.nativeEvent.layout;
|
||||||
|
|
||||||
setLayout(layout => {
|
setLayout((layout) => {
|
||||||
if (height === layout.height && width === layout.width) {
|
if (height === layout.height && width === layout.width) {
|
||||||
return layout;
|
return layout;
|
||||||
} else {
|
} else {
|
||||||
@@ -158,116 +166,123 @@ export default function BottomTabBar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultInsets = useSafeArea();
|
||||||
|
|
||||||
|
const insets = {
|
||||||
|
top: safeAreaInsets?.top ?? defaultInsets.top,
|
||||||
|
right: safeAreaInsets?.right ?? defaultInsets.right,
|
||||||
|
bottom: safeAreaInsets?.bottom ?? defaultInsets.bottom,
|
||||||
|
left: safeAreaInsets?.left ?? defaultInsets.left,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaConsumer>
|
<Animated.View
|
||||||
{insets => (
|
style={[
|
||||||
<Animated.View
|
styles.tabBar,
|
||||||
style={[
|
{
|
||||||
styles.tabBar,
|
backgroundColor: colors.card,
|
||||||
{
|
borderTopColor: colors.border,
|
||||||
backgroundColor: colors.card,
|
},
|
||||||
borderTopColor: colors.border,
|
keyboardHidesTabBar
|
||||||
},
|
? {
|
||||||
keyboardHidesTabBar
|
// When the keyboard is shown, slide down the tab bar
|
||||||
? {
|
transform: [
|
||||||
// When the keyboard is shown, slide down the tab bar
|
{
|
||||||
transform: [
|
translateY: visible.interpolate({
|
||||||
{
|
inputRange: [0, 1],
|
||||||
translateY: visible.interpolate({
|
outputRange: [layout.height, 0],
|
||||||
inputRange: [0, 1],
|
}),
|
||||||
outputRange: [layout.height, 0],
|
},
|
||||||
}),
|
],
|
||||||
},
|
// Absolutely position the tab bar so that the content is below it
|
||||||
],
|
// This is needed to avoid gap at bottom when the tab bar is hidden
|
||||||
// Absolutely position the tab bar so that the content is below it
|
position: keyboardShown ? 'absolute' : null,
|
||||||
// This is needed to avoid gap at bottom when the tab bar is hidden
|
}
|
||||||
position: keyboardShown ? 'absolute' : null,
|
: null,
|
||||||
}
|
{
|
||||||
: null,
|
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
|
||||||
{
|
paddingBottom: insets.bottom,
|
||||||
height: DEFAULT_TABBAR_HEIGHT + (insets ? insets.bottom : 0),
|
paddingHorizontal: Math.max(insets.left, insets.right),
|
||||||
paddingBottom: insets ? insets.bottom : 0,
|
},
|
||||||
},
|
style,
|
||||||
style,
|
]}
|
||||||
]}
|
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
|
||||||
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
|
>
|
||||||
>
|
<View style={styles.content} onLayout={handleLayout}>
|
||||||
<View style={styles.content} onLayout={handleLayout}>
|
{routes.map((route, index) => {
|
||||||
{routes.map((route, index) => {
|
const focused = index === state.index;
|
||||||
const focused = index === state.index;
|
const { options } = descriptors[route.key];
|
||||||
const { options } = descriptors[route.key];
|
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
const event = navigation.emit({
|
const event = navigation.emit({
|
||||||
type: 'tabPress',
|
type: 'tabPress',
|
||||||
target: route.key,
|
target: route.key,
|
||||||
canPreventDefault: true,
|
canPreventDefault: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!focused && !event.defaultPrevented) {
|
if (!focused && !event.defaultPrevented) {
|
||||||
navigation.dispatch({
|
navigation.dispatch({
|
||||||
...CommonActions.navigate(route.name),
|
...CommonActions.navigate(route.name),
|
||||||
target: state.key,
|
target: state.key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLongPress = () => {
|
const onLongPress = () => {
|
||||||
navigation.emit({
|
navigation.emit({
|
||||||
type: 'tabLongPress',
|
type: 'tabLongPress',
|
||||||
target: route.key,
|
target: route.key,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const label =
|
const label =
|
||||||
options.tabBarLabel !== undefined
|
options.tabBarLabel !== undefined
|
||||||
? options.tabBarLabel
|
? options.tabBarLabel
|
||||||
: options.title !== undefined
|
: options.title !== undefined
|
||||||
? options.title
|
? options.title
|
||||||
: route.name;
|
: route.name;
|
||||||
|
|
||||||
const accessibilityLabel =
|
const accessibilityLabel =
|
||||||
options.tabBarAccessibilityLabel !== undefined
|
options.tabBarAccessibilityLabel !== undefined
|
||||||
? options.tabBarAccessibilityLabel
|
? options.tabBarAccessibilityLabel
|
||||||
: typeof label === 'string'
|
: typeof label === 'string'
|
||||||
? `${label}, tab, ${index + 1} of ${routes.length}`
|
? `${label}, tab, ${index + 1} of ${routes.length}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContext.Provider
|
<NavigationContext.Provider
|
||||||
key={route.key}
|
key={route.key}
|
||||||
value={descriptors[route.key].navigation}
|
value={descriptors[route.key].navigation}
|
||||||
>
|
>
|
||||||
<NavigationRouteContext.Provider value={route}>
|
<NavigationRouteContext.Provider value={route}>
|
||||||
<BottomTabItem
|
<BottomTabItem
|
||||||
route={route}
|
route={route}
|
||||||
focused={focused}
|
focused={focused}
|
||||||
horizontal={shouldUseHorizontalLabels()}
|
horizontal={shouldUseHorizontalLabels()}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
testID={options.tabBarTestID}
|
to={buildLink(route.name, route.params)}
|
||||||
allowFontScaling={allowFontScaling}
|
testID={options.tabBarTestID}
|
||||||
activeTintColor={activeTintColor}
|
allowFontScaling={allowFontScaling}
|
||||||
inactiveTintColor={inactiveTintColor}
|
activeTintColor={activeTintColor}
|
||||||
activeBackgroundColor={activeBackgroundColor}
|
inactiveTintColor={inactiveTintColor}
|
||||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
activeBackgroundColor={activeBackgroundColor}
|
||||||
button={options.tabBarButton}
|
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||||
icon={options.tabBarIcon}
|
button={options.tabBarButton}
|
||||||
label={label}
|
icon={options.tabBarIcon}
|
||||||
showIcon={showIcon}
|
label={label}
|
||||||
showLabel={showLabel}
|
showIcon={showIcon}
|
||||||
labelStyle={labelStyle}
|
showLabel={showLabel}
|
||||||
style={tabStyle}
|
labelStyle={labelStyle}
|
||||||
/>
|
style={tabStyle}
|
||||||
</NavigationRouteContext.Provider>
|
/>
|
||||||
</NavigationContext.Provider>
|
</NavigationRouteContext.Provider>
|
||||||
);
|
</NavigationContext.Provider>
|
||||||
})}
|
);
|
||||||
</View>
|
})}
|
||||||
</Animated.View>
|
</View>
|
||||||
)}
|
</Animated.View>
|
||||||
</SafeAreaConsumer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Animated,
|
Animated,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
Platform,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
GestureResponderEvent,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Route, useTheme } from '@react-navigation/native';
|
import { Link, Route, useTheme } from '@react-navigation/native';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
import TabBarIcon from './TabBarIcon';
|
import TabBarIcon from './TabBarIcon';
|
||||||
@@ -37,6 +39,10 @@ type Props = {
|
|||||||
size: number;
|
size: number;
|
||||||
color: string;
|
color: string;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
/**
|
||||||
|
* URL to use for the link to the tab.
|
||||||
|
*/
|
||||||
|
to?: string;
|
||||||
/**
|
/**
|
||||||
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
|
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
|
||||||
*/
|
*/
|
||||||
@@ -50,13 +56,16 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
testID?: string;
|
testID?: string;
|
||||||
/**
|
/**
|
||||||
* Function to execute on press.
|
* Function to execute on press in React Native.
|
||||||
|
* On the web, this will use onClick.
|
||||||
*/
|
*/
|
||||||
onPress: () => void;
|
onPress: (
|
||||||
|
e: React.MouseEvent<HTMLElement, MouseEvent> | GestureResponderEvent
|
||||||
|
) => void;
|
||||||
/**
|
/**
|
||||||
* Function to execute on long press.
|
* Function to execute on long press.
|
||||||
*/
|
*/
|
||||||
onLongPress: () => void;
|
onLongPress: (e: GestureResponderEvent) => void;
|
||||||
/**
|
/**
|
||||||
* Whether the label should be aligned with the icon horizontally.
|
* Whether the label should be aligned with the icon horizontally.
|
||||||
*/
|
*/
|
||||||
@@ -104,11 +113,48 @@ export default function BottomTabBarItem({
|
|||||||
route,
|
route,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
button = ({ children, style, ...rest }: BottomTabBarButtonProps) => (
|
to,
|
||||||
<TouchableWithoutFeedback {...rest}>
|
button = ({
|
||||||
<View style={style}>{children}</View>
|
children,
|
||||||
</TouchableWithoutFeedback>
|
style,
|
||||||
),
|
onPress,
|
||||||
|
to,
|
||||||
|
accessibilityRole,
|
||||||
|
...rest
|
||||||
|
}: BottomTabBarButtonProps) => {
|
||||||
|
if (Platform.OS === 'web' && to) {
|
||||||
|
// React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
|
||||||
|
// We need to use `onClick` to be able to prevent default browser handling of links.
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
{...rest}
|
||||||
|
to={to}
|
||||||
|
style={[styles.button, style]}
|
||||||
|
onPress={(e: any) => {
|
||||||
|
if (
|
||||||
|
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
|
||||||
|
(e.button == null || e.button === 0) // ignore everything but left clicks
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
onPress?.(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
{...rest}
|
||||||
|
accessibilityRole={accessibilityRole}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<View style={style}>{children}</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
testID,
|
testID,
|
||||||
onPress,
|
onPress,
|
||||||
@@ -133,9 +179,7 @@ export default function BottomTabBarItem({
|
|||||||
|
|
||||||
const inactiveTintColor =
|
const inactiveTintColor =
|
||||||
customInactiveTintColor === undefined
|
customInactiveTintColor === undefined
|
||||||
? Color(colors.text)
|
? Color(colors.text).mix(Color(colors.card), 0.5).hex()
|
||||||
.mix(Color(colors.card), 0.5)
|
|
||||||
.hex()
|
|
||||||
: customInactiveTintColor;
|
: customInactiveTintColor;
|
||||||
|
|
||||||
const renderLabel = ({ focused }: { focused: boolean }) => {
|
const renderLabel = ({ focused }: { focused: boolean }) => {
|
||||||
@@ -198,6 +242,7 @@ export default function BottomTabBarItem({
|
|||||||
: inactiveBackgroundColor;
|
: inactiveBackgroundColor;
|
||||||
|
|
||||||
return button({
|
return button({
|
||||||
|
to,
|
||||||
onPress,
|
onPress,
|
||||||
onLongPress,
|
onLongPress,
|
||||||
testID,
|
testID,
|
||||||
@@ -250,4 +295,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginLeft: 20,
|
marginLeft: 20,
|
||||||
},
|
},
|
||||||
|
button: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { View, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import { TabNavigationState, useTheme } from '@react-navigation/native';
|
import {
|
||||||
|
NavigationHelpersContext,
|
||||||
|
TabNavigationState,
|
||||||
|
useTheme,
|
||||||
|
} from '@react-navigation/native';
|
||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { ScreenContainer } from 'react-native-screens';
|
import { ScreenContainer } from 'react-native-screens';
|
||||||
|
|
||||||
@@ -91,44 +95,46 @@ export default class BottomTabView extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { state, descriptors, lazy } = this.props;
|
const { state, descriptors, navigation, lazy } = this.props;
|
||||||
const { routes } = state;
|
const { routes } = state;
|
||||||
const { loaded } = this.state;
|
const { loaded } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProviderCompat>
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
<View style={styles.container}>
|
<SafeAreaProviderCompat>
|
||||||
<ScreenContainer style={styles.pages}>
|
<View style={styles.container}>
|
||||||
{routes.map((route, index) => {
|
<ScreenContainer style={styles.pages}>
|
||||||
const descriptor = descriptors[route.key];
|
{routes.map((route, index) => {
|
||||||
const { unmountOnBlur } = descriptor.options;
|
const descriptor = descriptors[route.key];
|
||||||
const isFocused = state.index === index;
|
const { unmountOnBlur } = descriptor.options;
|
||||||
|
const isFocused = state.index === index;
|
||||||
|
|
||||||
if (unmountOnBlur && !isFocused) {
|
if (unmountOnBlur && !isFocused) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lazy && !loaded.includes(index) && !isFocused) {
|
if (lazy && !loaded.includes(index) && !isFocused) {
|
||||||
// Don't render a screen if we've never navigated to it
|
// Don't render a screen if we've never navigated to it
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourceSavingScene
|
<ResourceSavingScene
|
||||||
key={route.key}
|
key={route.key}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
isVisible={isFocused}
|
isVisible={isFocused}
|
||||||
>
|
>
|
||||||
<SceneContent isFocused={isFocused}>
|
<SceneContent isFocused={isFocused}>
|
||||||
{descriptor.render()}
|
{descriptor.render()}
|
||||||
</SceneContent>
|
</SceneContent>
|
||||||
</ResourceSavingScene>
|
</ResourceSavingScene>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ScreenContainer>
|
</ScreenContainer>
|
||||||
{this.renderTabBar()}
|
{this.renderTabBar()}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaProviderCompat>
|
</SafeAreaProviderCompat>
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Platform, StyleSheet, View } from 'react-native';
|
import { Platform, StyleSheet, View } from 'react-native';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { Screen, screensEnabled } from 'react-native-screens';
|
import { Screen, screensEnabled } from 'react-native-screens';
|
||||||
|
|
||||||
@@ -10,12 +9,14 @@ type Props = {
|
|||||||
style?: any;
|
style?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
|
const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container
|
||||||
|
|
||||||
export default class ResourceSavingScene extends React.Component<Props> {
|
export default class ResourceSavingScene extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
if (screensEnabled?.()) {
|
// react-native-screens is buggy on web
|
||||||
|
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||||
const { isVisible, ...rest } = this.props;
|
const { isVisible, ...rest } = this.props;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return <Screen active={isVisible ? 1 : 0} {...rest} />;
|
return <Screen active={isVisible ? 1 : 0} {...rest} />;
|
||||||
}
|
}
|
||||||
@@ -24,7 +25,13 @@ export default class ResourceSavingScene extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[styles.container, style, { opacity: isVisible ? 1 : 0 }]}
|
style={[
|
||||||
|
styles.container,
|
||||||
|
Platform.OS === 'web'
|
||||||
|
? { display: isVisible ? 'flex' : 'none' }
|
||||||
|
: null,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
collapsable={false}
|
collapsable={false}
|
||||||
removeClippedSubviews={
|
removeClippedSubviews={
|
||||||
// On iOS, set removeClippedSubviews to true only when not focused
|
// On iOS, set removeClippedSubviews to true only when not focused
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type Props = {
|
|||||||
export default function SafeAreaProviderCompat({ children }: Props) {
|
export default function SafeAreaProviderCompat({ children }: Props) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaConsumer>
|
<SafeAreaConsumer>
|
||||||
{insets => {
|
{(insets) => {
|
||||||
if (insets) {
|
if (insets) {
|
||||||
// If we already have insets, don't wrap the stack in another safe area provider
|
// If we already have insets, don't wrap the stack in another safe area provider
|
||||||
// This avoids an issue with updates at the cost of potentially incorrect values
|
// This avoids an issue with updates at the cost of potentially incorrect values
|
||||||
|
|||||||
@@ -3,6 +3,168 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.20](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.19...@react-navigation/compat@5.1.20) (2020-05-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.16](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.15...@react-navigation/compat@5.1.16) (2020-05-08)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.15](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.14...@react-navigation/compat@5.1.15) (2020-05-05)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.13...@react-navigation/compat@5.1.14) (2020-05-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.12...@react-navigation/compat@5.1.13) (2020-05-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.11...@react-navigation/compat@5.1.12) (2020-04-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.10...@react-navigation/compat@5.1.11) (2020-04-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix typo in navigationOptions ([8cbb201](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/8cbb201f1a7fb90e45a078df6bc42ce4771cc6a6))
|
||||||
|
* spread parent params to children in compat navigator ([24febf6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/24febf6ea99be2e5f22005fdd2a82136d647255c)), closes [#6785](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/6785)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.9](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.8...@react-navigation/compat@5.1.9) (2020-04-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.8](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.7...@react-navigation/compat@5.1.8) (2020-04-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use 1 as default in compatibility pop action ([4408117](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/44081172d440c713ad3543a2d5e1e18ebc8f72a4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.6...@react-navigation/compat@5.1.7) (2020-03-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.5...@react-navigation/compat@5.1.6) (2020-03-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.4...@react-navigation/compat@5.1.5) (2020-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.3...@react-navigation/compat@5.1.4) (2020-03-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.2...@react-navigation/compat@5.1.3) (2020-03-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.1...@react-navigation/compat@5.1.2) (2020-03-16)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.0...@react-navigation/compat@5.1.1) (2020-03-03)
|
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.0...@react-navigation/compat@5.1.1) (2020-03-03)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/compat
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/compat",
|
"name": "@react-navigation/compat",
|
||||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||||
"version": "5.1.1",
|
"version": "5.1.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"homepage": "https://reactnavigation.org/docs/compatibility.html",
|
"homepage": "https://reactnavigation.org/docs/compatibility.html",
|
||||||
"main": "lib/commonjs/index.js",
|
"main": "lib/commonjs/index.js",
|
||||||
"react-native": "src/index.tsx",
|
"react-native": "src/index.tsx",
|
||||||
|
"source": "src/index.tsx",
|
||||||
"module": "lib/module/index.js",
|
"module": "lib/module/index.js",
|
||||||
"types": "lib/typescript/src/index.d.ts",
|
"types": "lib/typescript/src/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -25,11 +26,11 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.9.3",
|
"@react-native-community/bob": "^0.13.1",
|
||||||
"@react-navigation/native": "^5.0.9",
|
"@react-navigation/native": "^5.3.2",
|
||||||
"@types/react": "^16.9.19",
|
"@types/react": "^16.9.34",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"typescript": "^3.7.5"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-navigation/native": "^5.0.5",
|
"@react-navigation/native": "^5.0.5",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NavigationProp,
|
NavigationProp,
|
||||||
RouteProp,
|
RouteProp,
|
||||||
EventMapBase,
|
EventMapBase,
|
||||||
|
NavigationRouteContext,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import CompatScreen from './CompatScreen';
|
import CompatScreen from './CompatScreen';
|
||||||
import ScreenPropsContext from './ScreenPropsContext';
|
import ScreenPropsContext from './ScreenPropsContext';
|
||||||
@@ -67,9 +68,12 @@ export default function createCompatNavigatorFactory<
|
|||||||
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
|
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
|
||||||
|
|
||||||
function Navigator({ screenProps }: { screenProps?: unknown }) {
|
function Navigator({ screenProps }: { screenProps?: unknown }) {
|
||||||
|
const parentRouteParams = React.useContext(NavigationRouteContext)
|
||||||
|
?.params;
|
||||||
|
|
||||||
const screens = React.useMemo(
|
const screens = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
routeNames.map(name => {
|
routeNames.map((name) => {
|
||||||
let getScreenComponent: () => CompatScreenType<NavigationPropType>;
|
let getScreenComponent: () => CompatScreenType<NavigationPropType>;
|
||||||
|
|
||||||
let initialParams;
|
let initialParams;
|
||||||
@@ -135,7 +139,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
<Pair.Screen
|
<Pair.Screen
|
||||||
key={name}
|
key={name}
|
||||||
name={name}
|
name={name}
|
||||||
initialParams={initialParams}
|
initialParams={{ ...parentRouteParams, ...initialParams }}
|
||||||
options={screenOptions}
|
options={screenOptions}
|
||||||
>
|
>
|
||||||
{({ navigation, route }) => (
|
{({ navigation, route }) => (
|
||||||
@@ -148,7 +152,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
</Pair.Screen>
|
</Pair.Screen>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
[screenProps]
|
[parentRouteParams, screenProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -163,7 +167,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.navigationOtions = parentNavigationOptions;
|
Navigator.navigationOptions = parentNavigationOptions;
|
||||||
|
|
||||||
return Navigator;
|
return Navigator;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function push(routeName: string, params?: object, action?: never) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pop(n: number) {
|
export function pop(n: number = 1) {
|
||||||
return StackActions.pop(typeof n === 'number' ? { n } : n);
|
return StackActions.pop(typeof n === 'number' ? { n } : n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function useCompatNavigation<
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const isFirstRouteInParent = useNavigationState(
|
const isFirstRouteInParent = useNavigationState(
|
||||||
state => state.routes[0].key === route.key
|
(state) => state.routes[0].key === route.key
|
||||||
);
|
);
|
||||||
|
|
||||||
const context = React.useRef<Record<string, any>>({});
|
const context = React.useRef<Record<string, any>>({});
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ export default function withNavigation<
|
|||||||
return <Comp ref={onRef} navigation={navigation} {...rest} />;
|
return <Comp ref={onRef} navigation={navigation} {...rest} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
WrappedComponent.displayName = `withNavigation(${Comp.displayName ||
|
WrappedComponent.displayName = `withNavigation(${
|
||||||
Comp.name})`;
|
Comp.displayName || Comp.name
|
||||||
|
})`;
|
||||||
|
|
||||||
return WrappedComponent;
|
return WrappedComponent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ export default function withNavigationFocus<
|
|||||||
return <Comp ref={onRef} isFocused={isFocused} {...rest} />;
|
return <Comp ref={onRef} isFocused={isFocused} {...rest} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
WrappedComponent.displayName = `withNavigationFocus(${Comp.displayName ||
|
WrappedComponent.displayName = `withNavigationFocus(${
|
||||||
Comp.name})`;
|
Comp.displayName || Comp.name
|
||||||
|
})`;
|
||||||
|
|
||||||
return WrappedComponent;
|
return WrappedComponent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,176 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.6.0...@react-navigation/core@5.6.1) (2020-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't use flat since it's not supported in node ([21b397f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/21b397f0d6b96ec4875d3172f47533130bb08009))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.0...@react-navigation/core@5.5.1) (2020-05-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid cleaning up state when a new navigator is mounted. fixes [#8195](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8195) ([f6d0676](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/f6d06768d3c36d1f5beaffcb660f3c259209f2e7))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.4.0...@react-navigation/core@5.5.0) (2020-05-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for optional params to linking ([#8196](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8196)) ([fcd1cc6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/fcd1cc64c151e4941f3f544a54b5048d853821f6))
|
||||||
|
* support params anywhere in path segement ([#8184](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8184)) ([3999fc2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3999fc28365c3a06a17d963c7be7fb7e897f99e0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.5...@react-navigation/core@5.4.0) (2020-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* handle empty paths when parsing ([c3fa83e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3fa83efe0d73db76365f8be3d6a8ca1d1289b71))
|
||||||
|
* parsing url ([bd35b4f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/bd35b4fc202c3868fb75c3675b62de67557089e1))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add config to enable redux devtools integration ([c9c825b](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c9c825bee61426635a28ee149eeeff3d628171cd))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.3...@react-navigation/core@5.3.4) (2020-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add initial option for navigating to nested navigators ([004c7d7](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/004c7d7ab1f80faf04b2a1836ec6b79a5419e45f))
|
||||||
|
* add initial param for actions from deep link ([a3f7a5f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/a3f7a5feba2e6aa2158aeaea6cde73ae1603173e))
|
||||||
|
* handle initial: false for nested route after first initialization ([187aefe](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/187aefe9c400b499f920c212bf856414e25c5aaf))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.2...@react-navigation/core@5.3.3) (2020-04-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* switch order of focus and blur events. closes [#7963](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7963) ([ce3994c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/ce3994c82c28669d5742017eb7627e9adf996933))
|
||||||
|
* workaround warning about setState in another component in render ([d4fd906](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/d4fd906915cc20d6fb21508384c05a540d8644d8))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.1...@react-navigation/core@5.3.2) (2020-03-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* handle no path property and undefined query params ([#7911](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7911)) ([cd47915](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/cd47915861a56cd7eaa9ac79f5139cde56ca95a7))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.0...@react-navigation/core@5.3.1) (2020-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't emit events for screens that don't exist anymore ([1c00142](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/1c001424b595b40f9db9343096c833f75353b099))
|
||||||
|
* only call listeners for focused screen for global events ([3096de6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3096de62868a7ed9ed65e529c8ddfa001b9be486))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.3...@react-navigation/core@5.3.0) (2020-03-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* return correct value for isFocused after changing screens ([5b15c71](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/5b15c7164f5503f2f0d51006a3f23bd0c58fd9b7)), closes [#7843](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7843)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* support function in listeners prop ([3709e65](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3709e652f41a16c2c2b05d5dbbe1da2017ba2c3f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.2...@react-navigation/core@5.2.3) (2020-03-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.1...@react-navigation/core@5.2.2) (2020-03-16)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.0...@react-navigation/core@5.2.1) (2020-03-03)
|
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.0...@react-navigation/core@5.2.1) (2020-03-03)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/core",
|
"name": "@react-navigation/core",
|
||||||
"description": "Core utilities for building navigators",
|
"description": "Core utilities for building navigators",
|
||||||
"version": "5.2.1",
|
"version": "5.6.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"homepage": "https://reactnavigation.org",
|
"homepage": "https://reactnavigation.org",
|
||||||
"main": "lib/commonjs/index.js",
|
"main": "lib/commonjs/index.js",
|
||||||
"react-native": "src/index.tsx",
|
"react-native": "src/index.tsx",
|
||||||
|
"source": "src/index.tsx",
|
||||||
"module": "lib/module/index.js",
|
"module": "lib/module/index.js",
|
||||||
"types": "lib/typescript/src/index.d.ts",
|
"types": "lib/typescript/src/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -29,24 +30,23 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/routers": "^5.1.0",
|
"@react-navigation/routers": "^5.4.4",
|
||||||
"escape-string-regexp": "^2.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"query-string": "^6.10.1",
|
"nanoid": "^3.1.5",
|
||||||
"react-is": "^16.12.0",
|
"query-string": "^6.12.1",
|
||||||
"shortid": "^2.2.15",
|
"react-is": "^16.13.0",
|
||||||
"use-subscription": "^1.3.0"
|
"use-subscription": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.9.3",
|
"@react-native-community/bob": "^0.13.1",
|
||||||
"@types/react": "^16.9.19",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-is": "^16.7.1",
|
"@types/react-is": "^16.7.1",
|
||||||
"@types/shortid": "^0.0.29",
|
|
||||||
"@types/use-subscription": "^1.0.0",
|
"@types/use-subscription": "^1.0.0",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"react-native-testing-library": "^1.12.0",
|
"react-native-testing-library": "^1.13.2",
|
||||||
"react-test-renderer": "~16.12.0",
|
"react-test-renderer": "~16.13.1",
|
||||||
"typescript": "^3.7.5"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*"
|
"react": "*"
|
||||||
|
|||||||
@@ -9,17 +9,21 @@ import {
|
|||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||||
|
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
||||||
import useFocusedListeners from './useFocusedListeners';
|
import useFocusedListeners from './useFocusedListeners';
|
||||||
import useDevTools from './useDevTools';
|
import useDevTools from './useDevTools';
|
||||||
import useStateGetters from './useStateGetters';
|
import useStateGetters from './useStateGetters';
|
||||||
|
import useEventEmitter from './useEventEmitter';
|
||||||
|
import useSyncState from './useSyncState';
|
||||||
import isSerializable from './isSerializable';
|
import isSerializable from './isSerializable';
|
||||||
|
|
||||||
import { NavigationContainerRef, NavigationContainerProps } from './types';
|
import { NavigationContainerRef, NavigationContainerProps } from './types';
|
||||||
import useEventEmitter from './useEventEmitter';
|
|
||||||
import useSyncState from './useSyncState';
|
|
||||||
|
|
||||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||||
|
|
||||||
|
const DEVTOOLS_CONFIG_KEY =
|
||||||
|
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
|
||||||
|
|
||||||
const MISSING_CONTEXT_ERROR =
|
const MISSING_CONTEXT_ERROR =
|
||||||
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
|
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
|
||||||
|
|
||||||
@@ -73,7 +77,7 @@ const getPartialState = (
|
|||||||
return {
|
return {
|
||||||
...partialState,
|
...partialState,
|
||||||
stale: true,
|
stale: true,
|
||||||
routes: state.routes.map(route => {
|
routes: state.routes.map((route) => {
|
||||||
if (route.state === undefined) {
|
if (route.state === undefined) {
|
||||||
return route as Route<string> & {
|
return route as Route<string> & {
|
||||||
state?: PartialState<NavigationState>;
|
state?: PartialState<NavigationState>;
|
||||||
@@ -102,7 +106,7 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
independent,
|
independent,
|
||||||
children,
|
children,
|
||||||
}: NavigationContainerProps,
|
}: NavigationContainerProps,
|
||||||
ref: React.Ref<NavigationContainerRef>
|
ref?: React.Ref<NavigationContainerRef>
|
||||||
) {
|
) {
|
||||||
const parent = React.useContext(NavigationStateContext);
|
const parent = React.useContext(NavigationStateContext);
|
||||||
|
|
||||||
@@ -112,7 +116,13 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [state, getState, setState] = useSyncState<State>(() =>
|
const [
|
||||||
|
state,
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
scheduleUpdate,
|
||||||
|
flushUpdates,
|
||||||
|
] = useSyncState<State>(() =>
|
||||||
getPartialState(initialState == null ? undefined : initialState)
|
getPartialState(initialState == null ? undefined : initialState)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -136,6 +146,9 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { trackState, trackAction } = useDevTools({
|
const { trackState, trackAction } = useDevTools({
|
||||||
|
enabled:
|
||||||
|
// @ts-ignore
|
||||||
|
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
|
||||||
name: '@react-navigation',
|
name: '@react-navigation',
|
||||||
reset,
|
reset,
|
||||||
state,
|
state,
|
||||||
@@ -155,7 +168,7 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
throw new Error(NOT_INITIALIZED_ERROR);
|
throw new Error(NOT_INITIALIZED_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners[0](navigation => navigation.dispatch(action));
|
listeners[0]((navigation) => navigation.dispatch(action));
|
||||||
};
|
};
|
||||||
|
|
||||||
const canGoBack = () => {
|
const canGoBack = () => {
|
||||||
@@ -163,7 +176,7 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result, handled } = listeners[0](navigation =>
|
const { result, handled } = listeners[0]((navigation) =>
|
||||||
navigation.canGoBack()
|
navigation.canGoBack()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -206,6 +219,8 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
dispatch,
|
dispatch,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
getRootState,
|
getRootState,
|
||||||
|
dangerouslyGetState: () => state,
|
||||||
|
dangerouslyGetParent: () => undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const builderContext = React.useMemo(
|
const builderContext = React.useMemo(
|
||||||
@@ -217,6 +232,11 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
[addFocusedListener, trackAction, addStateGetter]
|
[addFocusedListener, trackAction, addStateGetter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const scheduleContext = React.useMemo(
|
||||||
|
() => ({ scheduleUpdate, flushUpdates }),
|
||||||
|
[scheduleUpdate, flushUpdates]
|
||||||
|
);
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
state,
|
state,
|
||||||
@@ -262,11 +282,13 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
}, [onStateChange, trackState, getRootState, emitter, state]);
|
}, [onStateChange, trackState, getRootState, emitter, state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationBuilderContext.Provider value={builderContext}>
|
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
||||||
<NavigationStateContext.Provider value={context}>
|
<NavigationBuilderContext.Provider value={builderContext}>
|
||||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
<NavigationStateContext.Provider value={context}>
|
||||||
</NavigationStateContext.Provider>
|
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||||
</NavigationBuilderContext.Provider>
|
</NavigationStateContext.Provider>
|
||||||
|
</NavigationBuilderContext.Provider>
|
||||||
|
</ScheduleUpdateContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
13
packages/core/src/NavigationHelpersContext.tsx
Normal file
13
packages/core/src/NavigationHelpersContext.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ParamListBase } from '@react-navigation/routers';
|
||||||
|
import { NavigationHelpers } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context which holds the navigation helpers of the parent navigator.
|
||||||
|
* Navigators should use this context in their view component.
|
||||||
|
*/
|
||||||
|
const NavigationHelpersContext = React.createContext<
|
||||||
|
NavigationHelpers<ParamListBase> | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export default NavigationHelpersContext;
|
||||||
@@ -51,7 +51,7 @@ export default function SceneView<
|
|||||||
|
|
||||||
const getCurrentState = React.useCallback(() => {
|
const getCurrentState = React.useCallback(() => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const currentRoute = state.routes.find(r => r.key === route.key);
|
const currentRoute = state.routes.find((r) => r.key === route.key);
|
||||||
|
|
||||||
return currentRoute ? currentRoute.state : undefined;
|
return currentRoute ? currentRoute.state : undefined;
|
||||||
}, [getState, route.key]);
|
}, [getState, route.key]);
|
||||||
@@ -62,7 +62,7 @@ export default function SceneView<
|
|||||||
|
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
routes: state.routes.map(r =>
|
routes: state.routes.map((r) =>
|
||||||
r.key === route.key ? { ...r, state: child } : r
|
r.key === route.key ? { ...r, state: child } : r
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ it('handle dispatching with ref', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{state.routes.map(route => descriptors[route.key].render())}
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -220,7 +220,7 @@ it('handle resetting state with ref', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{state.routes.map(route => descriptors[route.key].render())}
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -371,7 +371,7 @@ it('emits state events when the state changes', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{state.routes.map(route => descriptors[route.key].render())}
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
|||||||
key: String(MockRouterKey.current++),
|
key: String(MockRouterKey.current++),
|
||||||
index,
|
index,
|
||||||
routeNames,
|
routeNames,
|
||||||
routes: routeNames.map(name => ({
|
routes: routeNames.map((name) => ({
|
||||||
name,
|
name,
|
||||||
key: name,
|
key: name,
|
||||||
params: routeParamList[name],
|
params: routeParamList[name],
|
||||||
@@ -43,9 +43,9 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const routes = state.routes
|
const routes = state.routes
|
||||||
.filter(route => routeNames.includes(route.name))
|
.filter((route) => routeNames.includes(route.name))
|
||||||
.map(
|
.map(
|
||||||
route =>
|
(route) =>
|
||||||
({
|
({
|
||||||
...route,
|
...route,
|
||||||
key: route.key || `${route.name}-${MockRouterKey.current++}`,
|
key: route.key || `${route.name}-${MockRouterKey.current++}`,
|
||||||
@@ -73,7 +73,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getStateForRouteNamesChange(state, { routeNames }) {
|
getStateForRouteNamesChange(state, { routeNames }) {
|
||||||
const routes = state.routes.filter(route =>
|
const routes = state.routes.filter((route) =>
|
||||||
routeNames.includes(route.name)
|
routeNames.includes(route.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getStateForRouteFocus(state, key) {
|
getStateForRouteFocus(state, key) {
|
||||||
const index = state.routes.findIndex(r => r.key === key);
|
const index = state.routes.findIndex((r) => r.key === key);
|
||||||
|
|
||||||
if (index === -1 || index === state.index) {
|
if (index === -1 || index === state.index) {
|
||||||
return state;
|
return state;
|
||||||
@@ -105,7 +105,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
|||||||
|
|
||||||
case 'NAVIGATE': {
|
case 'NAVIGATE': {
|
||||||
const index = state.routes.findIndex(
|
const index = state.routes.findIndex(
|
||||||
route => route.name === action.payload.name
|
(route) => route.name === action.payload.name
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ it('gets navigate action from state', () => {
|
|||||||
author: 'jane',
|
author: 'jane',
|
||||||
},
|
},
|
||||||
screen: 'qux',
|
screen: 'qux',
|
||||||
|
initial: true,
|
||||||
},
|
},
|
||||||
screen: 'bar',
|
screen: 'bar',
|
||||||
|
initial: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: 'NAVIGATE',
|
type: 'NAVIGATE',
|
||||||
@@ -70,9 +72,11 @@ it('gets navigate action from state', () => {
|
|||||||
payload: {
|
payload: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
params: {
|
params: {
|
||||||
|
initial: true,
|
||||||
screen: 'bar',
|
screen: 'bar',
|
||||||
params: {
|
params: {
|
||||||
screen: 'quz',
|
screen: 'quz',
|
||||||
|
initial: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ it('converts state to path string with config', () => {
|
|||||||
Baz: {
|
Baz: {
|
||||||
path: 'baz/:author',
|
path: 'baz/:author',
|
||||||
parse: {
|
parse: {
|
||||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
author: (author: string) =>
|
||||||
|
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||||
id: (id: string) => Number(id.replace(/^x/, '')),
|
id: (id: string) => Number(id.replace(/^x/, '')),
|
||||||
valid: Boolean,
|
valid: Boolean,
|
||||||
},
|
},
|
||||||
@@ -116,7 +117,8 @@ it("doesn't add query param for empty params", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles state with config with nested screens', () => {
|
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 = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
path: 'foo',
|
path: 'foo',
|
||||||
@@ -128,7 +130,77 @@ it('handles state with config with nested screens', () => {
|
|||||||
Baz: {
|
Baz: {
|
||||||
path: 'baz/:author',
|
path: 'baz/:author',
|
||||||
parse: {
|
parse: {
|
||||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
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 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,
|
count: Number,
|
||||||
valid: Boolean,
|
valid: Boolean,
|
||||||
},
|
},
|
||||||
@@ -181,7 +253,7 @@ it('handles state with config with nested screens', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles state with config with nested screens and unused configs', () => {
|
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 = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
path: 'foo',
|
path: 'foo',
|
||||||
@@ -192,12 +264,74 @@ it('handles state with config with nested screens and unused configs', () => {
|
|||||||
Baz: {
|
Baz: {
|
||||||
path: 'baz/:author',
|
path: 'baz/:author',
|
||||||
parse: {
|
parse: {
|
||||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
author: (author: string) =>
|
||||||
|
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||||
count: Number,
|
count: Number,
|
||||||
valid: Boolean,
|
valid: Boolean,
|
||||||
},
|
},
|
||||||
stringify: {
|
stringify: {
|
||||||
author: (author: string) => author.replace(/^\w/, c => c.toLowerCase()),
|
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 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',
|
unknown: (_: unknown) => 'x',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -248,18 +382,91 @@ it('handles nested object with stringify in it', () => {
|
|||||||
},
|
},
|
||||||
Bar: 'bar/:type/:fruit',
|
Bar: 'bar/:type/:fruit',
|
||||||
Baz: {
|
Baz: {
|
||||||
path: 'baz',
|
|
||||||
screens: {
|
screens: {
|
||||||
Bos: 'bos',
|
Bos: 'bos',
|
||||||
Bis: {
|
Bis: {
|
||||||
path: 'bis/:author',
|
path: 'bis/:author',
|
||||||
stringify: {
|
stringify: {
|
||||||
author: (author: string) =>
|
author: (author: string) =>
|
||||||
author.replace(/^\w/, c => c.toLowerCase()),
|
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||||
},
|
},
|
||||||
parse: {
|
parse: {
|
||||||
author: (author: string) =>
|
author: (author: string) =>
|
||||||
author.replace(/^\w/, c => c.toUpperCase()),
|
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 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,
|
count: Number,
|
||||||
valid: Boolean,
|
valid: Boolean,
|
||||||
},
|
},
|
||||||
@@ -309,7 +516,7 @@ it('handles nested object with stringify in it', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles nested object for second route depth', () => {
|
it('handles nested object for second route depth', () => {
|
||||||
const path = '/baz';
|
const path = '/foo/bar/baz';
|
||||||
const config = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
path: 'foo',
|
path: 'foo',
|
||||||
@@ -347,7 +554,95 @@ it('handles nested object for second route depth', () => {
|
|||||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
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 path = '/baz';
|
||||||
const config = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
@@ -366,7 +661,10 @@ it('handles nested object for second route depth and and path and stringify in r
|
|||||||
id: Number,
|
id: Number,
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
Baz: 'baz',
|
Baz: {
|
||||||
|
path: 'baz',
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -422,8 +720,51 @@ it('ignores empty string paths', () => {
|
|||||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps query params if path is empty', () => {
|
||||||
|
const path = '/?foo=42';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Foe: 'foe',
|
||||||
|
Bar: {
|
||||||
|
screens: {
|
||||||
|
Qux: {
|
||||||
|
path: '',
|
||||||
|
parse: { foo: Number },
|
||||||
|
},
|
||||||
|
Baz: 'baz',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Bar',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: 'Qux', params: { foo: 42 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toEqual(
|
||||||
|
path
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('cuts nested configs too', () => {
|
it('cuts nested configs too', () => {
|
||||||
const path = '/baz';
|
const path = '/foo/baz';
|
||||||
const config = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
path: 'foo',
|
path: 'foo',
|
||||||
@@ -431,7 +772,48 @@ it('cuts nested configs too', () => {
|
|||||||
Bar: '',
|
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 = {
|
const state = {
|
||||||
@@ -457,7 +839,7 @@ it('cuts nested configs too', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty path at the end', () => {
|
it('handles empty path at the end', () => {
|
||||||
const path = '/bar';
|
const path = '/foo/bar';
|
||||||
const config = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
path: 'foo',
|
path: 'foo',
|
||||||
@@ -491,6 +873,8 @@ it('handles empty path at the end', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns "/" for empty path', () => {
|
it('returns "/" for empty path', () => {
|
||||||
|
const path = '/';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
Foo: {
|
Foo: {
|
||||||
path: '',
|
path: '',
|
||||||
@@ -515,5 +899,369 @@ it('returns "/" for empty path', () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getPathFromState(state, config)).toBe('/');
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses no path specified', () => {
|
||||||
|
const path = '/Foo/bar';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
screens: {
|
||||||
|
Foe: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bar: 'bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: 'Bar' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses no path specified in nested config', () => {
|
||||||
|
const path = '/Foo/Foe/bar';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
path: 'foo',
|
||||||
|
screens: {
|
||||||
|
Foe: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bar: 'bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foo',
|
||||||
|
state: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'Foe',
|
||||||
|
state: {
|
||||||
|
routes: [{ name: 'Bar' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips undefined query params', () => {
|
||||||
|
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: {
|
||||||
|
screens: {
|
||||||
|
Bos: 'bos',
|
||||||
|
Bis: {
|
||||||
|
path: 'bis/:author',
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getPathFromState(state, config)).toBe(path);
|
||||||
|
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles stripping all query params', () => {
|
||||||
|
const path = '/bar/sweet/apple/foo/bis/jane';
|
||||||
|
const config = {
|
||||||
|
Foo: {
|
||||||
|
path: 'foo',
|
||||||
|
screens: {
|
||||||
|
Foe: {
|
||||||
|
path: 'foe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bar: 'bar/:type/:fruit',
|
||||||
|
Baz: {
|
||||||
|
screens: {
|
||||||
|
Bos: 'bos',
|
||||||
|
Bis: {
|
||||||
|
path: 'bis/:author',
|
||||||
|
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('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);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -379,6 +379,8 @@ it("doesn't update state if action wasn't handled", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('cleans up state when the navigator unmounts', () => {
|
it('cleans up state when the navigator unmounts', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
const TestNavigator = (props: any) => {
|
const TestNavigator = (props: any) => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
@@ -426,6 +428,8 @@ it('cleans up state when the navigator unmounts', () => {
|
|||||||
<BaseNavigationContainer onStateChange={onStateChange} children={null} />
|
<BaseNavigationContainer onStateChange={onStateChange} children={null} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
act(() => jest.runAllTimers());
|
||||||
|
|
||||||
expect(onStateChange).toBeCalledTimes(2);
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
expect(onStateChange).lastCalledWith(undefined);
|
expect(onStateChange).lastCalledWith(undefined);
|
||||||
});
|
});
|
||||||
@@ -626,7 +630,7 @@ it('updates route params with setParams applied to parent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles change in route names', () => {
|
it('handles change in route names', async () => {
|
||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
useNavigationBuilder(MockRouter, props);
|
useNavigationBuilder(MockRouter, props);
|
||||||
return null;
|
return null;
|
||||||
@@ -635,7 +639,7 @@ it('handles change in route names', () => {
|
|||||||
const onStateChange = jest.fn();
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
const root = render(
|
const root = render(
|
||||||
<BaseNavigationContainer onStateChange={onStateChange}>
|
<BaseNavigationContainer>
|
||||||
<TestNavigator initialRouteName="bar">
|
<TestNavigator initialRouteName="bar">
|
||||||
<Screen name="foo" component={jest.fn()} />
|
<Screen name="foo" component={jest.fn()} />
|
||||||
<Screen name="bar" component={jest.fn()} />
|
<Screen name="bar" component={jest.fn()} />
|
||||||
@@ -737,6 +741,366 @@ it('navigates to nested child in a navigator', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('navigates to nested child in a navigator with initial: false', () => {
|
||||||
|
const TestRouter: typeof MockRouter = (options) => {
|
||||||
|
const router = MockRouter(options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...router,
|
||||||
|
|
||||||
|
getStateForAction(state, action, options) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'NAVIGATE': {
|
||||||
|
if (!options.routeNames.includes(action.payload.name as any)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
...state.routes,
|
||||||
|
{
|
||||||
|
key: String(MockRouterKey.current++),
|
||||||
|
name: action.payload.name,
|
||||||
|
params: action.payload.params,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
index: routes.length - 1,
|
||||||
|
routes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return router.getStateForAction(state, action, options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as typeof router;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestNavigator = (props: any): any => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
|
||||||
|
|
||||||
|
return descriptors[state.routes[state.index].key].render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestComponent = ({ route }: any): any =>
|
||||||
|
`[${route.name}, ${JSON.stringify(route.params)}]`;
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const navigation = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const first = render(
|
||||||
|
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo-a" component={TestComponent} />
|
||||||
|
<Screen name="foo-b" component={TestComponent} />
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
<Screen name="bar">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator initialRouteName="bar-a">
|
||||||
|
<Screen
|
||||||
|
name="bar-a"
|
||||||
|
component={TestComponent}
|
||||||
|
initialParams={{ lol: 'why' }}
|
||||||
|
/>
|
||||||
|
<Screen
|
||||||
|
name="bar-b"
|
||||||
|
component={TestComponent}
|
||||||
|
initialParams={{ some: 'stuff' }}
|
||||||
|
/>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||||
|
expect(navigation.current?.getRootState()).toEqual({
|
||||||
|
index: 0,
|
||||||
|
key: '0',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'foo',
|
||||||
|
name: 'foo',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: '1',
|
||||||
|
routeNames: ['foo-a', 'foo-b'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'foo-a',
|
||||||
|
name: 'foo-a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'foo-b',
|
||||||
|
name: 'foo-b',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'bar', name: 'bar' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(
|
||||||
|
() =>
|
||||||
|
navigation.current &&
|
||||||
|
navigation.current.navigate('bar', {
|
||||||
|
screen: 'bar-b',
|
||||||
|
params: { test: 42 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(first).toMatchInlineSnapshot(
|
||||||
|
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation.current?.getRootState()).toEqual({
|
||||||
|
index: 2,
|
||||||
|
key: '0',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo', name: 'foo' },
|
||||||
|
{ key: 'bar', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: 'bar',
|
||||||
|
params: { params: { test: 42 }, screen: 'bar-b' },
|
||||||
|
state: {
|
||||||
|
index: 1,
|
||||||
|
key: '3',
|
||||||
|
routeNames: ['bar-a', 'bar-b'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'bar-a',
|
||||||
|
name: 'bar-a',
|
||||||
|
params: { lol: 'why' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bar-b',
|
||||||
|
name: 'bar-b',
|
||||||
|
params: { some: 'stuff', test: 42 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const second = render(
|
||||||
|
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo-a" component={TestComponent} />
|
||||||
|
<Screen name="foo-b" component={TestComponent} />
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
<Screen name="bar">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator initialRouteName="bar-a">
|
||||||
|
<Screen
|
||||||
|
name="bar-a"
|
||||||
|
component={TestComponent}
|
||||||
|
initialParams={{ lol: 'why' }}
|
||||||
|
/>
|
||||||
|
<Screen
|
||||||
|
name="bar-b"
|
||||||
|
component={TestComponent}
|
||||||
|
initialParams={{ some: 'stuff' }}
|
||||||
|
/>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(second).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||||
|
expect(navigation.current?.getRootState()).toEqual({
|
||||||
|
index: 0,
|
||||||
|
key: '4',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'foo',
|
||||||
|
name: 'foo',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: '5',
|
||||||
|
routeNames: ['foo-a', 'foo-b'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-a', name: 'foo-a' },
|
||||||
|
{ key: 'foo-b', name: 'foo-b' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'bar', name: 'bar' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(
|
||||||
|
() =>
|
||||||
|
navigation.current &&
|
||||||
|
navigation.current.navigate('bar', {
|
||||||
|
screen: 'bar-b',
|
||||||
|
params: { test: 42 },
|
||||||
|
initial: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(second).toMatchInlineSnapshot(`"[bar-b, {\\"test\\":42}]"`);
|
||||||
|
|
||||||
|
expect(navigation.current?.getRootState()).toEqual({
|
||||||
|
index: 2,
|
||||||
|
key: '4',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo', name: 'foo' },
|
||||||
|
{ key: 'bar', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: '6',
|
||||||
|
name: 'bar',
|
||||||
|
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
|
||||||
|
state: {
|
||||||
|
index: 2,
|
||||||
|
key: '7',
|
||||||
|
routeNames: ['bar-a', 'bar-b'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'bar-a',
|
||||||
|
name: 'bar-a',
|
||||||
|
params: { lol: 'why' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bar-b',
|
||||||
|
name: 'bar-b',
|
||||||
|
params: { some: 'stuff' },
|
||||||
|
},
|
||||||
|
{ key: '8', name: 'bar-b', params: { test: 42 } },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const third = render(
|
||||||
|
<BaseNavigationContainer
|
||||||
|
ref={navigation}
|
||||||
|
initialState={{
|
||||||
|
index: 1,
|
||||||
|
routes: [
|
||||||
|
{ name: 'foo' },
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
|
||||||
|
state: {
|
||||||
|
index: 1,
|
||||||
|
key: '7',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'bar-a',
|
||||||
|
params: { lol: 'why' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar-b',
|
||||||
|
params: { some: 'stuff' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'test',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo" component={TestComponent} />
|
||||||
|
<Screen name="bar">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator initialRouteName="bar-a">
|
||||||
|
<Screen
|
||||||
|
name="bar-a"
|
||||||
|
component={TestComponent}
|
||||||
|
initialParams={{ lol: 'why' }}
|
||||||
|
/>
|
||||||
|
<Screen
|
||||||
|
name="bar-b"
|
||||||
|
component={TestComponent}
|
||||||
|
initialParams={{ some: 'stuff' }}
|
||||||
|
/>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(third).toMatchInlineSnapshot(`"[bar-b, {\\"some\\":\\"stuff\\"}]"`);
|
||||||
|
|
||||||
|
expect(navigation.current?.getRootState()).toEqual({
|
||||||
|
index: 1,
|
||||||
|
key: '11',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-9', name: 'foo' },
|
||||||
|
{
|
||||||
|
key: 'bar-10',
|
||||||
|
name: 'bar',
|
||||||
|
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
|
||||||
|
state: {
|
||||||
|
index: 1,
|
||||||
|
key: '14',
|
||||||
|
routeNames: ['bar-a', 'bar-b'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'bar-a-12',
|
||||||
|
name: 'bar-a',
|
||||||
|
params: { lol: 'why' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bar-b-13',
|
||||||
|
name: 'bar-b',
|
||||||
|
params: { some: 'stuff' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('gives access to internal state', () => {
|
it('gives access to internal state', () => {
|
||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ it('sets options with screenOptions prop as an object', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{state.routes.map(route => {
|
{state.routes.map((route) => {
|
||||||
const { render, options } = descriptors[route.key];
|
const { render, options } = descriptors[route.key];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -179,7 +179,7 @@ it('sets options with screenOptions prop as a fuction', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{state.routes.map(route => {
|
{state.routes.map((route) => {
|
||||||
const { render, options } = descriptors[route.key];
|
const { render, options } = descriptors[route.key];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -262,8 +262,10 @@ it('sets initial options with setOptions', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TestScreen = ({ navigation }: any): any => {
|
const TestScreen = ({ navigation }: any): any => {
|
||||||
navigation.setOptions({
|
React.useEffect(() => {
|
||||||
title: 'Hello world',
|
navigation.setOptions({
|
||||||
|
title: 'Hello world',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return 'Test screen';
|
return 'Test screen';
|
||||||
@@ -273,7 +275,7 @@ it('sets initial options with setOptions', () => {
|
|||||||
<BaseNavigationContainer>
|
<BaseNavigationContainer>
|
||||||
<TestNavigator>
|
<TestNavigator>
|
||||||
<Screen name="foo" options={{ color: 'blue' }}>
|
<Screen name="foo" options={{ color: 'blue' }}>
|
||||||
{props => <TestScreen {...props} />}
|
{(props) => <TestScreen {...props} />}
|
||||||
</Screen>
|
</Screen>
|
||||||
<Screen name="bar" component={jest.fn()} />
|
<Screen name="bar" component={jest.fn()} />
|
||||||
</TestNavigator>
|
</TestNavigator>
|
||||||
@@ -315,12 +317,12 @@ it('updates options with setOptions', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TestScreen = ({ navigation }: any): any => {
|
const TestScreen = ({ navigation }: any): any => {
|
||||||
navigation.setOptions({
|
|
||||||
title: 'Hello world',
|
|
||||||
description: 'Something here',
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
title: 'Hello world',
|
||||||
|
description: 'Something here',
|
||||||
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() =>
|
const timer = setTimeout(() =>
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
title: 'Hello again',
|
title: 'Hello again',
|
||||||
@@ -338,7 +340,7 @@ it('updates options with setOptions', () => {
|
|||||||
<BaseNavigationContainer>
|
<BaseNavigationContainer>
|
||||||
<TestNavigator>
|
<TestNavigator>
|
||||||
<Screen name="foo" options={{ color: 'blue' }}>
|
<Screen name="foo" options={{ color: 'blue' }}>
|
||||||
{props => <TestScreen {...props} />}
|
{(props) => <TestScreen {...props} />}
|
||||||
</Screen>
|
</Screen>
|
||||||
<Screen name="bar" component={jest.fn()} />
|
<Screen name="bar" component={jest.fn()} />
|
||||||
</TestNavigator>
|
</TestNavigator>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ it('fires focus and blur events in root navigator', () => {
|
|||||||
|
|
||||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstFocusCallback = jest.fn();
|
const firstFocusCallback = jest.fn();
|
||||||
@@ -97,6 +97,69 @@ it('fires focus and blur events in root navigator', () => {
|
|||||||
expect(fourthBlurCallback).toBeCalledTimes(0);
|
expect(fourthBlurCallback).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fires focus event after blur', () => {
|
||||||
|
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||||
|
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||||
|
MockRouter,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||||
|
|
||||||
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
|
});
|
||||||
|
|
||||||
|
const callback = jest.fn();
|
||||||
|
|
||||||
|
const Test = ({ route, navigation }: any) => {
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
navigation.addListener('focus', () => callback(route.name, 'focus')),
|
||||||
|
[navigation, route.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => navigation.addListener('blur', () => callback(route.name, 'blur')),
|
||||||
|
[navigation, route.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigation = React.createRef<any>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator ref={navigation}>
|
||||||
|
<Screen name="first" component={Test} />
|
||||||
|
<Screen name="second" component={Test} />
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element);
|
||||||
|
|
||||||
|
expect(callback.mock.calls).toEqual([['first', 'focus']]);
|
||||||
|
|
||||||
|
act(() => navigation.current.navigate('second'));
|
||||||
|
|
||||||
|
expect(callback.mock.calls).toEqual([
|
||||||
|
['first', 'focus'],
|
||||||
|
['first', 'blur'],
|
||||||
|
['second', 'focus'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
act(() => navigation.current.navigate('first'));
|
||||||
|
|
||||||
|
expect(callback.mock.calls).toEqual([
|
||||||
|
['first', 'focus'],
|
||||||
|
['first', 'blur'],
|
||||||
|
['second', 'focus'],
|
||||||
|
['second', 'blur'],
|
||||||
|
['first', 'focus'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('fires focus and blur events in nested navigator', () => {
|
it('fires focus and blur events in nested navigator', () => {
|
||||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||||
@@ -106,7 +169,7 @@ it('fires focus and blur events in nested navigator', () => {
|
|||||||
|
|
||||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstFocusCallback = jest.fn();
|
const firstFocusCallback = jest.fn();
|
||||||
@@ -376,7 +439,7 @@ it('fires custom events added with addListener', () => {
|
|||||||
state,
|
state,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstCallback = jest.fn();
|
const firstCallback = jest.fn();
|
||||||
@@ -456,7 +519,7 @@ it("doesn't call same listener multiple times with addListener", () => {
|
|||||||
state,
|
state,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
});
|
});
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
@@ -565,12 +628,10 @@ it('fires custom events added with listeners prop', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
|
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
|
||||||
expect(secondCallback.mock.calls[0][0].target).toBe(undefined);
|
|
||||||
expect(thirdCallback.mock.calls[1][0].target).toBe(undefined);
|
|
||||||
|
|
||||||
expect(firstCallback).toBeCalledTimes(1);
|
expect(firstCallback).toBeCalledTimes(1);
|
||||||
expect(secondCallback).toBeCalledTimes(1);
|
expect(secondCallback).toBeCalledTimes(0);
|
||||||
expect(thirdCallback).toBeCalledTimes(2);
|
expect(thirdCallback).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't call same listener multiple times with listeners", () => {
|
it("doesn't call same listener multiple times with listeners", () => {
|
||||||
@@ -624,6 +685,91 @@ it("doesn't call same listener multiple times with listeners", () => {
|
|||||||
expect(callback).toBeCalledTimes(1);
|
expect(callback).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fires listeners when callback is provided for listeners prop', () => {
|
||||||
|
const eventName = 'someSuperCoolEvent';
|
||||||
|
|
||||||
|
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||||
|
const { state, navigation } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||||
|
navigation,
|
||||||
|
state,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstCallback = jest.fn();
|
||||||
|
const secondCallback = jest.fn();
|
||||||
|
const thirdCallback = jest.fn();
|
||||||
|
|
||||||
|
const ref = React.createRef<any>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator ref={ref}>
|
||||||
|
<Screen
|
||||||
|
name="first"
|
||||||
|
listeners={({ route, navigation }) => ({
|
||||||
|
someSuperCoolEvent: (e) => firstCallback(e, route, navigation),
|
||||||
|
})}
|
||||||
|
component={jest.fn()}
|
||||||
|
/>
|
||||||
|
<Screen
|
||||||
|
name="second"
|
||||||
|
listeners={({ route, navigation }) => ({
|
||||||
|
someSuperCoolEvent: (e) => secondCallback(e, route, navigation),
|
||||||
|
})}
|
||||||
|
component={jest.fn()}
|
||||||
|
/>
|
||||||
|
<Screen
|
||||||
|
name="third"
|
||||||
|
listeners={({ route, navigation }) => ({
|
||||||
|
someSuperCoolEvent: (e) => thirdCallback(e, route, navigation),
|
||||||
|
})}
|
||||||
|
component={jest.fn()}
|
||||||
|
/>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element);
|
||||||
|
|
||||||
|
expect(firstCallback).toBeCalledTimes(0);
|
||||||
|
expect(secondCallback).toBeCalledTimes(0);
|
||||||
|
expect(thirdCallback).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
const target =
|
||||||
|
ref.current.state.routes[ref.current.state.routes.length - 1].key;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current.navigation.emit({
|
||||||
|
type: eventName,
|
||||||
|
target,
|
||||||
|
data: 42,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstCallback).toBeCalledTimes(0);
|
||||||
|
expect(secondCallback).toBeCalledTimes(0);
|
||||||
|
expect(thirdCallback).toBeCalledTimes(1);
|
||||||
|
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||||
|
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||||
|
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
|
||||||
|
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||||
|
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current.navigation.emit({ type: eventName });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
|
||||||
|
|
||||||
|
expect(firstCallback).toBeCalledTimes(1);
|
||||||
|
expect(secondCallback).toBeCalledTimes(0);
|
||||||
|
expect(thirdCallback).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('has option to prevent default', () => {
|
it('has option to prevent default', () => {
|
||||||
expect.assertions(5);
|
expect.assertions(5);
|
||||||
|
|
||||||
@@ -640,7 +786,7 @@ it('has option to prevent default', () => {
|
|||||||
state,
|
state,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
});
|
});
|
||||||
|
|
||||||
const callback = (e: any) => {
|
const callback = (e: any) => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ it('runs focus effect on focus change', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusEffect = jest.fn();
|
const focusEffect = jest.fn();
|
||||||
@@ -107,7 +107,7 @@ it('runs focus effect when initial state is given', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusEffect = jest.fn();
|
const focusEffect = jest.fn();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ it('renders correct focus state', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ it('gets navigation prop from context', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
@@ -38,7 +38,7 @@ it("gets navigation's parent from context", () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
@@ -70,7 +70,7 @@ it("gets navigation's parent's parent from context", () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { render } from 'react-native-testing-library';
|
import { render, act } from 'react-native-testing-library';
|
||||||
import useEventEmitter from '../useEventEmitter';
|
import useEventEmitter from '../useEventEmitter';
|
||||||
import useNavigationCache from '../useNavigationCache';
|
import useNavigationCache from '../useNavigationCache';
|
||||||
|
import useNavigationBuilder from '../useNavigationBuilder';
|
||||||
|
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||||
|
import Screen from '../Screen';
|
||||||
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
|
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
|
||||||
|
|
||||||
beforeEach(() => (MockRouterKey.current = 0));
|
beforeEach(() => (MockRouterKey.current = 0));
|
||||||
@@ -40,7 +43,7 @@ it('preserves reference for navigation objects', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (previous.current) {
|
if (previous.current) {
|
||||||
Object.keys(navigations).forEach(key => {
|
Object.keys(navigations).forEach((key) => {
|
||||||
expect(navigations[key]).toBe(previous.current[key]);
|
expect(navigations[key]).toBe(previous.current[key]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -56,3 +59,136 @@ it('preserves reference for navigation objects', () => {
|
|||||||
|
|
||||||
root.update(<Test />);
|
root.update(<Test />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns correct value for isFocused', () => {
|
||||||
|
const TestNavigator = (props: any): any => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
|
};
|
||||||
|
|
||||||
|
let navigation: any;
|
||||||
|
|
||||||
|
const Test = (props: any) => {
|
||||||
|
navigation = props.navigation;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="first">{() => null}</Screen>
|
||||||
|
<Screen name="second" component={Test} />
|
||||||
|
<Screen name="third">{() => null}</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(false);
|
||||||
|
|
||||||
|
act(() => navigation.navigate('second'));
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(true);
|
||||||
|
|
||||||
|
act(() => navigation.navigate('third'));
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(false);
|
||||||
|
|
||||||
|
act(() => navigation.navigate('second'));
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for isFocused after changing screens', () => {
|
||||||
|
const TestRouter = (
|
||||||
|
options: Parameters<typeof MockRouter>[0]
|
||||||
|
): ReturnType<typeof MockRouter> => {
|
||||||
|
const router = MockRouter(options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...router,
|
||||||
|
|
||||||
|
getStateForRouteNamesChange(state, { routeNames }) {
|
||||||
|
const routes = routeNames.map(
|
||||||
|
(name) =>
|
||||||
|
state.routes.find((r) => r.name === name) || {
|
||||||
|
name,
|
||||||
|
key: name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
routeNames,
|
||||||
|
routes,
|
||||||
|
index: routes.length - 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestNavigator = (props: any): any => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
|
||||||
|
|
||||||
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
|
};
|
||||||
|
|
||||||
|
let navigation: any;
|
||||||
|
|
||||||
|
const Test = (props: any) => {
|
||||||
|
navigation = props.navigation;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = render(
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="first">{() => null}</Screen>
|
||||||
|
<Screen name="second" component={Test} />
|
||||||
|
<Screen name="third">{() => null}</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(false);
|
||||||
|
|
||||||
|
root.update(
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="first">{() => null}</Screen>
|
||||||
|
<Screen name="third">{() => null}</Screen>
|
||||||
|
<Screen name="second" component={Test} />
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(true);
|
||||||
|
|
||||||
|
root.update(
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="first">{() => null}</Screen>
|
||||||
|
<Screen name="third">{() => null}</Screen>
|
||||||
|
<Screen name="fourth">{() => null}</Screen>
|
||||||
|
<Screen name="second" component={Test} />
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(true);
|
||||||
|
|
||||||
|
root.update(
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="first">{() => null}</Screen>
|
||||||
|
<Screen name="third">{() => null}</Screen>
|
||||||
|
<Screen name="second" component={Test} />
|
||||||
|
<Screen name="fourth">{() => null}</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation.isFocused()).toBe(false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ it('gets the current navigation state', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
const state = useNavigationState(state => state);
|
const state = useNavigationState((state) => state);
|
||||||
|
|
||||||
callback(state);
|
callback(state);
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@ it('gets the current navigation state with selector', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
const index = useNavigationState(state => state.index);
|
const index = useNavigationState((state) => state.index);
|
||||||
|
|
||||||
callback(index);
|
callback(index);
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ it('gets the correct value if selector changes', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
@@ -144,12 +144,12 @@ it('gets the correct value if selector changes', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const root = render(<App selector={state => state.index} />);
|
const root = render(<App selector={(state) => state.index} />);
|
||||||
|
|
||||||
expect(callback).toBeCalledTimes(1);
|
expect(callback).toBeCalledTimes(1);
|
||||||
expect(callback.mock.calls[0][0]).toBe(0);
|
expect(callback.mock.calls[0][0]).toBe(0);
|
||||||
|
|
||||||
root.update(<App selector={state => state.routes[state.index].name} />);
|
root.update(<App selector={(state) => state.routes[state.index].name} />);
|
||||||
|
|
||||||
expect(callback).toBeCalledTimes(2);
|
expect(callback).toBeCalledTimes(2);
|
||||||
expect(callback.mock.calls[1][0]).toBe('first');
|
expect(callback.mock.calls[1][0]).toBe('first');
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ it("lets children handle the action if parent didn't", () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{state.routes.map(route => descriptors[route.key].render())}
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -270,7 +270,7 @@ it("action doesn't bubble if target is specified", () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{state.routes.map(route => descriptors[route.key].render())}
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -317,7 +317,7 @@ it('logs error if no navigator handled the action', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{state.routes.map(route => descriptors[route.key].render())}
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ it('gets route prop from context', () => {
|
|||||||
const TestNavigator = (props: any): any => {
|
const TestNavigator = (props: any): any => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
return state.routes.map(route => descriptors[route.key].render());
|
return state.routes.map((route) => descriptors[route.key].render());
|
||||||
};
|
};
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function createNavigatorFactory<
|
|||||||
EventMap extends EventMapBase,
|
EventMap extends EventMapBase,
|
||||||
NavigatorComponent extends React.ComponentType<any>
|
NavigatorComponent extends React.ComponentType<any>
|
||||||
>(Navigator: NavigatorComponent) {
|
>(Navigator: NavigatorComponent) {
|
||||||
return function<ParamList extends ParamListBase>(): TypedNavigator<
|
return function <ParamList extends ParamListBase>(): TypedNavigator<
|
||||||
ParamList,
|
ParamList,
|
||||||
State,
|
State,
|
||||||
ScreenOptions,
|
ScreenOptions,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PartialState, NavigationState } from '@react-navigation/routers';
|
|||||||
type NavigateParams = {
|
type NavigateParams = {
|
||||||
screen?: string;
|
screen?: string;
|
||||||
params?: NavigateParams;
|
params?: NavigateParams;
|
||||||
|
initial?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NavigateAction = {
|
type NavigateAction = {
|
||||||
@@ -35,6 +36,7 @@ export default function getActionFromState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
route = current.routes[current.routes.length - 1];
|
route = current.routes[current.routes.length - 1];
|
||||||
|
params.initial = current.routes.length === 1;
|
||||||
params.screen = route.name;
|
params.screen = route.name;
|
||||||
|
|
||||||
if (route.state) {
|
if (route.state) {
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
|||||||
|
|
||||||
type StringifyConfig = Record<string, (value: any) => string>;
|
type StringifyConfig = Record<string, (value: any) => string>;
|
||||||
|
|
||||||
type Options = {
|
type OptionsItem = {
|
||||||
[routeName: string]:
|
path?: string;
|
||||||
| string
|
exact?: boolean;
|
||||||
| {
|
stringify?: StringifyConfig;
|
||||||
path?: string;
|
screens?: Options;
|
||||||
stringify?: StringifyConfig;
|
|
||||||
screens?: Options;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Options = Record<string, string | OptionsItem>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to serialize a navigation state object to a path string.
|
* Utility to serialize a navigation state object to a path string.
|
||||||
*
|
*
|
||||||
@@ -53,115 +52,188 @@ export default function getPathFromState(
|
|||||||
if (state === undefined) {
|
if (state === undefined) {
|
||||||
throw Error('NavigationState not passed');
|
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;
|
let current: State | undefined = state;
|
||||||
|
|
||||||
|
const allParams: Record<string, any> = {};
|
||||||
|
|
||||||
while (current) {
|
while (current) {
|
||||||
let index = typeof current.index === 'number' ? current.index : 0;
|
let index = typeof current.index === 'number' ? current.index : 0;
|
||||||
let route = current.routes[index] as Route<string> & {
|
let route = current.routes[index] as Route<string> & {
|
||||||
state?: State;
|
state?: State;
|
||||||
};
|
};
|
||||||
let currentOptions = options;
|
|
||||||
let pattern = route.name;
|
|
||||||
|
|
||||||
while (route.name in currentOptions) {
|
let pattern: string | undefined;
|
||||||
if (typeof currentOptions[route.name] === 'string') {
|
|
||||||
pattern = currentOptions[route.name] as string;
|
let currentParams: Record<string, any> = { ...route.params };
|
||||||
break;
|
let currentOptions = configs;
|
||||||
} else if (typeof currentOptions[route.name] === 'object') {
|
|
||||||
// if there is no `screens` property, we return pattern
|
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
||||||
if (
|
let nestedRouteNames = [];
|
||||||
!(currentOptions[route.name] as {
|
|
||||||
screens: Options;
|
let hasNext = true;
|
||||||
}).screens
|
|
||||||
) {
|
while (route.name in currentOptions && hasNext) {
|
||||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
pattern = currentOptions[route.name].pattern;
|
||||||
break;
|
|
||||||
|
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 {
|
} else {
|
||||||
// if it is the end of state, we return pattern
|
// If not, there is no sense in going deeper in config
|
||||||
if (route.state === undefined) {
|
hasNext = false;
|
||||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
|
||||||
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) {
|
|
||||||
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;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't add empty path strings to path
|
if (pattern === undefined) {
|
||||||
if (pattern !== '') {
|
pattern = nestedRouteNames.join('/');
|
||||||
const config =
|
}
|
||||||
currentOptions[route.name] !== undefined
|
|
||||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
|
||||||
.stringify
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const params = route.params
|
if (currentOptions[route.name] !== undefined) {
|
||||||
? // Stringify all of the param values before we use them
|
path += pattern
|
||||||
Object.entries(route.params).reduce<{
|
.split('/')
|
||||||
[key: string]: string;
|
.map((p) => {
|
||||||
}>((acc, [key, value]) => {
|
const name = p.replace(/^:/, '').replace(/\?$/, '');
|
||||||
acc[key] = config?.[key] ? config[key](value) : String(value);
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (currentOptions[route.name] !== undefined) {
|
// If the path has a pattern for a param, put the param in the path
|
||||||
path += pattern
|
if (p.startsWith(':')) {
|
||||||
.split('/')
|
const value = allParams[name];
|
||||||
.map(p => {
|
|
||||||
const name = p.replace(/^:/, '');
|
|
||||||
|
|
||||||
// If the path has a pattern for a param, put the param in the path
|
// Remove the used value from the params object since we'll use the rest for query string
|
||||||
if (params && name in params && p.startsWith(':')) {
|
if (currentParams) {
|
||||||
const value = params[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
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete params[name];
|
delete currentParams[name];
|
||||||
return encodeURIComponent(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return encodeURIComponent(p);
|
if (value === undefined && p.endsWith('?')) {
|
||||||
})
|
// Optional params without value assigned in route.params should be ignored
|
||||||
.join('/');
|
return '';
|
||||||
} else {
|
}
|
||||||
path += encodeURIComponent(route.name);
|
|
||||||
|
return encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeURIComponent(p);
|
||||||
|
})
|
||||||
|
.join('/');
|
||||||
|
} else {
|
||||||
|
path += encodeURIComponent(route.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.state) {
|
||||||
|
path += '/';
|
||||||
|
} else if (currentParams) {
|
||||||
|
for (let param in currentParams) {
|
||||||
|
if (currentParams[param] === 'undefined') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete currentParams[param];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.state) {
|
const query = queryString.stringify(currentParams);
|
||||||
path += '/';
|
|
||||||
} else if (params) {
|
|
||||||
const query = queryString.stringify(params);
|
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
path += `?${query}`;
|
path += `?${query}`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current = route.state;
|
current = route.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
path =
|
// Remove multiple as well as trailing slashes
|
||||||
path !== '/' && path.slice(path.length - 1) === '/'
|
path = path.replace(/\/+/g, '/');
|
||||||
? path.slice(0, -1)
|
path = path.length > 1 ? path.replace(/\/$/, '') : path;
|
||||||
: path;
|
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigItem = {
|
||||||
|
pattern?: string;
|
||||||
|
stringify?: StringifyConfig;
|
||||||
|
screens?: Record<string, ConfigItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function joinPaths(...paths: string[]): string {
|
||||||
|
return ([] as string[])
|
||||||
|
.concat(...paths.map((p) => p.split('/')))
|
||||||
|
.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
|
| string
|
||||||
| {
|
| {
|
||||||
path?: string;
|
path?: string;
|
||||||
|
exact?: boolean;
|
||||||
parse?: ParseConfig;
|
parse?: ParseConfig;
|
||||||
screens?: Options;
|
screens?: Options;
|
||||||
initialRouteName?: string;
|
initialRouteName?: string;
|
||||||
@@ -20,10 +21,12 @@ type Options = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RouteConfig = {
|
type RouteConfig = {
|
||||||
match: RegExp;
|
screen: string;
|
||||||
|
regex?: RegExp;
|
||||||
|
path: string;
|
||||||
pattern: string;
|
pattern: string;
|
||||||
routeNames: string[];
|
routeNames: string[];
|
||||||
parse: ParseConfig | undefined;
|
parse?: ParseConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitialRouteConfig = {
|
type InitialRouteConfig = {
|
||||||
@@ -58,32 +61,71 @@ export default function getStateFromPath(
|
|||||||
path: string,
|
path: string,
|
||||||
options: Options = {}
|
options: Options = {}
|
||||||
): ResultState | undefined {
|
): ResultState | undefined {
|
||||||
if (path === '') {
|
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(
|
||||||
|
(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
|
||||||
|
.replace(/^\//, '') // Remove extra leading slash
|
||||||
|
.replace(/\?.*$/, ''); // Remove query params which we will handle later
|
||||||
|
|
||||||
|
// Make sure there is a trailing slash
|
||||||
|
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
||||||
|
|
||||||
|
if (remaining === '/') {
|
||||||
|
// We need to add special handling of empty path so navigation to empty path also works
|
||||||
|
// When handling empty path, we should only look at the root level config
|
||||||
|
const match = configs.find(
|
||||||
|
(config) =>
|
||||||
|
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)?.path
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return createNestedStateObject(
|
||||||
|
match.routeNames.map((name, i, self) => {
|
||||||
|
if (i === self.length - 1) {
|
||||||
|
return { name, params: parseQueryParams(path, match.parse) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name };
|
||||||
|
}),
|
||||||
|
initialRoutes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let result: PartialState<NavigationState> | undefined;
|
let result: PartialState<NavigationState> | undefined;
|
||||||
let current: PartialState<NavigationState> | undefined;
|
let current: PartialState<NavigationState> | undefined;
|
||||||
|
|
||||||
let remaining = path
|
|
||||||
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
|
|
||||||
.replace(/^\//, '') // Remove extra leading slash
|
|
||||||
.replace(/\?.*/, ''); // Remove query params which we will handle later
|
|
||||||
|
|
||||||
while (remaining) {
|
while (remaining) {
|
||||||
let routeNames: string[] | undefined;
|
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
|
// Go through all configs, and see if the next path segment matches our regex
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
const match = remaining.match(config.match);
|
if (!config.regex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = remaining.match(config.regex);
|
||||||
|
|
||||||
// If our regex matches, we need to extract params from the path
|
// If our regex matches, we need to extract params from the path
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -91,24 +133,19 @@ export default function getStateFromPath(
|
|||||||
|
|
||||||
const paramPatterns = config.pattern
|
const paramPatterns = config.pattern
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter(p => p.startsWith(':'));
|
.filter((p) => p.startsWith(':'));
|
||||||
|
|
||||||
if (paramPatterns.length) {
|
if (paramPatterns.length) {
|
||||||
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||||
const key = p.replace(/^:/, '');
|
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
|
||||||
const value = match[i + 1]; // The param segments start from index 1 in the regex match result
|
|
||||||
|
|
||||||
acc[key] =
|
acc[p] = value;
|
||||||
config.parse && config.parse[key]
|
|
||||||
? config.parse[key](value)
|
|
||||||
: value;
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the matched segment from the remaining path
|
remaining = remaining.replace(match[1], '');
|
||||||
remaining = remaining.replace(match[0], '');
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -123,34 +160,46 @@ export default function getStateFromPath(
|
|||||||
remaining = segments.join('/');
|
remaining = segments.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
let state: InitialState;
|
const state = createNestedStateObject(
|
||||||
let routeName = routeNames.shift() as string;
|
routeNames.map((name) => {
|
||||||
let initialRoute = findInitialRoute(routeName, initialRoutes);
|
const config = configs.find((c) => c.screen === name);
|
||||||
|
|
||||||
state = createNestedState(
|
let params: object | undefined;
|
||||||
initialRoute,
|
|
||||||
routeName,
|
|
||||||
routeNames.length === 0,
|
|
||||||
params
|
|
||||||
);
|
|
||||||
|
|
||||||
if (routeNames.length > 0) {
|
if (allParams && config?.path) {
|
||||||
let nestedState = state;
|
const pattern = config.path;
|
||||||
|
|
||||||
while ((routeName = routeNames.shift() as string)) {
|
if (pattern) {
|
||||||
initialRoute = findInitialRoute(routeName, initialRoutes);
|
const paramPatterns = pattern
|
||||||
nestedState.routes[nestedState.index || 0].state = createNestedState(
|
.split('/')
|
||||||
initialRoute,
|
.filter((p) => p.startsWith(':'));
|
||||||
routeName,
|
|
||||||
routeNames.length === 0,
|
if (paramPatterns.length) {
|
||||||
params
|
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
|
||||||
);
|
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
||||||
if (routeNames.length > 0) {
|
const value = allParams![p];
|
||||||
nestedState = nestedState.routes[nestedState.index || 0]
|
|
||||||
.state as InitialState;
|
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) {
|
if (current) {
|
||||||
// The state should be nested inside the deepest route we parsed before
|
// The state should be nested inside the deepest route we parsed before
|
||||||
@@ -172,74 +221,79 @@ export default function getStateFromPath(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = path.split('?')[1];
|
const route = findFocusedRoute(current);
|
||||||
|
const params = parseQueryParams(
|
||||||
if (query) {
|
path,
|
||||||
while (current?.routes[current.index || 0].state) {
|
findParseConfigForRoute(route.name, configs)
|
||||||
// The query params apply to the deepest route
|
);
|
||||||
current = current.routes[current.index || 0].state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = (current as PartialState<NavigationState>).routes[
|
|
||||||
current?.index || 0
|
|
||||||
];
|
|
||||||
|
|
||||||
const params = queryString.parse(query);
|
|
||||||
const parseFunction = findParseConfigForRoute(route.name, configs);
|
|
||||||
|
|
||||||
if (parseFunction) {
|
|
||||||
Object.keys(params).forEach(name => {
|
|
||||||
if (parseFunction[name] && typeof params[name] === 'string') {
|
|
||||||
params[name] = parseFunction[name](params[name] as string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (params) {
|
||||||
route.params = { ...route.params, ...params };
|
route.params = { ...route.params, ...params };
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function joinPaths(...paths: string[]): string {
|
||||||
|
return ([] as string[])
|
||||||
|
.concat(...paths.map((p) => p.split('/')))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
function createNormalizedConfigs(
|
function createNormalizedConfigs(
|
||||||
key: string,
|
screen: string,
|
||||||
routeConfig: Options,
|
routeConfig: Options,
|
||||||
routeNames: string[] = [],
|
routeNames: string[] = [],
|
||||||
initials: InitialRouteConfig[]
|
initials: InitialRouteConfig[],
|
||||||
|
parentPattern?: string
|
||||||
): RouteConfig[] {
|
): RouteConfig[] {
|
||||||
const configs: 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
|
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
|
||||||
if (value !== '') {
|
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
|
||||||
configs.push(createConfigItem(routeNames, value));
|
|
||||||
}
|
configs.push(createConfigItem(screen, routeNames, pattern, config));
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof config === 'object') {
|
||||||
|
let pattern: string | undefined;
|
||||||
|
|
||||||
// if an object is specified as the value (e.g. Foo: { ... }),
|
// if an object is specified as the value (e.g. Foo: { ... }),
|
||||||
// it can have `path` property and
|
// it can have `path` property and
|
||||||
// it could have `screens` prop which has nested configs
|
// it could have `screens` prop which has nested configs
|
||||||
if (value.path && value.path !== '') {
|
if (typeof config.path === 'string') {
|
||||||
configs.push(createConfigItem(routeNames, value.path, value.parse));
|
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
|
// property `initialRouteName` without `screens` has no purpose
|
||||||
if (value.initialRouteName) {
|
if (config.initialRouteName) {
|
||||||
initials.push({
|
initials.push({
|
||||||
initialRouteName: value.initialRouteName,
|
initialRouteName: config.initialRouteName,
|
||||||
connectedRoutes: Object.keys(value.screens),
|
connectedRoutes: Object.keys(config.screens),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Object.keys(value.screens).forEach(nestedConfig => {
|
|
||||||
|
Object.keys(config.screens).forEach((nestedConfig) => {
|
||||||
const result = createNormalizedConfigs(
|
const result = createNormalizedConfigs(
|
||||||
nestedConfig,
|
nestedConfig,
|
||||||
value.screens as Options,
|
config.screens as Options,
|
||||||
routeNames,
|
routeNames,
|
||||||
initials
|
initials,
|
||||||
|
pattern
|
||||||
);
|
);
|
||||||
|
|
||||||
configs.push(...result);
|
configs.push(...result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -251,17 +305,35 @@ function createNormalizedConfigs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createConfigItem(
|
function createConfigItem(
|
||||||
|
screen: string,
|
||||||
routeNames: string[],
|
routeNames: string[],
|
||||||
pattern: string,
|
pattern: string,
|
||||||
|
path: string,
|
||||||
parse?: ParseConfig
|
parse?: ParseConfig
|
||||||
): RouteConfig {
|
): RouteConfig {
|
||||||
const match = new RegExp(
|
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
|
||||||
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
|
pattern = pattern.split('/').filter(Boolean).join('/');
|
||||||
);
|
|
||||||
|
const regex = pattern
|
||||||
|
? new RegExp(
|
||||||
|
`^(${pattern
|
||||||
|
.split('/')
|
||||||
|
.map((it) => {
|
||||||
|
if (it.startsWith(':')) {
|
||||||
|
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${escape(it)}\\/`;
|
||||||
|
})
|
||||||
|
.join('')})`
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
match,
|
screen,
|
||||||
|
regex,
|
||||||
pattern,
|
pattern,
|
||||||
|
path,
|
||||||
// The routeNames array is mutated, so copy it to keep the current state
|
// The routeNames array is mutated, so copy it to keep the current state
|
||||||
routeNames: [...routeNames],
|
routeNames: [...routeNames],
|
||||||
parse,
|
parse,
|
||||||
@@ -295,26 +367,23 @@ function findInitialRoute(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns nested state object with values depending on whether
|
// returns state object with values depending on whether
|
||||||
// it is the end of state and if there is initialRoute for this level
|
// it is the end of state and if there is initialRoute for this level
|
||||||
function createNestedState(
|
function createStateObject(
|
||||||
initialRoute: string | undefined,
|
initialRoute: string | undefined,
|
||||||
routeName: string,
|
routeName: string,
|
||||||
isEmpty: boolean,
|
params: Record<string, any> | undefined,
|
||||||
params?: Record<string, any> | undefined
|
isEmpty: boolean
|
||||||
): InitialState {
|
): InitialState {
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
if (initialRoute) {
|
if (initialRoute) {
|
||||||
return {
|
return {
|
||||||
index: 1,
|
index: 1,
|
||||||
routes: [
|
routes: [{ name: initialRoute }, { name: routeName as string, params }],
|
||||||
{ name: initialRoute },
|
|
||||||
{ name: routeName as string, ...(params && { params }) },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
routes: [{ name: routeName as string, ...(params && { params }) }],
|
routes: [{ name: routeName as string, params }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -323,11 +392,87 @@ function createNestedState(
|
|||||||
index: 1,
|
index: 1,
|
||||||
routes: [
|
routes: [
|
||||||
{ name: initialRoute },
|
{ name: initialRoute },
|
||||||
{ name: routeName as string, state: { routes: [] } },
|
{ name: routeName as string, params, state: { routes: [] } },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { routes: [{ name: routeName as string, state: { routes: [] } }] };
|
return {
|
||||||
|
routes: [{ name: routeName as string, params, state: { routes: [] } }],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createNestedStateObject(
|
||||||
|
routes: { name: string; params?: object }[],
|
||||||
|
initialRoutes: InitialRouteConfig[]
|
||||||
|
) {
|
||||||
|
let state: InitialState;
|
||||||
|
let route = routes.shift() as { name: string; params?: object };
|
||||||
|
let initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||||
|
|
||||||
|
state = createStateObject(
|
||||||
|
initialRoute,
|
||||||
|
route.name,
|
||||||
|
route.params,
|
||||||
|
routes.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (routes.length > 0) {
|
||||||
|
let nestedState = state;
|
||||||
|
|
||||||
|
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,
|
||||||
|
route.name,
|
||||||
|
route.params,
|
||||||
|
routes.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (routes.length > 0) {
|
||||||
|
nestedState = nestedState.routes[nestedStateIndex]
|
||||||
|
.state as InitialState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFocusedRoute(state: InitialState) {
|
||||||
|
let current: InitialState | undefined = state;
|
||||||
|
|
||||||
|
while (current?.routes[current.index || 0].state) {
|
||||||
|
// The query params apply to the deepest route
|
||||||
|
current = current.routes[current.index || 0].state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = (current as PartialState<NavigationState>).routes[
|
||||||
|
current?.index || 0
|
||||||
|
];
|
||||||
|
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseQueryParams(
|
||||||
|
path: string,
|
||||||
|
parseConfig?: Record<string, (value: string) => any>
|
||||||
|
) {
|
||||||
|
const query = path.split('?')[1];
|
||||||
|
const params = queryString.parse(query);
|
||||||
|
|
||||||
|
if (parseConfig) {
|
||||||
|
Object.keys(params).forEach((name) => {
|
||||||
|
if (parseConfig[name] && typeof params[name] === 'string') {
|
||||||
|
params[name] = parseConfig[name](params[name] as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(params).length ? params : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export * from '@react-navigation/routers';
|
|||||||
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
|
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
|
||||||
export { default as createNavigatorFactory } from './createNavigatorFactory';
|
export { default as createNavigatorFactory } from './createNavigatorFactory';
|
||||||
|
|
||||||
|
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
|
||||||
export { default as NavigationContext } from './NavigationContext';
|
export { default as NavigationContext } from './NavigationContext';
|
||||||
export { default as NavigationRouteContext } from './NavigationRouteContext';
|
export { default as NavigationRouteContext } from './NavigationRouteContext';
|
||||||
|
|
||||||
|
|||||||
@@ -168,18 +168,6 @@ type NavigationHelpersCommon<
|
|||||||
| { name: RouteName; key?: string; params: ParamList[RouteName] }
|
| { name: RouteName; key?: string; params: ParamList[RouteName] }
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the current route with a new one.
|
|
||||||
*
|
|
||||||
* @param name Route name of the new route.
|
|
||||||
* @param [params] Params object for the new route.
|
|
||||||
*/
|
|
||||||
replace<RouteName extends keyof ParamList>(
|
|
||||||
...args: ParamList[RouteName] extends undefined
|
|
||||||
? [RouteName] | [RouteName, ParamList[RouteName]]
|
|
||||||
: [RouteName, ParamList[RouteName]]
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the navigation state to the provided state.
|
* Reset the navigation state to the provided state.
|
||||||
*
|
*
|
||||||
@@ -205,6 +193,20 @@ type NavigationHelpersCommon<
|
|||||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||||
*/
|
*/
|
||||||
canGoBack(): boolean;
|
canGoBack(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parent navigator, if any. Reason why the function is called
|
||||||
|
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
|
||||||
|
* of parent and other hard-to-follow patterns.
|
||||||
|
*/
|
||||||
|
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the navigator's state. Reason why the function is called
|
||||||
|
* dangerouslyGetState is to discourage developers to use internal navigation's state.
|
||||||
|
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||||
|
*/
|
||||||
|
dangerouslyGetState(): State;
|
||||||
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
|
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
|
||||||
|
|
||||||
export type NavigationHelpers<
|
export type NavigationHelpers<
|
||||||
@@ -266,20 +268,6 @@ export type NavigationProp<
|
|||||||
* @param options Options object for the route.
|
* @param options Options object for the route.
|
||||||
*/
|
*/
|
||||||
setOptions(options: Partial<ScreenOptions>): void;
|
setOptions(options: Partial<ScreenOptions>): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the parent navigator, if any. Reason why the function is called
|
|
||||||
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
|
|
||||||
* of parent and other hard-to-follow patterns.
|
|
||||||
*/
|
|
||||||
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the navigator's state. Reason why the function is called
|
|
||||||
* dangerouslyGetState is to discourage developers to use internal navigation's state.
|
|
||||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
|
||||||
*/
|
|
||||||
dangerouslyGetState(): State;
|
|
||||||
} & EventConsumer<EventMap & EventMapCore<State>> &
|
} & EventConsumer<EventMap & EventMapCore<State>> &
|
||||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||||
|
|
||||||
@@ -358,6 +346,16 @@ export type Descriptor<
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScreenListeners<
|
||||||
|
State extends NavigationState,
|
||||||
|
EventMap extends EventMapBase
|
||||||
|
> = Partial<
|
||||||
|
{
|
||||||
|
[EventName in keyof (EventMap &
|
||||||
|
EventMapCore<State>)]: EventListenerCallback<EventMap, EventName>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export type RouteConfig<
|
export type RouteConfig<
|
||||||
ParamList extends ParamListBase,
|
ParamList extends ParamListBase,
|
||||||
RouteName extends keyof ParamList,
|
RouteName extends keyof ParamList,
|
||||||
@@ -383,12 +381,12 @@ export type RouteConfig<
|
|||||||
/**
|
/**
|
||||||
* Event listeners for this screen.
|
* Event listeners for this screen.
|
||||||
*/
|
*/
|
||||||
listeners?: Partial<
|
listeners?:
|
||||||
{
|
| ScreenListeners<State, EventMap>
|
||||||
[EventName in keyof (EventMap &
|
| ((props: {
|
||||||
EventMapCore<State>)]: EventListenerCallback<EventMap, EventName>;
|
route: RouteProp<ParamList, RouteName>;
|
||||||
}
|
navigation: any;
|
||||||
>;
|
}) => ScreenListeners<State, EventMap>);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial params object for the route.
|
* Initial params object for the route.
|
||||||
@@ -412,24 +410,19 @@ export type RouteConfig<
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type NavigationContainerRef =
|
export type NavigationContainerRef = NavigationHelpers<ParamListBase> &
|
||||||
| (NavigationHelpers<ParamListBase> &
|
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
|
||||||
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
|
/**
|
||||||
/**
|
* Reset the navigation state of the root navigator to the provided state.
|
||||||
* Reset the navigation state of the root navigator to the provided state.
|
*
|
||||||
*
|
* @param state Navigation state object.
|
||||||
* @param state Navigation state object.
|
*/
|
||||||
*/
|
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
|
||||||
resetRoot(
|
/**
|
||||||
state?: PartialState<NavigationState> | NavigationState
|
* Get the rehydrated navigation state of the navigation tree.
|
||||||
): void;
|
*/
|
||||||
/**
|
getRootState(): NavigationState;
|
||||||
* Get the rehydrated navigation state of the navigation tree.
|
};
|
||||||
*/
|
|
||||||
getRootState(): NavigationState;
|
|
||||||
})
|
|
||||||
| undefined
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export type TypedNavigator<
|
export type TypedNavigator<
|
||||||
ParamList extends ParamListBase,
|
ParamList extends ParamListBase,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
enabled: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
reset: (state: NavigationState) => void;
|
reset: (state: NavigationState) => void;
|
||||||
state: State;
|
state: State;
|
||||||
@@ -35,10 +36,11 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useDevTools({ name, reset, state }: Options) {
|
export default function useDevTools({ name, reset, state, enabled }: Options) {
|
||||||
const devToolsRef = React.useRef<DevTools>();
|
const devToolsRef = React.useRef<DevTools>();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
enabled &&
|
||||||
process.env.NODE_ENV !== 'production' &&
|
process.env.NODE_ENV !== 'production' &&
|
||||||
global.__REDUX_DEVTOOLS_EXTENSION__ &&
|
global.__REDUX_DEVTOOLS_EXTENSION__ &&
|
||||||
devToolsRef.current === undefined
|
devToolsRef.current === undefined
|
||||||
@@ -56,7 +58,7 @@ export default function useDevTools({ name, reset, state }: Options) {
|
|||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
() =>
|
() =>
|
||||||
devTools?.subscribe(message => {
|
devTools?.subscribe((message) => {
|
||||||
if (message.type === 'DISPATCH' && message.state) {
|
if (message.type === 'DISPATCH' && message.state) {
|
||||||
reset(JSON.parse(message.state));
|
reset(JSON.parse(message.state));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function useEventEmitter(
|
|||||||
target !== undefined
|
target !== undefined
|
||||||
? items[target] && items[target].slice()
|
? items[target] && items[target].slice()
|
||||||
: ([] as Listeners)
|
: ([] as Listeners)
|
||||||
.concat(...Object.keys(items).map(t => items[t]))
|
.concat(...Object.keys(items).map((t) => items[t]))
|
||||||
.filter((cb, i, self) => self.lastIndexOf(cb) === i);
|
.filter((cb, i, self) => self.lastIndexOf(cb) === i);
|
||||||
|
|
||||||
const event: EventArg<any, any, any> = {
|
const event: EventArg<any, any, any> = {
|
||||||
@@ -117,7 +117,7 @@ export default function useEventEmitter(
|
|||||||
|
|
||||||
listenRef.current?.(event);
|
listenRef.current?.(event);
|
||||||
|
|
||||||
callbacks?.forEach(cb => cb(event));
|
callbacks?.forEach((cb) => cb(event));
|
||||||
|
|
||||||
return event as any;
|
return event as any;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
|
|||||||
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should only dispatch events when the focused key changed and navigator is focused
|
// We should only emit events when the focused key changed and navigator is focused
|
||||||
// When navigator is not focused, screens inside shouldn't receive focused status either
|
// When navigator is not focused, screens inside shouldn't receive focused status either
|
||||||
if (
|
if (
|
||||||
lastFocusedKey === currentFocusedKey ||
|
lastFocusedKey === currentFocusedKey ||
|
||||||
@@ -62,7 +62,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
|
||||||
emitter.emit({ type: 'blur', target: lastFocusedKey });
|
emitter.emit({ type: 'blur', target: lastFocusedKey });
|
||||||
|
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
||||||
}, [currentFocusedKey, emitter, navigation]);
|
}, [currentFocusedKey, emitter, navigation]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import useNavigation from './useNavigation';
|
|||||||
*/
|
*/
|
||||||
export default function useIsFocused(): boolean {
|
export default function useIsFocused(): boolean {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
|
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
|
||||||
const subscribe = React.useCallback(
|
const subscribe = React.useCallback(
|
||||||
(callback: (value: boolean) => void) => {
|
(callback: (value: boolean) => void) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
RouterFactory,
|
RouterFactory,
|
||||||
PartialState,
|
PartialState,
|
||||||
NavigationAction,
|
NavigationAction,
|
||||||
|
Route,
|
||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import { NavigationStateContext } from './BaseNavigationContainer';
|
import { NavigationStateContext } from './BaseNavigationContainer';
|
||||||
import NavigationRouteContext from './NavigationRouteContext';
|
import NavigationRouteContext from './NavigationRouteContext';
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import useStateGetters from './useStateGetters';
|
import useStateGetters from './useStateGetters';
|
||||||
import useOnGetState from './useOnGetState';
|
import useOnGetState from './useOnGetState';
|
||||||
|
import useScheduleUpdate from './useScheduleUpdate';
|
||||||
|
|
||||||
// This is to make TypeScript compiler happy
|
// This is to make TypeScript compiler happy
|
||||||
// eslint-disable-next-line babel/no-unused-expressions
|
// eslint-disable-next-line babel/no-unused-expressions
|
||||||
@@ -41,6 +43,7 @@ type NavigatorRoute = {
|
|||||||
params?: {
|
params?: {
|
||||||
screen?: string;
|
screen?: string;
|
||||||
params?: object;
|
params?: object;
|
||||||
|
initial?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ const getRouteConfigsFromChildren = <
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
configs.forEach(config => {
|
configs.forEach((config) => {
|
||||||
const { name, children, component } = config as any;
|
const { name, children, component } = config as any;
|
||||||
|
|
||||||
if (typeof name !== 'string' || !name) {
|
if (typeof name !== 'string' || !name) {
|
||||||
@@ -174,17 +177,19 @@ export default function useNavigationBuilder<
|
|||||||
| NavigatorRoute
|
| NavigatorRoute
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
const previousRouteRef = React.useRef(route);
|
const previousNestedParamsRef = React.useRef(route?.params);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
previousRouteRef.current = route;
|
previousNestedParamsRef.current = route?.params;
|
||||||
}, [route]);
|
}, [route]);
|
||||||
|
|
||||||
const { children, ...rest } = options;
|
const { children, ...rest } = options;
|
||||||
const { current: router } = React.useRef<Router<State, any>>(
|
const { current: router } = React.useRef<Router<State, any>>(
|
||||||
createRouter({
|
createRouter({
|
||||||
...((rest as unknown) as RouterOptions),
|
...((rest as unknown) as RouterOptions),
|
||||||
...(route?.params && typeof route.params.screen === 'string'
|
...(route?.params &&
|
||||||
|
route.params.initial !== false &&
|
||||||
|
typeof route.params.screen === 'string'
|
||||||
? { initialRouteName: route.params.screen }
|
? { initialRouteName: route.params.screen }
|
||||||
: null),
|
: null),
|
||||||
})
|
})
|
||||||
@@ -212,12 +217,12 @@ export default function useNavigationBuilder<
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const routeNames = routeConfigs.map(config => config.name);
|
const routeNames = routeConfigs.map((config) => config.name);
|
||||||
const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
|
const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const { initialParams } = screens[curr];
|
const { initialParams } = screens[curr];
|
||||||
const initialParamsFromParams =
|
const initialParamsFromParams =
|
||||||
route?.params && route.params.screen === curr
|
route?.params?.initial !== false && route?.params?.screen === curr
|
||||||
? route.params.params
|
? route.params.params
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -241,12 +246,12 @@ export default function useNavigationBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isStateValid = React.useCallback(
|
const isStateValid = React.useCallback(
|
||||||
state => state.type === undefined || state.type === router.type,
|
(state) => state.type === undefined || state.type === router.type,
|
||||||
[router.type]
|
[router.type]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isStateInitialized = React.useCallback(
|
const isStateInitialized = React.useCallback(
|
||||||
state =>
|
(state) =>
|
||||||
state !== undefined && state.stale === false && isStateValid(state),
|
state !== undefined && state.stale === false && isStateValid(state),
|
||||||
[isStateValid]
|
[isStateValid]
|
||||||
);
|
);
|
||||||
@@ -264,6 +269,8 @@ export default function useNavigationBuilder<
|
|||||||
>();
|
>();
|
||||||
const initializedStateRef = React.useRef<State>();
|
const initializedStateRef = React.useRef<State>();
|
||||||
|
|
||||||
|
let isFirstStateInitialization = false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
initializedStateRef.current === undefined ||
|
initializedStateRef.current === undefined ||
|
||||||
currentState !== previousStateRef.current
|
currentState !== previousStateRef.current
|
||||||
@@ -272,16 +279,21 @@ export default function useNavigationBuilder<
|
|||||||
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
|
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
|
||||||
// Otherwise assume that the state was provided as initial state
|
// Otherwise assume that the state was provided as initial state
|
||||||
// So we need to rehydrate it to make it usable
|
// So we need to rehydrate it to make it usable
|
||||||
initializedStateRef.current =
|
if (currentState === undefined || !isStateValid(currentState)) {
|
||||||
currentState === undefined || !isStateValid(currentState)
|
isFirstStateInitialization = true;
|
||||||
? router.getInitialState({
|
initializedStateRef.current = router.getInitialState({
|
||||||
routeNames,
|
routeNames,
|
||||||
routeParamList,
|
routeParamList,
|
||||||
})
|
});
|
||||||
: router.getRehydratedState(currentState as PartialState<State>, {
|
} else {
|
||||||
routeNames,
|
initializedStateRef.current = router.getRehydratedState(
|
||||||
routeParamList,
|
currentState as PartialState<State>,
|
||||||
});
|
{
|
||||||
|
routeNames,
|
||||||
|
routeParamList,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -307,16 +319,14 @@ export default function useNavigationBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
previousRouteRef.current &&
|
typeof route?.params?.screen === 'string' &&
|
||||||
route &&
|
(route.params !== previousNestedParamsRef.current ||
|
||||||
route.params &&
|
(route.params.initial === false && isFirstStateInitialization))
|
||||||
typeof route.params.screen === 'string' &&
|
|
||||||
route.params !== previousRouteRef.current.params
|
|
||||||
) {
|
) {
|
||||||
// If the route was updated with new name and/or params, we should navigate there
|
// If the route was updated with new name and/or params, we should navigate there
|
||||||
// The update should be limited to current navigator only, so we call the router manually
|
// The update should be limited to current navigator only, so we call the router manually
|
||||||
const updatedState = router.getStateForAction(
|
const updatedState = router.getStateForAction(
|
||||||
state,
|
nextState,
|
||||||
CommonActions.navigate(route.params.screen, route.params.params),
|
CommonActions.navigate(route.params.screen, route.params.params),
|
||||||
{
|
{
|
||||||
routeNames,
|
routeNames,
|
||||||
@@ -330,17 +340,17 @@ export default function useNavigationBuilder<
|
|||||||
routeNames,
|
routeNames,
|
||||||
routeParamList,
|
routeParamList,
|
||||||
})
|
})
|
||||||
: state;
|
: nextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldUpdate = state !== nextState;
|
const shouldUpdate = state !== nextState;
|
||||||
|
|
||||||
React.useEffect(() => {
|
useScheduleUpdate(() => {
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
// If the state needs to be updated, we'll schedule an update with React
|
// If the state needs to be updated, we'll schedule an update
|
||||||
setState(nextState);
|
setState(nextState);
|
||||||
}
|
}
|
||||||
}, [nextState, setState, shouldUpdate]);
|
});
|
||||||
|
|
||||||
// The up-to-date state will come in next render, but we don't need to wait for it
|
// The up-to-date state will come in next render, but we don't need to wait for it
|
||||||
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
|
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
|
||||||
@@ -352,9 +362,14 @@ export default function useNavigationBuilder<
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// We need to clean up state for this navigator on unmount
|
// We need to clean up state for this navigator on unmount
|
||||||
if (getCurrentState() !== undefined && getKey() === navigatorKey) {
|
// We do it in a timeout because we need to detect if another navigator mounted in the meantime
|
||||||
setState(undefined);
|
// For example, if another navigator has started rendering, we should skip cleanup
|
||||||
}
|
// Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it
|
||||||
|
setTimeout(() => {
|
||||||
|
if (getCurrentState() !== undefined && getKey() === navigatorKey) {
|
||||||
|
setState(undefined);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -367,34 +382,49 @@ export default function useNavigationBuilder<
|
|||||||
: (initializedStateRef.current as State);
|
: (initializedStateRef.current as State);
|
||||||
}, [getCurrentState, isStateInitialized]);
|
}, [getCurrentState, isStateInitialized]);
|
||||||
|
|
||||||
const emitter = useEventEmitter(e => {
|
const emitter = useEventEmitter((e) => {
|
||||||
let routeNames = [];
|
let routeNames = [];
|
||||||
|
|
||||||
if (e.target) {
|
let route: Route<string> | undefined;
|
||||||
const name = state.routes.find(route => route.key === e.target)?.name;
|
|
||||||
|
|
||||||
if (name) {
|
if (e.target) {
|
||||||
routeNames.push(name);
|
route = state.routes.find((route) => route.key === e.target);
|
||||||
|
|
||||||
|
if (route?.name) {
|
||||||
|
routeNames.push(route.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
routeNames.push(...Object.keys(screens));
|
route = state.routes[state.index];
|
||||||
|
routeNames.push(
|
||||||
|
...Object.keys(screens).filter((name) => route?.name === name)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigation = descriptors[route.key].navigation;
|
||||||
|
|
||||||
const listeners = ([] as (((e: any) => void) | undefined)[])
|
const listeners = ([] as (((e: any) => void) | undefined)[])
|
||||||
.concat(
|
.concat(
|
||||||
...routeNames.map(name => {
|
...routeNames.map((name) => {
|
||||||
const { listeners } = screens[name];
|
const { listeners } = screens[name];
|
||||||
|
const map =
|
||||||
|
typeof listeners === 'function'
|
||||||
|
? listeners({ route: route as any, navigation })
|
||||||
|
: listeners;
|
||||||
|
|
||||||
return listeners
|
return map
|
||||||
? Object.keys(listeners)
|
? Object.keys(map)
|
||||||
.filter(type => type === e.type)
|
.filter((type) => type === e.type)
|
||||||
.map(type => listeners[type])
|
.map((type) => map?.[type])
|
||||||
: undefined;
|
: undefined;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter((cb, i, self) => cb && self.lastIndexOf(cb) === i);
|
.filter((cb, i, self) => cb && self.lastIndexOf(cb) === i);
|
||||||
|
|
||||||
listeners.forEach(listener => listener?.(e));
|
listeners.forEach((listener) => listener?.(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
useFocusEvents({ state, emitter });
|
useFocusEvents({ state, emitter });
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import { NavigationEventEmitter } from './useEventEmitter';
|
import { NavigationEventEmitter } from './useEventEmitter';
|
||||||
import NavigationContext from './NavigationContext';
|
|
||||||
|
|
||||||
import { NavigationHelpers, NavigationProp } from './types';
|
import { NavigationHelpers, NavigationProp } from './types';
|
||||||
|
|
||||||
@@ -49,12 +48,10 @@ export default function useNavigationCache<
|
|||||||
// Cache object which holds navigation objects for each screen
|
// Cache object which holds navigation objects for each screen
|
||||||
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
|
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
|
||||||
// In reality, these deps will rarely change, if ever
|
// In reality, these deps will rarely change, if ever
|
||||||
const parentNavigation = React.useContext(NavigationContext);
|
|
||||||
|
|
||||||
const cache = React.useMemo(
|
const cache = React.useMemo(
|
||||||
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
|
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[getState, navigation, setOptions, router, emitter, parentNavigation]
|
[getState, navigation, setOptions, router, emitter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -63,7 +60,7 @@ export default function useNavigationCache<
|
|||||||
};
|
};
|
||||||
|
|
||||||
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(
|
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(
|
||||||
(acc, route, index) => {
|
(acc, route) => {
|
||||||
const previous = cache.current[route.key];
|
const previous = cache.current[route.key];
|
||||||
|
|
||||||
if (previous) {
|
if (previous) {
|
||||||
@@ -99,18 +96,16 @@ export default function useNavigationCache<
|
|||||||
...rest,
|
...rest,
|
||||||
...helpers,
|
...helpers,
|
||||||
...emitter.create(route.key),
|
...emitter.create(route.key),
|
||||||
dangerouslyGetParent: () => parentNavigation as any,
|
|
||||||
dangerouslyGetState: getState,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
setOptions: (options: object) =>
|
setOptions: (options: object) =>
|
||||||
setOptions(o => ({
|
setOptions((o) => ({
|
||||||
...o,
|
...o,
|
||||||
[route.key]: { ...o[route.key], ...options },
|
[route.key]: { ...o[route.key], ...options },
|
||||||
})),
|
})),
|
||||||
isFocused: () => {
|
isFocused: () => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
if (index !== state.index) {
|
if (state.routes[state.index].key !== route.key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ export default function useNavigationHelpers<
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
dangerouslyGetParent: () => parentNavigationHelpers as any,
|
||||||
|
dangerouslyGetState: getState,
|
||||||
} as NavigationHelpers<ParamListBase, EventMap> &
|
} as NavigationHelpers<ParamListBase, EventMap> &
|
||||||
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
|
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
|
||||||
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);
|
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function useNavigationState<T>(selector: Selector<T>): T {
|
|||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const unsubscribe = navigation.addListener('state', e => {
|
const unsubscribe = navigation.addListener('state', (e) => {
|
||||||
setResult(selectorRef.current(e.data.state));
|
setResult(selectorRef.current(e.data.state));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function useOnGetState({
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
routes: state.routes.map(route => ({
|
routes: state.routes.map((route) => ({
|
||||||
...route,
|
...route,
|
||||||
state: getStateForRoute(route.key),
|
state: getStateForRoute(route.key),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import shortid from 'shortid';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import { SingleNavigatorContext } from './EnsureSingleNavigator';
|
import { SingleNavigatorContext } from './EnsureSingleNavigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,7 +7,7 @@ import { SingleNavigatorContext } from './EnsureSingleNavigator';
|
|||||||
* This is used to prevent multiple navigators under a single container or screen.
|
* This is used to prevent multiple navigators under a single container or screen.
|
||||||
*/
|
*/
|
||||||
export default function useRegisterNavigator() {
|
export default function useRegisterNavigator() {
|
||||||
const [key] = React.useState(() => shortid());
|
const [key] = React.useState(() => nanoid());
|
||||||
const container = React.useContext(SingleNavigatorContext);
|
const container = React.useContext(SingleNavigatorContext);
|
||||||
|
|
||||||
if (container === undefined) {
|
if (container === undefined) {
|
||||||
|
|||||||
32
packages/core/src/useScheduleUpdate.tsx
Normal file
32
packages/core/src/useScheduleUpdate.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const MISSING_CONTEXT_ERROR = "Couldn't find a schedule context.";
|
||||||
|
|
||||||
|
export const ScheduleUpdateContext = React.createContext<{
|
||||||
|
scheduleUpdate: (callback: () => void) => void;
|
||||||
|
flushUpdates: () => void;
|
||||||
|
}>({
|
||||||
|
scheduleUpdate() {
|
||||||
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
|
},
|
||||||
|
flushUpdates() {
|
||||||
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When screen config changes, we want to update the navigator in the same update phase.
|
||||||
|
* However, navigation state is in the root component and React won't let us update it from a child.
|
||||||
|
* This is a workaround for that, the scheduled update is stored in the ref without actually calling setState.
|
||||||
|
* It lets all subsequent updates access the latest state so it stays correct.
|
||||||
|
* Then we call setState during after the component updates.
|
||||||
|
*/
|
||||||
|
export default function useScheduleUpdate(callback: () => void) {
|
||||||
|
const { scheduleUpdate, flushUpdates } = React.useContext(
|
||||||
|
ScheduleUpdateContext
|
||||||
|
);
|
||||||
|
|
||||||
|
scheduleUpdate(callback);
|
||||||
|
|
||||||
|
React.useEffect(flushUpdates);
|
||||||
|
}
|
||||||
@@ -2,8 +2,21 @@ import * as React from 'react';
|
|||||||
|
|
||||||
const UNINTIALIZED_STATE = {};
|
const UNINTIALIZED_STATE = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is definitely not compatible with concurrent mode, but we don't have a solution for sync state yet.
|
||||||
|
*/
|
||||||
export default function useSyncState<T>(initialState?: (() => T) | T) {
|
export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||||
const stateRef = React.useRef<T>(UNINTIALIZED_STATE as any);
|
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) {
|
if (stateRef.current === UNINTIALIZED_STATE) {
|
||||||
stateRef.current =
|
stateRef.current =
|
||||||
@@ -11,18 +24,49 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
|
|||||||
typeof initialState === 'function' ? initialState() : initialState;
|
typeof initialState === 'function' ? initialState() : initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [state, setTrackingState] = React.useState(stateRef.current);
|
const [trackingState, setTrackingState] = React.useState(stateRef.current);
|
||||||
|
|
||||||
const getState = React.useCallback(() => stateRef.current, []);
|
const getState = React.useCallback(() => stateRef.current, []);
|
||||||
|
|
||||||
const setState = React.useCallback((state: T) => {
|
const setState = React.useCallback((state: T) => {
|
||||||
if (state === stateRef.current) {
|
if (state === stateRef.current || !isMountedRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stateRef.current = state;
|
stateRef.current = state;
|
||||||
setTrackingState(state);
|
|
||||||
|
if (!isSchedulingRef.current) {
|
||||||
|
setTrackingState(state);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return [state, getState, setState] as const;
|
const scheduleUpdate = React.useCallback((callback: () => void) => {
|
||||||
|
isSchedulingRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} finally {
|
||||||
|
isSchedulingRef.current = false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// If we're rendering and the tracking state is out of date, update it immediately
|
||||||
|
// This will make sure that our updates are applied as early as possible.
|
||||||
|
if (trackingState !== stateRef.current) {
|
||||||
|
setTrackingState(stateRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = stateRef.current;
|
||||||
|
|
||||||
|
return [state, getState, setState, scheduleUpdate, flushUpdates] as const;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,215 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.7.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.3...@react-navigation/drawer@5.7.4) (2020-05-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.7.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.4...@react-navigation/drawer@5.7.0) (2020-05-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7971)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.3...@react-navigation/drawer@5.6.4) (2020-05-05)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.2...@react-navigation/drawer@5.6.3) (2020-05-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.1...@react-navigation/drawer@5.6.2) (2020-05-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.0...@react-navigation/drawer@5.6.1) (2020-04-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.1...@react-navigation/drawer@5.6.0) (2020-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix closing drawer on web with tap on overlay ([70be3f6](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/70be3f6d863c56211e2f90bdf743bd8526338248))
|
||||||
|
* make sure the address bar hides when scrolling on web ([0a19e94](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/0a19e94b23a4d2b5f22d1d9deb0544f586f475ee))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||||
|
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/942d2be2c72720469475ce12ec8df23825994dbf))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.0...@react-navigation/drawer@5.5.1) (2020-04-27)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.1...@react-navigation/drawer@5.5.0) (2020-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix drawer not closing on web ([e2bcf51](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/e2bcf5168c389833eaaeadb4b8794aaea4a66d17)), closes [#6759](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6759)
|
||||||
|
* webkit style error in overlay ([821343f](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/821343fed38577cfdc87a78f13f991d5760bf8f5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add openByDefault option to drawer ([36689e2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/36689e24c21b474692bb7ecd0b901c8afbbe9a20))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.0...@react-navigation/drawer@5.4.1) (2020-04-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't hide content from accessibility with permanent drawer ([cb2f157](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/cb2f157a561a2ce3f073eb4ccb567532c77bd869)), closes [#7976](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7976)
|
||||||
|
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.4...@react-navigation/drawer@5.4.0) (2020-03-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* disable only swipe gesture on safari ([105da6a](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/105da6ab2fe69847b676c4d4117638212cda1f9a))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add swipeEnabled option to disable swipe gesture in drawer ([#7834](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7834)) ([ac7f972](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/ac7f972e922a82cd32d943356941d100b68bd8b0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.3...@react-navigation/drawer@5.3.4) (2020-03-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.2...@react-navigation/drawer@5.3.3) (2020-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.1...@react-navigation/drawer@5.3.2) (2020-03-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* close drawer on pressing Esc on web ([5c4afc5](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/5c4afc5cb40c1206a9d8c40efe3cf947030da48e)), closes [#6745](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6745)
|
||||||
|
* don't use react-native-screens on web ([b1a65fc](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/b1a65fc73e8603ae2c06ef101a74df31e80bb9b2)), closes [#7485](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7485)
|
||||||
|
* fix permanent sidebar position ([#7830](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7830)) ([3ea8eec](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/3ea8eec4324ea82f0ed427f4662e68e1115e60ab))
|
||||||
|
* initialize height and width to zero if undefined ([3df65e2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/3df65e28197db3bb8371059146546d57661c5ba3)), closes [#6789](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6789)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.0...@react-navigation/drawer@5.3.1) (2020-03-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.2.0...@react-navigation/drawer@5.3.0) (2020-03-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add permanent drawer type ([#7818](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7818)) ([6a5d0a0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/6a5d0a035afae60d91aef78401ec8826295746fe))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.1.1...@react-navigation/drawer@5.2.0) (2020-03-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* make useIsDrawerOpen workable inside drawer content ([#7746](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7746)) ([cb46d0b](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/cb46d0bca4e17e847fff46ac94276213ac9697bf))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.1.0...@react-navigation/drawer@5.1.1) (2020-03-03)
|
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.1.0...@react-navigation/drawer@5.1.1) (2020-03-03)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/drawer
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/drawer",
|
"name": "@react-navigation/drawer",
|
||||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||||
"version": "5.1.1",
|
"version": "5.7.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"homepage": "https://reactnavigation.org/docs/drawer-navigator.html",
|
"homepage": "https://reactnavigation.org/docs/drawer-navigator.html",
|
||||||
"main": "lib/commonjs/index.js",
|
"main": "lib/commonjs/index.js",
|
||||||
"react-native": "src/index.tsx",
|
"react-native": "src/index.tsx",
|
||||||
|
"source": "src/index.tsx",
|
||||||
"module": "lib/module/index.js",
|
"module": "lib/module/index.js",
|
||||||
"types": "lib/typescript/src/index.d.ts",
|
"types": "lib/typescript/src/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -39,18 +40,18 @@
|
|||||||
"react-native-iphone-x-helper": "^1.2.1"
|
"react-native-iphone-x-helper": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.9.3",
|
"@react-native-community/bob": "^0.13.1",
|
||||||
"@react-navigation/native": "^5.0.9",
|
"@react-navigation/native": "^5.3.2",
|
||||||
"@types/react": "^16.9.19",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-native": "^0.60.30",
|
"@types/react-native": "^0.62.7",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"react-native": "~0.61.5",
|
"react-native": "~0.61.5",
|
||||||
"react-native-gesture-handler": "^1.5.6",
|
"react-native-gesture-handler": "^1.6.0",
|
||||||
"react-native-reanimated": "^1.7.0",
|
"react-native-reanimated": "^1.8.0",
|
||||||
"react-native-safe-area-context": "^0.7.2",
|
"react-native-safe-area-context": "^0.7.3",
|
||||||
"react-native-screens": "^2.0.0-beta.2",
|
"react-native-screens": "^2.7.0",
|
||||||
"typescript": "^3.7.5"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-navigation/native": "^5.0.5",
|
"@react-navigation/native": "^5.0.5",
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
"react-native-gesture-handler": ">= 1.0.0",
|
"react-native-gesture-handler": ">= 1.0.0",
|
||||||
"react-native-reanimated": ">= 1.0.0",
|
"react-native-reanimated": ">= 1.0.0",
|
||||||
"react-native-safe-area-context": ">= 0.6.0",
|
"react-native-safe-area-context": ">= 0.6.0",
|
||||||
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0"
|
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
|
||||||
},
|
},
|
||||||
"@react-native-community/bob": {
|
"@react-native-community/bob": {
|
||||||
"source": "src",
|
"source": "src",
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ export { default as useIsDrawerOpen } from './utils/useIsDrawerOpen';
|
|||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
*/
|
*/
|
||||||
export {
|
export type {
|
||||||
DrawerNavigationOptions,
|
DrawerNavigationOptions,
|
||||||
DrawerNavigationProp,
|
DrawerNavigationProp,
|
||||||
|
DrawerScreenProps,
|
||||||
DrawerContentOptions,
|
DrawerContentOptions,
|
||||||
DrawerContentComponentProps,
|
DrawerContentComponentProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Props = DefaultNavigatorOptions<DrawerNavigationOptions> &
|
|||||||
|
|
||||||
function DrawerNavigator({
|
function DrawerNavigator({
|
||||||
initialRouteName,
|
initialRouteName,
|
||||||
|
openByDefault,
|
||||||
backBehavior,
|
backBehavior,
|
||||||
children,
|
children,
|
||||||
screenOptions,
|
screenOptions,
|
||||||
@@ -33,6 +34,7 @@ function DrawerNavigator({
|
|||||||
DrawerNavigationEventMap
|
DrawerNavigationEventMap
|
||||||
>(DrawerRouter, {
|
>(DrawerRouter, {
|
||||||
initialRouteName,
|
initialRouteName,
|
||||||
|
openByDefault,
|
||||||
backBehavior,
|
backBehavior,
|
||||||
children,
|
children,
|
||||||
screenOptions,
|
screenOptions,
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
Descriptor,
|
Descriptor,
|
||||||
NavigationHelpers,
|
NavigationHelpers,
|
||||||
DrawerNavigationState,
|
DrawerNavigationState,
|
||||||
|
DrawerActionHelpers,
|
||||||
|
RouteProp,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import { PanGestureHandler } from 'react-native-gesture-handler';
|
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
export type Scene = {
|
export type Scene = {
|
||||||
route: Route<string>;
|
route: Route<string>;
|
||||||
@@ -26,10 +28,12 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
|
|||||||
* - `front`: Traditional drawer which covers the screen with a overlay behind it.
|
* - `front`: Traditional drawer which covers the screen with a overlay behind it.
|
||||||
* - `back`: The drawer is revealed behind the screen on swipe.
|
* - `back`: The drawer is revealed behind the screen on swipe.
|
||||||
* - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
|
* - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
|
||||||
|
* - `permanent`: A permanent drawer is shown as a sidebar.
|
||||||
*/
|
*/
|
||||||
drawerType?: 'front' | 'back' | 'slide';
|
drawerType?: 'front' | 'back' | 'slide' | 'permanent';
|
||||||
/**
|
/**
|
||||||
* How far from the edge of the screen the swipe gesture should activate.
|
* How far from the edge of the screen the swipe gesture should activate.
|
||||||
|
* Not supported on Web.
|
||||||
*/
|
*/
|
||||||
edgeWidth?: number;
|
edgeWidth?: number;
|
||||||
/**
|
/**
|
||||||
@@ -56,8 +60,9 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
|
|||||||
statusBarAnimation?: 'slide' | 'none' | 'fade';
|
statusBarAnimation?: 'slide' | 'none' | 'fade';
|
||||||
/**
|
/**
|
||||||
* Props to pass to the underlying pan gesture handler.
|
* Props to pass to the underlying pan gesture handler.
|
||||||
|
* Not supported on Web.
|
||||||
*/
|
*/
|
||||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
gestureHandlerProps?: PanGestureHandlerProperties;
|
||||||
/**
|
/**
|
||||||
* Whether the screens should render the first time they are accessed. Defaults to `true`.
|
* Whether the screens should render the first time they are accessed. Defaults to `true`.
|
||||||
* Set it to `false` if you want to render all screens on initial render.
|
* Set it to `false` if you want to render all screens on initial render.
|
||||||
@@ -109,9 +114,20 @@ export type DrawerNavigationOptions = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether you can use gestures to open or close the drawer.
|
* Whether you can use gestures to open or close the drawer.
|
||||||
* Defaults to `true`
|
* Setting this to `false` disables swipe gestures as well as tap on overlay to close.
|
||||||
|
* See `swipeEnabled` to disable only the swipe gesture.
|
||||||
|
* Defaults to `true`.
|
||||||
|
* Not supported on Web.
|
||||||
*/
|
*/
|
||||||
gestureEnabled?: boolean;
|
gestureEnabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether you can use swipe gestures to open or close the drawer.
|
||||||
|
* Defaults to `true`.
|
||||||
|
* Not supported on Web.
|
||||||
|
*/
|
||||||
|
swipeEnabled?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this screen should be unmounted when navigating away from it.
|
* Whether this screen should be unmounted when navigating away from it.
|
||||||
* Defaults to `false`.
|
* Defaults to `false`.
|
||||||
@@ -190,21 +206,15 @@ export type DrawerNavigationProp<
|
|||||||
DrawerNavigationState,
|
DrawerNavigationState,
|
||||||
DrawerNavigationOptions,
|
DrawerNavigationOptions,
|
||||||
DrawerNavigationEventMap
|
DrawerNavigationEventMap
|
||||||
> & {
|
> &
|
||||||
/**
|
DrawerActionHelpers<ParamList>;
|
||||||
* Open the drawer sidebar.
|
|
||||||
*/
|
|
||||||
openDrawer(): void;
|
|
||||||
|
|
||||||
/**
|
export type DrawerScreenProps<
|
||||||
* Close the drawer sidebar.
|
ParamList extends ParamListBase,
|
||||||
*/
|
RouteName extends keyof ParamList = string
|
||||||
closeDrawer(): void;
|
> = {
|
||||||
|
navigation: DrawerNavigationProp<ParamList, RouteName>;
|
||||||
/**
|
route: RouteProp<ParamList, RouteName>;
|
||||||
* Open the drawer sidebar if closed, or close if opened.
|
|
||||||
*/
|
|
||||||
toggleDrawer(): void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DrawerDescriptor = Descriptor<
|
export type DrawerDescriptor = Descriptor<
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user