mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-13 09:30:30 +08:00
Compare commits
89 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef094d512b | ||
|
|
9f00d60bdb | ||
|
|
2b58c52f70 | ||
|
|
e5238f6084 | ||
|
|
65b6a3d864 | ||
|
|
211f1f2c0e | ||
|
|
22b7c3f6c1 | ||
|
|
6ebe0824df | ||
|
|
82900cceff | ||
|
|
dc4ffc0171 | ||
|
|
9c30c42c0b | ||
|
|
ea8ea20127 | ||
|
|
2f282f1070 | ||
|
|
5165eb76aa | ||
|
|
7c722d2028 | ||
|
|
7f015130df | ||
|
|
7580efce89 | ||
|
|
1179d56c50 | ||
|
|
a6e498170f | ||
|
|
4af9d10298 | ||
|
|
08e74af545 | ||
|
|
1e05895b24 | ||
|
|
929c3e3a3b | ||
|
|
b5d539a11b | ||
|
|
7da45e1e89 | ||
|
|
47134d7052 | ||
|
|
a369ba3645 | ||
|
|
4294318210 | ||
|
|
8da4c58065 | ||
|
|
7809bc0650 | ||
|
|
8f2b95ca97 | ||
|
|
6866ad2cda | ||
|
|
459fd27050 | ||
|
|
14786594c0 | ||
|
|
b28bfddc17 | ||
|
|
1a6aebefcb | ||
|
|
33b2dbb85c | ||
|
|
4bb0b43f1a | ||
|
|
260da9b103 | ||
|
|
9ac709ea1e | ||
|
|
60fa3b9be9 | ||
|
|
cf6a9e614d | ||
|
|
0ecd112ec9 | ||
|
|
def7c03d7d | ||
|
|
83242a7bef | ||
|
|
f48303f036 | ||
|
|
dc779b8d82 | ||
|
|
d7401b0200 | ||
|
|
372d5921b8 | ||
|
|
f940153d02 | ||
|
|
aef35c5046 | ||
|
|
338ed6ff07 | ||
|
|
47e371609d | ||
|
|
ef42fa2d36 | ||
|
|
cc5d195f9a | ||
|
|
7e10bcd089 | ||
|
|
c8dd70a033 | ||
|
|
277fec481b | ||
|
|
3241190b19 | ||
|
|
ef7370b215 | ||
|
|
1149e718c1 | ||
|
|
d85a4fd8ed | ||
|
|
b89396888f | ||
|
|
c38906a7a0 | ||
|
|
d87857e5d9 | ||
|
|
5ae0badc44 | ||
|
|
84020a0b27 | ||
|
|
5473982859 | ||
|
|
de805a3ebf | ||
|
|
cbaabc1288 | ||
|
|
dd48fe9b15 | ||
|
|
48851c9ebd | ||
|
|
197c916a23 | ||
|
|
31caaf3071 | ||
|
|
5bcce9926a | ||
|
|
aacc1b525d | ||
|
|
3ad2bcbaf8 | ||
|
|
faee245d2e | ||
|
|
e1ab06d3d7 | ||
|
|
a204edd012 | ||
|
|
78b00bd814 | ||
|
|
923722cbb0 | ||
|
|
2c1adc9043 | ||
|
|
40439ccb42 | ||
|
|
24b3f739da | ||
|
|
2c8401d5cb | ||
|
|
6cc463f20d | ||
|
|
e6c6cc8331 | ||
|
|
8a6511c491 |
@@ -3,14 +3,14 @@ version: 2.1
|
||||
executors:
|
||||
default:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
- image: circleci/node:14
|
||||
working_directory: ~/project
|
||||
environment:
|
||||
YARN_CACHE_FOLDER: "~/.cache/yarn"
|
||||
|
||||
playwright:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:bionic
|
||||
- image: mcr.microsoft.com/playwright:focal
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
|
||||
@@ -36,10 +36,12 @@ jobs:
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
paths: ~/.cache/yarn
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
lint-and-typecheck:
|
||||
executor: default
|
||||
|
||||
62
.github/workflows/check-repro.yml
vendored
62
.github/workflows/check-repro.yml
vendored
@@ -23,26 +23,54 @@ jobs:
|
||||
'gm'
|
||||
);
|
||||
|
||||
if (!regex.test(body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await github.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['repro provided'],
|
||||
});
|
||||
|
||||
try {
|
||||
await github.issues.removeLabel({
|
||||
if (regex.test(body)) {
|
||||
await github.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'needs repro',
|
||||
labels: ['repro provided'],
|
||||
});
|
||||
} catch (error) {
|
||||
if (!/Label does not exist/.test(error.message)) {
|
||||
throw error;
|
||||
|
||||
try {
|
||||
await github.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'needs repro',
|
||||
});
|
||||
} catch (error) {
|
||||
if (!/Label does not exist/.test(error.message)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (context.eventName !== 'issues') {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = "Hey! Thanks for opening the issue. The issue doesn't seem to contain a link to a repro (a [snack.expo.io](https://snack.expo.io) link or link to a GitHub repo under your username).\n\nCan you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? A repro will help us debug the issue faster. Please try to keep the repro as small as possible and make sure that we can run it without additional setup.";
|
||||
|
||||
const comments = await github.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
if (comments.data.some(comment => comment.body === body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body,
|
||||
});
|
||||
|
||||
await github.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['needs repro'],
|
||||
});
|
||||
}
|
||||
|
||||
8
.github/workflows/expo-preview.yml
vendored
8
.github/workflows/expo-preview.yml
vendored
@@ -13,14 +13,12 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@v5
|
||||
with:
|
||||
expo-version: 3.x
|
||||
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||
expo-token: ${{ secrets.EXPO_TOKEN }}
|
||||
expo-cache: true
|
||||
|
||||
- name: Restore yarn cache
|
||||
@@ -36,7 +34,7 @@ jobs:
|
||||
|
||||
- name: Publish Expo app
|
||||
working-directory: ./example
|
||||
run: expo publish --release-channel=pr-${{ github.event.number }}
|
||||
run: yarn expo publish --release-channel=pr-${{ github.event.number }}
|
||||
env:
|
||||
EXPO_USE_DEV_SERVER: true
|
||||
|
||||
|
||||
8
.github/workflows/expo.yml
vendored
8
.github/workflows/expo.yml
vendored
@@ -16,14 +16,12 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@v5
|
||||
with:
|
||||
expo-version: 3.x
|
||||
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||
expo-token: ${{ secrets.EXPO_TOKEN }}
|
||||
expo-cache: true
|
||||
|
||||
- name: Restore yarn cache
|
||||
@@ -39,4 +37,4 @@ jobs:
|
||||
|
||||
- name: Publish Expo app
|
||||
working-directory: ./example
|
||||
run: expo publish
|
||||
run: yarn expo publish
|
||||
|
||||
10
.github/workflows/first-pull-request.yml
vendored
10
.github/workflows/first-pull-request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: First pull request
|
||||
on: pull_request
|
||||
on: pull_request_target
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
@@ -12,13 +12,13 @@ jobs:
|
||||
// Get a list of all issues created by the PR opener
|
||||
// See: https://octokit.github.io/rest.js/#pagination
|
||||
const creator = context.payload.sender.login;
|
||||
const opts = github.issues.listForRepo.endpoint.merge({
|
||||
const options = github.issues.listForRepo.endpoint.merge({
|
||||
...context.issue,
|
||||
creator,
|
||||
state: 'all'
|
||||
});
|
||||
|
||||
const issues = await github.paginate(opts);
|
||||
const issues = await github.paginate(options);
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.number === context.issue.number) {
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
}
|
||||
|
||||
if (issue.pull_request) {
|
||||
return ;// Creator is already a contributor.
|
||||
return; // Creator is already a contributor.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Hey ${creator}! Thanks for opening the pull request. If you haven't already, make sure to read our [contribution guidelines](https://github.com/react-navigation/react-navigation/blob/main/CONTRIBUTING.md)."
|
||||
body: `Hey ${creator}! Thanks for opening your first pull request in this repo. If you haven't already, make sure to read our [contribution guidelines](https://github.com/react-navigation/react-navigation/blob/main/CONTRIBUTING.md).`
|
||||
});
|
||||
|
||||
15
.github/workflows/sponsor.yml
vendored
Normal file
15
.github/workflows/sponsor.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Label sponsors
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: is-sponsor-label
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: JasonEtco/is-sponsor-label-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
21
.github/workflows/stale.yml
vendored
Normal file
21
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Close stale issues and PRs
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
any-of-labels: 'needs more info,needs repro,needs response'
|
||||
exempt-issue-labels: 'repro provided,keep open'
|
||||
exempt-pr-labels: 'keep open'
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
stale-issue-message: 'Hello 👋, this issue has been open for more than a month without a repro or any activity. If the issue is still present in the latest version, please provide a repro or leave a comment within 7 days to keep it open, otherwise it will be closed automatically. If you found a solution or 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 it.'
|
||||
stale-pr-message: 'Hello 👋, this pull request has been open for more than a month 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.'
|
||||
2
.github/workflows/triage.yml
vendored
2
.github/workflows/triage.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "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.\n\nThe 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 please provide the repro in a GitHub repository."
|
||||
body: "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 and make sure that we can run it without additional setup.\n\nThe 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 please provide the repro in a GitHub repository."
|
||||
})
|
||||
|
||||
question:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,6 +26,7 @@ build
|
||||
|
||||
npm-debug.*
|
||||
|
||||
*.tsbuildinfo
|
||||
*.log
|
||||
*.jks
|
||||
*.p8
|
||||
@@ -33,3 +34,5 @@ npm-debug.*
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
|
||||
*.iml
|
||||
|
||||
@@ -2,5 +2,6 @@ module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
plugins: ['react-native-reanimated/plugin'],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,42 +4,41 @@ 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/gandalf'
|
||||
);
|
||||
const getPageInfo = async () => ({
|
||||
url: await page.evaluate(() => location.pathname + location.search),
|
||||
title: await page.evaluate(() => document.title),
|
||||
heading: (await page.accessibility.snapshot())?.children?.find(
|
||||
(it) => it.role === 'heading'
|
||||
)?.name,
|
||||
});
|
||||
|
||||
expect(
|
||||
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||
(it: any) => it.role === 'heading'
|
||||
)?.name
|
||||
).toBe('Article by Gandalf');
|
||||
it('loads the article page', async () => {
|
||||
const { url, title, heading } = await getPageInfo();
|
||||
|
||||
expect(url).toBe('/link-component/article/gandalf');
|
||||
expect(title).toBe('Article by Gandalf - React Navigation Example');
|
||||
expect(heading).toBe('Article by Gandalf');
|
||||
});
|
||||
|
||||
it('goes to the album page and goes back', async () => {
|
||||
await page.click('[href="/link-component/music"]');
|
||||
|
||||
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||
'/link-component/music'
|
||||
);
|
||||
{
|
||||
const { url, title, heading } = await getPageInfo();
|
||||
|
||||
expect(
|
||||
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||
(it: any) => it.role === 'heading'
|
||||
)?.name
|
||||
).toBe('Albums');
|
||||
expect(url).toBe('/link-component/music');
|
||||
expect(title).toBe('Albums - React Navigation Example');
|
||||
expect(heading).toBe('Albums');
|
||||
}
|
||||
|
||||
await page.click('[aria-label="Article by Gandalf, back"]');
|
||||
|
||||
await page.waitForNavigation();
|
||||
|
||||
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||
'/link-component/article/gandalf'
|
||||
);
|
||||
{
|
||||
const { url, title, heading } = await getPageInfo();
|
||||
|
||||
expect(
|
||||
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||
(it: any) => it.role === 'heading'
|
||||
)?.name
|
||||
).toBe('Article by Gandalf');
|
||||
expect(url).toBe('/link-component/article/gandalf');
|
||||
expect(title).toBe('Article by Gandalf - React Navigation Example');
|
||||
expect(heading).toBe('Article by Gandalf');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -13,50 +13,53 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^12.0.3",
|
||||
"@react-native-masked-view/masked-view": "0.2.0",
|
||||
"@expo/vector-icons": "^12.0.0",
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
"@react-native-masked-view/masked-view": "~0.2.4",
|
||||
"color": "^3.1.3",
|
||||
"expo": "^40.0.1",
|
||||
"expo-asset": "~8.2.2",
|
||||
"expo-blur": "~9.0.0",
|
||||
"expo-linking": "~2.1.1",
|
||||
"expo-updates": "~0.4.2",
|
||||
"expo": "^41.0.1",
|
||||
"expo-asset": "~8.3.1",
|
||||
"expo-blur": "~9.0.3",
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-updates": "~0.5.4",
|
||||
"koa": "^2.13.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
|
||||
"react-native-appearance": "~0.3.3",
|
||||
"react-native-gesture-handler": "~1.8.0",
|
||||
"react-native-pager-view": "^4.2.4",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-pager-view": "~5.0.12",
|
||||
"react-native-paper": "^4.7.2",
|
||||
"react-native-reanimated": "~1.13.0",
|
||||
"react-native-safe-area-context": "3.1.9",
|
||||
"react-native-screens": "~2.15.0",
|
||||
"react-native-tab-view": "^3.0.0",
|
||||
"react-native-reanimated": "~2.1.0",
|
||||
"react-native-safe-area-context": "~3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"react-native-vector-icons": "^8.1.0",
|
||||
"react-native-web": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/node": "^7.13.0",
|
||||
"@expo/webpack-config": "~0.12.60",
|
||||
"@types/cheerio": "^0.22.24",
|
||||
"@babel/node": "^7.13.13",
|
||||
"@expo/webpack-config": "~0.12.63",
|
||||
"@types/cheerio": "^0.22.28",
|
||||
"@types/jest-dev-server": "^4.2.0",
|
||||
"@types/koa": "^2.13.1",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/mock-require": "^2.0.0",
|
||||
"@types/node-fetch": "^2.5.9",
|
||||
"@types/react": "~16.9.35",
|
||||
"@types/react-dom": "~16.9.8",
|
||||
"@types/react-native": "~0.63.51",
|
||||
"@types/react-native": "~0.63.2",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-module-resolver": "^4.0.0",
|
||||
"babel-preset-expo": "8.3.0",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"expo-cli": "^4.2.1",
|
||||
"expo-cli": "^4.4.4",
|
||||
"jest": "^26.6.3",
|
||||
"jest-dev-server": "^4.4.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"mock-require-assets": "^0.0.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"playwright": "^1.9.1",
|
||||
"playwright": "^1.11.0",
|
||||
"serve": "^11.3.0",
|
||||
"typescript": "~4.2.3"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'mock-require-assets';
|
||||
|
||||
import mock from 'mock-require';
|
||||
import Module from 'module';
|
||||
|
||||
// We need to make sure that .web.xx extensions are resolved before .xx
|
||||
@@ -10,3 +11,14 @@ Module._extensions = Object.fromEntries(
|
||||
return b[0].split('.').length - a[0].split('.').length;
|
||||
})
|
||||
);
|
||||
|
||||
// Set __DEV__ that expo needs
|
||||
// @ts-expect-error
|
||||
global.__DEV__ = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// Reanimated doesn't support SSR :(
|
||||
mock(
|
||||
'react-native-reanimated',
|
||||
// eslint-disable-next-line import/no-commonjs
|
||||
{ ...require('react-native-reanimated/mock').default, call() {} }
|
||||
);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import * as Linking from 'expo-linking';
|
||||
export default [Linking.makeUrl('/')];
|
||||
@@ -1 +0,0 @@
|
||||
export default ['rne://127.0.0.1:19000/--/'];
|
||||
@@ -1,15 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { ScrollView, StyleSheet } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import {
|
||||
getFocusedRouteNameFromRoute,
|
||||
ParamListBase,
|
||||
NavigatorScreenParams,
|
||||
} from '@react-navigation/native';
|
||||
import type { StackScreenProps } from '@react-navigation/stack';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import {
|
||||
createBottomTabNavigator,
|
||||
useBottomTabBarHeight,
|
||||
} from '@react-navigation/bottom-tabs';
|
||||
import { HeaderBackButton } from '@react-navigation/elements';
|
||||
import TouchableBounce from '../Shared/TouchableBounce';
|
||||
import Albums from '../Shared/Albums';
|
||||
import Contacts from '../Shared/Contacts';
|
||||
import Chat from '../Shared/Chat';
|
||||
@@ -30,6 +33,16 @@ type BottomTabParams = {
|
||||
TabChat: undefined;
|
||||
};
|
||||
|
||||
const AlbumsScreen = () => {
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: tabBarHeight }}>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const BottomTabs = createBottomTabNavigator<BottomTabParams>();
|
||||
|
||||
export default function BottomTabsScreen({
|
||||
@@ -51,10 +64,6 @@ export default function BottomTabsScreen({
|
||||
headerLeft: (props) => (
|
||||
<HeaderBackButton {...props} onPress={navigation.goBack} />
|
||||
),
|
||||
tabBarButton:
|
||||
Platform.OS === 'web'
|
||||
? undefined
|
||||
: (props) => <TouchableBounce {...props} />,
|
||||
}}
|
||||
>
|
||||
<BottomTabs.Screen
|
||||
@@ -84,10 +93,18 @@ export default function BottomTabsScreen({
|
||||
/>
|
||||
<BottomTabs.Screen
|
||||
name="TabAlbums"
|
||||
component={Albums}
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
title: 'Albums',
|
||||
tabBarIcon: getTabBarIcon('image-album'),
|
||||
tabBarStyle: { position: 'absolute' },
|
||||
tabBarBackground: () => (
|
||||
<BlurView
|
||||
tint="light"
|
||||
intensity={100}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</BottomTabs.Navigator>
|
||||
|
||||
@@ -24,7 +24,8 @@ const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
const LinkButton = ({
|
||||
to,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof Button> & { to: string }) => {
|
||||
}: React.ComponentProps<typeof Button> &
|
||||
Parameters<typeof useLinkProps>[0]) => {
|
||||
const props = useLinkProps({ to });
|
||||
|
||||
return <Button {...props} {...rest} />;
|
||||
@@ -57,6 +58,13 @@ const ArticleScreen = ({
|
||||
>
|
||||
Go to /link-component/music
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
to={{ screen: 'Home' }}
|
||||
mode="contained"
|
||||
style={styles.button}
|
||||
>
|
||||
Go to /
|
||||
</LinkButton>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
|
||||
164
example/src/Screens/MixedHeaderMode.tsx
Normal file
164
example/src/Screens/MixedHeaderMode.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import * as React from 'react';
|
||||
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import type { ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackScreenProps,
|
||||
TransitionPresets,
|
||||
HeaderStyleInterpolators,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
export type SimpleStackParams = {
|
||||
Article: { author: string } | undefined;
|
||||
NewsFeed: { date: number };
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('NewsFeed', { date: Date.now() })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push feed
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params?.author ?? 'Unknown' }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const NewsFeedScreen = ({
|
||||
route,
|
||||
navigation,
|
||||
}: StackScreenProps<SimpleStackParams, 'NewsFeed'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Albums')}
|
||||
style={styles.button}
|
||||
>
|
||||
Navigate to album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: StackScreenProps<SimpleStackParams, 'Albums'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
export default function SimpleStackScreen({
|
||||
navigation,
|
||||
}: StackScreenProps<ParamListBase>) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator
|
||||
screenOptions={{
|
||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||
}}
|
||||
>
|
||||
<SimpleStack.Group
|
||||
screenOptions={{
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
headerMode: 'float',
|
||||
}}
|
||||
>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author ?? 'Unknown'}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
</SimpleStack.Group>
|
||||
<SimpleStack.Screen
|
||||
name="Albums"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
...TransitionPresets.ModalSlideFromBottomIOS,
|
||||
headerMode: 'screen',
|
||||
title: 'Albums',
|
||||
}}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
136
example/src/Screens/MixedStack.tsx
Normal file
136
example/src/Screens/MixedStack.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import type { ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackScreenProps,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
|
||||
type MixedStackParams = {
|
||||
Article: { author: string };
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: StackScreenProps<MixedStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<View>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'Dalek' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Albums')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({ navigation }: StackScreenProps<MixedStackParams>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<View>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Albums')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'The Doctor' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const MixedStack = createStackNavigator<MixedStackParams>();
|
||||
|
||||
type Props = StackScreenProps<ParamListBase>;
|
||||
|
||||
export default function MixedStackScreen({ navigation }: Props) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<MixedStack.Navigator>
|
||||
<MixedStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params.author}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<MixedStack.Screen
|
||||
name="Albums"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
title: 'Albums',
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>
|
||||
</MixedStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
@@ -82,7 +82,7 @@ export default function ModalStackScreen({ navigation }: Props) {
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<ModalStack.Navigator mode="modal">
|
||||
<ModalStack.Navigator screenOptions={{ presentation: 'modal' }}>
|
||||
<ModalStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
|
||||
@@ -86,8 +86,7 @@ const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||
StackScreenProps<ParamListBase>;
|
||||
type Props = StackScreenProps<ParamListBase>;
|
||||
|
||||
function CustomHeader(props: StackHeaderProps) {
|
||||
const { current, next } = props.progress;
|
||||
@@ -108,7 +107,7 @@ function CustomHeader(props: StackHeaderProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
export default function HeaderCustomizationScreen({ navigation }: Props) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
@@ -119,13 +118,13 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
const [headerTitleCentered, setHeaderTitleCentered] = React.useState(true);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator {...rest}>
|
||||
<SimpleStack.Navigator screenOptions={{ headerMode: 'float' }}>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author}`,
|
||||
header: CustomHeader,
|
||||
header: (props) => <CustomHeader {...props} />,
|
||||
headerTintColor: '#fff',
|
||||
headerStyle: { backgroundColor: '#ff005d' },
|
||||
headerBackTitleVisible: false,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
|
||||
type SimpleStackParams = {
|
||||
type TransparentStackParams = {
|
||||
Article: { author: string };
|
||||
Dialog: undefined;
|
||||
};
|
||||
@@ -18,7 +18,7 @@ const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
}: StackScreenProps<TransparentStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -45,7 +45,9 @@ const ArticleScreen = ({
|
||||
);
|
||||
};
|
||||
|
||||
const DialogScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||
const DialogScreen = ({
|
||||
navigation,
|
||||
}: StackScreenProps<TransparentStackParams>) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
@@ -70,12 +72,11 @@ const DialogScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
const TransparentStack = createStackNavigator<TransparentStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||
StackScreenProps<ParamListBase>;
|
||||
type Props = StackScreenProps<ParamListBase>;
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
export default function TransparentStackScreen({ navigation }: Props) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
@@ -83,13 +84,13 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator mode="modal" {...rest}>
|
||||
<SimpleStack.Screen
|
||||
<TransparentStack.Navigator screenOptions={{ presentation: 'modal' }}>
|
||||
<TransparentStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
<TransparentStack.Screen
|
||||
name="Dialog"
|
||||
component={DialogScreen}
|
||||
options={{
|
||||
@@ -122,7 +123,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
</TransparentStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// @ts-expect-error: there are no type definitions for deep imports
|
||||
import TouchableBounce from 'react-native/Libraries/Components/Touchable/TouchableBounce';
|
||||
|
||||
export default TouchableBounce;
|
||||
@@ -1,3 +0,0 @@
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
|
||||
export default TouchableOpacity;
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AsyncStorage,
|
||||
ScrollView,
|
||||
Platform,
|
||||
StatusBar,
|
||||
@@ -21,27 +20,31 @@ import {
|
||||
Divider,
|
||||
Text,
|
||||
} from 'react-native-paper';
|
||||
import { createURL } from 'expo-linking';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import {
|
||||
InitialState,
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
DarkTheme,
|
||||
PathConfigMap,
|
||||
NavigationContainerRef,
|
||||
useNavigationContainerRef,
|
||||
NavigatorScreenParams,
|
||||
} from '@react-navigation/native';
|
||||
import { createDrawerNavigator } from '@react-navigation/drawer';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackScreenProps,
|
||||
HeaderStyleInterpolators,
|
||||
StackNavigationProp,
|
||||
} from '@react-navigation/stack';
|
||||
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
|
||||
|
||||
import { restartApp } from './Restart';
|
||||
import LinkingPrefixes from './LinkingPrefixes';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import SimpleStack from './Screens/SimpleStack';
|
||||
import ModalStack from './Screens/ModalStack';
|
||||
import MixedStack from './Screens/MixedStack';
|
||||
import MixedHeaderMode from './Screens/MixedHeaderMode';
|
||||
import StackTransparent from './Screens/StackTransparent';
|
||||
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
|
||||
import BottomTabs from './Screens/BottomTabs';
|
||||
@@ -60,6 +63,13 @@ if (Platform.OS !== 'web') {
|
||||
|
||||
enableScreens();
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace ReactNavigation {
|
||||
interface RootParamList extends RootStackParamList {}
|
||||
}
|
||||
}
|
||||
|
||||
type RootDrawerParamList = {
|
||||
Examples: undefined;
|
||||
};
|
||||
@@ -70,6 +80,14 @@ const SCREENS = {
|
||||
title: 'Modal Stack',
|
||||
component: ModalStack,
|
||||
},
|
||||
MixedStack: {
|
||||
title: 'Regular + Modal Stack',
|
||||
component: MixedStack,
|
||||
},
|
||||
MixedHeaderMode: {
|
||||
title: 'Float + Screen Header Stack',
|
||||
component: MixedHeaderMode,
|
||||
},
|
||||
StackTransparent: {
|
||||
title: 'Transparent Stack',
|
||||
component: StackTransparent,
|
||||
@@ -110,7 +128,7 @@ const SCREENS = {
|
||||
};
|
||||
|
||||
type RootStackParamList = {
|
||||
Home: undefined;
|
||||
Home: NavigatorScreenParams<RootDrawerParamList>;
|
||||
NotFound: undefined;
|
||||
} & {
|
||||
[P in keyof typeof SCREENS]: undefined;
|
||||
@@ -188,7 +206,7 @@ export default function App() {
|
||||
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||
}, []);
|
||||
|
||||
const navigationRef = React.useRef<NavigationContainerRef>(null);
|
||||
const navigationRef = useNavigationContainerRef();
|
||||
|
||||
useReduxDevToolsExtension(navigationRef);
|
||||
|
||||
@@ -220,10 +238,12 @@ export default function App() {
|
||||
// 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,
|
||||
prefixes: [createURL('/')],
|
||||
config: {
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<PathConfigMap>(
|
||||
screens: Object.keys(SCREENS).reduce<
|
||||
PathConfigMap<RootStackParamList>
|
||||
>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
const path = name
|
||||
@@ -231,13 +251,15 @@ export default function App() {
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
|
||||
// @ts-expect-error: these types aren't accurate
|
||||
// But we aren't too concerned for now
|
||||
acc[name] = {
|
||||
path,
|
||||
screens: {
|
||||
Article: {
|
||||
path: 'article/:author?',
|
||||
parse: {
|
||||
author: (author) =>
|
||||
author: (author: string) =>
|
||||
author.charAt(0).toUpperCase() +
|
||||
author.slice(1).replace(/-/g, ' '),
|
||||
},
|
||||
@@ -299,7 +321,11 @@ export default function App() {
|
||||
),
|
||||
}}
|
||||
>
|
||||
{({ navigation }: StackScreenProps<RootStackParamList>) => (
|
||||
{({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: StackNavigationProp<RootStackParamList>;
|
||||
}) => (
|
||||
<ScrollView
|
||||
style={{ backgroundColor: theme.colors.background }}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"allowBranch": "main",
|
||||
"conventionalCommits": true,
|
||||
"createRelease": "github",
|
||||
"distTag": "next",
|
||||
"message": "chore: publish",
|
||||
"ignoreChanges": [
|
||||
"**/__fixtures__/**",
|
||||
|
||||
13
package.json
13
package.json
@@ -8,8 +8,7 @@
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -26,13 +25,13 @@
|
||||
"example": "yarn --cwd example"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/config-conventional": "^12.0.1",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@commitlint/config-conventional": "^12.1.1",
|
||||
"@types/jest": "^26.0.22",
|
||||
"babel-jest": "^26.6.3",
|
||||
"codecov": "^3.8.1",
|
||||
"commitlint": "^12.0.1",
|
||||
"eslint": "^7.21.0",
|
||||
"eslint-config-satya164": "^3.1.9",
|
||||
"commitlint": "^12.1.1",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-satya164": "^3.1.10",
|
||||
"husky": "^4.3.6",
|
||||
"jest": "^26.6.3",
|
||||
"lerna": "^4.0.0",
|
||||
|
||||
@@ -3,6 +3,120 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [6.0.0-next.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.10...@react-navigation/bottom-tabs@6.0.0-next.11) (2021-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix tab bar height including extra bottom inset ([7c722d2](https://github.com/react-navigation/react-navigation/commit/7c722d2028e914e8f143b9385ebf5e1c00131a01))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a tabBarBackground option to bottom tabs ([2f282f1](https://github.com/react-navigation/react-navigation/commit/2f282f107053a65b69f80edb5d9c858cfa569aa2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.9...@react-navigation/bottom-tabs@6.0.0-next.10) (2021-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add a deprecation warning for mode prop in stack ([a6e4981](https://github.com/react-navigation/react-navigation/commit/a6e498170f59648190fa5513e273ca523e56c5d5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* return a NavigationContent component from useNavigationBuilder ([1179d56](https://github.com/react-navigation/react-navigation/commit/1179d56c5008270753feef41acdc1dbd2191efcf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.8...@react-navigation/bottom-tabs@6.0.0-next.9) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.7...@react-navigation/bottom-tabs@6.0.0-next.8) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.6...@react-navigation/bottom-tabs@6.0.0-next.7) (2021-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable screens only on supported platforms ([#9494](https://github.com/react-navigation/react-navigation/issues/9494)) ([8da4c58](https://github.com/react-navigation/react-navigation/commit/8da4c58065607d44e9dc1ad8943e09537598dcd7))
|
||||
* make sure disabling react-native-screens works ([a369ba3](https://github.com/react-navigation/react-navigation/commit/a369ba36451ddc2bb5b247e61b725bce1e3fb5e5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.5...@react-navigation/bottom-tabs@6.0.0-next.6) (2021-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.4...@react-navigation/bottom-tabs@6.0.0-next.5) (2021-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update tab bar height correctly. fixes [#9296](https://github.com/react-navigation/react-navigation/issues/9296) ([338ed6f](https://github.com/react-navigation/react-navigation/commit/338ed6ff07bd2d6efa1abdb369612ea72f540502))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.3...@react-navigation/bottom-tabs@6.0.0-next.4) (2021-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove calls to removed Keyboard.removeListener in useIsKeyboardShown ([#9457](https://github.com/react-navigation/react-navigation/issues/9457)) ([d87857e](https://github.com/react-navigation/react-navigation/commit/d87857e5d93c19ebee2fd84eb4910e36001ec2a3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.2...@react-navigation/bottom-tabs@6.0.0-next.3) (2021-03-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use tab role on Android for accessibility ([de805a3](https://github.com/react-navigation/react-navigation/commit/de805a3ebf35db81cb7b7bcbf5cfd4a03e69c567))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.1...@react-navigation/bottom-tabs@6.0.0-next.2) (2021-03-12)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0...@react-navigation/bottom-tabs@6.0.0-next.1) (2021-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/bottom-tabs",
|
||||
"description": "Bottom tab navigator following iOS design guidelines",
|
||||
"version": "6.0.0-next.1",
|
||||
"version": "6.0.0-next.11",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -29,30 +29,29 @@
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": "^1.0.0-next.1",
|
||||
"@react-navigation/elements": "^1.0.0-next.10",
|
||||
"color": "^3.1.3",
|
||||
"warn-once": "^0.0.1"
|
||||
"warn-once": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-navigation/native": "^6.0.0-next.1",
|
||||
"@react-navigation/native": "^6.0.0-next.8",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "~0.63.51",
|
||||
"@types/react-native": "~0.64.4",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native": "~0.63.4",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"react-native-safe-area-context": "3.1.9",
|
||||
"react-native-screens": "~2.15.0",
|
||||
"react-native-safe-area-context": "~3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -60,7 +59,7 @@
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 3.0.0",
|
||||
"react-native-screens": ">= 2.15.0"
|
||||
"react-native-screens": ">= 3.0.0"
|
||||
},
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
|
||||
@@ -70,7 +70,12 @@ function BottomTabNavigator({
|
||||
);
|
||||
}
|
||||
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
const {
|
||||
state,
|
||||
descriptors,
|
||||
navigation,
|
||||
NavigationContent,
|
||||
} = useNavigationBuilder<
|
||||
TabNavigationState<ParamListBase>,
|
||||
TabRouterOptions,
|
||||
TabActionHelpers<ParamListBase>,
|
||||
@@ -85,13 +90,15 @@ function BottomTabNavigator({
|
||||
});
|
||||
|
||||
return (
|
||||
<BottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
sceneContainerStyle={sceneContainerStyle}
|
||||
/>
|
||||
<NavigationContent>
|
||||
<BottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
sceneContainerStyle={sceneContainerStyle}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export type BottomTabNavigationHelpers = NavigationHelpers<
|
||||
|
||||
export type BottomTabNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
RouteName extends keyof ParamList = keyof ParamList
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
@@ -54,7 +54,7 @@ export type BottomTabNavigationProp<
|
||||
|
||||
export type BottomTabScreenProps<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
RouteName extends keyof ParamList = keyof ParamList
|
||||
> = {
|
||||
navigation: BottomTabNavigationProp<ParamList, RouteName>;
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
@@ -229,6 +229,15 @@ export type BottomTabNavigationOptions = HeaderOptions & {
|
||||
*/
|
||||
tabBarStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
|
||||
/**
|
||||
* Component to use as background for the tab bar.
|
||||
* You could render an image, a gradient, blur view etc.
|
||||
*
|
||||
* When using `BlurView`, make sure to set `position: 'absolute'` in `tabBarStyle` as well.
|
||||
* You'd also need to use `useBottomTabBarHeight()` to add a bottom padding to your content.
|
||||
*/
|
||||
tabBarBackground?: () => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Whether this screen should be unmounted when navigating away from it.
|
||||
* Defaults to `false`.
|
||||
@@ -239,7 +248,7 @@ export type BottomTabNavigationOptions = HeaderOptions & {
|
||||
export type BottomTabDescriptor = Descriptor<
|
||||
BottomTabNavigationOptions,
|
||||
BottomTabNavigationProp<ParamListBase>,
|
||||
RouteProp<ParamListBase, string>
|
||||
RouteProp<ParamListBase>
|
||||
>;
|
||||
|
||||
export type BottomTabDescriptorMap = Record<string, BottomTabDescriptor>;
|
||||
@@ -283,7 +292,7 @@ export type BottomTabHeaderProps = {
|
||||
/**
|
||||
* Route object for the current screen.
|
||||
*/
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
route: RouteProp<ParamListBase>;
|
||||
/**
|
||||
* Navigation prop for the header.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Keyboard, Platform } from 'react-native';
|
||||
import { Keyboard, Platform, EmitterSubscription } from 'react-native';
|
||||
|
||||
export default function useIsKeyboardShown() {
|
||||
const [isKeyboardShown, setIsKeyboardShown] = React.useState(false);
|
||||
@@ -8,22 +8,22 @@ export default function useIsKeyboardShown() {
|
||||
const handleKeyboardShow = () => setIsKeyboardShown(true);
|
||||
const handleKeyboardHide = () => setIsKeyboardShown(false);
|
||||
|
||||
let subscriptions: EmitterSubscription[];
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
Keyboard.addListener('keyboardWillShow', handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardWillHide', handleKeyboardHide);
|
||||
subscriptions = [
|
||||
Keyboard.addListener('keyboardWillShow', handleKeyboardShow),
|
||||
Keyboard.addListener('keyboardWillHide', handleKeyboardHide),
|
||||
];
|
||||
} else {
|
||||
Keyboard.addListener('keyboardDidShow', handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
|
||||
subscriptions = [
|
||||
Keyboard.addListener('keyboardDidShow', handleKeyboardShow),
|
||||
Keyboard.addListener('keyboardDidHide', handleKeyboardHide),
|
||||
];
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
Keyboard.removeListener('keyboardWillShow', handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardWillHide', handleKeyboardHide);
|
||||
} else {
|
||||
Keyboard.removeListener('keyboardDidShow', handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardDidHide', handleKeyboardHide);
|
||||
}
|
||||
subscriptions.forEach((s) => s.remove());
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ export default function BottomTabBar({
|
||||
tabBarHideOnKeyboard = false,
|
||||
tabBarVisibilityAnimationConfig,
|
||||
tabBarStyle,
|
||||
tabBarBackground,
|
||||
} = focusedOptions;
|
||||
|
||||
const dimensions = useSafeAreaFrame();
|
||||
@@ -211,15 +212,7 @@ export default function BottomTabBar({
|
||||
const handleLayout = (e: LayoutChangeEvent) => {
|
||||
const { height, width } = e.nativeEvent.layout;
|
||||
|
||||
const topBorderWidth =
|
||||
// @ts-ignore
|
||||
StyleSheet.flatten([styles.tabBar, tabBarStyle])?.borderTopWidth;
|
||||
|
||||
onHeightChange?.(
|
||||
height +
|
||||
paddingBottom +
|
||||
(typeof topBorderWidth === 'number' ? topBorderWidth : 0)
|
||||
);
|
||||
onHeightChange?.(height);
|
||||
|
||||
setLayout((layout) => {
|
||||
if (height === layout.height && width === layout.width) {
|
||||
@@ -252,12 +245,15 @@ export default function BottomTabBar({
|
||||
layout,
|
||||
});
|
||||
|
||||
const tabBarBackgroundElement = tabBarBackground?.();
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.tabBar,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
backgroundColor:
|
||||
tabBarBackgroundElement != null ? 'transparent' : colors.card,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
{
|
||||
@@ -284,8 +280,12 @@ export default function BottomTabBar({
|
||||
tabBarStyle,
|
||||
]}
|
||||
pointerEvents={isTabBarHidden ? 'none' : 'auto'}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
<View style={styles.content} onLayout={handleLayout}>
|
||||
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
||||
{tabBarBackgroundElement}
|
||||
</View>
|
||||
<View accessibilityRole="tablist" style={styles.content}>
|
||||
{routes.map((route, index) => {
|
||||
const focused = index === state.index;
|
||||
const { options } = descriptors[route.key];
|
||||
@@ -322,7 +322,7 @@ export default function BottomTabBar({
|
||||
const accessibilityLabel =
|
||||
options.tabBarAccessibilityLabel !== undefined
|
||||
? options.tabBarAccessibilityLabel
|
||||
: typeof label === 'string'
|
||||
: typeof label === 'string' && Platform.OS === 'ios'
|
||||
? `${label}, tab, ${index + 1} of ${routes.length}`
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -263,7 +263,8 @@ export default function BottomTabBarItem({
|
||||
onLongPress,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityRole: 'button',
|
||||
// FIXME: accessibilityRole: 'tab' doesn't seem to work as expected on iOS
|
||||
accessibilityRole: Platform.select({ ios: 'button', default: 'tab' }),
|
||||
accessibilityState: { selected: focused },
|
||||
// @ts-expect-error: keep for compatibility with older React Native versions
|
||||
accessibilityStates: focused ? ['selected'] : [],
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import { StyleSheet, Platform } from 'react-native';
|
||||
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
import type {
|
||||
ParamListBase,
|
||||
TabNavigationState,
|
||||
useTheme,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
Header,
|
||||
@@ -14,8 +11,7 @@ import {
|
||||
SafeAreaProviderCompat,
|
||||
getHeaderTitle,
|
||||
} from '@react-navigation/elements';
|
||||
|
||||
import ScreenFallback from './ScreenFallback';
|
||||
import { MaybeScreenContainer, MaybeScreen } from './ScreenFallback';
|
||||
import BottomTabBar, { getTabBarHeight } from './BottomTabBar';
|
||||
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
|
||||
import BottomTabBarHeightContext from '../utils/BottomTabBarHeightContext';
|
||||
@@ -34,28 +30,6 @@ type Props = BottomTabNavigationConfig & {
|
||||
descriptors: BottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
function SceneContent({
|
||||
isFocused,
|
||||
children,
|
||||
style,
|
||||
}: {
|
||||
isFocused: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
accessibilityElementsHidden={!isFocused}
|
||||
importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'}
|
||||
style={[styles.content, { backgroundColor: colors.background }, style]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BottomTabView(props: Props) {
|
||||
const {
|
||||
tabBar = (props: BottomTabBarProps) => <BottomTabBar {...props} />,
|
||||
@@ -63,7 +37,9 @@ export default function BottomTabView(props: Props) {
|
||||
navigation,
|
||||
descriptors,
|
||||
safeAreaInsets,
|
||||
detachInactiveScreens = true,
|
||||
detachInactiveScreens = Platform.OS === 'web' ||
|
||||
Platform.OS === 'android' ||
|
||||
Platform.OS === 'ios',
|
||||
sceneContainerStyle,
|
||||
} = props;
|
||||
|
||||
@@ -112,73 +88,70 @@ export default function BottomTabView(props: Props) {
|
||||
const { routes } = state;
|
||||
|
||||
return (
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
<SafeAreaProviderCompat>
|
||||
<ScreenContainer
|
||||
// @ts-ignore
|
||||
enabled={detachInactiveScreens}
|
||||
style={styles.container}
|
||||
>
|
||||
{routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { lazy = true, unmountOnBlur } = descriptor.options;
|
||||
const isFocused = state.index === index;
|
||||
<SafeAreaProviderCompat>
|
||||
<MaybeScreenContainer
|
||||
enabled={detachInactiveScreens}
|
||||
style={styles.container}
|
||||
>
|
||||
{routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { lazy = true, unmountOnBlur } = descriptor.options;
|
||||
const isFocused = state.index === index;
|
||||
|
||||
if (unmountOnBlur && !isFocused) {
|
||||
return null;
|
||||
}
|
||||
if (unmountOnBlur && !isFocused) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lazy && !loaded.includes(route.key) && !isFocused) {
|
||||
// Don't render a lazy screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
if (lazy && !loaded.includes(route.key) && !isFocused) {
|
||||
// Don't render a lazy screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
header = ({ layout, options }: BottomTabHeaderProps) => (
|
||||
<Header
|
||||
{...options}
|
||||
layout={layout}
|
||||
title={getHeaderTitle(options, route.name)}
|
||||
/>
|
||||
),
|
||||
} = descriptor.options;
|
||||
const {
|
||||
header = ({ layout, options }: BottomTabHeaderProps) => (
|
||||
<Header
|
||||
{...options}
|
||||
layout={layout}
|
||||
title={getHeaderTitle(options, route.name)}
|
||||
/>
|
||||
),
|
||||
} = descriptor.options;
|
||||
|
||||
return (
|
||||
<ScreenFallback
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
visible={isFocused}
|
||||
enabled={detachInactiveScreens}
|
||||
>
|
||||
<SceneContent isFocused={isFocused} style={sceneContainerStyle}>
|
||||
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
|
||||
<Screen
|
||||
route={descriptor.route}
|
||||
navigation={descriptor.navigation}
|
||||
headerShown={descriptor.options.headerShown}
|
||||
headerStatusBarHeight={
|
||||
descriptor.options.headerStatusBarHeight
|
||||
}
|
||||
header={header({
|
||||
layout: dimensions,
|
||||
route: descriptor.route,
|
||||
navigation: descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
|
||||
options: descriptor.options,
|
||||
})}
|
||||
>
|
||||
{descriptor.render()}
|
||||
</Screen>
|
||||
</BottomTabBarHeightContext.Provider>
|
||||
</SceneContent>
|
||||
</ScreenFallback>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
<BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
|
||||
{renderTabBar()}
|
||||
</BottomTabBarHeightCallbackContext.Provider>
|
||||
</SafeAreaProviderCompat>
|
||||
</NavigationHelpersContext.Provider>
|
||||
return (
|
||||
<MaybeScreen
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
visible={isFocused}
|
||||
enabled={detachInactiveScreens}
|
||||
>
|
||||
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
|
||||
<Screen
|
||||
focused={isFocused}
|
||||
route={descriptor.route}
|
||||
navigation={descriptor.navigation}
|
||||
headerShown={descriptor.options.headerShown}
|
||||
headerStatusBarHeight={
|
||||
descriptor.options.headerStatusBarHeight
|
||||
}
|
||||
header={header({
|
||||
layout: dimensions,
|
||||
route: descriptor.route,
|
||||
navigation: descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
|
||||
options: descriptor.options,
|
||||
})}
|
||||
style={sceneContainerStyle}
|
||||
>
|
||||
{descriptor.render()}
|
||||
</Screen>
|
||||
</BottomTabBarHeightContext.Provider>
|
||||
</MaybeScreen>
|
||||
);
|
||||
})}
|
||||
</MaybeScreenContainer>
|
||||
<BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
|
||||
{renderTabBar()}
|
||||
</BottomTabBarHeightCallbackContext.Provider>
|
||||
</SafeAreaProviderCompat>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,7 +160,4 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleProp, ViewStyle } from 'react-native';
|
||||
import {
|
||||
Screen,
|
||||
screensEnabled,
|
||||
// @ts-ignore
|
||||
shouldUseActivityState,
|
||||
} from 'react-native-screens';
|
||||
import { ResourceSavingScene } from '@react-navigation/elements';
|
||||
import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
|
||||
import { ResourceSavingView } from '@react-navigation/elements';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
@@ -15,27 +9,40 @@ type Props = {
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function ScreenFallback({ visible, children, ...rest }: Props) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
<Screen activityState={visible ? 2 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Screen active={visible ? 1 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
let Screens: typeof import('react-native-screens') | undefined;
|
||||
|
||||
try {
|
||||
Screens = require('react-native-screens');
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
export const MaybeScreenContainer = ({
|
||||
enabled,
|
||||
...rest
|
||||
}: ViewProps & {
|
||||
enabled: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
if (Screens?.screensEnabled?.()) {
|
||||
return <Screens.ScreenContainer enabled={enabled} {...rest} />;
|
||||
}
|
||||
|
||||
return <View {...rest} />;
|
||||
};
|
||||
|
||||
export function MaybeScreen({ visible, children, ...rest }: Props) {
|
||||
if (Screens?.screensEnabled?.()) {
|
||||
return (
|
||||
<Screens.Screen activityState={visible ? 2 : 0} {...rest}>
|
||||
{children}
|
||||
</Screens.Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSavingScene visible={visible} {...rest}>
|
||||
<ResourceSavingView visible={visible} {...rest}>
|
||||
{children}
|
||||
</ResourceSavingScene>
|
||||
</ResourceSavingView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,90 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.7...@react-navigation/core@6.0.0-next.8) (2021-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix type error when passing unannotated navigation ref ([dc4ffc0](https://github.com/react-navigation/react-navigation/commit/dc4ffc0171b4535fe1b6e839b9d54350121bcf55))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.6...@react-navigation/core@6.0.0-next.7) (2021-05-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* return a NavigationContent component from useNavigationBuilder ([1179d56](https://github.com/react-navigation/react-navigation/commit/1179d56c5008270753feef41acdc1dbd2191efcf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.5...@react-navigation/core@6.0.0-next.6) (2021-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix type annotations for useNavigation (again) ([929c3e3](https://github.com/react-navigation/react-navigation/commit/929c3e3a3b3eb32d197ef1f887dc4cbdce48eaff))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.4...@react-navigation/core@6.0.0-next.5) (2021-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix type annotations for useNavigation ([7da45e1](https://github.com/react-navigation/react-navigation/commit/7da45e1e8951ff46e09db4ebc2c88085c67ab8e9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.3...@react-navigation/core@6.0.0-next.4) (2021-05-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a new component to group multiple screens with common options ([1a6aebe](https://github.com/react-navigation/react-navigation/commit/1a6aebefcb77ea708687475c55742407d69808ce))
|
||||
* add ability to specify root param list ([b28bfdd](https://github.com/react-navigation/react-navigation/commit/b28bfddc17cbf3996fac04a34b2a7085ecf88be5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.2...@react-navigation/core@6.0.0-next.3) (2021-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a CompositeScreenProps type ([def7c03](https://github.com/react-navigation/react-navigation/commit/def7c03d7d7b42cf322f4e277f8f76858717654e))
|
||||
* add helper and hook for container ref ([0ecd112](https://github.com/react-navigation/react-navigation/commit/0ecd112ec9786a26261ada3d33ef44dc1ec84da0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.1...@react-navigation/core@6.0.0-next.2) (2021-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly resolve initialRouteNames ([c38906a](https://github.com/react-navigation/react-navigation/commit/c38906a7a09b997f37ce56734ea823c75ea744db))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improve useNavigationState typing ([#9464](https://github.com/react-navigation/react-navigation/issues/9464)) ([84020a0](https://github.com/react-navigation/react-navigation/commit/84020a0b27ebae50d3037438a51d95eb31b02424))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0...@react-navigation/core@6.0.0-next.1) (2021-03-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/core
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "6.0.0-next.1",
|
||||
"version": "6.0.0-next.8",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -28,18 +28,17 @@
|
||||
"!**/__tests__"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^6.0.0-next.1",
|
||||
"@react-navigation/routers": "^6.0.0-next.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"nanoid": "^3.1.20",
|
||||
"query-string": "^6.14.1",
|
||||
"nanoid": "^3.1.22",
|
||||
"query-string": "^7.0.0",
|
||||
"react-is": "^16.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -47,7 +46,7 @@
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-is": "^16.7.1",
|
||||
"del-cli": "^3.0.1",
|
||||
"immer": "^8.0.1",
|
||||
"immer": "^9.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"react-test-renderer": "~16.13.1",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
InitialState,
|
||||
PartialState,
|
||||
NavigationAction,
|
||||
ParamListBase,
|
||||
} from '@react-navigation/routers';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import UnhandledActionContext from './UnhandledActionContext';
|
||||
@@ -22,6 +23,7 @@ import useSyncState from './useSyncState';
|
||||
import checkSerializable from './checkSerializable';
|
||||
import checkDuplicateRouteNames from './checkDuplicateRouteNames';
|
||||
import findFocusedRoute from './findFocusedRoute';
|
||||
import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef';
|
||||
import type {
|
||||
NavigationContainerEventMap,
|
||||
NavigationContainerRef,
|
||||
@@ -30,9 +32,6 @@ import type {
|
||||
|
||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
|
||||
const NOT_INITIALIZED_ERROR =
|
||||
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
|
||||
|
||||
const serializableWarnings: string[] = [];
|
||||
const duplicateNameWarnings: string[] = [];
|
||||
|
||||
@@ -103,7 +102,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
independent,
|
||||
children,
|
||||
}: NavigationContainerProps,
|
||||
ref?: React.Ref<NavigationContainerRef>
|
||||
ref?: React.Ref<NavigationContainerRef<ParamListBase>>
|
||||
) {
|
||||
const parent = React.useContext(NavigationStateContext);
|
||||
|
||||
@@ -202,16 +201,10 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({});
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
...(Object.keys(
|
||||
CommonActions
|
||||
) as (keyof typeof CommonActions)[]).reduce<any>((acc, name) => {
|
||||
...Object.keys(CommonActions).reduce<any>((acc, name) => {
|
||||
acc[name] = (...args: any[]) =>
|
||||
dispatch(
|
||||
CommonActions[name](
|
||||
// @ts-expect-error: we can't know the type statically
|
||||
...args
|
||||
)
|
||||
);
|
||||
// @ts-expect-error: this is ok
|
||||
dispatch(CommonActions[name](...args));
|
||||
return acc;
|
||||
}, {}),
|
||||
...emitter.create('root'),
|
||||
@@ -223,6 +216,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
getParent: () => undefined,
|
||||
getCurrentRoute,
|
||||
getCurrentOptions,
|
||||
isReady: () => listeners.focus[0] != null,
|
||||
}));
|
||||
|
||||
const onDispatchAction = React.useCallback(
|
||||
|
||||
13
packages/core/src/Group.tsx
Normal file
13
packages/core/src/Group.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ParamListBase } from '@react-navigation/routers';
|
||||
import type { RouteGroupConfig } from './types';
|
||||
|
||||
/**
|
||||
* Empty component used for grouping screen configs.
|
||||
*/
|
||||
export default function Group<
|
||||
ParamList extends ParamListBase,
|
||||
ScreenOptions extends {}
|
||||
>(_: RouteGroupConfig<ParamList, ScreenOptions>) {
|
||||
/* istanbul ignore next */
|
||||
return null;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import type { NavigationProp } from './types';
|
||||
* Context which holds the navigation prop for a screen.
|
||||
*/
|
||||
const NavigationContext = React.createContext<
|
||||
NavigationProp<ParamListBase, string, any, any> | undefined
|
||||
NavigationProp<ParamListBase> | undefined
|
||||
>(undefined);
|
||||
|
||||
export default NavigationContext;
|
||||
|
||||
@@ -9,14 +9,10 @@ import NavigationStateContext from './NavigationStateContext';
|
||||
import StaticContainer from './StaticContainer';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import useOptionsGetters from './useOptionsGetters';
|
||||
import type { NavigationProp, RouteConfig, EventMapBase } from './types';
|
||||
import type { NavigationProp, RouteConfigComponent } from './types';
|
||||
|
||||
type Props<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
> = {
|
||||
screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
|
||||
type Props<State extends NavigationState, ScreenOptions extends {}> = {
|
||||
screen: RouteConfigComponent<ParamListBase, string> & { name: string };
|
||||
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
|
||||
route: Route<string>;
|
||||
routeState: NavigationState | PartialState<NavigationState> | undefined;
|
||||
@@ -31,8 +27,7 @@ type Props<
|
||||
*/
|
||||
export default function SceneView<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
ScreenOptions extends {}
|
||||
>({
|
||||
screen,
|
||||
route,
|
||||
@@ -41,7 +36,7 @@ export default function SceneView<
|
||||
getState,
|
||||
setState,
|
||||
options,
|
||||
}: Props<State, ScreenOptions, EventMap>) {
|
||||
}: Props<State, ScreenOptions>) {
|
||||
const navigatorKeyRef = React.useRef<string | undefined>();
|
||||
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@ import { act, render } from '@testing-library/react-native';
|
||||
import {
|
||||
DefaultRouterOptions,
|
||||
NavigationState,
|
||||
ParamListBase,
|
||||
Router,
|
||||
StackRouter,
|
||||
TabRouter,
|
||||
} from '@react-navigation/routers';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
import NavigationStateContext from '../NavigationStateContext';
|
||||
import createNavigationContainerRef from '../createNavigationContainerRef';
|
||||
import MockRouter, { MockActions } from './__fixtures__/MockRouter';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import Screen from '../Screen';
|
||||
import type { NavigationContainerRef } from '../types';
|
||||
|
||||
it('throws when getState is accessed without a container', () => {
|
||||
expect.assertions(1);
|
||||
@@ -128,7 +129,7 @@ it('handle dispatching with ref', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
@@ -226,7 +227,7 @@ it('handle resetting state with ref', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
@@ -316,7 +317,7 @@ it('handles getRootState', () => {
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref}>
|
||||
@@ -378,7 +379,7 @@ it('emits state events when the state changes', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref}>
|
||||
@@ -448,7 +449,7 @@ it('emits state events when new navigator mounts', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const NestedNavigator = () => {
|
||||
const [isRendered, setIsRendered] = React.useState(false);
|
||||
@@ -537,7 +538,7 @@ it('emits option events when options change with tab router', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref}>
|
||||
@@ -611,7 +612,7 @@ it('emits option events when options change with stack router', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref}>
|
||||
@@ -677,7 +678,7 @@ it('emits option events when options change with stack router', () => {
|
||||
it('throws if there is no navigator rendered', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = <BaseNavigationContainer ref={ref} children={null} />;
|
||||
|
||||
@@ -697,7 +698,7 @@ it('throws if there is no navigator rendered', () => {
|
||||
it("throws if the ref hasn't finished initializing", () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
@@ -733,7 +734,7 @@ it("throws if the ref hasn't finished initializing", () => {
|
||||
});
|
||||
|
||||
it('invokes the unhandled action listener with the unhandled action', () => {
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
const fn = jest.fn();
|
||||
|
||||
const TestNavigator = (props: any) => {
|
||||
@@ -779,7 +780,7 @@ it('works with state change events in independent nested container', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
|
||||
@@ -33,8 +33,10 @@ it('converts state to path string', () => {
|
||||
|
||||
const path = '/foo/bar/baz%20qux?author=jane&valid=true';
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path) as State)).toBe(path);
|
||||
expect(getPathFromState<object>(state)).toBe(path);
|
||||
expect(
|
||||
getPathFromState<object>(getStateFromPath<object>(path) as State)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
it('converts state to path string with config', () => {
|
||||
@@ -97,9 +99,12 @@ it('converts state to path string with config', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -116,8 +121,10 @@ it('handles route without param', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path) as State)).toBe(path);
|
||||
expect(getPathFromState<object>(state)).toBe(path);
|
||||
expect(
|
||||
getPathFromState<object>(getStateFromPath<object>(path) as State)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
it("doesn't add query param for empty params", () => {
|
||||
@@ -131,8 +138,10 @@ it("doesn't add query param for empty params", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path) as State)).toBe(path);
|
||||
expect(getPathFromState<object>(state)).toBe(path);
|
||||
expect(
|
||||
getPathFromState<object>(getStateFromPath<object>(path) as State)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens', () => {
|
||||
@@ -208,9 +217,12 @@ it('handles state with config with nested screens', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -287,9 +299,12 @@ it('handles state with config with nested screens and exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -352,9 +367,12 @@ it('handles state with config with nested screens and unused configs', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -418,9 +436,12 @@ it('handles state with config with nested screens and unused configs with exact'
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -498,9 +519,12 @@ it('handles nested object with stringify in it', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -580,9 +604,12 @@ it('handles nested object with stringify in it with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -623,9 +650,12 @@ it('handles nested object for second route depth', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -669,9 +699,12 @@ it('handles nested object for second route depth with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -719,9 +752,12 @@ it('handles nested object for second route depth and path and stringify in roots
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -774,9 +810,12 @@ it('handles nested object for second route depth and path and stringify in roots
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -805,9 +844,12 @@ it('ignores empty string paths', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -850,9 +892,12 @@ it('keeps query params if path is empty', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toEqual(path);
|
||||
});
|
||||
|
||||
@@ -894,9 +939,12 @@ it('cuts nested configs too', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -939,9 +987,12 @@ it('cuts nested configs too with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -977,9 +1028,12 @@ it('handles empty path at the end', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1012,9 +1066,12 @@ it('returns "/" for empty path', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1042,9 +1099,12 @@ it('parses no path specified', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1121,9 +1181,12 @@ it('strips undefined query params', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1201,9 +1264,12 @@ it('strips undefined query params with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1278,9 +1344,12 @@ it('handles stripping all query params', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1357,9 +1426,12 @@ it('handles stripping all query params with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1380,9 +1452,12 @@ it('replaces undefined query params', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1405,9 +1480,12 @@ it('matches wildcard patterns at root', () => {
|
||||
routes: [{ name: '404' }],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe('/404');
|
||||
expect(getPathFromState<object>(state, config)).toBe('/404');
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe('/404');
|
||||
});
|
||||
|
||||
@@ -1447,9 +1525,12 @@ it('matches wildcard patterns at nested level', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe('/bar/42/404');
|
||||
expect(getPathFromState<object>(state, config)).toBe('/bar/42/404');
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe('/bar/42/404');
|
||||
});
|
||||
|
||||
@@ -1492,9 +1573,12 @@ it('matches wildcard patterns at nested level with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe('/404');
|
||||
expect(getPathFromState<object>(state, config)).toBe('/404');
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe('/404');
|
||||
});
|
||||
|
||||
@@ -1535,9 +1619,12 @@ it('tries to match wildcard patterns at the end', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState<object>(state, config)).toBe(path);
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1570,8 +1657,11 @@ it('uses nearest parent wildcard match for unmatched paths', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe('/404');
|
||||
expect(getPathFromState<object>(state, config)).toBe('/404');
|
||||
expect(
|
||||
getPathFromState(getStateFromPath(path, config) as State, config)
|
||||
getPathFromState<object>(
|
||||
getStateFromPath<object>(path, config) as State,
|
||||
config
|
||||
)
|
||||
).toBe('/404');
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const changePath = <T extends InitialState>(state: T, path: string): T =>
|
||||
});
|
||||
|
||||
it('returns undefined for invalid path', () => {
|
||||
expect(getStateFromPath('//')).toBe(undefined);
|
||||
expect(getStateFromPath<object>('//')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('converts path string to initial state', () => {
|
||||
@@ -41,8 +41,8 @@ it('converts path string to initial state', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state))).toEqual(
|
||||
expect(getStateFromPath<object>(path)).toEqual(state);
|
||||
expect(getStateFromPath<object>(getPathFromState<object>(state))).toEqual(
|
||||
changePath(state, '/foo/bar/baz%20qux?author=jane%20%26%20co&valid=true')
|
||||
);
|
||||
});
|
||||
@@ -106,16 +106,16 @@ it('converts path string to initial state with config', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles leading slash when converting', () => {
|
||||
const path = '/foo/bar/?count=42';
|
||||
|
||||
expect(getStateFromPath(path)).toEqual({
|
||||
expect(getStateFromPath<object>(path)).toEqual({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
@@ -136,7 +136,7 @@ it('handles leading slash when converting', () => {
|
||||
it('handles ending slash when converting', () => {
|
||||
const path = 'foo/bar/?count=42';
|
||||
|
||||
expect(getStateFromPath(path)).toEqual({
|
||||
expect(getStateFromPath<object>(path)).toEqual({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
@@ -167,8 +167,8 @@ it('handles route without param', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state))).toEqual(
|
||||
expect(getStateFromPath<object>(path)).toEqual(state);
|
||||
expect(getStateFromPath<object>(getPathFromState<object>(state))).toEqual(
|
||||
changePath(state, '/foo/bar')
|
||||
);
|
||||
});
|
||||
@@ -245,10 +245,10 @@ it('converts path string to initial state with config with nested screens', () =
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens and unused parse functions', () => {
|
||||
@@ -308,10 +308,10 @@ it('converts path string to initial state with config with nested screens and un
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/foe/baz/Jane?count=10&answer=42&valid=true')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/foe/baz/Jane?count=10&answer=42&valid=true'));
|
||||
});
|
||||
|
||||
it('handles nested object with unused configs and with parse in it', () => {
|
||||
@@ -400,10 +400,10 @@ it('handles nested object with unused configs and with parse in it', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles parse in nested object for second route depth', () => {
|
||||
@@ -450,10 +450,10 @@ it('handles parse in nested object for second route depth', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles parse in nested object for second route depth and and path and parse in roots', () => {
|
||||
@@ -501,10 +501,10 @@ it('handles parse in nested object for second route depth and and path and parse
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles initialRouteName at top level', () => {
|
||||
@@ -545,10 +545,10 @@ it('handles initialRouteName at top level', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles initialRouteName inside a screen', () => {
|
||||
@@ -591,10 +591,10 @@ it('handles initialRouteName inside a screen', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles initialRouteName included in path', () => {
|
||||
@@ -633,10 +633,10 @@ it('handles initialRouteName included in path', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles two initialRouteNames', () => {
|
||||
@@ -728,10 +728,10 @@ it('handles two initialRouteNames', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('accepts initialRouteName without config for it', () => {
|
||||
@@ -823,10 +823,10 @@ it('accepts initialRouteName without config for it', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('returns undefined if path is empty and no matching screen is present', () => {
|
||||
@@ -847,7 +847,7 @@ it('returns undefined if path is empty and no matching screen is present', () =>
|
||||
|
||||
const path = '';
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns matching screen if path is empty', () => {
|
||||
@@ -886,10 +886,10 @@ it('returns matching screen if path is empty', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/'));
|
||||
});
|
||||
|
||||
it('returns matching screen with params if path is empty', () => {
|
||||
@@ -931,10 +931,10 @@ it('returns matching screen with params if path is empty', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/?foo=42')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/?foo=42'));
|
||||
});
|
||||
|
||||
it("doesn't match nested screen if path is empty", () => {
|
||||
@@ -959,7 +959,7 @@ it("doesn't match nested screen if path is empty", () => {
|
||||
|
||||
const path = '';
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('chooses more exhaustive pattern', () => {
|
||||
@@ -1004,10 +1004,10 @@ it('chooses more exhaustive pattern', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles same paths beginnings', () => {
|
||||
@@ -1048,10 +1048,10 @@ it('handles same paths beginnings', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles same paths beginnings with params', () => {
|
||||
@@ -1096,10 +1096,10 @@ it('handles same paths beginnings with params', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles not taking path with too many segments', () => {
|
||||
@@ -1151,10 +1151,10 @@ it('handles not taking path with too many segments', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles differently ordered params v1', () => {
|
||||
@@ -1206,10 +1206,10 @@ it('handles differently ordered params v1', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles differently ordered params v2', () => {
|
||||
@@ -1261,10 +1261,10 @@ it('handles differently ordered params v2', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles differently ordered params v3', () => {
|
||||
@@ -1316,10 +1316,10 @@ it('handles differently ordered params v3', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handles differently ordered params v4', () => {
|
||||
@@ -1371,10 +1371,10 @@ it('handles differently ordered params v4', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/5/foos/res/20')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/5/foos/res/20'));
|
||||
});
|
||||
|
||||
it('handles simple optional params', () => {
|
||||
@@ -1426,10 +1426,10 @@ it('handles simple optional params', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle 2 optional params at the end v1', () => {
|
||||
@@ -1481,10 +1481,10 @@ it('handle 2 optional params at the end v1', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle 2 optional params at the end v2', () => {
|
||||
@@ -1536,10 +1536,10 @@ it('handle 2 optional params at the end v2', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle 2 optional params at the end v3', () => {
|
||||
@@ -1592,10 +1592,10 @@ it('handle 2 optional params at the end v3', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v1', () => {
|
||||
@@ -1648,10 +1648,10 @@ it('handle optional params in the middle v1', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v2', () => {
|
||||
@@ -1704,10 +1704,10 @@ it('handle optional params in the middle v2', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v3', () => {
|
||||
@@ -1761,10 +1761,10 @@ it('handle optional params in the middle v3', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v4', () => {
|
||||
@@ -1818,10 +1818,10 @@ it('handle optional params in the middle v4', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v5', () => {
|
||||
@@ -1875,10 +1875,10 @@ it('handle optional params in the middle v5', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('handle optional params in the beginning v1', () => {
|
||||
@@ -1932,10 +1932,10 @@ it('handle optional params in the beginning v1', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/5/10/foos/15')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/5/10/foos/15'));
|
||||
});
|
||||
|
||||
it('handle optional params in the beginning v2', () => {
|
||||
@@ -1989,10 +1989,10 @@ it('handle optional params in the beginning v2', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/5/10/foos/15')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/5/10/foos/15'));
|
||||
});
|
||||
|
||||
it('merges parent patterns if needed', () => {
|
||||
@@ -2030,10 +2030,10 @@ it('merges parent patterns if needed', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/foo/42/baz/babel')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/foo/42/baz/babel'));
|
||||
});
|
||||
|
||||
it('ignores extra slashes in the pattern', () => {
|
||||
@@ -2067,10 +2067,10 @@ it('ignores extra slashes in the pattern', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('matches wildcard patterns at root', () => {
|
||||
@@ -2092,10 +2092,10 @@ it('matches wildcard patterns at root', () => {
|
||||
routes: [{ name: '404', path }],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/404')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/404'));
|
||||
});
|
||||
|
||||
it('matches wildcard patterns at nested level', () => {
|
||||
@@ -2134,10 +2134,10 @@ it('matches wildcard patterns at nested level', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/bar/42/404')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/bar/42/404'));
|
||||
});
|
||||
|
||||
it('matches wildcard patterns at nested level with exact', () => {
|
||||
@@ -2179,10 +2179,10 @@ it('matches wildcard patterns at nested level with exact', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/404')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/404'));
|
||||
});
|
||||
|
||||
it('tries to match wildcard patterns at the end', () => {
|
||||
@@ -2222,10 +2222,10 @@ it('tries to match wildcard patterns at the end', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('uses nearest parent wildcard match for unmatched paths', () => {
|
||||
@@ -2257,17 +2257,17 @@ it('uses nearest parent wildcard match for unmatched paths', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
changePath(state, '/404')
|
||||
);
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(changePath(state, '/404'));
|
||||
});
|
||||
|
||||
it('throws if two screens map to the same pattern', () => {
|
||||
const path = '/bar/42/baz/test';
|
||||
|
||||
expect(() =>
|
||||
getStateFromPath(path, {
|
||||
getStateFromPath<object>(path, {
|
||||
screens: {
|
||||
Foo: {
|
||||
screens: {
|
||||
@@ -2287,7 +2287,7 @@ it('throws if two screens map to the same pattern', () => {
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
getStateFromPath(path, {
|
||||
getStateFromPath<object>(path, {
|
||||
screens: {
|
||||
Foo: {
|
||||
screens: {
|
||||
@@ -2303,3 +2303,119 @@ it('throws if two screens map to the same pattern', () => {
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('correctly applies initialRouteName for config with similar route names', () => {
|
||||
const path = '/weekly-earnings';
|
||||
|
||||
const config = {
|
||||
screens: {
|
||||
RootTabs: {
|
||||
screens: {
|
||||
HomeTab: {
|
||||
screens: {
|
||||
Home: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
EventDetails: 'event-details/:eventId',
|
||||
},
|
||||
},
|
||||
EarningsTab: {
|
||||
initialRouteName: 'Earnings',
|
||||
path: 'earnings',
|
||||
screens: {
|
||||
Earnings: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'RootTabs',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'HomeTab',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'WeeklyEarnings',
|
||||
path,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
it('correctly applies initialRouteName for config with similar route names v2', () => {
|
||||
const path = '/earnings/weekly-earnings';
|
||||
|
||||
const config = {
|
||||
screens: {
|
||||
RootTabs: {
|
||||
screens: {
|
||||
HomeTab: {
|
||||
initialRouteName: 'Home',
|
||||
screens: {
|
||||
Home: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
},
|
||||
},
|
||||
EarningsTab: {
|
||||
initialRouteName: 'Earnings',
|
||||
path: 'earnings',
|
||||
screens: {
|
||||
Earnings: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'RootTabs',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'EarningsTab',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Earnings',
|
||||
},
|
||||
{
|
||||
name: 'WeeklyEarnings',
|
||||
path,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath<object>(path, config)).toEqual(state);
|
||||
expect(
|
||||
getStateFromPath<object>(getPathFromState<object>(state, config), config)
|
||||
).toEqual(state);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from '@testing-library/react-native';
|
||||
import type { NavigationState } from '@react-navigation/routers';
|
||||
import type { NavigationState, ParamListBase } from '@react-navigation/routers';
|
||||
import Group from '../Group';
|
||||
import Screen from '../Screen';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import createNavigationContainerRef from '../createNavigationContainerRef';
|
||||
import useNavigation from '../useNavigation';
|
||||
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
|
||||
import type { NavigationContainerRef } from '../types';
|
||||
|
||||
beforeEach(() => (MockRouterKey.current = 0));
|
||||
|
||||
@@ -248,6 +249,53 @@ it('initializes state for nested screens in React.Fragment', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes state for nested screens in Group', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestScreen = (props: any) => {
|
||||
React.useEffect(() => {
|
||||
props.navigation.dispatch({ type: 'UPDATE' });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer onStateChange={onStateChange}>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={TestScreen} />
|
||||
<Group>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
<Screen name="baz" component={jest.fn()} />
|
||||
</Group>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element).update(element);
|
||||
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes state for nested navigator on navigation', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
@@ -679,7 +727,7 @@ it('navigates to nested child in a navigator', () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = render(
|
||||
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||
@@ -715,7 +763,7 @@ it('navigates to nested child in a navigator', () => {
|
||||
expect(element).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||
|
||||
act(() =>
|
||||
navigation.current?.navigate('bar', {
|
||||
navigation.navigate('bar', {
|
||||
screen: 'bar-b',
|
||||
params: { test: 42 },
|
||||
})
|
||||
@@ -726,7 +774,7 @@ it('navigates to nested child in a navigator', () => {
|
||||
);
|
||||
|
||||
act(() =>
|
||||
navigation.current?.navigate('bar', {
|
||||
navigation.navigate('bar', {
|
||||
screen: 'bar-a',
|
||||
params: { whoa: 'test' },
|
||||
})
|
||||
@@ -736,15 +784,15 @@ it('navigates to nested child in a navigator', () => {
|
||||
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
|
||||
);
|
||||
|
||||
act(() => navigation.current?.navigate('bar', { screen: 'bar-b' }));
|
||||
act(() => navigation.navigate('bar', { screen: 'bar-b' }));
|
||||
|
||||
act(() => navigation.current?.goBack());
|
||||
act(() => navigation.goBack());
|
||||
|
||||
expect(element).toMatchInlineSnapshot(
|
||||
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
|
||||
);
|
||||
|
||||
act(() => navigation.current?.navigate('bar', { screen: 'bar-b' }));
|
||||
act(() => navigation.navigate('bar', { screen: 'bar-b' }));
|
||||
|
||||
expect(element).toMatchInlineSnapshot(
|
||||
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42,\\"whoa\\":\\"test\\"}]"`
|
||||
@@ -799,7 +847,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const first = render(
|
||||
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||
@@ -833,7 +881,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
);
|
||||
|
||||
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -866,7 +914,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
});
|
||||
|
||||
act(() =>
|
||||
navigation.current?.navigate('bar', {
|
||||
navigation.navigate('bar', {
|
||||
screen: 'bar-b',
|
||||
params: { test: 42 },
|
||||
})
|
||||
@@ -876,7 +924,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
|
||||
);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 2,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -944,7 +992,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
);
|
||||
|
||||
expect(second).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 0,
|
||||
key: '4',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -971,7 +1019,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
});
|
||||
|
||||
act(() =>
|
||||
navigation.current?.navigate('bar', {
|
||||
navigation.navigate('bar', {
|
||||
screen: 'bar-b',
|
||||
params: { test: 42 },
|
||||
initial: false,
|
||||
@@ -980,7 +1028,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
|
||||
expect(second).toMatchInlineSnapshot(`"[bar-b, {\\"test\\":42}]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 2,
|
||||
key: '4',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -1071,7 +1119,7 @@ it('navigates to nested child in a navigator with initial: false', () => {
|
||||
|
||||
expect(third).toMatchInlineSnapshot(`"[bar-b, {\\"some\\":\\"stuff\\"}]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 1,
|
||||
key: '11',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -1119,7 +1167,7 @@ it('resets state of a nested child in a navigator', () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const first = render(
|
||||
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||
@@ -1150,7 +1198,7 @@ it('resets state of a nested child in a navigator', () => {
|
||||
|
||||
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -1183,7 +1231,7 @@ it('resets state of a nested child in a navigator', () => {
|
||||
});
|
||||
|
||||
act(() =>
|
||||
navigation.current?.navigate('bar', {
|
||||
navigation.navigate('bar', {
|
||||
state: {
|
||||
routes: [{ name: 'bar-a' }, { name: 'bar-b' }],
|
||||
},
|
||||
@@ -1192,7 +1240,7 @@ it('resets state of a nested child in a navigator', () => {
|
||||
|
||||
expect(first).toMatchInlineSnapshot(`"[bar-a, undefined]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 1,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -1231,7 +1279,7 @@ it('resets state of a nested child in a navigator', () => {
|
||||
});
|
||||
|
||||
act(() =>
|
||||
navigation.current?.navigate('bar', {
|
||||
navigation.navigate('bar', {
|
||||
state: {
|
||||
index: 2,
|
||||
routes: [
|
||||
@@ -1245,7 +1293,7 @@ it('resets state of a nested child in a navigator', () => {
|
||||
|
||||
expect(first).toMatchInlineSnapshot(`"[bar-a, {\\"test\\":18}]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
expect(navigation.getRootState()).toEqual({
|
||||
index: 1,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -1336,7 +1384,7 @@ it('preserves order of screens in state with non-numeric names', () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const root = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
@@ -1350,11 +1398,7 @@ it('preserves order of screens in state with non-numeric names', () => {
|
||||
|
||||
render(root);
|
||||
|
||||
expect(navigation.current?.getRootState().routeNames).toEqual([
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
]);
|
||||
expect(navigation.getRootState().routeNames).toEqual(['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
it('preserves order of screens in state with numeric names', () => {
|
||||
@@ -1363,7 +1407,7 @@ it('preserves order of screens in state with numeric names', () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const root = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
@@ -1377,11 +1421,7 @@ it('preserves order of screens in state with numeric names', () => {
|
||||
|
||||
render(root);
|
||||
|
||||
expect(navigation.current?.getRootState().routeNames).toEqual([
|
||||
'4',
|
||||
'7',
|
||||
'1',
|
||||
]);
|
||||
expect(navigation.getRootState().routeNames).toEqual(['4', '7', '1']);
|
||||
});
|
||||
|
||||
it("throws if navigator doesn't have any screens", () => {
|
||||
@@ -1458,7 +1498,7 @@ it('throws when Screen is not the direct children', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'Bar')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'Bar')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1483,7 +1523,7 @@ it('throws when undefined component is a direct children', () => {
|
||||
spy.mockRestore();
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'undefined' for the screen 'foo')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'undefined' for the screen 'foo')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1503,7 +1543,7 @@ it('throws when a tag is a direct children', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'screen' for the screen 'foo')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'screen' for the screen 'foo')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1523,7 +1563,7 @@ it('throws when a React Element is not the direct children', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'Hello world')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'Hello world')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1801,7 +1841,7 @@ it('returns currently focused route with getCurrentRoute', () => {
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
@@ -1824,7 +1864,7 @@ it('returns currently focused route with getCurrentRoute', () => {
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.current?.getCurrentRoute()).toEqual({
|
||||
expect(navigation.getCurrentRoute()).toEqual({
|
||||
key: 'bar-a',
|
||||
name: 'bar-a',
|
||||
});
|
||||
@@ -1839,26 +1879,23 @@ it("returns focused screen's options with getCurrentOptions when focused screen
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: 'data' })}
|
||||
>
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: 'data' }}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: 'data' }}
|
||||
options={{ sample2: '2' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
@@ -1870,16 +1907,123 @@ it("returns focused screen's options with getCurrentOptions when focused screen
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.current?.getCurrentOptions()).toEqual({
|
||||
sample: 'data',
|
||||
sample2: 'data',
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
});
|
||||
|
||||
act(() => navigation.current?.navigate('bar-b'));
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.current?.getCurrentOptions()).toEqual({
|
||||
sample2: 'data',
|
||||
sample3: 'data',
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when focused screen is rendered when using screenOptions", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when focused screen is rendered when using Group", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Group screenOptions={{ sample4: '4' }}>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</Group>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
sample4: '4',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1892,26 +2036,23 @@ it("returns focused screen's options with getCurrentOptions when all screens are
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: 'data' })}
|
||||
>
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: 'data' }}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: 'data' }}
|
||||
options={{ sample2: '2' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
@@ -1923,16 +2064,123 @@ it("returns focused screen's options with getCurrentOptions when all screens are
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.current?.getCurrentOptions()).toEqual({
|
||||
sample: 'data',
|
||||
sample2: 'data',
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
});
|
||||
|
||||
act(() => navigation.current?.navigate('bar-b'));
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.current?.getCurrentOptions()).toEqual({
|
||||
sample2: 'data',
|
||||
sample3: 'data',
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when all screens are rendered with screenOptions", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return <>{state.routes.map((route) => descriptors[route.key].render())}</>;
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when all screens are rendered with Group", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return <>{state.routes.map((route) => descriptors[route.key].render())}</>;
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Group screenOptions={{ sample4: '4' }}>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</Group>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
sample4: '4',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1945,7 +2193,7 @@ it('does not throw if while getting current options with no options defined', ()
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
@@ -1968,11 +2216,11 @@ it('does not throw if while getting current options with no options defined', ()
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.current?.getCurrentOptions()).toEqual({});
|
||||
expect(navigation.getCurrentOptions()).toEqual({});
|
||||
});
|
||||
|
||||
it('does not throw if while getting current options with empty container', () => {
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation} children={null} />
|
||||
@@ -1980,5 +2228,5 @@ it('does not throw if while getting current options with empty container', () =>
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.current?.getCurrentOptions()).toEqual(undefined);
|
||||
expect(navigation.getCurrentOptions()).toEqual(undefined);
|
||||
});
|
||||
|
||||
@@ -5,15 +5,16 @@ import {
|
||||
DefaultRouterOptions,
|
||||
NavigationState,
|
||||
StackRouter,
|
||||
ParamListBase,
|
||||
} from '@react-navigation/routers';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import createNavigationContainerRef from '../createNavigationContainerRef';
|
||||
import MockRouter, {
|
||||
MockActions,
|
||||
MockRouterKey,
|
||||
} from './__fixtures__/MockRouter';
|
||||
import type { NavigationContainerRef } from '../types';
|
||||
|
||||
jest.mock('nanoid/non-secure', () => {
|
||||
const m = { nanoid: () => String(++m.__key), __key: 0 };
|
||||
@@ -571,7 +572,7 @@ it("prevents removing a screen with 'beforeRemove' event", () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||
@@ -706,7 +707,7 @@ it("prevents removing a child screen with 'beforeRemove' event", () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||
@@ -867,7 +868,7 @@ it("prevents removing a grand child screen with 'beforeRemove' event", () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||
@@ -1065,7 +1066,7 @@ it("prevents removing by multiple screens with 'beforeRemove' event", () => {
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||
@@ -1217,7 +1218,7 @@ it("prevents removing a child screen with 'beforeRemove' event with 'resetRoot'"
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
const ref = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||
|
||||
47
packages/core/src/createNavigationContainerRef.tsx
Normal file
47
packages/core/src/createNavigationContainerRef.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { CommonActions } from '@react-navigation/routers';
|
||||
import type { NavigationContainerRefWithCurrent } from './types';
|
||||
|
||||
export const NOT_INITIALIZED_ERROR =
|
||||
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
|
||||
|
||||
export default function createNavigationContainerRef<
|
||||
ParamList extends {} = ReactNavigation.RootParamList
|
||||
>(): NavigationContainerRefWithCurrent<ParamList> {
|
||||
const methods = [
|
||||
...Object.keys(CommonActions),
|
||||
'addListener',
|
||||
'removeListener',
|
||||
'resetRoot',
|
||||
'dispatch',
|
||||
'canGoBack',
|
||||
'getRootState',
|
||||
'getState',
|
||||
'getParent',
|
||||
'getCurrentRoute',
|
||||
'getCurrentOptions',
|
||||
] as const;
|
||||
|
||||
const ref: NavigationContainerRefWithCurrent<ParamList> = {
|
||||
...methods.reduce<any>((acc, name) => {
|
||||
acc[name] = (...args: any[]) => {
|
||||
if (ref.current == null) {
|
||||
console.error(NOT_INITIALIZED_ERROR);
|
||||
} else {
|
||||
// @ts-expect-error: this is ok
|
||||
return ref.current[name](...args);
|
||||
}
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
isReady: () => {
|
||||
if (ref.current == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ref.current.isReady();
|
||||
},
|
||||
current: null,
|
||||
};
|
||||
|
||||
return ref;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type * as React from 'react';
|
||||
import type { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||
import Group from './Group';
|
||||
import Screen from './Screen';
|
||||
import type { TypedNavigator, EventMapBase } from './types';
|
||||
|
||||
@@ -31,6 +32,7 @@ export default function createNavigatorFactory<
|
||||
|
||||
return {
|
||||
Navigator,
|
||||
Group,
|
||||
Screen,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,7 +13,10 @@ type ConfigItem = {
|
||||
screens?: Record<string, ConfigItem>;
|
||||
};
|
||||
|
||||
type Options = { initialRouteName?: string; screens: PathConfigMap };
|
||||
type Options = {
|
||||
initialRouteName?: string;
|
||||
screens: PathConfigMap<object>;
|
||||
};
|
||||
|
||||
type NavigateAction<State extends NavigationState> = {
|
||||
type: 'NAVIGATE';
|
||||
@@ -29,7 +32,9 @@ export default function getActionFromState(
|
||||
options?: Options
|
||||
): NavigateAction<NavigationState> | CommonActions.Action | undefined {
|
||||
// Create a normalized configs object which will be easier to use
|
||||
const normalizedConfig = options ? createNormalizedConfigItem(options) : {};
|
||||
const normalizedConfig = options
|
||||
? createNormalizedConfigItem(options as PathConfig<object> | string)
|
||||
: {};
|
||||
|
||||
const routes =
|
||||
state.index != null ? state.routes.slice(0, state.index + 1) : state.routes;
|
||||
@@ -130,7 +135,7 @@ export default function getActionFromState(
|
||||
};
|
||||
}
|
||||
|
||||
const createNormalizedConfigItem = (config: PathConfig | string) =>
|
||||
const createNormalizedConfigItem = (config: PathConfig<object> | string) =>
|
||||
typeof config === 'object' && config != null
|
||||
? {
|
||||
initialRouteName: config.initialRouteName,
|
||||
@@ -141,7 +146,7 @@ const createNormalizedConfigItem = (config: PathConfig | string) =>
|
||||
}
|
||||
: {};
|
||||
|
||||
const createNormalizedConfigs = (options: PathConfigMap) =>
|
||||
const createNormalizedConfigs = (options: PathConfigMap<object>) =>
|
||||
Object.entries(options).reduce<Record<string, ConfigItem>>((acc, [k, v]) => {
|
||||
acc[k] = createNormalizedConfigItem(v);
|
||||
return acc;
|
||||
|
||||
@@ -7,7 +7,10 @@ import type {
|
||||
import fromEntries from './fromEntries';
|
||||
import type { PathConfig, PathConfigMap } from './types';
|
||||
|
||||
type Options = { initialRouteName?: string; screens: PathConfigMap };
|
||||
type Options<ParamList> = {
|
||||
initialRouteName?: string;
|
||||
screens: PathConfigMap<ParamList>;
|
||||
};
|
||||
|
||||
type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
||||
|
||||
@@ -61,9 +64,9 @@ const getActiveRoute = (state: State): { name: string; params?: object } => {
|
||||
* @param options Extra options to fine-tune how to serialize the path.
|
||||
* @returns Path representing the state, e.g. /foo/bar?count=42.
|
||||
*/
|
||||
export default function getPathFromState(
|
||||
export default function getPathFromState<ParamList extends {}>(
|
||||
state: State,
|
||||
options?: Options
|
||||
options?: Options<ParamList>
|
||||
): string {
|
||||
if (state == null) {
|
||||
throw Error(
|
||||
@@ -238,7 +241,7 @@ const joinPaths = (...paths: string[]): string =>
|
||||
.join('/');
|
||||
|
||||
const createConfigItem = (
|
||||
config: PathConfig | string,
|
||||
config: PathConfig<object> | string,
|
||||
parentPattern?: string
|
||||
): ConfigItem => {
|
||||
if (typeof config === 'string') {
|
||||
@@ -276,7 +279,7 @@ const createConfigItem = (
|
||||
};
|
||||
|
||||
const createNormalizedConfigs = (
|
||||
options: PathConfigMap,
|
||||
options: PathConfigMap<object>,
|
||||
pattern?: string
|
||||
): Record<string, ConfigItem> =>
|
||||
fromEntries(
|
||||
|
||||
@@ -8,9 +8,9 @@ import type {
|
||||
import findFocusedRoute from './findFocusedRoute';
|
||||
import type { PathConfigMap } from './types';
|
||||
|
||||
type Options = {
|
||||
type Options<ParamList extends {}> = {
|
||||
initialRouteName?: string;
|
||||
screens: PathConfigMap;
|
||||
screens: PathConfigMap<ParamList>;
|
||||
};
|
||||
|
||||
type ParseConfig = Record<string, (value: string) => any>;
|
||||
@@ -26,7 +26,7 @@ type RouteConfig = {
|
||||
|
||||
type InitialRouteConfig = {
|
||||
initialRouteName: string;
|
||||
connectedRoutes: string[];
|
||||
parentScreens: string[];
|
||||
};
|
||||
|
||||
type ResultState = PartialState<NavigationState> & {
|
||||
@@ -60,16 +60,16 @@ type ParsedRoute = {
|
||||
* @param path Path string to parse and convert, e.g. /foo/bar?count=42.
|
||||
* @param options Extra options to fine-tune how to parse the path.
|
||||
*/
|
||||
export default function getStateFromPath(
|
||||
export default function getStateFromPath<ParamList extends {}>(
|
||||
path: string,
|
||||
options?: Options
|
||||
options?: Options<ParamList>
|
||||
): ResultState | undefined {
|
||||
let initialRoutes: InitialRouteConfig[] = [];
|
||||
|
||||
if (options?.initialRouteName) {
|
||||
initialRoutes.push({
|
||||
initialRouteName: options.initialRouteName,
|
||||
connectedRoutes: Object.keys(options.screens),
|
||||
parentScreens: [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,9 +106,10 @@ export default function getStateFromPath(
|
||||
...Object.keys(screens).map((key) =>
|
||||
createNormalizedConfigs(
|
||||
key,
|
||||
screens as PathConfigMap,
|
||||
screens as PathConfigMap<object>,
|
||||
[],
|
||||
initialRoutes
|
||||
initialRoutes,
|
||||
[]
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -306,15 +307,19 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
|
||||
|
||||
const createNormalizedConfigs = (
|
||||
screen: string,
|
||||
routeConfig: PathConfigMap,
|
||||
routeConfig: PathConfigMap<object>,
|
||||
routeNames: string[] = [],
|
||||
initials: InitialRouteConfig[],
|
||||
parentScreens: string[],
|
||||
parentPattern?: string
|
||||
): RouteConfig[] => {
|
||||
const configs: RouteConfig[] = [];
|
||||
|
||||
routeNames.push(screen);
|
||||
|
||||
parentScreens.push(screen);
|
||||
|
||||
// @ts-expect-error: we can't strongly typecheck this for now
|
||||
const config = routeConfig[screen];
|
||||
|
||||
if (typeof config === 'string') {
|
||||
@@ -341,7 +346,13 @@ const createNormalizedConfigs = (
|
||||
: config.path || '';
|
||||
|
||||
configs.push(
|
||||
createConfigItem(screen, routeNames, pattern, config.path, config.parse)
|
||||
createConfigItem(
|
||||
screen,
|
||||
routeNames,
|
||||
pattern!,
|
||||
config.path,
|
||||
config.parse
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -350,16 +361,17 @@ const createNormalizedConfigs = (
|
||||
if (config.initialRouteName) {
|
||||
initials.push({
|
||||
initialRouteName: config.initialRouteName,
|
||||
connectedRoutes: Object.keys(config.screens),
|
||||
parentScreens,
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(config.screens).forEach((nestedConfig) => {
|
||||
const result = createNormalizedConfigs(
|
||||
nestedConfig,
|
||||
config.screens as PathConfigMap,
|
||||
config.screens as PathConfigMap<object>,
|
||||
routeNames,
|
||||
initials,
|
||||
[...parentScreens],
|
||||
pattern ?? parentPattern
|
||||
);
|
||||
|
||||
@@ -425,13 +437,23 @@ const findParseConfigForRoute = (
|
||||
// Try to find an initial route connected with the one passed
|
||||
const findInitialRoute = (
|
||||
routeName: string,
|
||||
parentScreens: string[],
|
||||
initialRoutes: InitialRouteConfig[]
|
||||
): string | undefined => {
|
||||
for (const config of initialRoutes) {
|
||||
if (config.connectedRoutes.includes(routeName)) {
|
||||
return config.initialRouteName === routeName
|
||||
? undefined
|
||||
: config.initialRouteName;
|
||||
if (parentScreens.length === config.parentScreens.length) {
|
||||
let sameParents = true;
|
||||
for (let i = 0; i < parentScreens.length; i++) {
|
||||
if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
|
||||
sameParents = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sameParents) {
|
||||
return routeName !== config.initialRouteName
|
||||
? config.initialRouteName
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -477,7 +499,11 @@ const createNestedStateObject = (
|
||||
) => {
|
||||
let state: InitialState;
|
||||
let route = routes.shift() as ParsedRoute;
|
||||
let initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||
const parentScreens: string[] = [];
|
||||
|
||||
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
||||
|
||||
parentScreens.push(route.name);
|
||||
|
||||
state = createStateObject(initialRoute, route, routes.length === 0);
|
||||
|
||||
@@ -485,7 +511,7 @@ const createNestedStateObject = (
|
||||
let nestedState = state;
|
||||
|
||||
while ((route = routes.shift() as ParsedRoute)) {
|
||||
initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
||||
|
||||
const nestedStateIndex =
|
||||
nestedState.index || nestedState.routes.length - 1;
|
||||
@@ -500,6 +526,8 @@ const createNestedStateObject = (
|
||||
nestedState = nestedState.routes[nestedStateIndex]
|
||||
.state as InitialState;
|
||||
}
|
||||
|
||||
parentScreens.push(route.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ export * from '@react-navigation/routers';
|
||||
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
|
||||
export { default as createNavigatorFactory } from './createNavigatorFactory';
|
||||
|
||||
export { default as createNavigationContainerRef } from './createNavigationContainerRef';
|
||||
export { default as useNavigationContainerRef } from './useNavigationContainerRef';
|
||||
|
||||
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
|
||||
export { default as NavigationContext } from './NavigationContext';
|
||||
export { default as NavigationRouteContext } from './NavigationRouteContext';
|
||||
|
||||
@@ -9,13 +9,23 @@ import type {
|
||||
ParamListBase,
|
||||
} from '@react-navigation/routers';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace ReactNavigation {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface RootParamList {}
|
||||
}
|
||||
}
|
||||
|
||||
type Keyof<T extends {}> = Extract<keyof T, string>;
|
||||
|
||||
export type DefaultNavigatorOptions<
|
||||
ScreenOptions extends {},
|
||||
ParamList extends ParamListBase = ParamListBase
|
||||
> = DefaultRouterOptions<Extract<keyof ParamList, string>> & {
|
||||
> = DefaultRouterOptions<Keyof<ParamList>> & {
|
||||
/**
|
||||
* Children React Elements to extract the route configuration from.
|
||||
* Only `Screen` components are supported as children.
|
||||
* Only `Screen`, `Group` and `React.Fragment` are supported as children.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
@@ -24,7 +34,7 @@ export type DefaultNavigatorOptions<
|
||||
screenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, keyof ParamList>;
|
||||
route: RouteProp<ParamList>;
|
||||
navigation: any;
|
||||
}) => ScreenOptions);
|
||||
/**
|
||||
@@ -34,7 +44,7 @@ export type DefaultNavigatorOptions<
|
||||
defaultScreenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, keyof ParamList>;
|
||||
route: RouteProp<ParamList>;
|
||||
navigation: any;
|
||||
options: ScreenOptions;
|
||||
}) => ScreenOptions);
|
||||
@@ -96,11 +106,11 @@ export type EventConsumer<EventMap extends EventMapBase> = {
|
||||
* @param type Type of the event (e.g. `focus`, `blur`)
|
||||
* @param callback Callback listener which is executed upon receiving the event.
|
||||
*/
|
||||
addListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
addListener<EventName extends Keyof<EventMap>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): () => void;
|
||||
removeListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
removeListener<EventName extends Keyof<EventMap>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): void;
|
||||
@@ -115,7 +125,7 @@ export type EventEmitter<EventMap extends EventMapBase> = {
|
||||
* @param [options.target] Key of the target route which should receive the event.
|
||||
* If not specified, all routes receive the event.
|
||||
*/
|
||||
emit<EventName extends Extract<keyof EventMap, string>>(
|
||||
emit<EventName extends Keyof<EventMap>>(
|
||||
options: {
|
||||
type: EventName;
|
||||
target?: string;
|
||||
@@ -263,8 +273,8 @@ export type NavigationContainerProps = {
|
||||
};
|
||||
|
||||
export type NavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string,
|
||||
ParamList extends {},
|
||||
RouteName extends keyof ParamList = Keyof<ParamList>,
|
||||
State extends NavigationState = NavigationState<ParamList>,
|
||||
ScreenOptions extends {} = {},
|
||||
EventMap extends EventMapBase = {}
|
||||
@@ -289,7 +299,7 @@ export type NavigationProp<
|
||||
|
||||
export type RouteProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList
|
||||
RouteName extends keyof ParamList = Keyof<ParamList>
|
||||
> = Route<Extract<RouteName, string>, ParamList[RouteName]>;
|
||||
|
||||
export type CompositeNavigationProp<
|
||||
@@ -325,6 +335,19 @@ export type CompositeNavigationProp<
|
||||
A extends NavigationProp<any, any, any, any, infer E> ? E : {}
|
||||
>;
|
||||
|
||||
export type CompositeScreenProps<
|
||||
A extends {
|
||||
navigation: NavigationProp<ParamListBase, string, any, any>;
|
||||
route: RouteProp<ParamListBase>;
|
||||
},
|
||||
B extends {
|
||||
navigation: NavigationHelpersCommon<ParamListBase, any>;
|
||||
}
|
||||
> = {
|
||||
navigation: CompositeNavigationProp<A['navigation'], B['navigation']>;
|
||||
route: A['route'];
|
||||
};
|
||||
|
||||
export type Descriptor<
|
||||
ScreenOptions extends {},
|
||||
Navigation extends NavigationProp<any, any, any, any, any>,
|
||||
@@ -361,6 +384,38 @@ export type ScreenListeners<
|
||||
}
|
||||
>;
|
||||
|
||||
export type RouteConfigComponent<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList
|
||||
> =
|
||||
| {
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
getComponent?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Lazily get a React component to render for this screen.
|
||||
*/
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
component?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Render callback to render content of this screen.
|
||||
*/
|
||||
children: (props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: any;
|
||||
}) => React.ReactNode;
|
||||
component?: never;
|
||||
getComponent?: never;
|
||||
};
|
||||
|
||||
export type RouteConfig<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList,
|
||||
@@ -405,35 +460,27 @@ export type RouteConfig<
|
||||
* Initial params object for the route.
|
||||
*/
|
||||
initialParams?: Partial<ParamList[RouteName]>;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
getComponent?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Lazily get a React component to render for this screen.
|
||||
*/
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
component?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Render callback to render content of this screen.
|
||||
*/
|
||||
children: (props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
} & RouteConfigComponent<ParamList, RouteName>;
|
||||
|
||||
export type RouteGroupConfig<
|
||||
ParamList extends ParamListBase,
|
||||
ScreenOptions extends {}
|
||||
> = {
|
||||
/**
|
||||
* Navigator options for this screen.
|
||||
*/
|
||||
screenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, keyof ParamList>;
|
||||
navigation: any;
|
||||
}) => React.ReactNode;
|
||||
component?: never;
|
||||
getComponent?: never;
|
||||
}
|
||||
);
|
||||
}) => ScreenOptions);
|
||||
/**
|
||||
* Children React Elements to extract the route configuration from.
|
||||
* Only `Screen`, `Group` and `React.Fragment` are supported as children.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export type NavigationContainerEventMap = {
|
||||
/**
|
||||
@@ -470,7 +517,9 @@ export type NavigationContainerEventMap = {
|
||||
};
|
||||
};
|
||||
|
||||
export type NavigationContainerRef = NavigationHelpers<ParamListBase> &
|
||||
export type NavigationContainerRef<
|
||||
ParamList extends {}
|
||||
> = NavigationHelpers<ParamList> &
|
||||
EventConsumer<NavigationContainerEventMap> & {
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
@@ -490,8 +539,18 @@ export type NavigationContainerRef = NavigationHelpers<ParamListBase> &
|
||||
* Get the currently focused route's options.
|
||||
*/
|
||||
getCurrentOptions(): object | undefined;
|
||||
/**
|
||||
* Whether the navigation container is ready to handle actions.
|
||||
*/
|
||||
isReady(): boolean;
|
||||
};
|
||||
|
||||
export type NavigationContainerRefWithCurrent<
|
||||
ParamList extends {}
|
||||
> = NavigationContainerRef<ParamList> & {
|
||||
current: NavigationContainerRef<ParamList> | null;
|
||||
};
|
||||
|
||||
export type TypedNavigator<
|
||||
ParamList extends ParamListBase,
|
||||
State extends NavigationState,
|
||||
@@ -509,6 +568,10 @@ export type TypedNavigator<
|
||||
> &
|
||||
DefaultNavigatorOptions<ScreenOptions, ParamList>
|
||||
>;
|
||||
/**
|
||||
* Component used for grouping multiple route configuration.
|
||||
*/
|
||||
Group: React.ComponentType<RouteGroupConfig<ParamList, ScreenOptions>>;
|
||||
/**
|
||||
* Component used for specifying route configuration.
|
||||
*/
|
||||
@@ -546,15 +609,20 @@ export type NavigatorScreenParams<
|
||||
};
|
||||
}[keyof ParamList];
|
||||
|
||||
export type PathConfig = {
|
||||
export type PathConfig<ParamList extends {}> = {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
parse?: Record<string, (value: string) => any>;
|
||||
stringify?: Record<string, (value: any) => string>;
|
||||
screens?: PathConfigMap;
|
||||
initialRouteName?: string;
|
||||
screens?: PathConfigMap<ParamList>;
|
||||
initialRouteName?: keyof ParamList;
|
||||
};
|
||||
|
||||
export type PathConfigMap = {
|
||||
[routeName: string]: string | PathConfig;
|
||||
export type PathConfigMap<ParamList extends {}> = {
|
||||
[RouteName in keyof ParamList]?: ParamList[RouteName] extends NavigatorScreenParams<
|
||||
infer T,
|
||||
any
|
||||
>
|
||||
? string | PathConfig<T>
|
||||
: string | Omit<PathConfig<{}>, 'screens' | 'initialRouteName'>;
|
||||
};
|
||||
|
||||
30
packages/core/src/useComponent.tsx
Normal file
30
packages/core/src/useComponent.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function useComponent<
|
||||
T extends React.ComponentType<any>,
|
||||
P extends {}
|
||||
>(Component: T, props: P) {
|
||||
const propsRef = React.useRef<P | null>(props);
|
||||
|
||||
// Normally refs shouldn't be mutated in render
|
||||
// But we return a component which will be rendered
|
||||
// So it's just for immediate consumption
|
||||
propsRef.current = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
propsRef.current = null;
|
||||
});
|
||||
|
||||
return React.useRef((rest: Omit<React.ComponentProps<T>, keyof P>) => {
|
||||
const props = propsRef.current;
|
||||
|
||||
if (props === null) {
|
||||
throw new Error(
|
||||
'The returned component must be rendered in the same render phase as the hook.'
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error: the props should be fine here
|
||||
return <Component {...props} {...rest} />;
|
||||
}).current;
|
||||
}
|
||||
@@ -13,11 +13,7 @@ type Options = {
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: Record<
|
||||
string,
|
||||
Descriptor<
|
||||
object,
|
||||
NavigationProp<ParamListBase>,
|
||||
RouteProp<ParamListBase, string>
|
||||
>
|
||||
Descriptor<object, NavigationProp<ParamListBase>, RouteProp<ParamListBase>>
|
||||
>;
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,22 @@ import type {
|
||||
NavigationProp,
|
||||
} from './types';
|
||||
|
||||
export type ScreenConfigWithParent<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
> = [
|
||||
(ScreenOptionsOrCallback<ScreenOptions> | undefined)[] | undefined,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
];
|
||||
|
||||
type ScreenOptionsOrCallback<ScreenOptions extends {}> =
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
navigation: any;
|
||||
}) => ScreenOptions);
|
||||
|
||||
type Options<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
@@ -32,19 +48,14 @@ type Options<
|
||||
state: State;
|
||||
screens: Record<
|
||||
string,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
ScreenConfigWithParent<State, ScreenOptions, EventMap>
|
||||
>;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
screenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
navigation: any;
|
||||
}) => ScreenOptions);
|
||||
screenOptions?: ScreenOptionsOrCallback<ScreenOptions>;
|
||||
defaultScreenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
route: RouteProp<ParamListBase>;
|
||||
navigation: any;
|
||||
options: ScreenOptions;
|
||||
}) => ScreenOptions);
|
||||
@@ -133,33 +144,35 @@ export default function useDescriptors<
|
||||
ScreenOptions,
|
||||
NavigationProp<ParamListBase, string, State, ScreenOptions, EventMap> &
|
||||
ActionHelpers,
|
||||
RouteProp<ParamListBase, string>
|
||||
RouteProp<ParamListBase>
|
||||
>
|
||||
>
|
||||
>((acc, route, i) => {
|
||||
const screen = screens[route.name];
|
||||
const config = screens[route.name];
|
||||
const screen = config[1];
|
||||
const navigation = navigations[route.key];
|
||||
|
||||
const customOptions = {
|
||||
const optionsList = [
|
||||
// The default `screenOptions` passed to the navigator
|
||||
...(typeof screenOptions === 'object' || screenOptions == null
|
||||
? screenOptions
|
||||
: // @ts-expect-error: this is a function, but typescript doesn't think so
|
||||
screenOptions({
|
||||
route,
|
||||
navigation,
|
||||
})),
|
||||
// The `options` prop passed to `Screen` elements
|
||||
...(typeof screen.options === 'object' || screen.options == null
|
||||
? screen.options
|
||||
: // @ts-expect-error: this is a function, but typescript doesn't think so
|
||||
screen.options({
|
||||
route,
|
||||
navigation,
|
||||
})),
|
||||
screenOptions,
|
||||
// The `screenOptions` props passed to `Group` elements
|
||||
...((config[0]
|
||||
? config[0].filter(Boolean)
|
||||
: []) as ScreenOptionsOrCallback<ScreenOptions>[]),
|
||||
// The `options` prop passed to `Screen` elements,
|
||||
screen.options,
|
||||
// The options set via `navigation.setOptions`
|
||||
...options[route.key],
|
||||
};
|
||||
options[route.key],
|
||||
];
|
||||
|
||||
const customOptions = optionsList.reduce<ScreenOptions>(
|
||||
(acc, curr) =>
|
||||
Object.assign(
|
||||
acc,
|
||||
typeof curr !== 'function' ? curr : curr({ route, navigation })
|
||||
),
|
||||
{} as ScreenOptions
|
||||
);
|
||||
|
||||
const mergedOptions = {
|
||||
...(typeof defaultScreenOptions === 'function'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import type { ParamListBase } from '@react-navigation/routers';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import type { NavigationProp } from './types';
|
||||
|
||||
@@ -9,7 +8,7 @@ import type { NavigationProp } from './types';
|
||||
* @returns Navigation prop of the parent screen.
|
||||
*/
|
||||
export default function useNavigation<
|
||||
T extends NavigationProp<ParamListBase>
|
||||
T = NavigationProp<ReactNavigation.RootParamList>
|
||||
>(): T {
|
||||
const navigation = React.useContext(NavigationContext);
|
||||
|
||||
@@ -19,5 +18,6 @@ export default function useNavigation<
|
||||
);
|
||||
}
|
||||
|
||||
return navigation as T;
|
||||
// FIXME: Figure out a better way to do this
|
||||
return (navigation as unknown) as T;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
} from '@react-navigation/routers';
|
||||
import NavigationStateContext from './NavigationStateContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import NavigationHelpersContext from './NavigationHelpersContext';
|
||||
import Group from './Group';
|
||||
import Screen from './Screen';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useRegisterNavigator from './useRegisterNavigator';
|
||||
import useDescriptors from './useDescriptors';
|
||||
import useDescriptors, { ScreenConfigWithParent } from './useDescriptors';
|
||||
import useNavigationHelpers from './useNavigationHelpers';
|
||||
import useOnAction from './useOnAction';
|
||||
import useFocusEvents from './useFocusEvents';
|
||||
@@ -28,6 +30,7 @@ import useKeyedChildListeners from './useKeyedChildListeners';
|
||||
import useOnGetState from './useOnGetState';
|
||||
import useScheduleUpdate from './useScheduleUpdate';
|
||||
import useCurrentRender from './useCurrentRender';
|
||||
import useComponent from './useComponent';
|
||||
import isArrayEqual from './isArrayEqual';
|
||||
import {
|
||||
DefaultNavigatorOptions,
|
||||
@@ -57,33 +60,40 @@ const getRouteConfigsFromChildren = <
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
>(
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode,
|
||||
options?: ScreenConfigWithParent<State, ScreenOptions, EventMap>[0]
|
||||
) => {
|
||||
const configs = React.Children.toArray(children).reduce<
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>[]
|
||||
ScreenConfigWithParent<State, ScreenOptions, EventMap>[]
|
||||
>((acc, child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
if (child.type === Screen) {
|
||||
// We can only extract the config from `Screen` elements
|
||||
// If something else was rendered, it's probably a bug
|
||||
acc.push(
|
||||
acc.push([
|
||||
options,
|
||||
child.props as RouteConfig<
|
||||
ParamListBase,
|
||||
string,
|
||||
State,
|
||||
ScreenOptions,
|
||||
EventMap
|
||||
>
|
||||
);
|
||||
>,
|
||||
]);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (child.type === React.Fragment) {
|
||||
// When we encounter a fragment, we need to dive into its children to extract the configs
|
||||
if (child.type === React.Fragment || child.type === Group) {
|
||||
// When we encounter a fragment or group, we need to dive into its children to extract the configs
|
||||
// This is handy to conditionally define a group of screens
|
||||
acc.push(
|
||||
...getRouteConfigsFromChildren<State, ScreenOptions, EventMap>(
|
||||
child.props.children
|
||||
child.props.children,
|
||||
child.type !== Group
|
||||
? options
|
||||
: options != null
|
||||
? [...options, child.props.screenOptions]
|
||||
: [child.props.screenOptions]
|
||||
)
|
||||
);
|
||||
return acc;
|
||||
@@ -91,7 +101,7 @@ const getRouteConfigsFromChildren = <
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`A navigator can only contain 'Screen' components as its direct children (found ${
|
||||
`A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found ${
|
||||
React.isValidElement(child)
|
||||
? `'${
|
||||
typeof child.type === 'string' ? child.type : child.type?.name
|
||||
@@ -107,7 +117,7 @@ const getRouteConfigsFromChildren = <
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
configs.forEach((config) => {
|
||||
const { name, children, component, getComponent } = config;
|
||||
const { name, children, component, getComponent } = config[1];
|
||||
|
||||
if (typeof name !== 'string' || !name) {
|
||||
throw new Error(
|
||||
@@ -220,25 +230,22 @@ export default function useNavigationBuilder<
|
||||
>(children);
|
||||
|
||||
const screens = routeConfigs.reduce<
|
||||
Record<
|
||||
string,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
>
|
||||
Record<string, ScreenConfigWithParent<State, ScreenOptions, EventMap>>
|
||||
>((acc, config) => {
|
||||
if (config.name in acc) {
|
||||
if (config[1].name in acc) {
|
||||
throw new Error(
|
||||
`A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named '${config.name}')`
|
||||
`A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named '${config[1].name}')`
|
||||
);
|
||||
}
|
||||
|
||||
acc[config.name] = config;
|
||||
acc[config[1].name] = config;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const routeNames = routeConfigs.map((config) => config.name);
|
||||
const routeNames = routeConfigs.map((config) => config[1].name);
|
||||
const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
|
||||
(acc, curr) => {
|
||||
const { initialParams } = screens[curr];
|
||||
const { initialParams } = screens[curr][1];
|
||||
const initialParamsFromParams =
|
||||
route?.params?.state == null &&
|
||||
route?.params?.initial !== false &&
|
||||
@@ -263,7 +270,7 @@ export default function useNavigationBuilder<
|
||||
>(
|
||||
(acc, curr) =>
|
||||
Object.assign(acc, {
|
||||
[curr]: screens[curr].getId,
|
||||
[curr]: screens[curr][1].getId,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
@@ -481,7 +488,7 @@ export default function useNavigationBuilder<
|
||||
const listeners = ([] as (((e: any) => void) | undefined)[])
|
||||
.concat(
|
||||
...routeNames.map((name) => {
|
||||
const { listeners } = screens[name];
|
||||
const { listeners } = screens[name][1];
|
||||
const map =
|
||||
typeof listeners === 'function'
|
||||
? listeners({ route: route as any, navigation })
|
||||
@@ -581,9 +588,14 @@ export default function useNavigationBuilder<
|
||||
descriptors,
|
||||
});
|
||||
|
||||
const NavigationContent = useComponent(NavigationHelpersContext.Provider, {
|
||||
value: navigation,
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
NavigationContent,
|
||||
};
|
||||
}
|
||||
|
||||
17
packages/core/src/useNavigationContainerRef.tsx
Normal file
17
packages/core/src/useNavigationContainerRef.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import createNavigationContainerRef from './createNavigationContainerRef';
|
||||
import type { NavigationContainerRefWithCurrent } from './types';
|
||||
|
||||
export default function useNavigationContainerRef<
|
||||
ParamList extends {} = ReactNavigation.RootParamList
|
||||
>(): NavigationContainerRefWithCurrent<ParamList> {
|
||||
const navigation = React.useRef<NavigationContainerRefWithCurrent<ParamList> | null>(
|
||||
null
|
||||
);
|
||||
|
||||
if (navigation.current == null) {
|
||||
navigation.current = createNavigationContainerRef<ParamList>();
|
||||
}
|
||||
|
||||
return navigation.current;
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import type { NavigationState } from '@react-navigation/routers';
|
||||
import type { NavigationState, ParamListBase } from '@react-navigation/routers';
|
||||
import useNavigation from './useNavigation';
|
||||
import type { NavigationProp } from './types';
|
||||
|
||||
type Selector<T> = (state: NavigationState) => T;
|
||||
type Selector<ParamList extends ParamListBase, T> = (
|
||||
state: NavigationState<ParamList>
|
||||
) => T;
|
||||
|
||||
/**
|
||||
* Hook to get a value from the current navigation state using a selector.
|
||||
*
|
||||
* @param selector Selector function to get a value from the state.
|
||||
*/
|
||||
export default function useNavigationState<T>(selector: Selector<T>): T {
|
||||
const navigation = useNavigation();
|
||||
export default function useNavigationState<ParamList extends ParamListBase, T>(
|
||||
selector: Selector<ParamList, T>
|
||||
): T {
|
||||
const navigation = useNavigation<NavigationProp<ParamList>>();
|
||||
|
||||
// We don't care about the state value, we run the selector again at the end
|
||||
// The state is only to make sure that there's a re-render when we have a new value
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import type { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||
import type { ParamListBase } from '@react-navigation/routers';
|
||||
import NavigationStateContext from './NavigationStateContext';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import type { NavigationProp } from './types';
|
||||
|
||||
type Options = {
|
||||
key?: string;
|
||||
navigation?: NavigationProp<ParamListBase, string, NavigationState, object>;
|
||||
navigation?: NavigationProp<ParamListBase>;
|
||||
options?: object | undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import type { RouteProp } from './types';
|
||||
*
|
||||
* @returns Route prop of the parent screen.
|
||||
*/
|
||||
export default function useRoute<
|
||||
T extends RouteProp<ParamListBase, string>
|
||||
>(): T {
|
||||
export default function useRoute<T extends RouteProp<ParamListBase>>(): T {
|
||||
const route = React.useContext(NavigationRouteContext);
|
||||
|
||||
if (route === undefined) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@react-navigation/routers';
|
||||
import type { RouteProp } from './types';
|
||||
|
||||
type RouteCache = Map<Route<string>, RouteProp<ParamListBase, string>>;
|
||||
type RouteCache = Map<Route<string>, RouteProp<ParamListBase>>;
|
||||
|
||||
/**
|
||||
* Utilites such as `getFocusedRouteNameFromRoute` need to access state.
|
||||
|
||||
@@ -3,6 +3,68 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.7...@react-navigation/devtools@6.0.0-next.8) (2021-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix type error when passing unannotated navigation ref ([dc4ffc0](https://github.com/react-navigation/react-navigation/commit/dc4ffc0171b4535fe1b6e839b9d54350121bcf55))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.6...@react-navigation/devtools@6.0.0-next.7) (2021-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.5...@react-navigation/devtools@6.0.0-next.6) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.4...@react-navigation/devtools@6.0.0-next.5) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.3...@react-navigation/devtools@6.0.0-next.4) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.2...@react-navigation/devtools@6.0.0-next.3) (2021-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add helper and hook for container ref ([0ecd112](https://github.com/react-navigation/react-navigation/commit/0ecd112ec9786a26261ada3d33ef44dc1ec84da0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.1...@react-navigation/devtools@6.0.0-next.2) (2021-04-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0...@react-navigation/devtools@6.0.0-next.1) (2021-03-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/devtools",
|
||||
"description": "Developer tools for React Navigation",
|
||||
"version": "6.0.0-next.1",
|
||||
"version": "6.0.0-next.8",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -29,15 +29,14 @@
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^6.0.0-next.1",
|
||||
"@react-navigation/core": "^6.0.0-next.8",
|
||||
"deep-equal": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -22,7 +22,7 @@ type DevToolsExtension = {
|
||||
declare const __REDUX_DEVTOOLS_EXTENSION__: DevToolsExtension | undefined;
|
||||
|
||||
export default function useReduxDevToolsExtension(
|
||||
ref: React.RefObject<NavigationContainerRef>
|
||||
ref: React.RefObject<NavigationContainerRef<any>>
|
||||
) {
|
||||
const devToolsRef = React.useRef<DevToolsConnection>();
|
||||
|
||||
|
||||
@@ -3,6 +3,104 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [6.0.0-next.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.9...@react-navigation/drawer@6.0.0-next.10) (2021-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix drawer content padding in RTL ([ea8ea20](https://github.com/react-navigation/react-navigation/commit/ea8ea20127d979d8c8ddbddf56de1bdfdf0243f9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.8...@react-navigation/drawer@6.0.0-next.9) (2021-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add a deprecation warning for mode prop in stack ([a6e4981](https://github.com/react-navigation/react-navigation/commit/a6e498170f59648190fa5513e273ca523e56c5d5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* return a NavigationContent component from useNavigationBuilder ([1179d56](https://github.com/react-navigation/react-navigation/commit/1179d56c5008270753feef41acdc1dbd2191efcf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.7...@react-navigation/drawer@6.0.0-next.8) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.6...@react-navigation/drawer@6.0.0-next.7) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.5...@react-navigation/drawer@6.0.0-next.6) (2021-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable screens only on supported platforms ([#9494](https://github.com/react-navigation/react-navigation/issues/9494)) ([8da4c58](https://github.com/react-navigation/react-navigation/commit/8da4c58065607d44e9dc1ad8943e09537598dcd7))
|
||||
* make sure disabling react-native-screens works ([a369ba3](https://github.com/react-navigation/react-navigation/commit/a369ba36451ddc2bb5b247e61b725bce1e3fb5e5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.4...@react-navigation/drawer@6.0.0-next.5) (2021-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.3...@react-navigation/drawer@6.0.0-next.4) (2021-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't handle back button with permanent drawer ([b893968](https://github.com/react-navigation/react-navigation/commit/b89396888f46ba79af3cfd84be55fba79d8387d2))
|
||||
* fix drawer overlay on web ([3241190](https://github.com/react-navigation/react-navigation/commit/3241190b19946c1cd0a744fb09a19d79ba683d74))
|
||||
* only handle back button in drawer when focused ([5ae0bad](https://github.com/react-navigation/react-navigation/commit/5ae0badc44b576d464f8841822a911b18a698403))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.2...@react-navigation/drawer@6.0.0-next.3) (2021-03-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.1...@react-navigation/drawer@6.0.0-next.2) (2021-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* export drawer button ([2c8401d](https://github.com/react-navigation/react-navigation/commit/2c8401d5cb347d37c96e5b30f8ad05c17fd22ea4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0...@react-navigation/drawer@6.0.0-next.1) (2021-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/drawer",
|
||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||
"version": "6.0.0-next.1",
|
||||
"version": "6.0.0-next.10",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -34,31 +34,30 @@
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": "^1.0.0-next.1",
|
||||
"@react-navigation/elements": "^1.0.0-next.10",
|
||||
"color": "^3.1.3",
|
||||
"warn-once": "^0.0.1"
|
||||
"warn-once": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-navigation/native": "^6.0.0-next.1",
|
||||
"@react-navigation/native": "^6.0.0-next.8",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "~0.63.51",
|
||||
"@types/react-native": "~0.64.4",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native": "~0.63.4",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"react-native-gesture-handler": "~1.8.0",
|
||||
"react-native-reanimated": "~1.13.0",
|
||||
"react-native-safe-area-context": "3.1.9",
|
||||
"react-native-screens": "~2.15.0",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-reanimated": "~2.1.0",
|
||||
"react-native-safe-area-context": "~3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -68,7 +67,7 @@
|
||||
"react-native-gesture-handler": ">= 1.0.0",
|
||||
"react-native-reanimated": ">= 1.0.0",
|
||||
"react-native-safe-area-context": ">= 3.0.0",
|
||||
"react-native-screens": ">= 2.15.0"
|
||||
"react-native-screens": ">= 3.0.0"
|
||||
},
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
|
||||
@@ -11,12 +11,16 @@ export { default as DrawerItem } from './views/DrawerItem';
|
||||
export { default as DrawerItemList } from './views/DrawerItemList';
|
||||
export { default as DrawerContent } from './views/DrawerContent';
|
||||
export { default as DrawerContentScrollView } from './views/DrawerContentScrollView';
|
||||
export { default as DrawerToggleButton } from './views/DrawerToggleButton';
|
||||
|
||||
/**
|
||||
* Utilities
|
||||
*/
|
||||
export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
|
||||
|
||||
export { default as DrawerProgressContext } from './utils/DrawerProgressContext';
|
||||
export { default as useDrawerProgress } from './utils/useDrawerProgress';
|
||||
|
||||
export { default as getDrawerStatusFromState } from './utils/getDrawerStatusFromState';
|
||||
export { default as useDrawerStatus } from './utils/useDrawerStatus';
|
||||
|
||||
|
||||
@@ -67,7 +67,12 @@ function DrawerNavigator({
|
||||
);
|
||||
}
|
||||
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
const {
|
||||
state,
|
||||
descriptors,
|
||||
navigation,
|
||||
NavigationContent,
|
||||
} = useNavigationBuilder<
|
||||
DrawerNavigationState<ParamListBase>,
|
||||
DrawerRouterOptions,
|
||||
DrawerActionHelpers<ParamListBase>,
|
||||
@@ -83,12 +88,14 @@ function DrawerNavigator({
|
||||
});
|
||||
|
||||
return (
|
||||
<DrawerView
|
||||
{...rest}
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<NavigationContent>
|
||||
<DrawerView
|
||||
{...rest}
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
||||
import type Animated from 'react-native-reanimated';
|
||||
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
|
||||
import type {
|
||||
PanGestureHandler,
|
||||
PanGestureHandlerProperties,
|
||||
} from 'react-native-gesture-handler';
|
||||
import type {
|
||||
Route,
|
||||
ParamListBase,
|
||||
@@ -33,6 +35,18 @@ export type DrawerNavigationConfig = {
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
detachInactiveScreens?: boolean;
|
||||
/**
|
||||
* Whether to use the legacy implementation based on Reanimated 1.
|
||||
* The new implementation based on Reanimated 2 will perform better,
|
||||
* but you need additional configuration and need to use Hermes with Flipper to debug.
|
||||
*
|
||||
* This defaults to `true` in following cases:
|
||||
* - Reanimated 2 is not configured
|
||||
* - App is connected to Chrome debugger (Reanimated 2 cannot be used with Chrome debugger)
|
||||
*
|
||||
* Otherwise, it defaults to `false`
|
||||
*/
|
||||
useLegacyImplementation?: boolean;
|
||||
};
|
||||
|
||||
export type DrawerNavigationOptions = HeaderOptions & {
|
||||
@@ -207,11 +221,6 @@ export type DrawerContentComponentProps = {
|
||||
state: DrawerNavigationState<ParamListBase>;
|
||||
navigation: DrawerNavigationHelpers;
|
||||
descriptors: DrawerDescriptorMap;
|
||||
/**
|
||||
* Animated node which represents the current progress of the drawer's open state.
|
||||
* `0` is closed, `1` is open.
|
||||
*/
|
||||
progress: Animated.Node<number>;
|
||||
};
|
||||
|
||||
export type DrawerHeaderProps = {
|
||||
@@ -226,7 +235,7 @@ export type DrawerHeaderProps = {
|
||||
/**
|
||||
* Route object for the current screen.
|
||||
*/
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
route: RouteProp<ParamListBase>;
|
||||
/**
|
||||
* Navigation prop for the header.
|
||||
*/
|
||||
@@ -243,7 +252,7 @@ export type DrawerNavigationHelpers = NavigationHelpers<
|
||||
|
||||
export type DrawerNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
RouteName extends keyof ParamList = keyof ParamList
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
@@ -255,7 +264,7 @@ export type DrawerNavigationProp<
|
||||
|
||||
export type DrawerScreenProps<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
RouteName extends keyof ParamList = keyof ParamList
|
||||
> = {
|
||||
navigation: DrawerNavigationProp<ParamList, RouteName>;
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
@@ -264,7 +273,28 @@ export type DrawerScreenProps<
|
||||
export type DrawerDescriptor = Descriptor<
|
||||
DrawerNavigationOptions,
|
||||
DrawerNavigationProp<ParamListBase>,
|
||||
RouteProp<ParamListBase, string>
|
||||
RouteProp<ParamListBase>
|
||||
>;
|
||||
|
||||
export type DrawerDescriptorMap = Record<string, DrawerDescriptor>;
|
||||
|
||||
export type DrawerProps = {
|
||||
dimensions: { width: number; height: number };
|
||||
drawerPosition: 'left' | 'right';
|
||||
drawerStyle?: StyleProp<ViewStyle>;
|
||||
drawerType: 'front' | 'back' | 'slide' | 'permanent';
|
||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
||||
hideStatusBarOnOpen: boolean;
|
||||
keyboardDismissMode: 'none' | 'on-drag';
|
||||
onClose: () => void;
|
||||
onOpen: () => void;
|
||||
open: boolean;
|
||||
overlayStyle?: StyleProp<ViewStyle>;
|
||||
renderDrawerContent: () => React.ReactNode;
|
||||
renderSceneContent: () => React.ReactNode;
|
||||
statusBarAnimation: 'slide' | 'none' | 'fade';
|
||||
swipeDistanceThreshold: number;
|
||||
swipeEdgeWidth: number;
|
||||
swipeEnabled: boolean;
|
||||
swipeVelocityThreshold: number;
|
||||
};
|
||||
|
||||
6
packages/drawer/src/utils/DrawerProgressContext.tsx
Normal file
6
packages/drawer/src/utils/DrawerProgressContext.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import type Animated from 'react-native-reanimated';
|
||||
|
||||
export default React.createContext<
|
||||
Readonly<Animated.SharedValue<number>> | Animated.Node<number> | undefined
|
||||
>(undefined);
|
||||
17
packages/drawer/src/utils/useDrawerProgress.tsx
Normal file
17
packages/drawer/src/utils/useDrawerProgress.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import type Animated from 'react-native-reanimated';
|
||||
import DrawerProgressContext from './DrawerProgressContext';
|
||||
|
||||
export default function useDrawerProgress():
|
||||
| Readonly<Animated.SharedValue<number>>
|
||||
| Animated.Node<number> {
|
||||
const progress = React.useContext(DrawerProgressContext);
|
||||
|
||||
if (progress === undefined) {
|
||||
throw new Error(
|
||||
"Couldn't find a drawer. Is your component inside a drawer navigator?"
|
||||
);
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { ScrollView, StyleSheet, ScrollViewProps } from 'react-native';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
I18nManager,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import DrawerPositionContext from '../utils/DrawerPositionContext';
|
||||
|
||||
@@ -16,14 +21,18 @@ export default function DrawerContentScrollView({
|
||||
const drawerPosition = React.useContext(DrawerPositionContext);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const isRight = I18nManager.isRTL
|
||||
? drawerPosition === 'left'
|
||||
: drawerPosition === 'right';
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
{...rest}
|
||||
contentContainerStyle={[
|
||||
{
|
||||
paddingTop: insets.top + 4,
|
||||
paddingLeft: drawerPosition === 'left' ? insets.left : 0,
|
||||
paddingRight: drawerPosition === 'right' ? insets.right : 0,
|
||||
paddingStart: !isRight ? insets.left : 0,
|
||||
paddingEnd: isRight ? insets.right : 0,
|
||||
},
|
||||
contentContainerStyle,
|
||||
]}
|
||||
|
||||
@@ -5,12 +5,10 @@ import {
|
||||
I18nManager,
|
||||
Platform,
|
||||
BackHandler,
|
||||
NativeEventSubscription,
|
||||
} from 'react-native';
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
DrawerNavigationState,
|
||||
DrawerActions,
|
||||
useTheme,
|
||||
@@ -22,12 +20,10 @@ import {
|
||||
SafeAreaProviderCompat,
|
||||
getHeaderTitle,
|
||||
} from '@react-navigation/elements';
|
||||
|
||||
import { MaybeScreenContainer, MaybeScreen } from './ScreenFallback';
|
||||
import { GestureHandlerRootView } from './GestureHandler';
|
||||
import ScreenFallback from './ScreenFallback';
|
||||
import DrawerToggleButton from './DrawerToggleButton';
|
||||
import DrawerContent from './DrawerContent';
|
||||
import Drawer from './Drawer';
|
||||
import DrawerStatusContext from '../utils/DrawerStatusContext';
|
||||
import DrawerPositionContext from '../utils/DrawerPositionContext';
|
||||
import getDrawerStatusFromState from '../utils/getDrawerStatusFromState';
|
||||
@@ -38,6 +34,7 @@ import type {
|
||||
DrawerContentComponentProps,
|
||||
DrawerHeaderProps,
|
||||
DrawerNavigationProp,
|
||||
DrawerProps,
|
||||
} from '../types';
|
||||
|
||||
type Props = DrawerNavigationConfig & {
|
||||
@@ -76,8 +73,20 @@ function DrawerViewBase({
|
||||
drawerContent = (props: DrawerContentComponentProps) => (
|
||||
<DrawerContent {...props} />
|
||||
),
|
||||
detachInactiveScreens = true,
|
||||
detachInactiveScreens = Platform.OS === 'web' ||
|
||||
Platform.OS === 'android' ||
|
||||
Platform.OS === 'ios',
|
||||
// Running in chrome debugger
|
||||
// @ts-expect-error
|
||||
useLegacyImplementation = !global.nativeCallSyncHook ||
|
||||
// Reanimated 2 is not configured
|
||||
// @ts-expect-error: the type definitions are incomplete
|
||||
!Animated.isConfigured?.(),
|
||||
}: Props) {
|
||||
const Drawer: React.ComponentType<DrawerProps> = useLegacyImplementation
|
||||
? require('./legacy/Drawer').default
|
||||
: require('./modern/Drawer').default;
|
||||
|
||||
const focusedRouteKey = state.routes[state.index].key;
|
||||
const {
|
||||
drawerHideStatusBarOnOpen = false,
|
||||
@@ -85,14 +94,14 @@ function DrawerViewBase({
|
||||
drawerStatusBarAnimation = 'slide',
|
||||
drawerStyle,
|
||||
drawerType = Platform.select({ ios: 'slide', default: 'front' }),
|
||||
gestureEnabled,
|
||||
gestureHandlerProps,
|
||||
keyboardDismissMode = 'on-drag',
|
||||
overlayColor = 'rgba(0, 0, 0, 0.5)',
|
||||
sceneContainerStyle,
|
||||
swipeEdgeWidth,
|
||||
swipeEnabled,
|
||||
swipeMinDistance,
|
||||
swipeEdgeWidth = 32,
|
||||
swipeEnabled = Platform.OS !== 'web' &&
|
||||
Platform.OS !== 'windows' &&
|
||||
Platform.OS !== 'macos',
|
||||
swipeMinDistance = 60,
|
||||
} = descriptors[focusedRouteKey].options;
|
||||
|
||||
const [loaded, setLoaded] = React.useState([focusedRouteKey]);
|
||||
@@ -122,27 +131,53 @@ function DrawerViewBase({
|
||||
}, [navigation, state.key]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let subscription: NativeEventSubscription | undefined;
|
||||
|
||||
if (drawerStatus === 'open') {
|
||||
// We only add the subscription when drawer opens
|
||||
// This way we can make sure that the subscription is added as late as possible
|
||||
// This will make sure that our handler will run first when back button is pressed
|
||||
subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
handleDrawerClose();
|
||||
|
||||
return true;
|
||||
});
|
||||
if (drawerStatus !== 'open' || drawerType === 'permanent') {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => subscription?.remove();
|
||||
}, [handleDrawerClose, drawerStatus, navigation, state.key]);
|
||||
const handleClose = () => {
|
||||
// We shouldn't handle the back button if the parent screen isn't focused
|
||||
// This will avoid the drawer overriding event listeners from a focused screen
|
||||
if (!navigation.isFocused()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderDrawerContent = ({ progress }: any) => {
|
||||
handleDrawerClose();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
// We only add the listeners when drawer opens
|
||||
// This way we can make sure that the listener is added as late as possible
|
||||
// This will make sure that our handler will run first when back button is pressed
|
||||
const subscription = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
handleClose
|
||||
);
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.addEventListener?.('keyup', handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.removeEventListener?.('keyup', handleEscape);
|
||||
}
|
||||
};
|
||||
}, [drawerStatus, drawerType, handleDrawerClose, navigation]);
|
||||
|
||||
const renderDrawerContent = () => {
|
||||
return (
|
||||
<DrawerPositionContext.Provider value={drawerPosition}>
|
||||
{drawerContent({
|
||||
progress: progress,
|
||||
state: state,
|
||||
navigation: navigation,
|
||||
descriptors: descriptors,
|
||||
@@ -153,8 +188,10 @@ function DrawerViewBase({
|
||||
|
||||
const renderSceneContent = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ScreenContainer enabled={detachInactiveScreens} style={styles.content}>
|
||||
<MaybeScreenContainer
|
||||
enabled={detachInactiveScreens}
|
||||
style={styles.content}
|
||||
>
|
||||
{state.routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { lazy = true, unmountOnBlur } = descriptor.options;
|
||||
@@ -181,16 +218,18 @@ function DrawerViewBase({
|
||||
}
|
||||
/>
|
||||
),
|
||||
sceneContainerStyle,
|
||||
} = descriptor.options;
|
||||
|
||||
return (
|
||||
<ScreenFallback
|
||||
<MaybeScreen
|
||||
key={route.key}
|
||||
style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]}
|
||||
visible={isFocused}
|
||||
enabled={detachInactiveScreens}
|
||||
>
|
||||
<Screen
|
||||
focused={isFocused}
|
||||
route={descriptor.route}
|
||||
navigation={descriptor.navigation}
|
||||
headerShown={descriptor.options.headerShown}
|
||||
@@ -201,13 +240,14 @@ function DrawerViewBase({
|
||||
navigation: descriptor.navigation as DrawerNavigationProp<ParamListBase>,
|
||||
options: descriptor.options,
|
||||
})}
|
||||
style={sceneContainerStyle}
|
||||
>
|
||||
{descriptor.render()}
|
||||
</Screen>
|
||||
</ScreenFallback>
|
||||
</MaybeScreen>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
</MaybeScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -215,17 +255,18 @@ function DrawerViewBase({
|
||||
<DrawerStatusContext.Provider value={drawerStatus}>
|
||||
<Drawer
|
||||
open={drawerStatus !== 'closed'}
|
||||
gestureEnabled={gestureEnabled}
|
||||
swipeEnabled={swipeEnabled}
|
||||
onOpen={handleDrawerOpen}
|
||||
onClose={handleDrawerClose}
|
||||
gestureHandlerProps={gestureHandlerProps}
|
||||
swipeEnabled={swipeEnabled}
|
||||
swipeEdgeWidth={swipeEdgeWidth}
|
||||
swipeVelocityThreshold={500}
|
||||
swipeDistanceThreshold={swipeMinDistance}
|
||||
hideStatusBarOnOpen={drawerHideStatusBarOnOpen}
|
||||
statusBarAnimation={drawerStatusBarAnimation}
|
||||
keyboardDismissMode={keyboardDismissMode}
|
||||
drawerType={drawerType}
|
||||
drawerPosition={drawerPosition}
|
||||
sceneContainerStyle={[
|
||||
{ backgroundColor: colors.background },
|
||||
sceneContainerStyle,
|
||||
]}
|
||||
drawerStyle={[
|
||||
{
|
||||
width: getDefaultDrawerWidth(dimensions),
|
||||
@@ -244,13 +285,8 @@ function DrawerViewBase({
|
||||
drawerStyle,
|
||||
]}
|
||||
overlayStyle={{ backgroundColor: overlayColor }}
|
||||
swipeEdgeWidth={swipeEdgeWidth}
|
||||
swipeDistanceThreshold={swipeMinDistance}
|
||||
hideStatusBarOnOpen={drawerHideStatusBarOnOpen}
|
||||
statusBarAnimation={drawerStatusBarAnimation}
|
||||
renderDrawerContent={renderDrawerContent}
|
||||
renderSceneContent={renderSceneContent}
|
||||
keyboardDismissMode={keyboardDismissMode}
|
||||
dimensions={dimensions}
|
||||
/>
|
||||
</DrawerStatusContext.Provider>
|
||||
@@ -259,13 +295,11 @@ function DrawerViewBase({
|
||||
|
||||
export default function DrawerView({ navigation, ...rest }: Props) {
|
||||
return (
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
<SafeAreaProviderCompat>
|
||||
<GestureHandlerWrapper style={styles.content}>
|
||||
<DrawerViewBase navigation={navigation} {...rest} />
|
||||
</GestureHandlerWrapper>
|
||||
</SafeAreaProviderCompat>
|
||||
</NavigationHelpersContext.Provider>
|
||||
<SafeAreaProviderCompat>
|
||||
<GestureHandlerWrapper style={styles.content}>
|
||||
<DrawerViewBase navigation={navigation} {...rest} />
|
||||
</GestureHandlerWrapper>
|
||||
</SafeAreaProviderCompat>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleProp, ViewStyle } from 'react-native';
|
||||
import {
|
||||
Screen,
|
||||
screensEnabled,
|
||||
// @ts-ignore
|
||||
shouldUseActivityState,
|
||||
} from 'react-native-screens';
|
||||
import { ResourceSavingScene } from '@react-navigation/elements';
|
||||
import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
|
||||
import { ResourceSavingView } from '@react-navigation/elements';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
@@ -15,27 +9,40 @@ type Props = {
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function ScreenFallback({ visible, children, ...rest }: Props) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
<Screen activityState={visible ? 2 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Screen active={visible ? 1 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
let Screens: typeof import('react-native-screens') | undefined;
|
||||
|
||||
try {
|
||||
Screens = require('react-native-screens');
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
export const MaybeScreenContainer = ({
|
||||
enabled,
|
||||
...rest
|
||||
}: ViewProps & {
|
||||
enabled: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
if (Screens?.screensEnabled?.()) {
|
||||
return <Screens.ScreenContainer enabled={enabled} {...rest} />;
|
||||
}
|
||||
|
||||
return <View {...rest} />;
|
||||
};
|
||||
|
||||
export function MaybeScreen({ visible, children, ...rest }: Props) {
|
||||
if (Screens?.screensEnabled?.()) {
|
||||
return (
|
||||
<Screens.Screen activityState={visible ? 2 : 0} {...rest}>
|
||||
{children}
|
||||
</Screens.Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSavingScene visible={visible} {...rest}>
|
||||
<ResourceSavingView visible={visible} {...rest}>
|
||||
{children}
|
||||
</ResourceSavingScene>
|
||||
</ResourceSavingView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
LayoutChangeEvent,
|
||||
I18nManager,
|
||||
Platform,
|
||||
Keyboard,
|
||||
StatusBar,
|
||||
StyleProp,
|
||||
View,
|
||||
InteractionManager,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
TapGestureHandler,
|
||||
GestureState,
|
||||
} from './GestureHandler';
|
||||
import { PanGestureHandler, GestureState } from '../GestureHandler';
|
||||
import Overlay from './Overlay';
|
||||
import DrawerProgressContext from '../../utils/DrawerProgressContext';
|
||||
import type { DrawerProps } from '../../types';
|
||||
|
||||
const {
|
||||
Clock,
|
||||
@@ -56,7 +51,6 @@ const UNSET = -1;
|
||||
const DIRECTION_LEFT = 1;
|
||||
const DIRECTION_RIGHT = -1;
|
||||
|
||||
const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60;
|
||||
const SWIPE_DISTANCE_MINIMUM = 5;
|
||||
|
||||
const DEFAULT_DRAWER_WIDTH = '80%';
|
||||
@@ -75,54 +69,8 @@ const ANIMATED_ONE = new Animated.Value(1);
|
||||
|
||||
type Binary = 0 | 1;
|
||||
|
||||
type Renderer = (props: { progress: Animated.Node<number> }) => React.ReactNode;
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
gestureEnabled: boolean;
|
||||
swipeEnabled: boolean;
|
||||
drawerPosition: 'left' | 'right';
|
||||
drawerType: 'front' | 'back' | 'slide' | 'permanent';
|
||||
keyboardDismissMode: 'none' | 'on-drag';
|
||||
swipeEdgeWidth: number;
|
||||
swipeDistanceThreshold?: number;
|
||||
swipeVelocityThreshold: number;
|
||||
hideStatusBarOnOpen: boolean;
|
||||
statusBarAnimation: 'slide' | 'none' | 'fade';
|
||||
overlayStyle?: StyleProp<ViewStyle>;
|
||||
drawerStyle?: StyleProp<ViewStyle>;
|
||||
sceneContainerStyle?: StyleProp<ViewStyle>;
|
||||
renderDrawerContent: Renderer;
|
||||
renderSceneContent: Renderer;
|
||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
||||
dimensions: { width: number; height: number };
|
||||
};
|
||||
|
||||
export default class DrawerView extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
drawerPosition: I18nManager.isRTL ? 'left' : 'right',
|
||||
drawerType: 'front',
|
||||
gestureEnabled: true,
|
||||
swipeEnabled:
|
||||
Platform.OS !== 'web' &&
|
||||
Platform.OS !== 'windows' &&
|
||||
Platform.OS !== 'macos',
|
||||
swipeEdgeWidth: 32,
|
||||
swipeVelocityThreshold: 500,
|
||||
keyboardDismissMode: 'on-drag',
|
||||
hideStatusBarOnOpen: false,
|
||||
statusBarAnimation: 'slide',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.addEventListener?.('keyup', this.handleEscape);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
export default class DrawerView extends React.Component<DrawerProps> {
|
||||
componentDidUpdate(prevProps: DrawerProps) {
|
||||
const {
|
||||
open,
|
||||
drawerPosition,
|
||||
@@ -157,11 +105,7 @@ export default class DrawerView extends React.Component<Props> {
|
||||
}
|
||||
|
||||
if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) {
|
||||
this.swipeDistanceThreshold.setValue(
|
||||
swipeDistanceThreshold !== undefined
|
||||
? swipeDistanceThreshold
|
||||
: SWIPE_DISTANCE_THRESHOLD_DEFAULT
|
||||
);
|
||||
this.swipeDistanceThreshold.setValue(swipeDistanceThreshold);
|
||||
}
|
||||
|
||||
if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) {
|
||||
@@ -172,22 +116,8 @@ export default class DrawerView extends React.Component<Props> {
|
||||
componentWillUnmount() {
|
||||
this.toggleStatusBar(false);
|
||||
this.handleEndInteraction();
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.removeEventListener?.('keyup', this.handleEscape);
|
||||
}
|
||||
}
|
||||
|
||||
private handleEscape = (e: KeyboardEvent) => {
|
||||
const { open, onClose } = this.props;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
if (open) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleEndInteraction = () => {
|
||||
if (this.interactionHandle !== undefined) {
|
||||
InteractionManager.clearInteractionHandle(this.interactionHandle);
|
||||
@@ -304,9 +234,7 @@ export default class DrawerView extends React.Component<Props> {
|
||||
);
|
||||
|
||||
private swipeDistanceThreshold = new Value<number>(
|
||||
this.props.swipeDistanceThreshold !== undefined
|
||||
? this.props.swipeDistanceThreshold
|
||||
: SWIPE_DISTANCE_THRESHOLD_DEFAULT
|
||||
this.props.swipeDistanceThreshold
|
||||
);
|
||||
private swipeVelocityThreshold = new Value<number>(
|
||||
this.props.swipeVelocityThreshold
|
||||
@@ -516,18 +444,6 @@ export default class DrawerView extends React.Component<Props> {
|
||||
},
|
||||
]);
|
||||
|
||||
private handleTapStateChange = event([
|
||||
{
|
||||
nativeEvent: {
|
||||
oldState: (s: Animated.Value<number>) =>
|
||||
cond(
|
||||
eq(s, GestureState.ACTIVE),
|
||||
set(this.manuallyTriggerSpring, TRUE)
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
private handleContainerLayout = (e: LayoutChangeEvent) =>
|
||||
this.containerWidth.setValue(e.nativeEvent.layout.width);
|
||||
|
||||
@@ -568,12 +484,10 @@ export default class DrawerView extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
open,
|
||||
gestureEnabled,
|
||||
swipeEnabled,
|
||||
drawerPosition,
|
||||
drawerType,
|
||||
swipeEdgeWidth,
|
||||
sceneContainerStyle,
|
||||
drawerStyle,
|
||||
overlayStyle,
|
||||
renderDrawerContent,
|
||||
@@ -619,109 +533,103 @@ export default class DrawerView extends React.Component<Props> {
|
||||
const progress = drawerType === 'permanent' ? ANIMATED_ONE : this.progress;
|
||||
|
||||
return (
|
||||
<PanGestureHandler
|
||||
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
onGestureEvent={this.handleGestureEvent}
|
||||
onHandlerStateChange={this.handleGestureStateChange}
|
||||
hitSlop={hitSlop}
|
||||
enabled={drawerType !== 'permanent' && gestureEnabled && swipeEnabled}
|
||||
{...gestureHandlerProps}
|
||||
>
|
||||
<Animated.View
|
||||
onLayout={this.handleContainerLayout}
|
||||
style={[
|
||||
styles.main,
|
||||
{
|
||||
flexDirection:
|
||||
drawerType === 'permanent' && !isRight ? 'row-reverse' : 'row',
|
||||
},
|
||||
]}
|
||||
<DrawerProgressContext.Provider value={progress}>
|
||||
<PanGestureHandler
|
||||
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
onGestureEvent={this.handleGestureEvent}
|
||||
onHandlerStateChange={this.handleGestureStateChange}
|
||||
hitSlop={hitSlop}
|
||||
enabled={drawerType !== 'permanent' && swipeEnabled}
|
||||
{...gestureHandlerProps}
|
||||
>
|
||||
<Animated.View
|
||||
onLayout={this.handleContainerLayout}
|
||||
style={[
|
||||
styles.content,
|
||||
{ transform: [{ translateX: contentTranslateX }] },
|
||||
sceneContainerStyle as any,
|
||||
]}
|
||||
>
|
||||
<View
|
||||
accessibilityElementsHidden={isOpen && drawerType !== 'permanent'}
|
||||
importantForAccessibility={
|
||||
isOpen && drawerType !== 'permanent'
|
||||
? 'no-hide-descendants'
|
||||
: 'auto'
|
||||
}
|
||||
style={styles.content}
|
||||
>
|
||||
{renderSceneContent({ progress })}
|
||||
</View>
|
||||
{
|
||||
// Disable overlay if sidebar is permanent
|
||||
drawerType === 'permanent' ? null : Platform.OS === 'web' ||
|
||||
Platform.OS === 'windows' ||
|
||||
Platform.OS === 'macos' ? (
|
||||
<Pressable
|
||||
onPress={
|
||||
gestureEnabled ? () => this.toggleDrawer(false) : undefined
|
||||
}
|
||||
>
|
||||
<Overlay progress={progress} style={overlayStyle as any} />
|
||||
</Pressable>
|
||||
) : (
|
||||
<TapGestureHandler
|
||||
enabled={gestureEnabled}
|
||||
onHandlerStateChange={this.handleTapStateChange}
|
||||
>
|
||||
<Overlay progress={progress} style={overlayStyle as any} />
|
||||
</TapGestureHandler>
|
||||
)
|
||||
}
|
||||
</Animated.View>
|
||||
<Animated.Code
|
||||
// This is needed to make sure that container width updates with `setValue`
|
||||
// Without this, it won't update when not used in styles
|
||||
exec={this.containerWidth}
|
||||
/>
|
||||
{drawerType === 'permanent' ? null : (
|
||||
<Animated.Code
|
||||
exec={block([
|
||||
onChange(this.manuallyTriggerSpring, [
|
||||
cond(eq(this.manuallyTriggerSpring, TRUE), [
|
||||
set(this.nextIsOpen, FALSE),
|
||||
call([], () => (this.currentOpenValue = false)),
|
||||
]),
|
||||
]),
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
<Animated.View
|
||||
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
|
||||
removeClippedSubviews={Platform.OS !== 'ios'}
|
||||
onLayout={this.handleDrawerLayout}
|
||||
style={[
|
||||
styles.container,
|
||||
styles.main,
|
||||
{
|
||||
transform: [{ translateX: drawerTranslateX }],
|
||||
opacity: this.drawerOpacity,
|
||||
flexDirection:
|
||||
drawerType === 'permanent' && !isRight
|
||||
? 'row-reverse'
|
||||
: 'row',
|
||||
},
|
||||
drawerType === 'permanent'
|
||||
? // Without this, the `left`/`right` values don't get reset
|
||||
isRight
|
||||
? { right: 0 }
|
||||
: { left: 0 }
|
||||
: [
|
||||
styles.nonPermanent,
|
||||
isRight ? { right: offset } : { left: offset },
|
||||
{ zIndex: drawerType === 'back' ? -1 : 0 },
|
||||
],
|
||||
drawerStyle as any,
|
||||
]}
|
||||
>
|
||||
{renderDrawerContent({ progress })}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.content,
|
||||
{ transform: [{ translateX: contentTranslateX }] },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
accessibilityElementsHidden={
|
||||
isOpen && drawerType !== 'permanent'
|
||||
}
|
||||
importantForAccessibility={
|
||||
isOpen && drawerType !== 'permanent'
|
||||
? 'no-hide-descendants'
|
||||
: 'auto'
|
||||
}
|
||||
style={styles.content}
|
||||
>
|
||||
{renderSceneContent()}
|
||||
</View>
|
||||
{
|
||||
// Disable overlay if sidebar is permanent
|
||||
drawerType === 'permanent' ? null : (
|
||||
<Overlay
|
||||
progress={progress}
|
||||
onPress={() => this.toggleDrawer(false)}
|
||||
style={overlayStyle as any}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Animated.View>
|
||||
<Animated.Code
|
||||
// This is needed to make sure that container width updates with `setValue`
|
||||
// Without this, it won't update when not used in styles
|
||||
exec={this.containerWidth}
|
||||
/>
|
||||
{drawerType === 'permanent' ? null : (
|
||||
<Animated.Code
|
||||
exec={block([
|
||||
onChange(this.manuallyTriggerSpring, [
|
||||
cond(eq(this.manuallyTriggerSpring, TRUE), [
|
||||
set(this.nextIsOpen, FALSE),
|
||||
call([], () => (this.currentOpenValue = false)),
|
||||
]),
|
||||
]),
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
<Animated.View
|
||||
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
|
||||
removeClippedSubviews={Platform.OS !== 'ios'}
|
||||
onLayout={this.handleDrawerLayout}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
transform: [{ translateX: drawerTranslateX }],
|
||||
opacity: this.drawerOpacity,
|
||||
},
|
||||
drawerType === 'permanent'
|
||||
? // Without this, the `left`/`right` values don't get reset
|
||||
isRight
|
||||
? { right: 0 }
|
||||
: { left: 0 }
|
||||
: [
|
||||
styles.nonPermanent,
|
||||
isRight ? { right: offset } : { left: offset },
|
||||
{ zIndex: drawerType === 'back' ? -1 : 0 },
|
||||
],
|
||||
drawerStyle as any,
|
||||
]}
|
||||
>
|
||||
{renderDrawerContent()}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</PanGestureHandler>
|
||||
</DrawerProgressContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { Pressable, Platform, StyleSheet } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
const {
|
||||
interpolate: interpolateDeprecated,
|
||||
// @ts-expect-error: this property is only present in Reanimated 2
|
||||
interpolateNode,
|
||||
cond,
|
||||
greaterThan,
|
||||
} = Animated;
|
||||
|
||||
const interpolate: typeof interpolateDeprecated =
|
||||
const interpolate: typeof interpolateNode =
|
||||
interpolateNode ?? interpolateDeprecated;
|
||||
|
||||
const PROGRESS_EPSILON = 0.05;
|
||||
|
||||
type Props = React.ComponentProps<typeof Animated.View> & {
|
||||
progress: Animated.Node<number>;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const Overlay = React.forwardRef(function Overlay(
|
||||
{ progress, style, ...props }: Props,
|
||||
{ progress, onPress, style, ...props }: Props,
|
||||
ref: React.Ref<Animated.View>
|
||||
) {
|
||||
const animatedStyle = {
|
||||
@@ -46,7 +46,9 @@ const Overlay = React.forwardRef(function Overlay(
|
||||
{...props}
|
||||
ref={ref}
|
||||
style={[styles.overlay, overlayStyle, animatedStyle, style]}
|
||||
/>
|
||||
>
|
||||
<Pressable onPress={onPress} style={styles.pressable} />
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,6 +66,9 @@ const styles = StyleSheet.create({
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default Overlay;
|
||||
379
packages/drawer/src/views/modern/Drawer.tsx
Normal file
379
packages/drawer/src/views/modern/Drawer.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
Platform,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
PanGestureHandlerGestureEvent,
|
||||
State as GestureState,
|
||||
} from 'react-native-gesture-handler';
|
||||
import Animated, {
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useAnimatedGestureHandler,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
} from 'react-native-reanimated';
|
||||
import type { DrawerProps } from '../../types';
|
||||
import DrawerProgressContext from '../../utils/DrawerProgressContext';
|
||||
import Overlay from './Overlay';
|
||||
|
||||
const SWIPE_DISTANCE_MINIMUM = 5;
|
||||
const DEFAULT_DRAWER_WIDTH = '80%';
|
||||
|
||||
const minmax = (value: number, start: number, end: number) => {
|
||||
'worklet';
|
||||
|
||||
return Math.min(Math.max(value, start), end);
|
||||
};
|
||||
|
||||
export default function Drawer({
|
||||
dimensions,
|
||||
drawerPosition,
|
||||
drawerStyle,
|
||||
drawerType,
|
||||
gestureHandlerProps,
|
||||
hideStatusBarOnOpen,
|
||||
keyboardDismissMode,
|
||||
onClose,
|
||||
onOpen,
|
||||
open,
|
||||
overlayStyle,
|
||||
renderDrawerContent,
|
||||
renderSceneContent,
|
||||
statusBarAnimation,
|
||||
swipeDistanceThreshold,
|
||||
swipeEdgeWidth,
|
||||
swipeEnabled,
|
||||
swipeVelocityThreshold,
|
||||
}: DrawerProps) {
|
||||
const getDrawerWidth = (): number => {
|
||||
const { width = DEFAULT_DRAWER_WIDTH } =
|
||||
StyleSheet.flatten(drawerStyle) || {};
|
||||
|
||||
if (typeof width === 'string' && width.endsWith('%')) {
|
||||
// Try to calculate width if a percentage is given
|
||||
const percentage = Number(width.replace(/%$/, ''));
|
||||
|
||||
if (Number.isFinite(percentage)) {
|
||||
return dimensions.width * (percentage / 100);
|
||||
}
|
||||
}
|
||||
|
||||
return typeof width === 'number' ? width : 0;
|
||||
};
|
||||
|
||||
const drawerWidth = getDrawerWidth();
|
||||
|
||||
const isOpen = drawerType === 'permanent' ? true : open;
|
||||
const isRight = drawerPosition === 'right';
|
||||
|
||||
const getDrawerTranslationX = React.useCallback(
|
||||
(open: boolean) => {
|
||||
'worklet';
|
||||
|
||||
if (drawerPosition === 'left') {
|
||||
return open ? 0 : -drawerWidth;
|
||||
}
|
||||
|
||||
return open ? 0 : drawerWidth;
|
||||
},
|
||||
[drawerPosition, drawerWidth]
|
||||
);
|
||||
|
||||
const hideStatusBar = React.useCallback(
|
||||
(hide: boolean) => {
|
||||
if (hideStatusBarOnOpen) {
|
||||
StatusBar.setHidden(hide, statusBarAnimation);
|
||||
}
|
||||
},
|
||||
[hideStatusBarOnOpen, statusBarAnimation]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
hideStatusBar(isOpen);
|
||||
|
||||
return () => hideStatusBar(false);
|
||||
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
|
||||
|
||||
const interactionHandleRef = React.useRef<number | null>(null);
|
||||
|
||||
const startInteraction = () => {
|
||||
interactionHandleRef.current = InteractionManager.createInteractionHandle();
|
||||
};
|
||||
|
||||
const endInteraction = () => {
|
||||
if (interactionHandleRef.current != null) {
|
||||
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
|
||||
interactionHandleRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const hideKeyboard = () => {
|
||||
if (keyboardDismissMode === 'on-drag') {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
const onGestureStart = () => {
|
||||
startInteraction();
|
||||
hideKeyboard();
|
||||
hideStatusBar(true);
|
||||
};
|
||||
|
||||
const onGestureEnd = () => {
|
||||
endInteraction();
|
||||
};
|
||||
|
||||
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
|
||||
// https://github.com/kmagiera/react-native-gesture-handler/issues/569
|
||||
const hitSlop = isRight
|
||||
? // Extend hitSlop to the side of the screen when drawer is closed
|
||||
// This lets the user drag the drawer from the side of the screen
|
||||
{ right: 0, width: isOpen ? undefined : swipeEdgeWidth }
|
||||
: { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
|
||||
|
||||
const touchStartX = useSharedValue(0);
|
||||
const touchX = useSharedValue(0);
|
||||
const translationX = useSharedValue(getDrawerTranslationX(open));
|
||||
const gestureState = useSharedValue<GestureState>(GestureState.UNDETERMINED);
|
||||
|
||||
const toggleDrawer = React.useCallback(
|
||||
(open: boolean, velocity?: number) => {
|
||||
'worklet';
|
||||
|
||||
const translateX = getDrawerTranslationX(open);
|
||||
|
||||
touchStartX.value = 0;
|
||||
touchX.value = 0;
|
||||
translationX.value = withSpring(
|
||||
translateX,
|
||||
{
|
||||
velocity,
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
overshootClamping: true,
|
||||
restDisplacementThreshold: 0.01,
|
||||
restSpeedThreshold: 0.01,
|
||||
},
|
||||
() => {
|
||||
if (translationX.value === getDrawerTranslationX(true)) {
|
||||
runOnJS(onOpen)();
|
||||
} else if (translationX.value === getDrawerTranslationX(false)) {
|
||||
runOnJS(onClose)();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
[getDrawerTranslationX, onClose, onOpen, touchStartX, touchX, translationX]
|
||||
);
|
||||
|
||||
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
|
||||
|
||||
const onGestureEvent = useAnimatedGestureHandler<
|
||||
PanGestureHandlerGestureEvent,
|
||||
{ startX: number }
|
||||
>({
|
||||
onStart: (event, ctx) => {
|
||||
ctx.startX = translationX.value;
|
||||
gestureState.value = event.state;
|
||||
touchStartX.value = event.x;
|
||||
|
||||
runOnJS(onGestureStart)();
|
||||
},
|
||||
onActive: (event, ctx) => {
|
||||
touchX.value = event.x;
|
||||
translationX.value = ctx.startX + event.translationX;
|
||||
gestureState.value = event.state;
|
||||
},
|
||||
onEnd: (event) => {
|
||||
gestureState.value = event.state;
|
||||
|
||||
const nextOpen =
|
||||
(Math.abs(event.translationX) > SWIPE_DISTANCE_MINIMUM &&
|
||||
Math.abs(event.translationX) > swipeVelocityThreshold) ||
|
||||
Math.abs(event.translationX) > swipeDistanceThreshold
|
||||
? drawerPosition === 'left'
|
||||
? // If swiped to right, open the drawer, otherwise close it
|
||||
(event.velocityX === 0 ? event.translationX : event.velocityX) > 0
|
||||
: // If swiped to left, open the drawer, otherwise close it
|
||||
(event.velocityX === 0 ? event.translationX : event.velocityX) < 0
|
||||
: open;
|
||||
|
||||
toggleDrawer(nextOpen, event.velocityX);
|
||||
runOnJS(onGestureEnd)();
|
||||
},
|
||||
});
|
||||
|
||||
const translateX = useDerivedValue(() => {
|
||||
// Comment stolen from react-native-gesture-handler/DrawerLayout
|
||||
//
|
||||
// While closing the drawer when user starts gesture outside of its area (in greyed
|
||||
// out part of the window), we want the drawer to follow only once finger reaches the
|
||||
// edge of the drawer.
|
||||
// E.g. on the diagram below drawer is illustrate by X signs and the greyed out area by
|
||||
// dots. The touch gesture starts at '*' and moves left, touch path is indicated by
|
||||
// an arrow pointing left
|
||||
// 1) +---------------+ 2) +---------------+ 3) +---------------+ 4) +---------------+
|
||||
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
|
||||
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
|
||||
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
|
||||
// |XXXXXXXX|......| |XXXXXXXX|.<-*..| |XXXXXXXX|<--*..| |XXXXX|<-----*..|
|
||||
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
|
||||
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
|
||||
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
|
||||
// +---------------+ +---------------+ +---------------+ +---------------+
|
||||
//
|
||||
// For the above to work properly we define animated value that will keep start position
|
||||
// of the gesture. Then we use that value to calculate how much we need to subtract from
|
||||
// the translationX. If the gesture started on the greyed out area we take the distance from the
|
||||
// edge of the drawer to the start position. Otherwise we don't subtract at all and the
|
||||
// drawer be pulled back as soon as you start the pan.
|
||||
//
|
||||
// This is used only when drawerType is "front"
|
||||
const touchDistance =
|
||||
drawerType === 'front' && gestureState.value === GestureState.ACTIVE
|
||||
? minmax(
|
||||
drawerPosition === 'left'
|
||||
? touchStartX.value - drawerWidth
|
||||
: dimensions.width - drawerWidth - touchStartX.value,
|
||||
0,
|
||||
dimensions.width
|
||||
)
|
||||
: 0;
|
||||
|
||||
const translateX =
|
||||
drawerPosition === 'left'
|
||||
? minmax(translationX.value + touchDistance, -drawerWidth, 0)
|
||||
: minmax(translationX.value - touchDistance, 0, drawerWidth);
|
||||
|
||||
return translateX;
|
||||
});
|
||||
|
||||
const drawerAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateX:
|
||||
drawerType === 'permanent' || drawerType === 'back'
|
||||
? 0
|
||||
: translateX.value,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateX:
|
||||
drawerType === 'permanent' || drawerType === 'front'
|
||||
? 0
|
||||
: drawerPosition === 'left'
|
||||
? drawerWidth + translateX.value
|
||||
: translateX.value - drawerWidth,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const progress = useDerivedValue(() => {
|
||||
return drawerType === 'permanent'
|
||||
? 1
|
||||
: interpolate(
|
||||
translateX.value,
|
||||
[getDrawerTranslationX(false), getDrawerTranslationX(true)],
|
||||
[0, 1]
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<DrawerProgressContext.Provider value={progress}>
|
||||
<PanGestureHandler
|
||||
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
hitSlop={hitSlop}
|
||||
enabled={drawerType !== 'permanent' && swipeEnabled}
|
||||
onGestureEvent={onGestureEvent}
|
||||
{...gestureHandlerProps}
|
||||
>
|
||||
{/* Immediate child of gesture handler needs to be an Animated.View */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.main,
|
||||
{
|
||||
flexDirection:
|
||||
drawerType === 'permanent' && !isRight ? 'row-reverse' : 'row',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Animated.View style={[styles.content, contentAnimatedStyle]}>
|
||||
<View
|
||||
accessibilityElementsHidden={isOpen && drawerType !== 'permanent'}
|
||||
importantForAccessibility={
|
||||
isOpen && drawerType !== 'permanent'
|
||||
? 'no-hide-descendants'
|
||||
: 'auto'
|
||||
}
|
||||
style={styles.content}
|
||||
>
|
||||
{renderSceneContent()}
|
||||
</View>
|
||||
{drawerType !== 'permanent' ? (
|
||||
<Overlay
|
||||
progress={progress}
|
||||
onPress={() => toggleDrawer(false)}
|
||||
style={overlayStyle}
|
||||
/>
|
||||
) : null}
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
|
||||
removeClippedSubviews={Platform.OS !== 'ios'}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
position: drawerType === 'permanent' ? 'relative' : 'absolute',
|
||||
zIndex: drawerType === 'back' ? -1 : 0,
|
||||
},
|
||||
drawerAnimatedStyle,
|
||||
drawerStyle as any,
|
||||
]}
|
||||
>
|
||||
{renderDrawerContent()}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</DrawerProgressContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
maxWidth: '100%',
|
||||
width: DEFAULT_DRAWER_WIDTH,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
main: {
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
// FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
|
||||
// But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
|
||||
web: null,
|
||||
default: { overflow: 'hidden' },
|
||||
}),
|
||||
},
|
||||
});
|
||||
56
packages/drawer/src/views/modern/Overlay.tsx
Normal file
56
packages/drawer/src/views/modern/Overlay.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { Pressable, Platform, StyleSheet } from 'react-native';
|
||||
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
||||
|
||||
const PROGRESS_EPSILON = 0.05;
|
||||
|
||||
type Props = React.ComponentProps<typeof Animated.View> & {
|
||||
progress: Animated.SharedValue<number>;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const Overlay = React.forwardRef(function Overlay(
|
||||
{ progress, onPress, style, ...props }: Props,
|
||||
ref: React.Ref<Animated.View>
|
||||
) {
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: progress.value,
|
||||
// We don't want the user to be able to press through the overlay when drawer is open
|
||||
// One approach is to adjust the pointerEvents based on the progress
|
||||
// But we can also send the overlay behind the screen
|
||||
zIndex: progress.value > PROGRESS_EPSILON ? 0 : -1,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
{...props}
|
||||
ref={ref}
|
||||
style={[styles.overlay, overlayStyle, animatedStyle, style]}
|
||||
>
|
||||
<Pressable onPress={onPress} style={styles.pressable} />
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
const overlayStyle = Platform.select<Record<string, string>>({
|
||||
web: {
|
||||
// Disable touch highlight on mobile Safari.
|
||||
// WebkitTapHighlightColor must be used outside of StyleSheet.create because react-native-web will omit the property.
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
},
|
||||
default: {},
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default Overlay;
|
||||
@@ -3,6 +3,95 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-next.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.9...@react-navigation/elements@1.0.0-next.10) (2021-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix drawer content padding in RTL ([ea8ea20](https://github.com/react-navigation/react-navigation/commit/ea8ea20127d979d8c8ddbddf56de1bdfdf0243f9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.8...@react-navigation/elements@1.0.0-next.9) (2021-05-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/elements
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.7...@react-navigation/elements@1.0.0-next.8) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/elements
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.6...@react-navigation/elements@1.0.0-next.7) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/elements
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.5...@react-navigation/elements@1.0.0-next.6) (2021-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* animate pressable opacity ([459fd27](https://github.com/react-navigation/react-navigation/commit/459fd270503075343b71ad446efdc2517eedcf21))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.4...@react-navigation/elements@1.0.0-next.5) (2021-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/elements
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.3...@react-navigation/elements@1.0.0-next.4) (2021-04-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/elements
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.2...@react-navigation/elements@1.0.0-next.3) (2021-03-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.1...@react-navigation/elements@1.0.0-next.2) (2021-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use theme in PlatformPressable ([40439cc](https://github.com/react-navigation/react-navigation/commit/40439ccb420825a1aa480648526a816f2422ea6e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* return nearest parent header height for useHeaderHeight ([24b3f73](https://github.com/react-navigation/react-navigation/commit/24b3f739da4b8af8dca77d92c72cfdaa762e564a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0...@react-navigation/elements@1.0.0-next.1) (2021-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/elements",
|
||||
"description": "UI Components for React Navigation",
|
||||
"version": "1.0.0-next.1",
|
||||
"version": "1.0.0-next.10",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
@@ -30,22 +30,21 @@
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-masked-view/masked-view": "^0.2.2",
|
||||
"@react-navigation/native": "^6.0.0-next.1",
|
||||
"@react-native-masked-view/masked-view": "^0.2.3",
|
||||
"@react-navigation/native": "^6.0.0-next.8",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "~0.63.51",
|
||||
"@types/react-native": "~0.64.4",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native": "~0.63.4",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
|
||||
18
packages/elements/src/Background.tsx
Normal file
18
packages/elements/src/Background.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { View, ViewProps } from 'react-native';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
type Props = ViewProps & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function Background({ style, ...rest }: Props) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...rest}
|
||||
style={[{ flex: 1, backgroundColor: colors.background }, style]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export default function Header(props: Props) {
|
||||
style={[
|
||||
styles.left,
|
||||
headerTitleAlign === 'center' && styles.expand,
|
||||
{ marginLeft: insets.left },
|
||||
{ marginStart: insets.left },
|
||||
leftContainerStyle,
|
||||
]}
|
||||
>
|
||||
@@ -236,7 +236,7 @@ export default function Header(props: Props) {
|
||||
style={[
|
||||
styles.right,
|
||||
styles.expand,
|
||||
{ marginRight: insets.right },
|
||||
{ marginEnd: insets.right },
|
||||
rightContainerStyle,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,8 @@ export default function HeaderBackButton({
|
||||
labelVisible = Platform.OS === 'ios',
|
||||
onLabelLayout,
|
||||
onPress,
|
||||
pressColorAndroid: customPressColorAndroid,
|
||||
pressColor,
|
||||
pressOpacity,
|
||||
screenLayout,
|
||||
tintColor: customTintColor,
|
||||
titleLayout,
|
||||
@@ -31,7 +32,7 @@ export default function HeaderBackButton({
|
||||
testID,
|
||||
style,
|
||||
}: HeaderBackButtonProps) {
|
||||
const { dark, colors } = useTheme();
|
||||
const { colors } = useTheme();
|
||||
|
||||
const [initialLabelWidth, setInitialLabelWidth] = React.useState<
|
||||
undefined | number
|
||||
@@ -45,13 +46,6 @@ export default function HeaderBackButton({
|
||||
default: colors.text,
|
||||
});
|
||||
|
||||
const pressColorAndroid =
|
||||
customPressColorAndroid !== undefined
|
||||
? customPressColorAndroid
|
||||
: dark
|
||||
? 'rgba(255, 255, 255, .32)'
|
||||
: 'rgba(0, 0, 0, .32)';
|
||||
|
||||
const handleLabelLayout = (e: LayoutChangeEvent) => {
|
||||
onLabelLayout?.(e);
|
||||
|
||||
@@ -156,7 +150,8 @@ export default function HeaderBackButton({
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
testID={testID}
|
||||
onPress={disabled ? undefined : handlePress}
|
||||
pressColor={pressColorAndroid}
|
||||
pressColor={pressColor}
|
||||
pressOpacity={pressOpacity}
|
||||
android_ripple={{ borderless: true }}
|
||||
style={[styles.container, disabled && styles.disabled, style]}
|
||||
hitSlop={Platform.select({
|
||||
|
||||
@@ -1,41 +1,85 @@
|
||||
import * as React from 'react';
|
||||
import { Platform, Pressable, PressableProps } from 'react-native';
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
GestureResponderEvent,
|
||||
Platform,
|
||||
Pressable,
|
||||
PressableProps,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
export type Props = PressableProps & {
|
||||
export type Props = Omit<PressableProps, 'style'> & {
|
||||
pressColor?: string;
|
||||
pressOpacity?: number;
|
||||
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
const ANDROID_VERSION_LOLLIPOP = 21;
|
||||
const ANDROID_SUPPORTS_RIPPLE =
|
||||
Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_LOLLIPOP;
|
||||
|
||||
/**
|
||||
* PlatformPressable provides an abstraction on top of TouchableNativeFeedback and
|
||||
* TouchableOpacity to handle platform differences.
|
||||
*
|
||||
* On Android, you can pass the props of TouchableNativeFeedback.
|
||||
* On other platforms, you can pass the props of TouchableOpacity.
|
||||
* PlatformPressable provides an abstraction on top of Pressable to handle platform differences.
|
||||
*/
|
||||
export default function PlatformPressable({
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
android_ripple,
|
||||
pressColor = 'rgba(0, 0, 0, .32)',
|
||||
pressOpacity,
|
||||
pressColor,
|
||||
pressOpacity = 0.3,
|
||||
style,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { dark } = useTheme();
|
||||
const [opacity] = React.useState(() => new Animated.Value(1));
|
||||
|
||||
const animateTo = (toValue: number, duration: number) => {
|
||||
if (ANDROID_SUPPORTS_RIPPLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
Animated.timing(opacity, {
|
||||
toValue,
|
||||
duration,
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const handlePressIn = (e: GestureResponderEvent) => {
|
||||
animateTo(pressOpacity, 150);
|
||||
onPressIn?.(e);
|
||||
};
|
||||
|
||||
const handlePressOut = (e: GestureResponderEvent) => {
|
||||
animateTo(1, 200);
|
||||
onPressOut?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<AnimatedPressable
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
android_ripple={
|
||||
ANDROID_SUPPORTS_RIPPLE
|
||||
? { color: pressColor, ...android_ripple }
|
||||
? {
|
||||
color:
|
||||
pressColor !== undefined
|
||||
? pressColor
|
||||
: dark
|
||||
? 'rgba(255, 255, 255, .32)'
|
||||
: 'rgba(0, 0, 0, .32)',
|
||||
...android_ripple,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={({ pressed }) => [
|
||||
{ opacity: pressed && !ANDROID_SUPPORTS_RIPPLE ? pressOpacity : 1 },
|
||||
typeof style === 'function' ? style({ pressed }) : style,
|
||||
]}
|
||||
style={[{ opacity: !ANDROID_SUPPORTS_RIPPLE ? opacity : 1 }, style]}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import {
|
||||
useSafeAreaFrame,
|
||||
useSafeAreaInsets,
|
||||
@@ -12,16 +12,19 @@ import {
|
||||
ParamListBase,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
import Background from './Background';
|
||||
import HeaderShownContext from './Header/HeaderShownContext';
|
||||
import HeaderHeightContext from './Header/HeaderHeightContext';
|
||||
import getDefaultHeaderHeight from './Header/getDefaultHeaderHeight';
|
||||
|
||||
type Props = {
|
||||
focused: boolean;
|
||||
navigation: NavigationProp<ParamListBase>;
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
route: RouteProp<ParamListBase>;
|
||||
header: React.ReactNode;
|
||||
headerShown?: boolean;
|
||||
headerStatusBarHeight?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -30,14 +33,17 @@ export default function Screen(props: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
||||
const parentHeaderHeight = React.useContext(HeaderHeightContext);
|
||||
|
||||
const {
|
||||
focused,
|
||||
header,
|
||||
headerShown = true,
|
||||
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
|
||||
children,
|
||||
navigation,
|
||||
route,
|
||||
children,
|
||||
style,
|
||||
} = props;
|
||||
|
||||
const [headerHeight, setHeaderHeight] = React.useState(() =>
|
||||
@@ -45,12 +51,18 @@ export default function Screen(props: Props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Background
|
||||
accessibilityElementsHidden={!focused}
|
||||
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
|
||||
style={[styles.container, style]}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<HeaderShownContext.Provider
|
||||
value={isParentHeaderShown || headerShown !== false}
|
||||
>
|
||||
<HeaderHeightContext.Provider value={headerShown ? headerHeight : 0}>
|
||||
<HeaderHeightContext.Provider
|
||||
value={headerShown ? headerHeight : parentHeaderHeight}
|
||||
>
|
||||
{children}
|
||||
</HeaderHeightContext.Provider>
|
||||
</HeaderShownContext.Provider>
|
||||
@@ -70,7 +82,7 @@ export default function Screen(props: Props) {
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
) : null}
|
||||
</View>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ export { default as getHeaderTitle } from './Header/getHeaderTitle';
|
||||
|
||||
export { default as MissingIcon } from './MissingIcon';
|
||||
export { default as PlatformPressable } from './PlatformPressable';
|
||||
export { default as ResourceSavingScene } from './ResourceSavingScene';
|
||||
export { default as ResourceSavingView } from './ResourceSavingView';
|
||||
export { default as SafeAreaProviderCompat } from './SafeAreaProviderCompat';
|
||||
export { default as Screen } from './Screen';
|
||||
export { default as Background } from './Background';
|
||||
|
||||
export const Assets = [
|
||||
// eslint-disable-next-line import/no-commonjs
|
||||
|
||||
@@ -127,13 +127,16 @@ export type HeaderBackButtonProps = {
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Callback to call when the button is pressed.
|
||||
* By default, this triggers `goBack`.
|
||||
*/
|
||||
onPress?: () => void;
|
||||
/**
|
||||
* Color for material ripple (Android >= 5.0 only).
|
||||
*/
|
||||
pressColorAndroid?: string;
|
||||
pressColor?: string;
|
||||
/**
|
||||
* Opacity when the button is pressed, used when ripple is not supported.
|
||||
*/
|
||||
pressOpacity?: number;
|
||||
/**
|
||||
* Function which returns a React Element to display custom image in header's back button.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,65 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.7...@react-navigation/material-bottom-tabs@6.0.0-next.8) (2021-05-16)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.6...@react-navigation/material-bottom-tabs@6.0.0-next.7) (2021-05-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* return a NavigationContent component from useNavigationBuilder ([1179d56](https://github.com/react-navigation/react-navigation/commit/1179d56c5008270753feef41acdc1dbd2191efcf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.5...@react-navigation/material-bottom-tabs@6.0.0-next.6) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.4...@react-navigation/material-bottom-tabs@6.0.0-next.5) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.3...@react-navigation/material-bottom-tabs@6.0.0-next.4) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.2...@react-navigation/material-bottom-tabs@6.0.0-next.3) (2021-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.1...@react-navigation/material-bottom-tabs@6.0.0-next.2) (2021-04-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0...@react-navigation/material-bottom-tabs@6.0.0-next.1) (2021-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-bottom-tabs",
|
||||
"description": "Integration for bottom navigation component from react-native-paper",
|
||||
"version": "6.0.0-next.1",
|
||||
"version": "6.0.0-next.8",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -34,22 +34,21 @@
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-navigation/native": "^6.0.0-next.1",
|
||||
"@react-navigation/native": "^6.0.0-next.8",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "~0.63.51",
|
||||
"@types/react-native": "~0.64.4",
|
||||
"@types/react-native-vector-icons": "^6.4.6",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native": "~0.63.4",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"react-native-paper": "^4.7.2",
|
||||
"react-native-vector-icons": "^8.1.0",
|
||||
|
||||
@@ -28,7 +28,12 @@ function MaterialBottomTabNavigator({
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
const {
|
||||
state,
|
||||
descriptors,
|
||||
navigation,
|
||||
NavigationContent,
|
||||
} = useNavigationBuilder<
|
||||
TabNavigationState<ParamListBase>,
|
||||
TabRouterOptions,
|
||||
TabActionHelpers<ParamListBase>,
|
||||
@@ -42,12 +47,14 @@ function MaterialBottomTabNavigator({
|
||||
});
|
||||
|
||||
return (
|
||||
<MaterialBottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
<NavigationContent>
|
||||
<MaterialBottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export type MaterialBottomTabNavigationHelpers = NavigationHelpers<
|
||||
|
||||
export type MaterialBottomTabNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
RouteName extends keyof ParamList = keyof ParamList
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
@@ -36,7 +36,7 @@ export type MaterialBottomTabNavigationProp<
|
||||
|
||||
export type MaterialBottomTabScreenProps<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
RouteName extends keyof ParamList = keyof ParamList
|
||||
> = {
|
||||
navigation: MaterialBottomTabNavigationProp<ParamList, RouteName>;
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
@@ -85,7 +85,7 @@ export type MaterialBottomTabNavigationOptions = {
|
||||
export type MaterialBottomTabDescriptor = Descriptor<
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationProp<ParamListBase>,
|
||||
RouteProp<ParamListBase, string>
|
||||
RouteProp<ParamListBase>
|
||||
>;
|
||||
|
||||
export type MaterialBottomTabDescriptorMap = Record<
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { Text, StyleSheet, Platform } from 'react-native';
|
||||
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
Route,
|
||||
TabNavigationState,
|
||||
TabActions,
|
||||
@@ -75,7 +74,7 @@ try {
|
||||
};
|
||||
}
|
||||
|
||||
function MaterialBottomTabViewInner({
|
||||
export default function MaterialBottomTabView({
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
@@ -192,14 +191,6 @@ function MaterialBottomTabViewInner({
|
||||
);
|
||||
}
|
||||
|
||||
export default function MaterialBottomTabView(props: Props) {
|
||||
return (
|
||||
<NavigationHelpersContext.Provider value={props.navigation}>
|
||||
<MaterialBottomTabViewInner {...props} />
|
||||
</NavigationHelpersContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
@@ -3,6 +3,78 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [6.0.0-next.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.8...@react-navigation/material-top-tabs@6.0.0-next.9) (2021-05-16)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.7...@react-navigation/material-top-tabs@6.0.0-next.8) (2021-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add a deprecation warning for mode prop in stack ([a6e4981](https://github.com/react-navigation/react-navigation/commit/a6e498170f59648190fa5513e273ca523e56c5d5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* return a NavigationContent component from useNavigationBuilder ([1179d56](https://github.com/react-navigation/react-navigation/commit/1179d56c5008270753feef41acdc1dbd2191efcf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.6...@react-navigation/material-top-tabs@6.0.0-next.7) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.5...@react-navigation/material-top-tabs@6.0.0-next.6) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.4...@react-navigation/material-top-tabs@6.0.0-next.5) (2021-05-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.3...@react-navigation/material-top-tabs@6.0.0-next.4) (2021-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.2...@react-navigation/material-top-tabs@6.0.0-next.3) (2021-04-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.1...@react-navigation/material-top-tabs@6.0.0-next.2) (2021-04-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0...@react-navigation/material-top-tabs@6.0.0-next.1) (2021-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-top-tabs",
|
||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||
"version": "6.0.0-next.1",
|
||||
"version": "6.0.0-next.9",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -34,8 +34,7 @@
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "alpha"
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
@@ -43,19 +42,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"color": "^3.1.3",
|
||||
"warn-once": "^0.0.1"
|
||||
"warn-once": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-navigation/native": "^6.0.0-next.1",
|
||||
"@react-navigation/native": "^6.0.0-next.8",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "~0.63.51",
|
||||
"@types/react-native": "~0.64.4",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native": "~0.63.4",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"react-native-pager-view": "^4.2.4",
|
||||
"react-native-tab-view": "^3.0.0",
|
||||
"react-native-pager-view": "^5.0.12",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -26,9 +26,7 @@ function MaterialTopTabNavigator({
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
// @ts-expect-error: lazy is deprecated
|
||||
lazy,
|
||||
// @ts-expect-error: tabBarOptions is deprecated
|
||||
tabBarOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
@@ -73,7 +71,12 @@ function MaterialTopTabNavigator({
|
||||
);
|
||||
}
|
||||
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
const {
|
||||
state,
|
||||
descriptors,
|
||||
navigation,
|
||||
NavigationContent,
|
||||
} = useNavigationBuilder<
|
||||
TabNavigationState<ParamListBase>,
|
||||
TabRouterOptions,
|
||||
TabActionHelpers<ParamListBase>,
|
||||
@@ -87,12 +90,14 @@ function MaterialTopTabNavigator({
|
||||
});
|
||||
|
||||
return (
|
||||
<MaterialTopTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
<NavigationContent>
|
||||
<MaterialTopTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user