mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-13 17:32:55 +08:00
Compare commits
125 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee3038a4d | ||
|
|
961b519fb1 | ||
|
|
0a19e94b23 | ||
|
|
1e73fed6de | ||
|
|
3193a30da6 | ||
|
|
499c50cd43 | ||
|
|
420f6926e1 | ||
|
|
70be3f6d86 | ||
|
|
bd35b4fc20 | ||
|
|
c511bc0b2b | ||
|
|
b4834ce703 | ||
|
|
ae5442ebe8 | ||
|
|
6dd52d35cf | ||
|
|
d6fa279d93 | ||
|
|
c3fa83efe0 | ||
|
|
f2291d110f | ||
|
|
942d2be2c7 | ||
|
|
b747e527a4 | ||
|
|
38020de80b | ||
|
|
67404f4999 | ||
|
|
2792f438fe | ||
|
|
2573b5beaa | ||
|
|
2697355ab2 | ||
|
|
a695cf9c05 | ||
|
|
c9c825bee6 | ||
|
|
b172b51f17 | ||
|
|
9c05af50b4 | ||
|
|
24febf6ea9 | ||
|
|
8cbb201f1a | ||
|
|
2467ce4ff7 | ||
|
|
5683bebfd6 | ||
|
|
78485cea69 | ||
|
|
1613915669 | ||
|
|
335a04edc1 | ||
|
|
5e0069a896 | ||
|
|
249248e741 | ||
|
|
821343fed3 | ||
|
|
82edb2581b | ||
|
|
cb67530dc5 | ||
|
|
36689e24c2 | ||
|
|
6e51f596fa | ||
|
|
402df73aa2 | ||
|
|
187aefe9c4 | ||
|
|
2613a62874 | ||
|
|
6bdf6ae4ed | ||
|
|
e2bcf5168c | ||
|
|
dfdba8d741 | ||
|
|
a3f7a5feba | ||
|
|
004c7d7ab1 | ||
|
|
49f658fbc0 | ||
|
|
cb2f157a56 | ||
|
|
c4acdaa703 | ||
|
|
f1a8bceba5 | ||
|
|
44081172d4 | ||
|
|
de5d985f3b | ||
|
|
b71de6cc79 | ||
|
|
303f0b78a5 | ||
|
|
ce3994c82c | ||
|
|
ba1f405129 | ||
|
|
d4fd906915 | ||
|
|
b7fa90bf8d | ||
|
|
9556aa9eff | ||
|
|
9a8fea8f2c | ||
|
|
9973db86f0 | ||
|
|
8432e5ab25 | ||
|
|
9bb5cfded3 | ||
|
|
4ac40b5c5d | ||
|
|
cd47915861 | ||
|
|
d649fbc669 | ||
|
|
105da6ab2f | ||
|
|
ac7f972e92 | ||
|
|
babb5027f9 | ||
|
|
78d7a66b2b | ||
|
|
a248c453ba | ||
|
|
e097df880a | ||
|
|
856449b200 | ||
|
|
d94e43c3c8 | ||
|
|
3096de6286 | ||
|
|
1c001424b5 | ||
|
|
0f2368965c | ||
|
|
61f16d3f25 | ||
|
|
853740bfaf | ||
|
|
179b6312fe | ||
|
|
043924ca48 | ||
|
|
813a5903b5 | ||
|
|
3709e652f4 | ||
|
|
5b15c7164f | ||
|
|
e030932497 | ||
|
|
adbfedcd58 | ||
|
|
bc9b044fb3 | ||
|
|
f24d3a3461 | ||
|
|
3df65e2819 | ||
|
|
5c4afc5cb4 | ||
|
|
d5bb357053 | ||
|
|
b1fe73097f | ||
|
|
49f6fed6d3 | ||
|
|
b1a65fc73e | ||
|
|
3ea8eec432 | ||
|
|
00e0f05190 | ||
|
|
193c344ba5 | ||
|
|
358d9e9feb | ||
|
|
6a5d0a035a | ||
|
|
b75744abd5 | ||
|
|
6dbda1a0c2 | ||
|
|
70029d6c13 | ||
|
|
469d0542c7 | ||
|
|
0dcaea3242 | ||
|
|
646cbfb28e | ||
|
|
660cac3557 | ||
|
|
e637250a7e | ||
|
|
82af7bed71 | ||
|
|
cb46d0bca4 | ||
|
|
b3665a325d | ||
|
|
0cc7a12b9c | ||
|
|
90e417248d | ||
|
|
e071a978e6 | ||
|
|
296c836064 | ||
|
|
09f6808d7d | ||
|
|
5bb0f405ce | ||
|
|
2dfa4f3629 | ||
|
|
cf41288760 | ||
|
|
3677818f63 | ||
|
|
162410843c | ||
|
|
028c2887c6 | ||
|
|
7a44cda136 |
@@ -16,7 +16,9 @@ jobs:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "yarn.lock" }}
|
||||
- v1-dependencies-
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run:
|
||||
name: Install project dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: v1-dependencies-{{ checksum "yarn.lock" }}
|
||||
paths: node_modules
|
||||
@@ -28,28 +30,57 @@ jobs:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run: |
|
||||
yarn lint
|
||||
yarn typescript
|
||||
- run:
|
||||
name: Lint files
|
||||
command: yarn lint
|
||||
- run:
|
||||
name: Typecheck files
|
||||
command: yarn typescript
|
||||
unit-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run: |
|
||||
yarn test --coverage
|
||||
cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
||||
- run:
|
||||
name: Run unit tests
|
||||
command: yarn test --coverage
|
||||
- run:
|
||||
name: Upload test coverage
|
||||
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
destination: coverage
|
||||
integration-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run:
|
||||
name: Install Headless Chrome dependencies
|
||||
command: |
|
||||
sudo apt-get install -yq \
|
||||
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
||||
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
|
||||
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
|
||||
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
- run:
|
||||
name: Build example for web
|
||||
command: yarn example expo build:web --no-pwa
|
||||
- run:
|
||||
name: Run integration tests
|
||||
command: yarn example test --maxWorkers=2
|
||||
build-packages:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run: |
|
||||
yarn lerna run prepare
|
||||
node scripts/check-types-path.js
|
||||
- run:
|
||||
name: Build packages in the monorepo
|
||||
command: yarn lerna run prepare
|
||||
- run:
|
||||
name: Verify paths for types
|
||||
command: node scripts/check-types-path.js
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -62,6 +93,9 @@ workflows:
|
||||
- unit-tests:
|
||||
requires:
|
||||
- install-dependencies
|
||||
- integration-tests:
|
||||
requires:
|
||||
- install-dependencies
|
||||
- build-packages:
|
||||
requires:
|
||||
- install-dependencies
|
||||
|
||||
2
.github/workflows/expo-preview.yml
vendored
2
.github/workflows/expo-preview.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Get expo link
|
||||
id: expo
|
||||
run: echo "::set-output name=path::@react-navigation/react-react-navigationample?release-channel=pr-${{ github.event.number }}"
|
||||
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
|
||||
|
||||
- name: Comment on PR
|
||||
uses: unsplash/comment-on-pr@master
|
||||
|
||||
20
.github/workflows/stale.yml
vendored
20
.github/workflows/stale.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: "Close stale issues and pull requests"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Hello 👋, this issue has been open for more than 3 months with no activity on it. If the issue is still present in the latest version, please leave a comment within 7 days to keep it open, otherwise it will be closed automatically. If you found a solution on workaround for the issue, please comment here for others to find. If this issue is critical for you, please consider sending a pull request to fix the issue.'
|
||||
stale-pr-message: 'Hello 👋, this pull request has been open for more than 3 months with no activity on it. If you think this is still necessary with the latest version, please comment and ping a maintainer to get this reviewed, otherwise it will be closed automatically in 7 days.'
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-issue-label: 'keep open'
|
||||
exempt-pr-label: 'keep open'
|
||||
15
.github/workflows/triage.yml
vendored
15
.github/workflows/triage.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to reproduce the issue with minimal code (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
|
||||
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
|
||||
|
||||
needs-repro:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: comment "Hey! Thanks for opening the issue. Can you provide a minimal repro which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
|
||||
args: comment "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
|
||||
|
||||
question:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,3 +36,14 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. This helps us prioritize fixing bugs in the library. Seems you have a usage question. Please ask the question on [StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation) instead using the `react-navigation` label. You can also chat with other community members on [Reactiflux Discord server](https://www.reactiflux.com/) in the `#react-navigation` channel."
|
||||
|
||||
feature-request:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'feature-request'
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/github@v1.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. Seems you have a feature request. Please post the feature request on [Canny](https://react-navigation.canny.io/feature-requests). This lets other users upvote your feature request and helps us prioritize the most requested features."
|
||||
|
||||
27
.github/workflows/versions.yml
vendored
Normal file
27
.github/workflows/versions.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Check versions
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-versions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: react-navigation/check-versions-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
packages: |
|
||||
@react-navigation/bottom-tabs
|
||||
@react-navigation/compat
|
||||
@react-navigation/core
|
||||
@react-navigation/drawer
|
||||
@react-navigation/material-bottom-tabs
|
||||
@react-navigation/material-top-tabs
|
||||
@react-navigation/native
|
||||
@react-navigation/routers
|
||||
@react-navigation/stack
|
||||
react-navigation-animated-switch
|
||||
react-navigation-drawer
|
||||
react-navigation-material-bottom-tabs
|
||||
react-navigation-stack
|
||||
react-navigation-tabs
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
.gradle
|
||||
.project
|
||||
.settings
|
||||
.history
|
||||
|
||||
local.properties
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"slug": "react-navigation-example",
|
||||
"description": "Demo app to showcase various functionality of React Navigation",
|
||||
"privacy": "public",
|
||||
"sdkVersion": "36.0.0",
|
||||
"sdkVersion": "37.0.0",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = function(api) {
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"settings": {
|
||||
"import/core-modules": [
|
||||
"detox",
|
||||
"detox/runners/jest/adapter",
|
||||
"detox/runners/jest/specReporter"
|
||||
]
|
||||
},
|
||||
"env": { "jest": true, "jasmine": true }
|
||||
}
|
||||
44
example/e2e/__integration_tests__/Link.test.tsx
Normal file
44
example/e2e/__integration_tests__/Link.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { page } from '../config/setup-playwright';
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.click('[data-testid=LinkComponent]');
|
||||
});
|
||||
|
||||
it('loads the article page', async () => {
|
||||
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||
'/link-component/Article?author=Gandalf'
|
||||
);
|
||||
expect(
|
||||
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||
(it: any) => it.role === 'heading'
|
||||
)?.name
|
||||
).toBe('Article by Gandalf');
|
||||
});
|
||||
|
||||
it('goes to the album page and goes back', async () => {
|
||||
await page.click('[href="/link-component/Album"]');
|
||||
|
||||
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||
'/link-component/Album'
|
||||
);
|
||||
|
||||
expect(
|
||||
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||
(it: any) => it.role === 'heading'
|
||||
)?.name
|
||||
).toBe('Album');
|
||||
|
||||
await page.click('[aria-label="Article by Gandalf, back"]');
|
||||
|
||||
await page.waitForNavigation();
|
||||
|
||||
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
|
||||
'/link-component/Article?author=Gandalf'
|
||||
);
|
||||
|
||||
expect(
|
||||
((await page.accessibility.snapshot()) as any)?.children?.find(
|
||||
(it: any) => it.role === 'heading'
|
||||
)?.name
|
||||
).toBe('Article by Gandalf');
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { by, element, expect, device } from 'detox';
|
||||
|
||||
beforeEach(async () => {
|
||||
await device.reloadReactNative();
|
||||
});
|
||||
|
||||
it('has dark theme toggle', async () => {
|
||||
await expect(element(by.text('Dark theme'))).toBeVisible();
|
||||
});
|
||||
13
example/e2e/__integration_tests__/index.test.tsx
Normal file
13
example/e2e/__integration_tests__/index.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { page } from '../config/setup-playwright';
|
||||
|
||||
it('loads the example app', async () => {
|
||||
const snapshot = await page.accessibility.snapshot();
|
||||
|
||||
// @ts-ignore
|
||||
expect(snapshot?.children?.find((it) => it.role === 'heading')?.name).toBe(
|
||||
'Examples'
|
||||
);
|
||||
const title = await page.$eval('[role=heading]', (el) => el.textContent);
|
||||
|
||||
expect(title).toBe('Examples');
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"setupFilesAfterEnv": ["./init.js"],
|
||||
"testEnvironment": "node",
|
||||
"reporters": ["detox/runners/jest/streamlineReporter"],
|
||||
"verbose": true
|
||||
}
|
||||
24
example/e2e/config/setup-playwright.tsx
Normal file
24
example/e2e/config/setup-playwright.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { chromium, Browser, BrowserContext, Page } from 'playwright';
|
||||
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await page.goto('http://localhost:3579');
|
||||
});
|
||||
|
||||
export { browser, context, page };
|
||||
8
example/e2e/config/setup-server.tsx
Normal file
8
example/e2e/config/setup-server.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { setup } from 'jest-dev-server';
|
||||
|
||||
export default async function () {
|
||||
await setup({
|
||||
command: 'yarn serve -l 3579 web-build',
|
||||
port: 3579,
|
||||
});
|
||||
}
|
||||
5
example/e2e/config/teardown-server.tsx
Normal file
5
example/e2e/config/teardown-server.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { teardown } from 'jest-dev-server';
|
||||
|
||||
export default async function () {
|
||||
await teardown();
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/* eslint-disable jest/no-jasmine-globals, import/no-commonjs */
|
||||
|
||||
const detox = require('detox');
|
||||
const config = require('../../package.json').detox;
|
||||
const adapter = require('detox/runners/jest/adapter');
|
||||
const specReporter = require('detox/runners/jest/specReporter');
|
||||
|
||||
// Set the default timeout
|
||||
jest.setTimeout(120000);
|
||||
|
||||
jasmine.getEnv().addReporter(adapter);
|
||||
|
||||
// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
|
||||
// This is strictly optional.
|
||||
jasmine.getEnv().addReporter(specReporter);
|
||||
|
||||
beforeAll(async () => {
|
||||
await detox.init(config);
|
||||
}, 300000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await adapter.beforeEach();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await adapter.afterAll();
|
||||
await detox.cleanup();
|
||||
});
|
||||
6
example/jest.config.js
Normal file
6
example/jest.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
testRegex: '/__integration_tests__/.*\\.(test|spec)\\.(js|tsx?)$',
|
||||
globalSetup: './e2e/config/setup-server.tsx',
|
||||
globalTeardown: './e2e/config/teardown-server.tsx',
|
||||
setupFilesAfterEnv: ['./e2e/config/setup-playwright.tsx'],
|
||||
};
|
||||
@@ -15,8 +15,8 @@ const modules = ['@expo/vector-icons']
|
||||
// List all packages under `packages/`
|
||||
.readdirSync(packages)
|
||||
// Ignore hidden files such as .DS_Store
|
||||
.filter(p => !p.startsWith('.'))
|
||||
.map(p => {
|
||||
.filter((p) => !p.startsWith('.'))
|
||||
.map((p) => {
|
||||
const pak = JSON.parse(
|
||||
fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8')
|
||||
);
|
||||
@@ -50,9 +50,9 @@ module.exports = {
|
||||
blacklistRE: blacklist(
|
||||
fs
|
||||
.readdirSync(packages)
|
||||
.map(p => path.join(packages, p))
|
||||
.map((p) => path.join(packages, p))
|
||||
.map(
|
||||
it => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
|
||||
(it) => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
server: {
|
||||
enhanceMiddleware: middleware => {
|
||||
enhanceMiddleware: (middleware) => {
|
||||
return (req, res, next) => {
|
||||
// When an asset is imported outside the project root, it has wrong path on Android
|
||||
// This happens for the back button in stack, so we fix the path to correct one
|
||||
|
||||
@@ -8,35 +8,40 @@
|
||||
"web": "expo start:web",
|
||||
"native": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios"
|
||||
"ios": "react-native run-ios",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^10.0.0",
|
||||
"@react-native-community/masked-view": "0.1.6",
|
||||
"@types/react-native-restart": "^0.0.0",
|
||||
"@react-native-community/masked-view": "^0.1.7",
|
||||
"color": "^3.1.2",
|
||||
"expo": "^36.0.2",
|
||||
"expo-asset": "~8.0.0",
|
||||
"expo-blur": "^8.0.0",
|
||||
"expo": "^37.0.0",
|
||||
"expo-asset": "~8.1.3",
|
||||
"expo-blur": "~8.1.0",
|
||||
"react": "~16.9.0",
|
||||
"react-dom": "~16.9.0",
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-gesture-handler": "^1.5.6",
|
||||
"react-native-paper": "^3.6.0",
|
||||
"react-native-gesture-handler": "^1.6.0",
|
||||
"react-native-paper": "^3.7.0",
|
||||
"react-native-reanimated": "^1.7.0",
|
||||
"react-native-restart": "^0.0.13",
|
||||
"react-native-safe-area-context": "^0.7.2",
|
||||
"react-native-screens": "^2.0.0-beta.2",
|
||||
"react-native-tab-view": "2.13.0",
|
||||
"react-native-unimodules": "^0.7.0",
|
||||
"react-native-restart": "^0.0.14",
|
||||
"react-native-safe-area-context": "^0.7.3",
|
||||
"react-native-screens": "^2.3.0",
|
||||
"react-native-tab-view": "2.14.0",
|
||||
"react-native-unimodules": "~0.8.1",
|
||||
"react-native-web": "^0.11.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/webpack-config": "^0.10.12",
|
||||
"@types/react": "^16.9.19",
|
||||
"@types/react-native": "^0.60.30",
|
||||
"babel-preset-expo": "^8.0.0",
|
||||
"expo-cli": "^3.11.9",
|
||||
"typescript": "^3.7.5"
|
||||
"@expo/webpack-config": "^0.11.19",
|
||||
"@types/jest-dev-server": "^4.2.0",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.60.22",
|
||||
"babel-preset-expo": "^8.1.0",
|
||||
"expo-cli": "^3.17.18",
|
||||
"jest": "^25.2.7",
|
||||
"jest-dev-server": "^4.4.0",
|
||||
"playwright": "^0.14.0",
|
||||
"serve": "^11.3.0",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
3
example/src/AsyncStorage.native.tsx
Normal file
3
example/src/AsyncStorage.native.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { AsyncStorage } from 'react-native';
|
||||
|
||||
export default AsyncStorage;
|
||||
14
example/src/AsyncStorage.tsx
Normal file
14
example/src/AsyncStorage.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
getItem(key: string) {
|
||||
return Promise.resolve(localStorage.getItem(key));
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
return Promise.resolve(localStorage.setItem(key, value));
|
||||
},
|
||||
removeItem(key: string) {
|
||||
return Promise.resolve(localStorage.removeItem(key));
|
||||
},
|
||||
clear() {
|
||||
return Promise.resolve(localStorage.clear());
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import TouchableBounce from '../Shared/TouchableBounce';
|
||||
@@ -28,7 +29,10 @@ export default function BottomTabsScreen() {
|
||||
return (
|
||||
<BottomTabs.Navigator
|
||||
screenOptions={{
|
||||
tabBarButton: props => <TouchableBounce {...props} />,
|
||||
tabBarButton:
|
||||
Platform.OS === 'web'
|
||||
? undefined
|
||||
: (props) => <TouchableBounce {...props} />,
|
||||
}}
|
||||
>
|
||||
<BottomTabs.Screen
|
||||
@@ -38,7 +42,7 @@ export default function BottomTabsScreen() {
|
||||
tabBarIcon: getTabBarIcon('file-document-box'),
|
||||
}}
|
||||
>
|
||||
{props => <SimpleStackScreen {...props} headerMode="none" />}
|
||||
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
|
||||
</BottomTabs.Screen>
|
||||
<BottomTabs.Screen
|
||||
name="Chat"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import {
|
||||
createCompatNavigatorFactory,
|
||||
@@ -11,25 +11,32 @@ import {
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
type CompatStackParams = {
|
||||
Article: { author: string };
|
||||
Album: undefined;
|
||||
Albums: undefined;
|
||||
Nested: { author: string };
|
||||
};
|
||||
|
||||
const ArticleScreen: CompatScreenType<StackNavigationProp<
|
||||
CompatStackParams,
|
||||
'Article'
|
||||
type NestedStackParams = {
|
||||
Feed: undefined;
|
||||
Article: { author: string };
|
||||
};
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||
CompatStackParams
|
||||
>> = ({ navigation }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Album')}
|
||||
onPress={() => navigation.push('Nested', { author: 'Babel fish' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
Push nested
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
@@ -39,24 +46,20 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: navigation.getParam('author') }} />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
ArticleScreen.navigationOptions = ({ navigation }) => ({
|
||||
title: `Article by ${navigation.getParam('author')}`,
|
||||
});
|
||||
|
||||
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||
CompatStackParams
|
||||
>> = ({ navigation }) => {
|
||||
const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
|
||||
navigation,
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
||||
onPress={() => navigation.push('Article')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
@@ -69,22 +72,69 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const CompatStack = createCompatNavigatorFactory(createStackNavigator)<
|
||||
const ArticleScreen: CompatScreenType<StackNavigationProp<
|
||||
NestedStackParams,
|
||||
'Article'
|
||||
>> = ({ navigation }) => {
|
||||
navigation.dangerouslyGetParent();
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Albums')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push albums
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: navigation.getParam('author') }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
ArticleScreen.navigationOptions = ({ navigation }) => ({
|
||||
title: `Article by ${navigation.getParam('author')}`,
|
||||
});
|
||||
|
||||
const createCompatStackNavigator = createCompatNavigatorFactory(
|
||||
createStackNavigator
|
||||
);
|
||||
|
||||
const CompatStack = createCompatStackNavigator<
|
||||
StackNavigationProp<CompatStackParams>
|
||||
>(
|
||||
{
|
||||
Article: {
|
||||
screen: ArticleScreen,
|
||||
Albums: AlbumsScreen,
|
||||
Nested: {
|
||||
screen: createCompatStackNavigator<
|
||||
StackNavigationProp<NestedStackParams>
|
||||
>(
|
||||
{
|
||||
Feed: FeedScreen,
|
||||
Article: ArticleScreen,
|
||||
},
|
||||
{ navigationOptions: { headerShown: false } }
|
||||
),
|
||||
params: {
|
||||
author: 'Gandalf',
|
||||
},
|
||||
},
|
||||
Album: AlbumsScreen,
|
||||
},
|
||||
{
|
||||
mode: 'modal',
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function BottomTabsScreen() {
|
||||
|
||||
return (
|
||||
<BottomTabs.Navigator>
|
||||
{tabs.map(i => (
|
||||
{tabs.map((i) => (
|
||||
<BottomTabs.Screen
|
||||
key={i}
|
||||
name={`tab-${i}`}
|
||||
@@ -29,12 +29,14 @@ export default function BottomTabsScreen() {
|
||||
{() => (
|
||||
<View style={styles.container}>
|
||||
<Title>Tab {i}</Title>
|
||||
<Button onPress={() => setTabs(tabs => [...tabs, tabs.length])}>
|
||||
<Button onPress={() => setTabs((tabs) => [...tabs, tabs.length])}>
|
||||
Add a tab
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() =>
|
||||
setTabs(tabs => (tabs.length > 1 ? tabs.slice(0, -1) : tabs))
|
||||
setTabs((tabs) =>
|
||||
tabs.length > 1 ? tabs.slice(0, -1) : tabs
|
||||
)
|
||||
}
|
||||
>
|
||||
Remove a tab
|
||||
|
||||
162
example/src/Screens/LinkComponent.tsx
Normal file
162
example/src/Screens/LinkComponent.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import {
|
||||
Link,
|
||||
StackActions,
|
||||
RouteProp,
|
||||
ParamListBase,
|
||||
useLinkProps,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
|
||||
type SimpleStackParams = {
|
||||
Article: { author: string };
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const LinkButton = ({
|
||||
to,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof Button> & { to: string }) => {
|
||||
const { onPress, ...props } = useLinkProps({ to });
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
{...rest}
|
||||
{...Platform.select({
|
||||
web: { onClick: onPress } as any,
|
||||
default: { onPress },
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Link
|
||||
to="/link-component/Album"
|
||||
style={[styles.button, { padding: 8 }]}
|
||||
>
|
||||
Go to /link-component/Album
|
||||
</Link>
|
||||
<Link
|
||||
to="/link-component/Album"
|
||||
action={StackActions.replace('Album')}
|
||||
style={[styles.button, { padding: 8 }]}
|
||||
>
|
||||
Replace with /link-component/Album
|
||||
</Link>
|
||||
<LinkButton
|
||||
to="/link-component/Album"
|
||||
mode="contained"
|
||||
style={styles.button}
|
||||
>
|
||||
Go to /link-component/Album
|
||||
</LinkButton>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Link
|
||||
to="/link-component/Article?author=Babel"
|
||||
style={[styles.button, { padding: 8 }]}
|
||||
>
|
||||
Go to /link-component/Article
|
||||
</Link>
|
||||
<LinkButton
|
||||
to="/link-component/Article?author=Babel"
|
||||
mode="contained"
|
||||
style={styles.button}
|
||||
>
|
||||
Go to /link-component/Article
|
||||
</LinkButton>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator {...rest}>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params.author}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
options={{ title: 'Album' }}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
127
example/src/Screens/MasterDetail.tsx
Normal file
127
example/src/Screens/MasterDetail.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from 'react';
|
||||
import { Dimensions, ScaledSize } from 'react-native';
|
||||
import { Appbar } from 'react-native-paper';
|
||||
import { ParamListBase } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import {
|
||||
createDrawerNavigator,
|
||||
DrawerNavigationProp,
|
||||
DrawerContent,
|
||||
} from '@react-navigation/drawer';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
type DrawerParams = {
|
||||
Article: undefined;
|
||||
NewsFeed: undefined;
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
|
||||
|
||||
const useIsLargeScreen = () => {
|
||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||
|
||||
React.useEffect(() => {
|
||||
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
|
||||
setDimensions(window);
|
||||
};
|
||||
|
||||
Dimensions.addEventListener('change', onDimensionsChange);
|
||||
|
||||
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||
}, []);
|
||||
|
||||
return dimensions.width > 414;
|
||||
};
|
||||
|
||||
const Header = ({
|
||||
onGoBack,
|
||||
title,
|
||||
}: {
|
||||
onGoBack: () => void;
|
||||
title: string;
|
||||
}) => {
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
{isLargeScreen ? null : <Appbar.BackAction onPress={onGoBack} />}
|
||||
<Appbar.Content title={title} />
|
||||
</Appbar.Header>
|
||||
);
|
||||
};
|
||||
|
||||
const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
||||
return (
|
||||
<>
|
||||
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
|
||||
<Article />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
||||
return (
|
||||
<>
|
||||
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
|
||||
<NewsFeed />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
||||
return (
|
||||
<>
|
||||
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
|
||||
<Albums />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Drawer = createDrawerNavigator<DrawerParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function DrawerScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
gestureEnabled: false,
|
||||
});
|
||||
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
|
||||
return (
|
||||
<Drawer.Navigator
|
||||
openByDefault
|
||||
drawerType={isLargeScreen ? 'permanent' : 'back'}
|
||||
drawerStyle={isLargeScreen ? null : { width: '100%' }}
|
||||
overlayColor="transparent"
|
||||
drawerContent={(props) => (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
|
||||
<Appbar.Content title="Pages" />
|
||||
</Appbar.Header>
|
||||
<DrawerContent {...props} />
|
||||
</>
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<Drawer.Screen name="Article" component={ArticleScreen} />
|
||||
<Drawer.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
options={{ title: 'Album' }}
|
||||
/>
|
||||
</Drawer.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default function MaterialBottomTabsScreen() {
|
||||
tabBarColor: '#C9E7F8',
|
||||
}}
|
||||
>
|
||||
{props => <SimpleStackScreen {...props} headerMode="none" />}
|
||||
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
|
||||
</MaterialBottomTabs.Screen>
|
||||
<MaterialBottomTabs.Screen
|
||||
name="Chat"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -17,6 +17,8 @@ type ModalStackParams = {
|
||||
|
||||
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -42,7 +44,10 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -66,7 +71,7 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={false} />
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -18,6 +18,8 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -43,7 +45,10 @@ const ArticleScreen = ({
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -71,7 +76,7 @@ const NewsFeedScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={false} />
|
||||
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -99,7 +104,7 @@ const AlbumsScreen = ({
|
||||
Pop by 2
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={false} />
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -45,7 +47,10 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -75,7 +80,7 @@ const AlbumsScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={false} />
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { Button, Paragraph } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -15,6 +15,8 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -40,7 +42,10 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ScrollViewProps,
|
||||
Dimensions,
|
||||
Platform,
|
||||
ScaledSize,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop } from '@react-navigation/native';
|
||||
|
||||
@@ -40,15 +41,38 @@ const COVERS = [
|
||||
];
|
||||
|
||||
export default function Albums(props: Partial<ScrollViewProps>) {
|
||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||
|
||||
React.useEffect(() => {
|
||||
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
|
||||
setDimensions(window);
|
||||
};
|
||||
|
||||
Dimensions.addEventListener('change', onDimensionsChange);
|
||||
|
||||
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||
}, []);
|
||||
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
|
||||
const itemSize = dimensions.width / Math.floor(dimensions.width / 150);
|
||||
|
||||
return (
|
||||
<ScrollView ref={ref} contentContainerStyle={styles.content} {...props}>
|
||||
{COVERS.map((source, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<View key={i} style={styles.item}>
|
||||
<View
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
style={[
|
||||
styles.item,
|
||||
Platform.OS !== 'web' && {
|
||||
height: itemSize,
|
||||
width: itemSize,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Image source={source} style={styles.photo} />
|
||||
</View>
|
||||
))}
|
||||
@@ -76,10 +100,6 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
item: {
|
||||
height: Dimensions.get('window').width / 2,
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
}),
|
||||
photo: {
|
||||
|
||||
@@ -68,10 +68,7 @@ export default function Chat(props: Partial<ScrollViewProps>) {
|
||||
styles.input,
|
||||
{ backgroundColor: colors.card, color: colors.text },
|
||||
]}
|
||||
placeholderTextColor={Color(colors.text)
|
||||
.alpha(0.5)
|
||||
.rgb()
|
||||
.string()}
|
||||
placeholderTextColor={Color(colors.text).alpha(0.5).rgb().string()}
|
||||
placeholder="Write a message"
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
|
||||
@@ -51,10 +51,7 @@ export default function NewsFeed(props: Props) {
|
||||
<Card style={styles.card}>
|
||||
<TextInput
|
||||
placeholder="What's on your mind?"
|
||||
placeholderTextColor={Color(colors.text)
|
||||
.alpha(0.5)
|
||||
.rgb()
|
||||
.string()}
|
||||
placeholderTextColor={Color(colors.text).alpha(0.5).rgb().string()}
|
||||
style={styles.input}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
AsyncStorage,
|
||||
YellowBox,
|
||||
Platform,
|
||||
StatusBar,
|
||||
I18nManager,
|
||||
Dimensions,
|
||||
ScaledSize,
|
||||
} from 'react-native';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
@@ -20,10 +21,10 @@ import {
|
||||
Appbar,
|
||||
List,
|
||||
Divider,
|
||||
Text,
|
||||
} from 'react-native-paper';
|
||||
import {
|
||||
InitialState,
|
||||
useLinking,
|
||||
NavigationContainerRef,
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
@@ -40,7 +41,9 @@ import {
|
||||
HeaderStyleInterpolators,
|
||||
} from '@react-navigation/stack';
|
||||
|
||||
import AsyncStorage from './AsyncStorage';
|
||||
import LinkingPrefixes from './LinkingPrefixes';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import SimpleStack from './Screens/SimpleStack';
|
||||
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
||||
import StackTransparent from './Screens/StackTransparent';
|
||||
@@ -51,12 +54,16 @@ import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||
import DynamicTabs from './Screens/DynamicTabs';
|
||||
import AuthFlow from './Screens/AuthFlow';
|
||||
import CompatAPI from './Screens/CompatAPI';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import MasterDetail from './Screens/MasterDetail';
|
||||
import LinkComponent from './Screens/LinkComponent';
|
||||
|
||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||
|
||||
enableScreens();
|
||||
|
||||
// @ts-ignore
|
||||
global.REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED = true;
|
||||
|
||||
type RootDrawerParamList = {
|
||||
Root: undefined;
|
||||
Another: undefined;
|
||||
@@ -95,6 +102,10 @@ const SCREENS = {
|
||||
title: 'Dynamic Tabs',
|
||||
component: DynamicTabs,
|
||||
},
|
||||
MasterDetail: {
|
||||
title: 'Master Detail',
|
||||
component: MasterDetail,
|
||||
},
|
||||
AuthFlow: {
|
||||
title: 'Auth Flow',
|
||||
component: AuthFlow,
|
||||
@@ -103,6 +114,10 @@ const SCREENS = {
|
||||
title: 'Compat Layer',
|
||||
component: CompatAPI,
|
||||
},
|
||||
LinkComponent: {
|
||||
title: '<Link />',
|
||||
component: LinkComponent,
|
||||
},
|
||||
};
|
||||
|
||||
const Drawer = createDrawerNavigator<RootDrawerParamList>();
|
||||
@@ -114,35 +129,7 @@ const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
|
||||
Asset.loadAsync(StackAssets);
|
||||
|
||||
export default function App() {
|
||||
const containerRef = React.useRef<NavigationContainerRef>();
|
||||
|
||||
// To test deep linking on, run the following in the Terminal:
|
||||
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
|
||||
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
|
||||
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
||||
const { getInitialState } = useLinking(containerRef, {
|
||||
prefixes: LinkingPrefixes,
|
||||
config: {
|
||||
Root: {
|
||||
path: '',
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
acc[name] = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ Home: '' }
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
const containerRef = React.useRef<NavigationContainerRef>(null);
|
||||
|
||||
const [theme, setTheme] = React.useState(DefaultTheme);
|
||||
|
||||
@@ -154,12 +141,13 @@ export default function App() {
|
||||
React.useEffect(() => {
|
||||
const restoreState = async () => {
|
||||
try {
|
||||
let state = await getInitialState();
|
||||
let state;
|
||||
|
||||
if (Platform.OS !== 'web' && state === undefined) {
|
||||
const savedState = await AsyncStorage.getItem(
|
||||
NAVIGATION_PERSISTENCE_KEY
|
||||
);
|
||||
|
||||
state = savedState ? JSON.parse(savedState) : undefined;
|
||||
}
|
||||
|
||||
@@ -180,7 +168,7 @@ export default function App() {
|
||||
};
|
||||
|
||||
restoreState();
|
||||
}, [getInitialState]);
|
||||
}, []);
|
||||
|
||||
const paperTheme = React.useMemo(() => {
|
||||
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
|
||||
@@ -196,10 +184,24 @@ export default function App() {
|
||||
};
|
||||
}, [theme.colors, theme.dark]);
|
||||
|
||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||
|
||||
React.useEffect(() => {
|
||||
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
|
||||
setDimensions(window);
|
||||
};
|
||||
|
||||
Dimensions.addEventListener('change', onDimensionsChange);
|
||||
|
||||
return () => Dimensions.removeEventListener('change', onDimensionsChange);
|
||||
}, []);
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLargeScreen = dimensions.width >= 1024;
|
||||
|
||||
return (
|
||||
<PaperProvider theme={paperTheme}>
|
||||
{Platform.OS === 'ios' && (
|
||||
@@ -208,15 +210,43 @@ export default function App() {
|
||||
<NavigationContainer
|
||||
ref={containerRef}
|
||||
initialState={initialState}
|
||||
onStateChange={state =>
|
||||
onStateChange={(state) =>
|
||||
AsyncStorage.setItem(
|
||||
NAVIGATION_PERSISTENCE_KEY,
|
||||
JSON.stringify(state)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
linking={{
|
||||
// To test deep linking on, run the following in the Terminal:
|
||||
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
|
||||
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
|
||||
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
||||
prefixes: LinkingPrefixes,
|
||||
config: {
|
||||
Root: {
|
||||
path: '',
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
acc[name] = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ Home: '' }
|
||||
),
|
||||
},
|
||||
},
|
||||
}}
|
||||
fallback={<Text>Loading…</Text>}
|
||||
>
|
||||
<Drawer.Navigator>
|
||||
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||
<Drawer.Screen
|
||||
name="Root"
|
||||
options={{
|
||||
@@ -240,13 +270,15 @@ export default function App() {
|
||||
name="Home"
|
||||
options={{
|
||||
title: 'Examples',
|
||||
headerLeft: () => (
|
||||
<Appbar.Action
|
||||
color={theme.colors.text}
|
||||
icon="menu"
|
||||
onPress={() => navigation.toggleDrawer()}
|
||||
/>
|
||||
),
|
||||
headerLeft: isLargeScreen
|
||||
? undefined
|
||||
: () => (
|
||||
<Appbar.Action
|
||||
color={theme.colors.text}
|
||||
icon="menu"
|
||||
onPress={() => navigation.toggleDrawer()}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{({
|
||||
@@ -280,14 +312,15 @@ export default function App() {
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
|
||||
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
|
||||
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme));
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
name => (
|
||||
(name) => (
|
||||
<List.Item
|
||||
key={name}
|
||||
testID={name}
|
||||
title={SCREENS[name].title}
|
||||
onPress={() => navigation.navigate(name)}
|
||||
/>
|
||||
@@ -297,7 +330,7 @@ export default function App() {
|
||||
)}
|
||||
</Stack.Screen>
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
name => (
|
||||
(name) => (
|
||||
<Stack.Screen
|
||||
key={name}
|
||||
name={name}
|
||||
|
||||
1
example/web/_redirects
Normal file
1
example/web/_redirects
Normal file
@@ -0,0 +1 @@
|
||||
/* /index.html 200
|
||||
@@ -7,7 +7,7 @@ const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const node_modules = path.resolve(__dirname, '..', 'node_modules');
|
||||
const packages = path.resolve(__dirname, '..', 'packages');
|
||||
|
||||
module.exports = async function(env, argv) {
|
||||
module.exports = async function (env, argv) {
|
||||
const config = await createExpoWebpackConfigAsync(env, argv);
|
||||
|
||||
config.context = path.resolve(__dirname, '..');
|
||||
@@ -20,7 +20,7 @@ module.exports = async function(env, argv) {
|
||||
});
|
||||
|
||||
config.resolve.plugins = config.resolve.plugins.filter(
|
||||
p => !(p instanceof ModuleScopePlugin)
|
||||
(p) => !(p instanceof ModuleScopePlugin)
|
||||
);
|
||||
|
||||
Object.assign(config.resolve.alias, {
|
||||
@@ -30,7 +30,7 @@ module.exports = async function(env, argv) {
|
||||
'@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'),
|
||||
});
|
||||
|
||||
fs.readdirSync(packages).forEach(name => {
|
||||
fs.readdirSync(packages).forEach((name) => {
|
||||
config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
|
||||
packages,
|
||||
name,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const error = console.error;
|
||||
|
||||
console.error = (...args) =>
|
||||
// Supress error messages regarding error boundary in tests
|
||||
// Suppress error messages regarding error boundary in tests
|
||||
/(Consider adding an error boundary to your tree to customize error handling behavior|React will try to recreate this component tree from scratch using the error boundary you provided|Error boundaries should implement getDerivedStateFromError)/m.test(
|
||||
args[0]
|
||||
)
|
||||
|
||||
5
netlify.toml
Normal file
5
netlify.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
[build]
|
||||
base = "/"
|
||||
publish = "example/web-build"
|
||||
command = "yarn example expo build:web"
|
||||
29
package.json
29
package.json
@@ -18,32 +18,33 @@
|
||||
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext '.js,.ts,.tsx' .",
|
||||
"typescript": "tsc --noEmit",
|
||||
"typescript": "tsc --noEmit --composite false",
|
||||
"test": "jest",
|
||||
"prerelease": "lerna run clean",
|
||||
"release": "lerna publish",
|
||||
"example": "yarn --cwd example"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.4",
|
||||
"@babel/preset-flow": "^7.8.3",
|
||||
"@babel/preset-react": "^7.8.3",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@babel/runtime": "^7.8.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"@babel/preset-flow": "^7.9.0",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/preset-typescript": "^7.9.0",
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@commitlint/config-conventional": "^8.3.4",
|
||||
"@types/jest": "^25.1.2",
|
||||
"@types/jest": "^25.2.1",
|
||||
"babel-jest": "^25.2.6",
|
||||
"codecov": "^3.6.5",
|
||||
"commitlint": "^8.3.5",
|
||||
"core-js": "^3.6.4",
|
||||
"detox": "^15.1.4",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-satya164": "^3.1.5",
|
||||
"husky": "^4.2.1",
|
||||
"jest": "^25.1.0",
|
||||
"eslint-config-satya164": "^3.1.6",
|
||||
"husky": "^4.2.3",
|
||||
"jest": "^25.2.7",
|
||||
"lerna": "^3.20.2",
|
||||
"prettier": "^1.19.1",
|
||||
"typescript": "^3.7.5"
|
||||
"prettier": "^2.0.4",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"react": "~16.9.0",
|
||||
|
||||
@@ -3,6 +3,119 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.8...@react-navigation/bottom-tabs@5.3.0) (2020-04-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/942d2be2c72720469475ce12ec8df23825994dbf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.6...@react-navigation/bottom-tabs@5.2.7) (2020-04-17)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.5...@react-navigation/bottom-tabs@5.2.6) (2020-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.5](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.4...@react-navigation/bottom-tabs@5.2.5) (2020-03-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.3...@react-navigation/bottom-tabs@5.2.4) (2020-03-23)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.2...@react-navigation/bottom-tabs@5.2.3) (2020-03-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.1...@react-navigation/bottom-tabs@5.2.2) (2020-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't use react-native-screens on web ([b1a65fc](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b1a65fc73e8603ae2c06ef101a74df31e80bb9b2)), closes [#7485](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/7485)
|
||||
* initialize height and width to zero if undefined ([3df65e2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/3df65e28197db3bb8371059146546d57661c5ba3)), closes [#6789](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6789)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.0...@react-navigation/bottom-tabs@5.2.1) (2020-03-17)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.1...@react-navigation/bottom-tabs@5.2.0) (2020-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add safeAreaInsets to bottom tabs ([82af7be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/82af7bed7135e42e24693b48cf7f1c6f9f5a6981))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.0...@react-navigation/bottom-tabs@5.1.1) (2020-03-03)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.7...@react-navigation/bottom-tabs@5.1.0) (2020-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability add listeners with listeners prop ([1624108](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6756)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.6...@react-navigation/bottom-tabs@5.0.7) (2020-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/bottom-tabs",
|
||||
"description": "Bottom tab navigator following iOS design guidelines",
|
||||
"version": "5.0.7",
|
||||
"version": "5.3.0",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -34,22 +34,24 @@
|
||||
"react-native-iphone-x-helper": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.9.3",
|
||||
"@react-navigation/native": "^5.0.7",
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.19",
|
||||
"@types/react-native": "^0.60.30",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.61.22",
|
||||
"del-cli": "^3.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-safe-area-context": "^0.7.2",
|
||||
"typescript": "^3.7.5"
|
||||
"react-native-safe-area-context": "^0.7.3",
|
||||
"react-native-screens": "^2.3.0",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^5.0.5",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 0.6.0"
|
||||
"react-native-safe-area-context": ">= 0.6.0",
|
||||
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"source": "src",
|
||||
|
||||
@@ -12,7 +12,7 @@ export { default as BottomTabBar } from './views/BottomTabBar';
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export {
|
||||
export type {
|
||||
BottomTabNavigationOptions,
|
||||
BottomTabNavigationProp,
|
||||
BottomTabBarProps,
|
||||
|
||||
@@ -48,6 +48,8 @@ function BottomTabNavigator({
|
||||
}
|
||||
|
||||
export default createNavigatorFactory<
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions,
|
||||
BottomTabNavigationEventMap,
|
||||
typeof BottomTabNavigator
|
||||
>(BottomTabNavigator);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
StyleProp,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
GestureResponderEvent,
|
||||
} from 'react-native';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
ParamListBase,
|
||||
Descriptor,
|
||||
TabNavigationState,
|
||||
TabActionHelpers,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
export type BottomTabNavigationEventMap = {
|
||||
@@ -40,19 +42,8 @@ export type BottomTabNavigationProp<
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions,
|
||||
BottomTabNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Jump to an existing tab.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
|
||||
...args: ParamList[RouteName] extends undefined | any
|
||||
? [RouteName] | [RouteName, ParamList[RouteName]]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
};
|
||||
> &
|
||||
TabActionHelpers<ParamList>;
|
||||
|
||||
export type BottomTabNavigationOptions = {
|
||||
/**
|
||||
@@ -148,7 +139,7 @@ export type BottomTabBarOptions = {
|
||||
*/
|
||||
inactiveTintColor?: string;
|
||||
/**
|
||||
* Background olor for the active tab.
|
||||
* Background color for the active tab.
|
||||
*/
|
||||
activeBackgroundColor?: string;
|
||||
/**
|
||||
@@ -184,6 +175,16 @@ export type BottomTabBarOptions = {
|
||||
* Whether the label position should adapt to the orientation.
|
||||
*/
|
||||
adaptive?: boolean;
|
||||
/**
|
||||
* Safe area insets for the tab bar. This is used to avoid elements like the navigation bar on Android and bottom safe area on iOS.
|
||||
* By default, the device's safe area insets are automatically detected. You can override the behavior with this option.
|
||||
*/
|
||||
safeAreaInsets?: {
|
||||
top?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
};
|
||||
/**
|
||||
* Style object for the tab bar container.
|
||||
*/
|
||||
@@ -196,6 +197,13 @@ export type BottomTabBarProps = BottomTabBarOptions & {
|
||||
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
|
||||
};
|
||||
|
||||
export type BottomTabBarButtonProps = TouchableWithoutFeedbackProps & {
|
||||
export type BottomTabBarButtonProps = Omit<
|
||||
TouchableWithoutFeedbackProps,
|
||||
'onPress'
|
||||
> & {
|
||||
to?: string;
|
||||
children: React.ReactNode;
|
||||
onPress?: (
|
||||
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
NavigationRouteContext,
|
||||
CommonActions,
|
||||
useTheme,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import { SafeAreaConsumer } from 'react-native-safe-area-context';
|
||||
import { useSafeArea } from 'react-native-safe-area-context';
|
||||
|
||||
import BottomTabItem from './BottomTabItem';
|
||||
import { BottomTabBarProps } from '../types';
|
||||
@@ -43,14 +44,21 @@ export default function BottomTabBar({
|
||||
keyboardHidesTabBar = false,
|
||||
labelPosition,
|
||||
labelStyle,
|
||||
safeAreaInsets,
|
||||
showIcon,
|
||||
showLabel,
|
||||
style,
|
||||
tabStyle,
|
||||
}: Props) {
|
||||
const { colors } = useTheme();
|
||||
const buildLink = useLinkBuilder();
|
||||
|
||||
const [dimensions, setDimensions] = React.useState(() => {
|
||||
const { height = 0, width = 0 } = Dimensions.get('window');
|
||||
|
||||
return { height, width };
|
||||
});
|
||||
|
||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||
const [layout, setLayout] = React.useState({
|
||||
height: 0,
|
||||
width: dimensions.width,
|
||||
@@ -115,7 +123,7 @@ export default function BottomTabBar({
|
||||
const handleLayout = (e: LayoutChangeEvent) => {
|
||||
const { height, width } = e.nativeEvent.layout;
|
||||
|
||||
setLayout(layout => {
|
||||
setLayout((layout) => {
|
||||
if (height === layout.height && width === layout.width) {
|
||||
return layout;
|
||||
} else {
|
||||
@@ -158,116 +166,123 @@ export default function BottomTabBar({
|
||||
}
|
||||
};
|
||||
|
||||
const defaultInsets = useSafeArea();
|
||||
|
||||
const insets = {
|
||||
top: safeAreaInsets?.top ?? defaultInsets.top,
|
||||
right: safeAreaInsets?.right ?? defaultInsets.right,
|
||||
bottom: safeAreaInsets?.bottom ?? defaultInsets.bottom,
|
||||
left: safeAreaInsets?.left ?? defaultInsets.left,
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaConsumer>
|
||||
{insets => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.tabBar,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
keyboardHidesTabBar
|
||||
? {
|
||||
// When the keyboard is shown, slide down the tab bar
|
||||
transform: [
|
||||
{
|
||||
translateY: visible.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [layout.height, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
// Absolutely position the tab bar so that the content is below it
|
||||
// This is needed to avoid gap at bottom when the tab bar is hidden
|
||||
position: keyboardShown ? 'absolute' : null,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
height: DEFAULT_TABBAR_HEIGHT + (insets ? insets.bottom : 0),
|
||||
paddingBottom: insets ? insets.bottom : 0,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
|
||||
>
|
||||
<View style={styles.content} onLayout={handleLayout}>
|
||||
{routes.map((route, index) => {
|
||||
const focused = index === state.index;
|
||||
const { options } = descriptors[route.key];
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.tabBar,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
keyboardHidesTabBar
|
||||
? {
|
||||
// When the keyboard is shown, slide down the tab bar
|
||||
transform: [
|
||||
{
|
||||
translateY: visible.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [layout.height, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
// Absolutely position the tab bar so that the content is below it
|
||||
// This is needed to avoid gap at bottom when the tab bar is hidden
|
||||
position: keyboardShown ? 'absolute' : null,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingHorizontal: Math.max(insets.left, insets.right),
|
||||
},
|
||||
style,
|
||||
]}
|
||||
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
|
||||
>
|
||||
<View style={styles.content} onLayout={handleLayout}>
|
||||
{routes.map((route, index) => {
|
||||
const focused = index === state.index;
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
const onPress = () => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
const onPress = () => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (!focused && !event.defaultPrevented) {
|
||||
navigation.dispatch({
|
||||
...CommonActions.navigate(route.name),
|
||||
target: state.key,
|
||||
});
|
||||
}
|
||||
};
|
||||
if (!focused && !event.defaultPrevented) {
|
||||
navigation.dispatch({
|
||||
...CommonActions.navigate(route.name),
|
||||
target: state.key,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onLongPress = () => {
|
||||
navigation.emit({
|
||||
type: 'tabLongPress',
|
||||
target: route.key,
|
||||
});
|
||||
};
|
||||
const onLongPress = () => {
|
||||
navigation.emit({
|
||||
type: 'tabLongPress',
|
||||
target: route.key,
|
||||
});
|
||||
};
|
||||
|
||||
const label =
|
||||
options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
const label =
|
||||
options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const accessibilityLabel =
|
||||
options.tabBarAccessibilityLabel !== undefined
|
||||
? options.tabBarAccessibilityLabel
|
||||
: typeof label === 'string'
|
||||
? `${label}, tab, ${index + 1} of ${routes.length}`
|
||||
: undefined;
|
||||
const accessibilityLabel =
|
||||
options.tabBarAccessibilityLabel !== undefined
|
||||
? options.tabBarAccessibilityLabel
|
||||
: typeof label === 'string'
|
||||
? `${label}, tab, ${index + 1} of ${routes.length}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider
|
||||
key={route.key}
|
||||
value={descriptors[route.key].navigation}
|
||||
>
|
||||
<NavigationRouteContext.Provider value={route}>
|
||||
<BottomTabItem
|
||||
route={route}
|
||||
focused={focused}
|
||||
horizontal={shouldUseHorizontalLabels()}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
testID={options.tabBarTestID}
|
||||
allowFontScaling={allowFontScaling}
|
||||
activeTintColor={activeTintColor}
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
activeBackgroundColor={activeBackgroundColor}
|
||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||
button={options.tabBarButton}
|
||||
icon={options.tabBarIcon}
|
||||
label={label}
|
||||
showIcon={showIcon}
|
||||
showLabel={showLabel}
|
||||
labelStyle={labelStyle}
|
||||
style={tabStyle}
|
||||
/>
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</SafeAreaConsumer>
|
||||
return (
|
||||
<NavigationContext.Provider
|
||||
key={route.key}
|
||||
value={descriptors[route.key].navigation}
|
||||
>
|
||||
<NavigationRouteContext.Provider value={route}>
|
||||
<BottomTabItem
|
||||
route={route}
|
||||
focused={focused}
|
||||
horizontal={shouldUseHorizontalLabels()}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
to={buildLink(route.name, route.params)}
|
||||
testID={options.tabBarTestID}
|
||||
allowFontScaling={allowFontScaling}
|
||||
activeTintColor={activeTintColor}
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
activeBackgroundColor={activeBackgroundColor}
|
||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||
button={options.tabBarButton}
|
||||
icon={options.tabBarIcon}
|
||||
label={label}
|
||||
showIcon={showIcon}
|
||||
showLabel={showLabel}
|
||||
labelStyle={labelStyle}
|
||||
style={tabStyle}
|
||||
/>
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
TouchableWithoutFeedback,
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
GestureResponderEvent,
|
||||
} from 'react-native';
|
||||
import { Route, useTheme } from '@react-navigation/native';
|
||||
import { Link, Route, useTheme } from '@react-navigation/native';
|
||||
import Color from 'color';
|
||||
|
||||
import TabBarIcon from './TabBarIcon';
|
||||
@@ -37,6 +39,10 @@ type Props = {
|
||||
size: number;
|
||||
color: string;
|
||||
}) => React.ReactNode;
|
||||
/**
|
||||
* URL to use for the link to the tab.
|
||||
*/
|
||||
to?: string;
|
||||
/**
|
||||
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
|
||||
*/
|
||||
@@ -50,13 +56,16 @@ type Props = {
|
||||
*/
|
||||
testID?: string;
|
||||
/**
|
||||
* Function to execute on press.
|
||||
* Function to execute on press in React Native.
|
||||
* On the web, this will use onClick.
|
||||
*/
|
||||
onPress: () => void;
|
||||
onPress: (
|
||||
e: React.MouseEvent<HTMLElement, MouseEvent> | GestureResponderEvent
|
||||
) => void;
|
||||
/**
|
||||
* Function to execute on long press.
|
||||
*/
|
||||
onLongPress: () => void;
|
||||
onLongPress: (e: GestureResponderEvent) => void;
|
||||
/**
|
||||
* Whether the label should be aligned with the icon horizontally.
|
||||
*/
|
||||
@@ -104,11 +113,48 @@ export default function BottomTabBarItem({
|
||||
route,
|
||||
label,
|
||||
icon,
|
||||
button = ({ children, style, ...rest }: BottomTabBarButtonProps) => (
|
||||
<TouchableWithoutFeedback {...rest}>
|
||||
<View style={style}>{children}</View>
|
||||
</TouchableWithoutFeedback>
|
||||
),
|
||||
to,
|
||||
button = ({
|
||||
children,
|
||||
style,
|
||||
onPress,
|
||||
to,
|
||||
accessibilityRole,
|
||||
...rest
|
||||
}: BottomTabBarButtonProps) => {
|
||||
if (Platform.OS === 'web' && to) {
|
||||
// React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
|
||||
// We need to use `onClick` to be able to prevent default browser handling of links.
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
to={to}
|
||||
style={[styles.button, style]}
|
||||
onPress={(e: any) => {
|
||||
if (
|
||||
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
|
||||
(e.button == null || e.button === 0) // ignore everything but left clicks
|
||||
) {
|
||||
e.preventDefault();
|
||||
onPress?.(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
{...rest}
|
||||
accessibilityRole={accessibilityRole}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={style}>{children}</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
},
|
||||
accessibilityLabel,
|
||||
testID,
|
||||
onPress,
|
||||
@@ -133,9 +179,7 @@ export default function BottomTabBarItem({
|
||||
|
||||
const inactiveTintColor =
|
||||
customInactiveTintColor === undefined
|
||||
? Color(colors.text)
|
||||
.mix(Color(colors.card), 0.5)
|
||||
.hex()
|
||||
? Color(colors.text).mix(Color(colors.card), 0.5).hex()
|
||||
: customInactiveTintColor;
|
||||
|
||||
const renderLabel = ({ focused }: { focused: boolean }) => {
|
||||
@@ -198,6 +242,7 @@ export default function BottomTabBarItem({
|
||||
: inactiveBackgroundColor;
|
||||
|
||||
return button({
|
||||
to,
|
||||
onPress,
|
||||
onLongPress,
|
||||
testID,
|
||||
@@ -250,4 +295,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
marginLeft: 20,
|
||||
},
|
||||
button: {
|
||||
display: 'flex',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
|
||||
import { TabNavigationState, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
TabNavigationState,
|
||||
useTheme,
|
||||
} from '@react-navigation/native';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
|
||||
@@ -91,44 +95,46 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, descriptors, lazy } = this.props;
|
||||
const { state, descriptors, navigation, lazy } = this.props;
|
||||
const { routes } = state;
|
||||
const { loaded } = this.state;
|
||||
|
||||
return (
|
||||
<SafeAreaProviderCompat>
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { unmountOnBlur } = descriptor.options;
|
||||
const isFocused = state.index === index;
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
<SafeAreaProviderCompat>
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { unmountOnBlur } = descriptor.options;
|
||||
const isFocused = state.index === index;
|
||||
|
||||
if (unmountOnBlur && !isFocused) {
|
||||
return null;
|
||||
}
|
||||
if (unmountOnBlur && !isFocused) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lazy && !loaded.includes(index) && !isFocused) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
if (lazy && !loaded.includes(index) && !isFocused) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
<SceneContent isFocused={isFocused}>
|
||||
{descriptor.render()}
|
||||
</SceneContent>
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
{this.renderTabBar()}
|
||||
</View>
|
||||
</SafeAreaProviderCompat>
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
<SceneContent isFocused={isFocused}>
|
||||
{descriptor.render()}
|
||||
</SceneContent>
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
{this.renderTabBar()}
|
||||
</View>
|
||||
</SafeAreaProviderCompat>
|
||||
</NavigationHelpersContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { Screen, screensEnabled } from 'react-native-screens';
|
||||
|
||||
@@ -10,12 +9,14 @@ type Props = {
|
||||
style?: any;
|
||||
};
|
||||
|
||||
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
|
||||
const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container
|
||||
|
||||
export default class ResourceSavingScene extends React.Component<Props> {
|
||||
render() {
|
||||
if (screensEnabled?.()) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
const { isVisible, ...rest } = this.props;
|
||||
|
||||
// @ts-ignore
|
||||
return <Screen active={isVisible ? 1 : 0} {...rest} />;
|
||||
}
|
||||
@@ -24,7 +25,13 @@ export default class ResourceSavingScene extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, style, { opacity: isVisible ? 1 : 0 }]}
|
||||
style={[
|
||||
styles.container,
|
||||
Platform.OS === 'web'
|
||||
? { display: isVisible ? 'flex' : 'none' }
|
||||
: null,
|
||||
style,
|
||||
]}
|
||||
collapsable={false}
|
||||
removeClippedSubviews={
|
||||
// On iOS, set removeClippedSubviews to true only when not focused
|
||||
|
||||
@@ -30,7 +30,7 @@ type Props = {
|
||||
export default function SafeAreaProviderCompat({ children }: Props) {
|
||||
return (
|
||||
<SafeAreaConsumer>
|
||||
{insets => {
|
||||
{(insets) => {
|
||||
if (insets) {
|
||||
// If we already have insets, don't wrap the stack in another safe area provider
|
||||
// This avoids an issue with updates at the cost of potentially incorrect values
|
||||
|
||||
@@ -3,6 +3,112 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.10...@react-navigation/compat@5.1.11) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix typo in navigationOptions ([8cbb201](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/8cbb201f1a7fb90e45a078df6bc42ce4771cc6a6))
|
||||
* spread parent params to children in compat navigator ([24febf6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/24febf6ea99be2e5f22005fdd2a82136d647255c)), closes [#6785](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/6785)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.9](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.8...@react-navigation/compat@5.1.9) (2020-04-17)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.8](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.7...@react-navigation/compat@5.1.8) (2020-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use 1 as default in compatibility pop action ([4408117](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/44081172d440c713ad3543a2d5e1e18ebc8f72a4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.6...@react-navigation/compat@5.1.7) (2020-03-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.5...@react-navigation/compat@5.1.6) (2020-03-23)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.4...@react-navigation/compat@5.1.5) (2020-03-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.3...@react-navigation/compat@5.1.4) (2020-03-19)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.2...@react-navigation/compat@5.1.3) (2020-03-17)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.1...@react-navigation/compat@5.1.2) (2020-03-16)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.0...@react-navigation/compat@5.1.1) (2020-03-03)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.7...@react-navigation/compat@5.1.0) (2020-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability add listeners with listeners prop ([1624108](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/6756)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.6...@react-navigation/compat@5.0.7) (2020-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.0.7",
|
||||
"version": "5.1.11",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||
"bugs": {
|
||||
@@ -25,11 +25,11 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.9.3",
|
||||
"@react-navigation/native": "^5.0.7",
|
||||
"@types/react": "^16.9.19",
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.0",
|
||||
"@types/react": "^16.9.23",
|
||||
"react": "~16.9.0",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^5.0.5",
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
TypedNavigator,
|
||||
NavigationProp,
|
||||
RouteProp,
|
||||
EventMapBase,
|
||||
NavigationRouteContext,
|
||||
} from '@react-navigation/native';
|
||||
import CompatScreen from './CompatScreen';
|
||||
import ScreenPropsContext from './ScreenPropsContext';
|
||||
@@ -15,7 +17,9 @@ import { CompatScreenType, CompatRouteConfig } from './types';
|
||||
export default function createCompatNavigatorFactory<
|
||||
CreateNavigator extends () => TypedNavigator<
|
||||
ParamListBase,
|
||||
NavigationState,
|
||||
{},
|
||||
EventMapBase,
|
||||
React.ComponentType<any>
|
||||
>
|
||||
>(createNavigator: CreateNavigator) {
|
||||
@@ -64,9 +68,12 @@ export default function createCompatNavigatorFactory<
|
||||
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
|
||||
|
||||
function Navigator({ screenProps }: { screenProps?: unknown }) {
|
||||
const parentRouteParams = React.useContext(NavigationRouteContext)
|
||||
?.params;
|
||||
|
||||
const screens = React.useMemo(
|
||||
() =>
|
||||
routeNames.map(name => {
|
||||
routeNames.map((name) => {
|
||||
let getScreenComponent: () => CompatScreenType<NavigationPropType>;
|
||||
|
||||
let initialParams;
|
||||
@@ -132,7 +139,7 @@ export default function createCompatNavigatorFactory<
|
||||
<Pair.Screen
|
||||
key={name}
|
||||
name={name}
|
||||
initialParams={initialParams}
|
||||
initialParams={{ ...parentRouteParams, ...initialParams }}
|
||||
options={screenOptions}
|
||||
>
|
||||
{({ navigation, route }) => (
|
||||
@@ -145,7 +152,7 @@ export default function createCompatNavigatorFactory<
|
||||
</Pair.Screen>
|
||||
);
|
||||
}),
|
||||
[screenProps]
|
||||
[parentRouteParams, screenProps]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -160,7 +167,7 @@ export default function createCompatNavigatorFactory<
|
||||
);
|
||||
}
|
||||
|
||||
Navigator.navigationOtions = parentNavigationOptions;
|
||||
Navigator.navigationOptions = parentNavigationOptions;
|
||||
|
||||
return Navigator;
|
||||
};
|
||||
|
||||
@@ -22,5 +22,7 @@ function SwitchNavigator(props: Props) {
|
||||
}
|
||||
|
||||
export default createCompatNavigatorFactory(
|
||||
createNavigatorFactory<{}, typeof SwitchNavigator>(SwitchNavigator)
|
||||
createNavigatorFactory<TabNavigationState, {}, {}, typeof SwitchNavigator>(
|
||||
SwitchNavigator
|
||||
)
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export function push(routeName: string, params?: object, action?: never) {
|
||||
});
|
||||
}
|
||||
|
||||
export function pop(n: number) {
|
||||
export function pop(n: number = 1) {
|
||||
return StackActions.pop(typeof n === 'number' ? { n } : n);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function useCompatNavigation<
|
||||
const route = useRoute();
|
||||
|
||||
const isFirstRouteInParent = useNavigationState(
|
||||
state => state.routes[0].key === route.key
|
||||
(state) => state.routes[0].key === route.key
|
||||
);
|
||||
|
||||
const context = React.useRef<Record<string, any>>({});
|
||||
|
||||
@@ -26,8 +26,9 @@ export default function withNavigation<
|
||||
return <Comp ref={onRef} navigation={navigation} {...rest} />;
|
||||
};
|
||||
|
||||
WrappedComponent.displayName = `withNavigation(${Comp.displayName ||
|
||||
Comp.name})`;
|
||||
WrappedComponent.displayName = `withNavigation(${
|
||||
Comp.displayName || Comp.name
|
||||
})`;
|
||||
|
||||
return WrappedComponent;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ export default function withNavigationFocus<
|
||||
return <Comp ref={onRef} isFocused={isFocused} {...rest} />;
|
||||
};
|
||||
|
||||
WrappedComponent.displayName = `withNavigationFocus(${Comp.displayName ||
|
||||
Comp.name})`;
|
||||
WrappedComponent.displayName = `withNavigationFocus(${
|
||||
Comp.displayName || Comp.name
|
||||
})`;
|
||||
|
||||
return WrappedComponent;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,137 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.5...@react-navigation/core@5.4.0) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle empty paths when parsing ([c3fa83e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3fa83efe0d73db76365f8be3d6a8ca1d1289b71))
|
||||
* parsing url ([bd35b4f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/bd35b4fc202c3868fb75c3675b62de67557089e1))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add config to enable redux devtools integration ([c9c825b](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c9c825bee61426635a28ee149eeeff3d628171cd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.3...@react-navigation/core@5.3.4) (2020-04-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add initial option for navigating to nested navigators ([004c7d7](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/004c7d7ab1f80faf04b2a1836ec6b79a5419e45f))
|
||||
* add initial param for actions from deep link ([a3f7a5f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/a3f7a5feba2e6aa2158aeaea6cde73ae1603173e))
|
||||
* handle initial: false for nested route after first initialization ([187aefe](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/187aefe9c400b499f920c212bf856414e25c5aaf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.2...@react-navigation/core@5.3.3) (2020-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* switch order of focus and blur events. closes [#7963](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7963) ([ce3994c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/ce3994c82c28669d5742017eb7627e9adf996933))
|
||||
* workaround warning about setState in another component in render ([d4fd906](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/d4fd906915cc20d6fb21508384c05a540d8644d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.1...@react-navigation/core@5.3.2) (2020-03-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle no path property and undefined query params ([#7911](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7911)) ([cd47915](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/cd47915861a56cd7eaa9ac79f5139cde56ca95a7))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.0...@react-navigation/core@5.3.1) (2020-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't emit events for screens that don't exist anymore ([1c00142](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/1c001424b595b40f9db9343096c833f75353b099))
|
||||
* only call listeners for focused screen for global events ([3096de6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3096de62868a7ed9ed65e529c8ddfa001b9be486))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.3...@react-navigation/core@5.3.0) (2020-03-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return correct value for isFocused after changing screens ([5b15c71](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/5b15c7164f5503f2f0d51006a3f23bd0c58fd9b7)), closes [#7843](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7843)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support function in listeners prop ([3709e65](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3709e652f41a16c2c2b05d5dbbe1da2017ba2c3f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.2...@react-navigation/core@5.2.3) (2020-03-19)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.1...@react-navigation/core@5.2.2) (2020-03-16)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.0...@react-navigation/core@5.2.1) (2020-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix links for documentation ([5bb0f40](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/5bb0f405ceb5755d39a0b5b1f2e4ecee0da051bc))
|
||||
* move updating state to useEffect ([2dfa4f3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2dfa4f36293a2acb718814f6b2fa79d7c7ddf09c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.6...@react-navigation/core@5.2.0) (2020-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability add listeners with listeners prop ([1624108](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/6756)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.5...@react-navigation/core@5.1.6) (2020-02-21)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "5.1.6",
|
||||
"version": "5.4.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -29,24 +29,23 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.2",
|
||||
"@react-navigation/routers": "^5.4.2",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"query-string": "^6.10.1",
|
||||
"react-is": "^16.12.0",
|
||||
"shortid": "^2.2.15",
|
||||
"use-subscription": "^1.3.0"
|
||||
"nanoid": "^3.0.2",
|
||||
"query-string": "^6.12.0",
|
||||
"react-is": "^16.13.0",
|
||||
"use-subscription": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.9.3",
|
||||
"@types/react": "^16.9.19",
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-is": "^16.7.1",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/use-subscription": "^1.0.0",
|
||||
"del-cli": "^3.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-native-testing-library": "^1.12.0",
|
||||
"react-test-renderer": "~16.12.0",
|
||||
"typescript": "^3.7.5"
|
||||
"react-test-renderer": "~16.13.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
|
||||
@@ -9,22 +9,26 @@ import {
|
||||
} from '@react-navigation/routers';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
||||
import useFocusedListeners from './useFocusedListeners';
|
||||
import useDevTools from './useDevTools';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useSyncState from './useSyncState';
|
||||
import isSerializable from './isSerializable';
|
||||
|
||||
import { NavigationContainerRef, NavigationContainerProps } from './types';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useSyncState from './useSyncState';
|
||||
|
||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
|
||||
const DEVTOOLS_CONFIG_KEY =
|
||||
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
|
||||
|
||||
const MISSING_CONTEXT_ERROR =
|
||||
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/en/getting-started.html for setup instructions.";
|
||||
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
|
||||
|
||||
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/en/navigating-without-navigation-prop.html#handling-initialization for more details.";
|
||||
"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 const NavigationStateContext = React.createContext<{
|
||||
isDefault?: true;
|
||||
@@ -73,7 +77,7 @@ const getPartialState = (
|
||||
return {
|
||||
...partialState,
|
||||
stale: true,
|
||||
routes: state.routes.map(route => {
|
||||
routes: state.routes.map((route) => {
|
||||
if (route.state === undefined) {
|
||||
return route as Route<string> & {
|
||||
state?: PartialState<NavigationState>;
|
||||
@@ -102,7 +106,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
independent,
|
||||
children,
|
||||
}: NavigationContainerProps,
|
||||
ref: React.Ref<NavigationContainerRef>
|
||||
ref?: React.Ref<NavigationContainerRef>
|
||||
) {
|
||||
const parent = React.useContext(NavigationStateContext);
|
||||
|
||||
@@ -112,7 +116,13 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
);
|
||||
}
|
||||
|
||||
const [state, getState, setState] = useSyncState<State>(() =>
|
||||
const [
|
||||
state,
|
||||
getState,
|
||||
setState,
|
||||
scheduleUpdate,
|
||||
flushUpdates,
|
||||
] = useSyncState<State>(() =>
|
||||
getPartialState(initialState == null ? undefined : initialState)
|
||||
);
|
||||
|
||||
@@ -136,6 +146,9 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
);
|
||||
|
||||
const { trackState, trackAction } = useDevTools({
|
||||
enabled:
|
||||
// @ts-ignore
|
||||
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
|
||||
name: '@react-navigation',
|
||||
reset,
|
||||
state,
|
||||
@@ -155,7 +168,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
throw new Error(NOT_INITIALIZED_ERROR);
|
||||
}
|
||||
|
||||
listeners[0](navigation => navigation.dispatch(action));
|
||||
listeners[0]((navigation) => navigation.dispatch(action));
|
||||
};
|
||||
|
||||
const canGoBack = () => {
|
||||
@@ -163,7 +176,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
return false;
|
||||
}
|
||||
|
||||
const { result, handled } = listeners[0](navigation =>
|
||||
const { result, handled } = listeners[0]((navigation) =>
|
||||
navigation.canGoBack()
|
||||
);
|
||||
|
||||
@@ -206,6 +219,8 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
dispatch,
|
||||
canGoBack,
|
||||
getRootState,
|
||||
dangerouslyGetState: () => state,
|
||||
dangerouslyGetParent: () => undefined,
|
||||
}));
|
||||
|
||||
const builderContext = React.useMemo(
|
||||
@@ -217,6 +232,11 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
[addFocusedListener, trackAction, addStateGetter]
|
||||
);
|
||||
|
||||
const scheduleContext = React.useMemo(
|
||||
() => ({ scheduleUpdate, flushUpdates }),
|
||||
[scheduleUpdate, flushUpdates]
|
||||
);
|
||||
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
state,
|
||||
@@ -238,7 +258,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
hasWarnedForSerialization = true;
|
||||
|
||||
console.warn(
|
||||
"We found non-serializable values in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/en/troubleshooting.html#i-get-the-warning-we-found-non-serializable-values-in-the-navigation-state for more details."
|
||||
"Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -262,11 +282,13 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
}, [onStateChange, trackState, getRootState, emitter, state]);
|
||||
|
||||
return (
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</NavigationStateContext.Provider>
|
||||
</NavigationBuilderContext.Provider>
|
||||
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</NavigationStateContext.Provider>
|
||||
</NavigationBuilderContext.Provider>
|
||||
</ScheduleUpdateContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const MULTIPLE_NAVIGATOR_ERROR = `Another navigator is already registered for this container. You likely have multiple navigators under a single "NavigationContainer" or "Screen". Make sure each navigator is under a separate "Screen" container. See https://reactnavigation.org/docs/en/nesting-navigators.html for a guide on nesting.`;
|
||||
const MULTIPLE_NAVIGATOR_ERROR = `Another navigator is already registered for this container. You likely have multiple navigators under a single "NavigationContainer" or "Screen". Make sure each navigator is under a separate "Screen" container. See https://reactnavigation.org/docs/nesting-navigators for a guide on nesting.`;
|
||||
|
||||
export const SingleNavigatorContext = React.createContext<
|
||||
| {
|
||||
|
||||
13
packages/core/src/NavigationHelpersContext.tsx
Normal file
13
packages/core/src/NavigationHelpersContext.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { ParamListBase } from '@react-navigation/routers';
|
||||
import { NavigationHelpers } from './types';
|
||||
|
||||
/**
|
||||
* Context which holds the navigation helpers of the parent navigator.
|
||||
* Navigators should use this context in their view component.
|
||||
*/
|
||||
const NavigationHelpersContext = React.createContext<
|
||||
NavigationHelpers<ParamListBase> | undefined
|
||||
>(undefined);
|
||||
|
||||
export default NavigationHelpersContext;
|
||||
@@ -10,10 +10,14 @@ import NavigationContext from './NavigationContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import StaticContainer from './StaticContainer';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import { NavigationProp, RouteConfig } from './types';
|
||||
import { NavigationProp, RouteConfig, EventMapBase } from './types';
|
||||
|
||||
type Props<State extends NavigationState, ScreenOptions extends object> = {
|
||||
screen: RouteConfig<ParamListBase, string, ScreenOptions>;
|
||||
type Props<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
> = {
|
||||
screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
|
||||
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
|
||||
route: Route<string> & {
|
||||
state?: NavigationState | PartialState<NavigationState>;
|
||||
@@ -28,14 +32,15 @@ type Props<State extends NavigationState, ScreenOptions extends object> = {
|
||||
*/
|
||||
export default function SceneView<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
>({
|
||||
screen,
|
||||
route,
|
||||
navigation,
|
||||
getState,
|
||||
setState,
|
||||
}: Props<State, ScreenOptions>) {
|
||||
}: Props<State, ScreenOptions, EventMap>) {
|
||||
const navigatorKeyRef = React.useRef<string | undefined>();
|
||||
|
||||
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
||||
@@ -46,7 +51,7 @@ export default function SceneView<
|
||||
|
||||
const getCurrentState = React.useCallback(() => {
|
||||
const state = getState();
|
||||
const currentRoute = state.routes.find(r => r.key === route.key);
|
||||
const currentRoute = state.routes.find((r) => r.key === route.key);
|
||||
|
||||
return currentRoute ? currentRoute.state : undefined;
|
||||
}, [getState, route.key]);
|
||||
@@ -57,7 +62,7 @@ export default function SceneView<
|
||||
|
||||
setState({
|
||||
...state,
|
||||
routes: state.routes.map(r =>
|
||||
routes: state.routes.map((r) =>
|
||||
r.key === route.key ? { ...r, state: child } : r
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ParamListBase } from '@react-navigation/routers';
|
||||
import { RouteConfig } from './types';
|
||||
import { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||
import { RouteConfig, EventMapBase } from './types';
|
||||
|
||||
/**
|
||||
* Empty component used for specifying route configuration.
|
||||
@@ -7,8 +7,10 @@ import { RouteConfig } from './types';
|
||||
export default function Screen<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList,
|
||||
ScreenOptions extends object
|
||||
>(_: RouteConfig<ParamList, RouteName, ScreenOptions>) {
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
>(_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>) {
|
||||
/* istanbul ignore next */
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ it('throws when getState is accessed without a container', () => {
|
||||
const element = <Test />;
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
||||
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ it('throws when setState is accessed without a container', () => {
|
||||
const element = <Test />;
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
||||
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ it('handle dispatching with ref', () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -220,7 +220,7 @@ it('handle resetting state with ref', () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -371,7 +371,7 @@ it('emits state events when the state changes', () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
key: String(MockRouterKey.current++),
|
||||
index,
|
||||
routeNames,
|
||||
routes: routeNames.map(name => ({
|
||||
routes: routeNames.map((name) => ({
|
||||
name,
|
||||
key: name,
|
||||
params: routeParamList[name],
|
||||
@@ -43,9 +43,9 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
}
|
||||
|
||||
const routes = state.routes
|
||||
.filter(route => routeNames.includes(route.name))
|
||||
.filter((route) => routeNames.includes(route.name))
|
||||
.map(
|
||||
route =>
|
||||
(route) =>
|
||||
({
|
||||
...route,
|
||||
key: route.key || `${route.name}-${MockRouterKey.current++}`,
|
||||
@@ -73,7 +73,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
},
|
||||
|
||||
getStateForRouteNamesChange(state, { routeNames }) {
|
||||
const routes = state.routes.filter(route =>
|
||||
const routes = state.routes.filter((route) =>
|
||||
routeNames.includes(route.name)
|
||||
);
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
},
|
||||
|
||||
getStateForRouteFocus(state, key) {
|
||||
const index = state.routes.findIndex(r => r.key === key);
|
||||
const index = state.routes.findIndex((r) => r.key === key);
|
||||
|
||||
if (index === -1 || index === state.index) {
|
||||
return state;
|
||||
@@ -105,7 +105,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
|
||||
case 'NAVIGATE': {
|
||||
const index = state.routes.findIndex(
|
||||
route => route.name === action.payload.name
|
||||
(route) => route.name === action.payload.name
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
|
||||
@@ -35,8 +35,10 @@ it('gets navigate action from state', () => {
|
||||
author: 'jane',
|
||||
},
|
||||
screen: 'qux',
|
||||
initial: true,
|
||||
},
|
||||
screen: 'bar',
|
||||
initial: true,
|
||||
},
|
||||
},
|
||||
type: 'NAVIGATE',
|
||||
@@ -70,9 +72,11 @@ it('gets navigate action from state', () => {
|
||||
payload: {
|
||||
name: 'foo',
|
||||
params: {
|
||||
initial: true,
|
||||
screen: 'bar',
|
||||
params: {
|
||||
screen: 'quz',
|
||||
initial: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,7 +42,8 @@ it('converts state to path string with config', () => {
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
id: (id: string) => Number(id.replace(/^x/, '')),
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -128,7 +129,8 @@ it('handles state with config with nested screens', () => {
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -192,12 +194,14 @@ it('handles state with config with nested screens and unused configs', () => {
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toLowerCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
@@ -255,11 +259,11 @@ it('handles nested object with stringify in it', () => {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -422,6 +426,49 @@ it('ignores empty string paths', () => {
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('keeps query params if path is empty', () => {
|
||||
const path = '/?foo=42';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
screens: {
|
||||
Qux: {
|
||||
path: '',
|
||||
parse: { foo: Number },
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Qux', params: { foo: 42 } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toEqual(
|
||||
path
|
||||
);
|
||||
});
|
||||
|
||||
it('cuts nested configs too', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
@@ -491,6 +538,8 @@ it('handles empty path at the end', () => {
|
||||
});
|
||||
|
||||
it('returns "/" for empty path', () => {
|
||||
const path = '/';
|
||||
|
||||
const config = {
|
||||
Foo: {
|
||||
path: '',
|
||||
@@ -515,5 +564,212 @@ it('returns "/" for empty path', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe('/');
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('parses no path specified', () => {
|
||||
const path = '/Foo/bar';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: {},
|
||||
},
|
||||
},
|
||||
Bar: 'bar',
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [{ name: 'Bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('parses no path specified in nested config', () => {
|
||||
const path = '/Foo/Foe/bar';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {},
|
||||
},
|
||||
},
|
||||
Bar: 'bar',
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [{ name: 'Bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('strips undefined query params', () => {
|
||||
const path = '/bar/sweet/apple/foo/bis/jane?count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: undefined,
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles stripping all query params', () => {
|
||||
const path = '/bar/sweet/apple/foo/bis/jane';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: {
|
||||
path: 'foe',
|
||||
},
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: undefined,
|
||||
answer: undefined,
|
||||
valid: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
@@ -42,7 +42,8 @@ it('converts path string to initial state with config', () => {
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -153,7 +154,8 @@ it('converts path string to initial state with config with nested screens', () =
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -217,7 +219,8 @@ it('converts path string to initial state with config with nested screens and un
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
id: Boolean,
|
||||
@@ -277,11 +280,11 @@ it('handles nested object with unused configs and with parse in it', () => {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -528,11 +531,11 @@ it('handles two initialRouteNames', () => {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -610,11 +613,11 @@ it('accepts initialRouteName without config for it', () => {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
author.replace(/^\w/, (c) => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
author.replace(/^\w/, (c) => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
@@ -674,7 +677,7 @@ it('accepts initialRouteName without config for it', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined if path is empty', () => {
|
||||
it('returns undefined if path is empty and no matching screen is present', () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
@@ -692,3 +695,292 @@ it('returns undefined if path is empty', () => {
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns matching screen if path is empty', () => {
|
||||
const path = '';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
screens: {
|
||||
Qux: '',
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Qux' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('returns matching screen with params if path is empty', () => {
|
||||
const path = '?foo=42';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
screens: {
|
||||
Qux: {
|
||||
path: '',
|
||||
parse: { foo: Number },
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Qux', params: { foo: 42 } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't match nested screen if path is empty", () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Qux: {
|
||||
path: '',
|
||||
parse: { foo: Number },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const path = '';
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('chooses more exhaustive pattern', () => {
|
||||
const path = '/foo/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles same paths beginnings', () => {
|
||||
const path = '/foos';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foos',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles same paths beginnings with params', () => {
|
||||
const path = '/foos/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foos/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles not taking path with too many segments', () => {
|
||||
const path = '/foos/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foos/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip',
|
||||
parse: {
|
||||
id: Number,
|
||||
pwd: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
@@ -372,7 +372,7 @@ it("doesn't update state if action wasn't handled", () => {
|
||||
expect(onStateChange).toBeCalledTimes(0);
|
||||
|
||||
expect(spy.mock.calls[0][0]).toMatch(
|
||||
"The action 'INVALID' with payload 'undefined' was not handled by any navigator."
|
||||
"The action 'INVALID' was not handled by any navigator."
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
@@ -626,7 +626,7 @@ it('updates route params with setParams applied to parent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles change in route names', () => {
|
||||
it('handles change in route names', async () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
@@ -635,7 +635,7 @@ it('handles change in route names', () => {
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const root = render(
|
||||
<BaseNavigationContainer onStateChange={onStateChange}>
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator initialRouteName="bar">
|
||||
<Screen name="foo" component={jest.fn()} />
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
@@ -737,6 +737,366 @@ it('navigates to nested child in a navigator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to nested child in a navigator with initial: false', () => {
|
||||
const TestRouter: typeof MockRouter = (options) => {
|
||||
const router = MockRouter(options);
|
||||
|
||||
return {
|
||||
...router,
|
||||
|
||||
getStateForAction(state, action, options) {
|
||||
switch (action.type) {
|
||||
case 'NAVIGATE': {
|
||||
if (!options.routeNames.includes(action.payload.name as any)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const routes = [
|
||||
...state.routes,
|
||||
{
|
||||
key: String(MockRouterKey.current++),
|
||||
name: action.payload.name,
|
||||
params: action.payload.params,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
...state,
|
||||
index: routes.length - 1,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return router.getStateForAction(state, action, options);
|
||||
}
|
||||
},
|
||||
} as typeof router;
|
||||
};
|
||||
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestComponent = ({ route }: any): any =>
|
||||
`[${route.name}, ${JSON.stringify(route.params)}]`;
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const navigation = React.createRef<NavigationContainerRef>();
|
||||
|
||||
const first = render(
|
||||
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||
<TestNavigator>
|
||||
<Screen name="foo">
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="foo-a" component={TestComponent} />
|
||||
<Screen name="foo-b" component={TestComponent} />
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="bar">
|
||||
{() => (
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestComponent}
|
||||
initialParams={{ lol: 'why' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestComponent}
|
||||
initialParams={{ some: 'stuff' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 0,
|
||||
key: '1',
|
||||
routeNames: ['foo-a', 'foo-b'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo-a',
|
||||
name: 'foo-a',
|
||||
},
|
||||
{
|
||||
key: 'foo-b',
|
||||
name: 'foo-b',
|
||||
},
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
act(
|
||||
() =>
|
||||
navigation.current &&
|
||||
navigation.current.navigate('bar', {
|
||||
screen: 'bar-b',
|
||||
params: { test: 42 },
|
||||
})
|
||||
);
|
||||
|
||||
expect(first).toMatchInlineSnapshot(
|
||||
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
|
||||
);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
index: 2,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{
|
||||
key: '2',
|
||||
name: 'bar',
|
||||
params: { params: { test: 42 }, screen: 'bar-b' },
|
||||
state: {
|
||||
index: 1,
|
||||
key: '3',
|
||||
routeNames: ['bar-a', 'bar-b'],
|
||||
routes: [
|
||||
{
|
||||
key: 'bar-a',
|
||||
name: 'bar-a',
|
||||
params: { lol: 'why' },
|
||||
},
|
||||
{
|
||||
key: 'bar-b',
|
||||
name: 'bar-b',
|
||||
params: { some: 'stuff', test: 42 },
|
||||
},
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
const second = render(
|
||||
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
|
||||
<TestNavigator>
|
||||
<Screen name="foo">
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="foo-a" component={TestComponent} />
|
||||
<Screen name="foo-b" component={TestComponent} />
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="bar">
|
||||
{() => (
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestComponent}
|
||||
initialParams={{ lol: 'why' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestComponent}
|
||||
initialParams={{ some: 'stuff' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(second).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
index: 0,
|
||||
key: '4',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 0,
|
||||
key: '5',
|
||||
routeNames: ['foo-a', 'foo-b'],
|
||||
routes: [
|
||||
{ key: 'foo-a', name: 'foo-a' },
|
||||
{ key: 'foo-b', name: 'foo-b' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
act(
|
||||
() =>
|
||||
navigation.current &&
|
||||
navigation.current.navigate('bar', {
|
||||
screen: 'bar-b',
|
||||
params: { test: 42 },
|
||||
initial: false,
|
||||
})
|
||||
);
|
||||
|
||||
expect(second).toMatchInlineSnapshot(`"[bar-b, {\\"test\\":42}]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
index: 2,
|
||||
key: '4',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{
|
||||
key: '6',
|
||||
name: 'bar',
|
||||
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
|
||||
state: {
|
||||
index: 2,
|
||||
key: '7',
|
||||
routeNames: ['bar-a', 'bar-b'],
|
||||
routes: [
|
||||
{
|
||||
key: 'bar-a',
|
||||
name: 'bar-a',
|
||||
params: { lol: 'why' },
|
||||
},
|
||||
{
|
||||
key: 'bar-b',
|
||||
name: 'bar-b',
|
||||
params: { some: 'stuff' },
|
||||
},
|
||||
{ key: '8', name: 'bar-b', params: { test: 42 } },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
const third = render(
|
||||
<BaseNavigationContainer
|
||||
ref={navigation}
|
||||
initialState={{
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'foo' },
|
||||
{
|
||||
name: 'bar',
|
||||
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
|
||||
state: {
|
||||
index: 1,
|
||||
key: '7',
|
||||
routes: [
|
||||
{
|
||||
name: 'bar-a',
|
||||
params: { lol: 'why' },
|
||||
},
|
||||
{
|
||||
name: 'bar-b',
|
||||
params: { some: 'stuff' },
|
||||
},
|
||||
],
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'test',
|
||||
}}
|
||||
>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={TestComponent} />
|
||||
<Screen name="bar">
|
||||
{() => (
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestComponent}
|
||||
initialParams={{ lol: 'why' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestComponent}
|
||||
initialParams={{ some: 'stuff' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(third).toMatchInlineSnapshot(`"[bar-b, {\\"some\\":\\"stuff\\"}]"`);
|
||||
|
||||
expect(navigation.current?.getRootState()).toEqual({
|
||||
index: 1,
|
||||
key: '11',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{ key: 'foo-9', name: 'foo' },
|
||||
{
|
||||
key: 'bar-10',
|
||||
name: 'bar',
|
||||
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
|
||||
state: {
|
||||
index: 1,
|
||||
key: '14',
|
||||
routeNames: ['bar-a', 'bar-b'],
|
||||
routes: [
|
||||
{
|
||||
key: 'bar-a-12',
|
||||
name: 'bar-a',
|
||||
params: { lol: 'why' },
|
||||
},
|
||||
{
|
||||
key: 'bar-b-13',
|
||||
name: 'bar-b',
|
||||
params: { some: 'stuff' },
|
||||
},
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('gives access to internal state', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
@@ -1085,7 +1445,7 @@ it('throws descriptive error for invalid screen component', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"Got an invalid value for 'component' prop for the screen 'foo'. It must be a a valid React Component."
|
||||
"Got an invalid value for 'component' prop for the screen 'foo'. It must be a valid React Component."
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ it('sets options with screenOptions prop as an object', () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.routes.map(route => {
|
||||
{state.routes.map((route) => {
|
||||
const { render, options } = descriptors[route.key];
|
||||
|
||||
return (
|
||||
@@ -179,7 +179,7 @@ it('sets options with screenOptions prop as a fuction', () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.routes.map(route => {
|
||||
{state.routes.map((route) => {
|
||||
const { render, options } = descriptors[route.key];
|
||||
|
||||
return (
|
||||
@@ -262,8 +262,10 @@ it('sets initial options with setOptions', () => {
|
||||
};
|
||||
|
||||
const TestScreen = ({ navigation }: any): any => {
|
||||
navigation.setOptions({
|
||||
title: 'Hello world',
|
||||
React.useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: 'Hello world',
|
||||
});
|
||||
});
|
||||
|
||||
return 'Test screen';
|
||||
@@ -273,7 +275,7 @@ it('sets initial options with setOptions', () => {
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" options={{ color: 'blue' }}>
|
||||
{props => <TestScreen {...props} />}
|
||||
{(props) => <TestScreen {...props} />}
|
||||
</Screen>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
@@ -315,12 +317,12 @@ it('updates options with setOptions', () => {
|
||||
};
|
||||
|
||||
const TestScreen = ({ navigation }: any): any => {
|
||||
navigation.setOptions({
|
||||
title: 'Hello world',
|
||||
description: 'Something here',
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: 'Hello world',
|
||||
description: 'Something here',
|
||||
});
|
||||
|
||||
const timer = setTimeout(() =>
|
||||
navigation.setOptions({
|
||||
title: 'Hello again',
|
||||
@@ -338,7 +340,7 @@ it('updates options with setOptions', () => {
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" options={{ color: 'blue' }}>
|
||||
{props => <TestScreen {...props} />}
|
||||
{(props) => <TestScreen {...props} />}
|
||||
</Screen>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
|
||||
@@ -15,7 +15,7 @@ it('fires focus and blur events in root navigator', () => {
|
||||
|
||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const firstFocusCallback = jest.fn();
|
||||
@@ -97,6 +97,69 @@ it('fires focus and blur events in root navigator', () => {
|
||||
expect(fourthBlurCallback).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('fires focus event after blur', () => {
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = ({ route, navigation }: any) => {
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener('focus', () => callback(route.name, 'focus')),
|
||||
[navigation, route.name]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => navigation.addListener('blur', () => callback(route.name, 'blur')),
|
||||
[navigation, route.name]
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator ref={navigation}>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback.mock.calls).toEqual([['first', 'focus']]);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback.mock.calls).toEqual([
|
||||
['first', 'focus'],
|
||||
['first', 'blur'],
|
||||
['second', 'focus'],
|
||||
]);
|
||||
|
||||
act(() => navigation.current.navigate('first'));
|
||||
|
||||
expect(callback.mock.calls).toEqual([
|
||||
['first', 'focus'],
|
||||
['first', 'blur'],
|
||||
['second', 'focus'],
|
||||
['second', 'blur'],
|
||||
['first', 'focus'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('fires focus and blur events in nested navigator', () => {
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
@@ -106,7 +169,7 @@ it('fires focus and blur events in nested navigator', () => {
|
||||
|
||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const firstFocusCallback = jest.fn();
|
||||
@@ -362,7 +425,7 @@ it('fires blur event when a route is removed with a delay', async () => {
|
||||
expect(blurCallback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fires custom events', () => {
|
||||
it('fires custom events added with addListener', () => {
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
@@ -376,7 +439,7 @@ it('fires custom events', () => {
|
||||
state,
|
||||
]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const firstCallback = jest.fn();
|
||||
@@ -409,10 +472,13 @@ it('fires custom events', () => {
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(0);
|
||||
|
||||
const target =
|
||||
ref.current.state.routes[ref.current.state.routes.length - 1].key;
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
target: ref.current.state.routes[ref.current.state.routes.length - 1].key,
|
||||
target,
|
||||
data: 42,
|
||||
});
|
||||
});
|
||||
@@ -422,6 +488,7 @@ it('fires custom events', () => {
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
|
||||
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||
|
||||
@@ -429,11 +496,280 @@ it('fires custom events', () => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
});
|
||||
|
||||
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
|
||||
expect(secondCallback.mock.calls[0][0].target).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[1][0].target).toBe(undefined);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(1);
|
||||
expect(secondCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("doesn't call same listener multiple times with addListener", () => {
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = ({ navigation }: any) => {
|
||||
React.useEffect(() => navigation.addListener(eventName, callback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second" component={Test} />
|
||||
<Screen name="third" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback).toBeCalledTimes(0);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
});
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fires custom events added with listeners prop', () => {
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const firstCallback = jest.fn();
|
||||
const secondCallback = jest.fn();
|
||||
const thirdCallback = jest.fn();
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen
|
||||
name="first"
|
||||
listeners={{ someSuperCoolEvent: firstCallback }}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
<Screen
|
||||
name="second"
|
||||
listeners={{ someSuperCoolEvent: secondCallback }}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
<Screen
|
||||
name="third"
|
||||
listeners={{ someSuperCoolEvent: thirdCallback }}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(0);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(0);
|
||||
|
||||
const target =
|
||||
ref.current.state.routes[ref.current.state.routes.length - 1].key;
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
target,
|
||||
data: 42,
|
||||
});
|
||||
});
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(0);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
|
||||
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
});
|
||||
|
||||
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(1);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("doesn't call same listener multiple times with listeners", () => {
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen
|
||||
name="first"
|
||||
listeners={{ someSuperCoolEvent: callback }}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
<Screen
|
||||
name="second"
|
||||
listeners={{ someSuperCoolEvent: callback }}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
<Screen
|
||||
name="third"
|
||||
listeners={{ someSuperCoolEvent: callback }}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback).toBeCalledTimes(0);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
});
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fires listeners when callback is provided for listeners prop', () => {
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const firstCallback = jest.fn();
|
||||
const secondCallback = jest.fn();
|
||||
const thirdCallback = jest.fn();
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen
|
||||
name="first"
|
||||
listeners={({ route, navigation }) => ({
|
||||
someSuperCoolEvent: (e) => firstCallback(e, route, navigation),
|
||||
})}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
<Screen
|
||||
name="second"
|
||||
listeners={({ route, navigation }) => ({
|
||||
someSuperCoolEvent: (e) => secondCallback(e, route, navigation),
|
||||
})}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
<Screen
|
||||
name="third"
|
||||
listeners={({ route, navigation }) => ({
|
||||
someSuperCoolEvent: (e) => thirdCallback(e, route, navigation),
|
||||
})}
|
||||
component={jest.fn()}
|
||||
/>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(0);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(0);
|
||||
|
||||
const target =
|
||||
ref.current.state.routes[ref.current.state.routes.length - 1].key;
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
target,
|
||||
data: 42,
|
||||
});
|
||||
});
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(0);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
|
||||
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
});
|
||||
|
||||
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(1);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('has option to prevent default', () => {
|
||||
expect.assertions(5);
|
||||
|
||||
@@ -450,7 +786,7 @@ it('has option to prevent default', () => {
|
||||
state,
|
||||
]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const callback = (e: any) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ it('runs focus effect on focus change', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
@@ -107,7 +107,7 @@ it('runs focus effect when initial state is given', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
|
||||
@@ -10,7 +10,7 @@ it('renders correct focus state', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ it('gets navigation prop from context', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
@@ -38,7 +38,7 @@ it("gets navigation's parent from context", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
@@ -70,7 +70,7 @@ it("gets navigation's parent's parent from context", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
@@ -112,7 +112,7 @@ it('throws if called outside a navigation context', () => {
|
||||
const Test = () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
expect(() => useNavigation()).toThrow(
|
||||
"We couldn't find a navigation object. Is your component inside a screen in a navigator?"
|
||||
"Couldn't find a navigation object. Is your component inside a screen in a navigator?"
|
||||
);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-native-testing-library';
|
||||
import { render, act } from 'react-native-testing-library';
|
||||
import useEventEmitter from '../useEventEmitter';
|
||||
import useNavigationCache from '../useNavigationCache';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
|
||||
|
||||
beforeEach(() => (MockRouterKey.current = 0));
|
||||
@@ -40,7 +43,7 @@ it('preserves reference for navigation objects', () => {
|
||||
});
|
||||
|
||||
if (previous.current) {
|
||||
Object.keys(navigations).forEach(key => {
|
||||
Object.keys(navigations).forEach((key) => {
|
||||
expect(navigations[key]).toBe(previous.current[key]);
|
||||
});
|
||||
}
|
||||
@@ -56,3 +59,136 @@ it('preserves reference for navigation objects', () => {
|
||||
|
||||
root.update(<Test />);
|
||||
});
|
||||
|
||||
it('returns correct value for isFocused', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
let navigation: any;
|
||||
|
||||
const Test = (props: any) => {
|
||||
navigation = props.navigation;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render(
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="second" component={Test} />
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(navigation.isFocused()).toBe(false);
|
||||
|
||||
act(() => navigation.navigate('second'));
|
||||
|
||||
expect(navigation.isFocused()).toBe(true);
|
||||
|
||||
act(() => navigation.navigate('third'));
|
||||
|
||||
expect(navigation.isFocused()).toBe(false);
|
||||
|
||||
act(() => navigation.navigate('second'));
|
||||
|
||||
expect(navigation.isFocused()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns correct value for isFocused after changing screens', () => {
|
||||
const TestRouter = (
|
||||
options: Parameters<typeof MockRouter>[0]
|
||||
): ReturnType<typeof MockRouter> => {
|
||||
const router = MockRouter(options);
|
||||
|
||||
return {
|
||||
...router,
|
||||
|
||||
getStateForRouteNamesChange(state, { routeNames }) {
|
||||
const routes = routeNames.map(
|
||||
(name) =>
|
||||
state.routes.find((r) => r.name === name) || {
|
||||
name,
|
||||
key: name,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
routeNames,
|
||||
routes,
|
||||
index: routes.length - 1,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
|
||||
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
let navigation: any;
|
||||
|
||||
const Test = (props: any) => {
|
||||
navigation = props.navigation;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const root = render(
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="second" component={Test} />
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(navigation.isFocused()).toBe(false);
|
||||
|
||||
root.update(
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
<Screen name="second" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(navigation.isFocused()).toBe(true);
|
||||
|
||||
root.update(
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
<Screen name="fourth">{() => null}</Screen>
|
||||
<Screen name="second" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(navigation.isFocused()).toBe(true);
|
||||
|
||||
root.update(
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
<Screen name="second" component={Test} />
|
||||
<Screen name="fourth">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(navigation.isFocused()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -11,13 +11,13 @@ it('gets the current navigation state', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const state = useNavigationState(state => state);
|
||||
const state = useNavigationState((state) => state);
|
||||
|
||||
callback(state);
|
||||
|
||||
@@ -62,13 +62,13 @@ it('gets the current navigation state with selector', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const index = useNavigationState(state => state.index);
|
||||
const index = useNavigationState((state) => state.index);
|
||||
|
||||
callback(index);
|
||||
|
||||
@@ -112,7 +112,7 @@ it('gets the correct value if selector changes', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
@@ -144,12 +144,12 @@ it('gets the correct value if selector changes', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const root = render(<App selector={state => state.index} />);
|
||||
const root = render(<App selector={(state) => state.index} />);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toBe(0);
|
||||
|
||||
root.update(<App selector={state => state.routes[state.index].name} />);
|
||||
root.update(<App selector={(state) => state.routes[state.index].name} />);
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0]).toBe('first');
|
||||
|
||||
@@ -137,7 +137,7 @@ it("lets children handle the action if parent didn't", () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -270,7 +270,7 @@ it("action doesn't bubble if target is specified", () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -317,7 +317,7 @@ it('logs error if no navigator handled the action', () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -374,7 +374,7 @@ it('logs error if no navigator handled the action', () => {
|
||||
render(element).update(element);
|
||||
|
||||
expect(spy.mock.calls[0][0]).toMatch(
|
||||
"The action 'UNKNOWN' with payload 'undefined' was not handled by any navigator."
|
||||
"The action 'UNKNOWN' was not handled by any navigator."
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
@@ -13,7 +13,7 @@ it('gets route prop from context', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
return state.routes.map((route) => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { ParamListBase } from '@react-navigation/routers';
|
||||
import { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||
import Screen from './Screen';
|
||||
import { TypedNavigator } from './types';
|
||||
import { TypedNavigator, EventMapBase } from './types';
|
||||
|
||||
/**
|
||||
* Higher order component to create a `Navigator` and `Screen` pair.
|
||||
@@ -11,17 +11,21 @@ import { TypedNavigator } from './types';
|
||||
* @returns Factory method to create a `Navigator` and `Screen` pair.
|
||||
*/
|
||||
export default function createNavigatorFactory<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase,
|
||||
NavigatorComponent extends React.ComponentType<any>
|
||||
>(Navigator: NavigatorComponent) {
|
||||
return function<ParamList extends ParamListBase>(): TypedNavigator<
|
||||
return function <ParamList extends ParamListBase>(): TypedNavigator<
|
||||
ParamList,
|
||||
State,
|
||||
ScreenOptions,
|
||||
EventMap,
|
||||
typeof Navigator
|
||||
> {
|
||||
if (arguments[0] !== undefined) {
|
||||
throw new Error(
|
||||
"Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5? See https://reactnavigation.org/docs/en/upgrading-from-4.x.html for migration guide."
|
||||
"Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5? See https://reactnavigation.org/docs/upgrading-from-4.x for migration guide."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PartialState, NavigationState } from '@react-navigation/routers';
|
||||
type NavigateParams = {
|
||||
screen?: string;
|
||||
params?: NavigateParams;
|
||||
initial?: boolean;
|
||||
};
|
||||
|
||||
type NavigateAction = {
|
||||
@@ -35,6 +36,7 @@ export default function getActionFromState(
|
||||
}
|
||||
|
||||
route = current.routes[current.routes.length - 1];
|
||||
params.initial = current.routes.length === 1;
|
||||
params.screen = route.name;
|
||||
|
||||
if (route.state) {
|
||||
|
||||
@@ -64,6 +64,8 @@ export default function getPathFromState(
|
||||
};
|
||||
let currentOptions = options;
|
||||
let pattern = route.name;
|
||||
// we keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
||||
let nestedRouteNames = '';
|
||||
|
||||
while (route.name in currentOptions) {
|
||||
if (typeof currentOptions[route.name] === 'string') {
|
||||
@@ -77,11 +79,13 @@ export default function getPathFromState(
|
||||
}).screens
|
||||
) {
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
break;
|
||||
} else {
|
||||
// if it is the end of state, we return pattern
|
||||
if (route.state === undefined) {
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
break;
|
||||
} else {
|
||||
index =
|
||||
@@ -92,11 +96,13 @@ export default function getPathFromState(
|
||||
}).screens;
|
||||
// if there is config for next route name, we go deeper
|
||||
if (nextRoute.name in deeperConfig) {
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
route = nextRoute as Route<string> & { state?: State };
|
||||
currentOptions = deeperConfig;
|
||||
} else {
|
||||
// if not, there is no sense in going deeper in config
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -104,64 +110,71 @@ export default function getPathFromState(
|
||||
}
|
||||
}
|
||||
|
||||
// we don't add empty path strings to path
|
||||
if (pattern !== '') {
|
||||
const config =
|
||||
currentOptions[route.name] !== undefined
|
||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
||||
.stringify
|
||||
: undefined;
|
||||
if (pattern === undefined) {
|
||||
// cut the first `/`
|
||||
pattern = nestedRouteNames.substring(1);
|
||||
}
|
||||
|
||||
const params = route.params
|
||||
? // Stringify all of the param values before we use them
|
||||
Object.entries(route.params).reduce<{
|
||||
[key: string]: string;
|
||||
}>((acc, [key, value]) => {
|
||||
acc[key] = config?.[key] ? config[key](value) : String(value);
|
||||
return acc;
|
||||
}, {})
|
||||
const config =
|
||||
currentOptions[route.name] !== undefined
|
||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
||||
.stringify
|
||||
: undefined;
|
||||
|
||||
if (currentOptions[route.name] !== undefined) {
|
||||
path += pattern
|
||||
.split('/')
|
||||
.map(p => {
|
||||
const name = p.replace(/^:/, '');
|
||||
const params = route.params
|
||||
? // Stringify all of the param values before we use them
|
||||
Object.entries(route.params).reduce<{
|
||||
[key: string]: string;
|
||||
}>((acc, [key, value]) => {
|
||||
acc[key] = config?.[key] ? config[key](value) : String(value);
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
// If the path has a pattern for a param, put the param in the path
|
||||
if (params && name in params && p.startsWith(':')) {
|
||||
const value = params[name];
|
||||
// Remove the used value from the params object since we'll use the rest for query string
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[name];
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
if (currentOptions[route.name] !== undefined) {
|
||||
path += pattern
|
||||
.split('/')
|
||||
.map((p) => {
|
||||
const name = p.replace(/^:/, '');
|
||||
|
||||
return encodeURIComponent(p);
|
||||
})
|
||||
.join('/');
|
||||
} else {
|
||||
path += encodeURIComponent(route.name);
|
||||
}
|
||||
// If the path has a pattern for a param, put the param in the path
|
||||
if (params && name in params && p.startsWith(':')) {
|
||||
const value = params[name];
|
||||
// Remove the used value from the params object since we'll use the rest for query string
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[name];
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (params) {
|
||||
const query = queryString.stringify(params);
|
||||
return encodeURIComponent(p);
|
||||
})
|
||||
.join('/');
|
||||
} else {
|
||||
path += encodeURIComponent(route.name);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
path += `?${query}`;
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (params) {
|
||||
for (let param in params) {
|
||||
if (params[param] === 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[param];
|
||||
}
|
||||
}
|
||||
const query = queryString.stringify(params);
|
||||
|
||||
if (query) {
|
||||
path += `?${query}`;
|
||||
}
|
||||
}
|
||||
|
||||
current = route.state;
|
||||
}
|
||||
|
||||
path =
|
||||
path !== '/' && path.slice(path.length - 1) === '/'
|
||||
? path.slice(0, -1)
|
||||
: path;
|
||||
// Remove multiple as well as trailing slashes
|
||||
path = path.replace(/\/+/, '/');
|
||||
path = path.length > 1 ? path.replace(/\/$/, '') : path;
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import escape from 'escape-string-regexp';
|
||||
import queryString from 'query-string';
|
||||
import {
|
||||
NavigationState,
|
||||
@@ -20,7 +19,8 @@ type Options = {
|
||||
};
|
||||
|
||||
type RouteConfig = {
|
||||
match: RegExp;
|
||||
screen: string;
|
||||
match: string | null;
|
||||
pattern: string;
|
||||
routeNames: string[];
|
||||
parse: ParseConfig | undefined;
|
||||
@@ -58,46 +58,93 @@ export default function getStateFromPath(
|
||||
path: string,
|
||||
options: Options = {}
|
||||
): ResultState | undefined {
|
||||
if (path === '') {
|
||||
return undefined;
|
||||
}
|
||||
let initialRoutes: InitialRouteConfig[] = [];
|
||||
|
||||
// Create a normalized configs array which will be easier to use
|
||||
const configs = ([] as RouteConfig[]).concat(
|
||||
...Object.keys(options).map(key =>
|
||||
...Object.keys(options).map((key) =>
|
||||
createNormalizedConfigs(key, options, [], initialRoutes)
|
||||
)
|
||||
);
|
||||
|
||||
let result: PartialState<NavigationState> | undefined;
|
||||
let current: PartialState<NavigationState> | undefined;
|
||||
// sort configs so the most exhaustive is always first to be chosen
|
||||
configs.sort(
|
||||
(config1, config2) =>
|
||||
config2.pattern.split('/').length - config1.pattern.split('/').length
|
||||
);
|
||||
|
||||
let remaining = path
|
||||
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
|
||||
.replace(/^\//, '') // Remove extra leading slash
|
||||
.replace(/\?.*/, ''); // Remove query params which we will handle later
|
||||
|
||||
if (remaining === '') {
|
||||
// We need to add special handling of empty path so navigation to empty path also works
|
||||
// When handling empty path, we should only look at the root level config
|
||||
const match = configs.find(
|
||||
(config) =>
|
||||
config.pattern === '' &&
|
||||
config.routeNames.every(
|
||||
// make sure that none of the parent configs have a non-empty path defined
|
||||
(name) => !configs.find((c) => c.screen === name)?.pattern
|
||||
)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
return createNestedStateObject(
|
||||
match.routeNames,
|
||||
initialRoutes,
|
||||
parseQueryParams(path, match.parse)
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let result: PartialState<NavigationState> | undefined;
|
||||
let current: PartialState<NavigationState> | undefined;
|
||||
|
||||
while (remaining) {
|
||||
let routeNames: string[] | undefined;
|
||||
let params: Record<string, any> | undefined;
|
||||
|
||||
// Go through all configs, and see if the next path segment matches our regex
|
||||
for (const config of configs) {
|
||||
const match = remaining.match(config.match);
|
||||
if (!config.match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
let didMatch = true;
|
||||
const matchParts = config.match.split('/');
|
||||
const remainingParts = remaining.split('/');
|
||||
|
||||
// we check if remaining path has enough segments to be handled with this pattern
|
||||
if (config.pattern.split('/').length > remainingParts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we keep info about the index of segment on which the params start
|
||||
let paramsIndex = 0;
|
||||
// the beginning of the remaining path should be the same as the part of config before params
|
||||
for (paramsIndex; paramsIndex < matchParts.length; paramsIndex++) {
|
||||
if (matchParts[paramsIndex] !== remainingParts[paramsIndex]) {
|
||||
didMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the first part of the path matches, we need to extract params from the path
|
||||
if (didMatch) {
|
||||
routeNames = [...config.routeNames];
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
.split('/')
|
||||
.filter(p => p.startsWith(':'));
|
||||
.filter((p) => p.startsWith(':'));
|
||||
|
||||
if (paramPatterns.length) {
|
||||
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||
const key = p.replace(/^:/, '');
|
||||
const value = match[i + 1]; // The param segments start from index 1 in the regex match result
|
||||
|
||||
const value = remainingParts[i + paramsIndex]; // The param segments start from the end of matched part
|
||||
acc[key] =
|
||||
config.parse && config.parse[key]
|
||||
? config.parse[key](value)
|
||||
@@ -107,8 +154,16 @@ export default function getStateFromPath(
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Remove the matched segment from the remaining path
|
||||
remaining = remaining.replace(match[0], '');
|
||||
// if pattern and remaining path have same amount of segments, there should be nothing left
|
||||
if (config.pattern.split('/').length === remainingParts.length) {
|
||||
remaining = '';
|
||||
} else {
|
||||
// For each segment of the pattern, remove one segment from remaining path
|
||||
let i = config.pattern.split('/').length;
|
||||
while (i--) {
|
||||
remaining = remaining.substr(remaining.indexOf('/') + 1);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -123,34 +178,7 @@ export default function getStateFromPath(
|
||||
remaining = segments.join('/');
|
||||
}
|
||||
|
||||
let state: InitialState;
|
||||
let routeName = routeNames.shift() as string;
|
||||
let initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
|
||||
state = createNestedState(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
|
||||
if (routeNames.length > 0) {
|
||||
let nestedState = state;
|
||||
|
||||
while ((routeName = routeNames.shift() as string)) {
|
||||
initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
nestedState.routes[nestedState.index || 0].state = createNestedState(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
if (routeNames.length > 0) {
|
||||
nestedState = nestedState.routes[nestedState.index || 0]
|
||||
.state as InitialState;
|
||||
}
|
||||
}
|
||||
}
|
||||
const state = createNestedStateObject(routeNames, initialRoutes, params);
|
||||
|
||||
if (current) {
|
||||
// The state should be nested inside the deepest route we parsed before
|
||||
@@ -172,29 +200,13 @@ export default function getStateFromPath(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const query = path.split('?')[1];
|
||||
|
||||
if (query) {
|
||||
while (current?.routes[current.index || 0].state) {
|
||||
// The query params apply to the deepest route
|
||||
current = current.routes[current.index || 0].state;
|
||||
}
|
||||
|
||||
const route = (current as PartialState<NavigationState>).routes[
|
||||
current?.index || 0
|
||||
];
|
||||
|
||||
const params = queryString.parse(query);
|
||||
const parseFunction = findParseConfigForRoute(route.name, configs);
|
||||
|
||||
if (parseFunction) {
|
||||
Object.keys(params).forEach(name => {
|
||||
if (parseFunction[name] && typeof params[name] === 'string') {
|
||||
params[name] = parseFunction[name](params[name] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
const route = findFocusedRoute(current);
|
||||
const params = parseQueryParams(
|
||||
path,
|
||||
findParseConfigForRoute(route.name, configs)
|
||||
);
|
||||
|
||||
if (params) {
|
||||
route.params = { ...route.params, ...params };
|
||||
}
|
||||
|
||||
@@ -215,16 +227,15 @@ function createNormalizedConfigs(
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
|
||||
if (value !== '') {
|
||||
configs.push(createConfigItem(routeNames, value));
|
||||
}
|
||||
configs.push(createConfigItem(key, routeNames, value));
|
||||
} else if (typeof value === 'object') {
|
||||
// if an object is specified as the value (e.g. Foo: { ... }),
|
||||
// it can have `path` property and
|
||||
// it could have `screens` prop which has nested configs
|
||||
if (value.path && value.path !== '') {
|
||||
configs.push(createConfigItem(routeNames, value.path, value.parse));
|
||||
if (typeof value.path === 'string') {
|
||||
configs.push(createConfigItem(key, routeNames, value.path, value.parse));
|
||||
}
|
||||
|
||||
if (value.screens) {
|
||||
// property `initialRouteName` without `screens` has no purpose
|
||||
if (value.initialRouteName) {
|
||||
@@ -233,7 +244,7 @@ function createNormalizedConfigs(
|
||||
connectedRoutes: Object.keys(value.screens),
|
||||
});
|
||||
}
|
||||
Object.keys(value.screens).forEach(nestedConfig => {
|
||||
Object.keys(value.screens).forEach((nestedConfig) => {
|
||||
const result = createNormalizedConfigs(
|
||||
nestedConfig,
|
||||
value.screens as Options,
|
||||
@@ -251,15 +262,16 @@ function createNormalizedConfigs(
|
||||
}
|
||||
|
||||
function createConfigItem(
|
||||
screen: string,
|
||||
routeNames: string[],
|
||||
pattern: string,
|
||||
parse?: ParseConfig
|
||||
): RouteConfig {
|
||||
const match = new RegExp(
|
||||
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
|
||||
);
|
||||
// part being matched ends on the first param
|
||||
const match = pattern !== '' ? pattern.split('/:')[0] : null;
|
||||
|
||||
return {
|
||||
screen,
|
||||
match,
|
||||
pattern,
|
||||
// The routeNames array is mutated, so copy it to keep the current state
|
||||
@@ -295,9 +307,9 @@ function findInitialRoute(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// returns nested state object with values depending on whether
|
||||
// returns state object with values depending on whether
|
||||
// it is the end of state and if there is initialRoute for this level
|
||||
function createNestedState(
|
||||
function createStateObject(
|
||||
initialRoute: string | undefined,
|
||||
routeName: string,
|
||||
isEmpty: boolean,
|
||||
@@ -331,3 +343,73 @@ function createNestedState(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createNestedStateObject(
|
||||
routeNames: string[],
|
||||
initialRoutes: InitialRouteConfig[],
|
||||
params: object | undefined
|
||||
) {
|
||||
let state: InitialState;
|
||||
let routeName = routeNames.shift() as string;
|
||||
let initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
|
||||
state = createStateObject(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
|
||||
if (routeNames.length > 0) {
|
||||
let nestedState = state;
|
||||
|
||||
while ((routeName = routeNames.shift() as string)) {
|
||||
initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
nestedState.routes[nestedState.index || 0].state = createStateObject(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
if (routeNames.length > 0) {
|
||||
nestedState = nestedState.routes[nestedState.index || 0]
|
||||
.state as InitialState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function findFocusedRoute(state: InitialState) {
|
||||
let current: InitialState | undefined = state;
|
||||
|
||||
while (current?.routes[current.index || 0].state) {
|
||||
// The query params apply to the deepest route
|
||||
current = current.routes[current.index || 0].state;
|
||||
}
|
||||
|
||||
const route = (current as PartialState<NavigationState>).routes[
|
||||
current?.index || 0
|
||||
];
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
function parseQueryParams(
|
||||
path: string,
|
||||
parseConfig?: Record<string, (value: string) => any>
|
||||
) {
|
||||
const query = path.split('?')[1];
|
||||
const params = queryString.parse(query);
|
||||
|
||||
if (parseConfig) {
|
||||
Object.keys(params).forEach((name) => {
|
||||
if (parseConfig[name] && typeof params[name] === 'string') {
|
||||
params[name] = parseConfig[name](params[name] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(params).length ? params : undefined;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from '@react-navigation/routers';
|
||||
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
|
||||
export { default as createNavigatorFactory } from './createNavigatorFactory';
|
||||
|
||||
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
|
||||
export { default as NavigationContext } from './NavigationContext';
|
||||
export { default as NavigationRouteContext } from './NavigationRouteContext';
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export type EventArg<
|
||||
* Type of the event (e.g. `focus`, `blur`)
|
||||
*/
|
||||
readonly type: EventName;
|
||||
readonly target?: string;
|
||||
} & (CanPreventDefault extends true
|
||||
? {
|
||||
/**
|
||||
@@ -167,18 +168,6 @@ type NavigationHelpersCommon<
|
||||
| { name: RouteName; key?: string; params: ParamList[RouteName] }
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Replace the current route with a new one.
|
||||
*
|
||||
* @param name Route name of the new route.
|
||||
* @param [params] Params object for the new route.
|
||||
*/
|
||||
replace<RouteName extends keyof ParamList>(
|
||||
...args: ParamList[RouteName] extends undefined
|
||||
? [RouteName] | [RouteName, ParamList[RouteName]]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Reset the navigation state to the provided state.
|
||||
*
|
||||
@@ -204,6 +193,20 @@ type NavigationHelpersCommon<
|
||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||
*/
|
||||
canGoBack(): boolean;
|
||||
|
||||
/**
|
||||
* Returns the parent navigator, if any. Reason why the function is called
|
||||
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
|
||||
* of parent and other hard-to-follow patterns.
|
||||
*/
|
||||
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
|
||||
|
||||
/**
|
||||
* Returns the navigator's state. Reason why the function is called
|
||||
* dangerouslyGetState is to discourage developers to use internal navigation's state.
|
||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||
*/
|
||||
dangerouslyGetState(): State;
|
||||
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
|
||||
|
||||
export type NavigationHelpers<
|
||||
@@ -265,20 +268,6 @@ export type NavigationProp<
|
||||
* @param options Options object for the route.
|
||||
*/
|
||||
setOptions(options: Partial<ScreenOptions>): void;
|
||||
|
||||
/**
|
||||
* Returns the parent navigator, if any. Reason why the function is called
|
||||
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
|
||||
* of parent and other hard-to-follow patterns.
|
||||
*/
|
||||
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
|
||||
|
||||
/**
|
||||
* Returns the navigator's state. Reason why the function is called
|
||||
* dangerouslyGetState is to discourage developers to use internal navigation's state.
|
||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||
*/
|
||||
dangerouslyGetState(): State;
|
||||
} & EventConsumer<EventMap & EventMapCore<State>> &
|
||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||
|
||||
@@ -357,10 +346,22 @@ export type Descriptor<
|
||||
>;
|
||||
};
|
||||
|
||||
export type ScreenListeners<
|
||||
State extends NavigationState,
|
||||
EventMap extends EventMapBase
|
||||
> = Partial<
|
||||
{
|
||||
[EventName in keyof (EventMap &
|
||||
EventMapCore<State>)]: EventListenerCallback<EventMap, EventName>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type RouteConfig<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList,
|
||||
ScreenOptions extends object
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
> = {
|
||||
/**
|
||||
* Route name of this screen.
|
||||
@@ -377,6 +378,16 @@ export type RouteConfig<
|
||||
navigation: any;
|
||||
}) => ScreenOptions);
|
||||
|
||||
/**
|
||||
* Event listeners for this screen.
|
||||
*/
|
||||
listeners?:
|
||||
| ScreenListeners<State, EventMap>
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: any;
|
||||
}) => ScreenListeners<State, EventMap>);
|
||||
|
||||
/**
|
||||
* Initial params object for the route.
|
||||
*/
|
||||
@@ -399,28 +410,25 @@ export type RouteConfig<
|
||||
}
|
||||
);
|
||||
|
||||
export type NavigationContainerRef =
|
||||
| (NavigationHelpers<ParamListBase> &
|
||||
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
*
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(
|
||||
state?: PartialState<NavigationState> | NavigationState
|
||||
): void;
|
||||
/**
|
||||
* Get the rehydrated navigation state of the navigation tree.
|
||||
*/
|
||||
getRootState(): NavigationState;
|
||||
})
|
||||
| undefined
|
||||
| null;
|
||||
export type NavigationContainerRef = NavigationHelpers<ParamListBase> &
|
||||
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
*
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
|
||||
/**
|
||||
* Get the rehydrated navigation state of the navigation tree.
|
||||
*/
|
||||
getRootState(): NavigationState;
|
||||
};
|
||||
|
||||
export type TypedNavigator<
|
||||
ParamList extends ParamListBase,
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase,
|
||||
Navigator extends React.ComponentType<any>
|
||||
> = {
|
||||
/**
|
||||
@@ -451,6 +459,6 @@ export type TypedNavigator<
|
||||
* Component used for specifying route configuration.
|
||||
*/
|
||||
Screen: <RouteName extends keyof ParamList>(
|
||||
_: RouteConfig<ParamList, RouteName, ScreenOptions>
|
||||
_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>
|
||||
) => null;
|
||||
};
|
||||
|
||||
@@ -13,11 +13,24 @@ import NavigationBuilderContext, {
|
||||
} from './NavigationBuilderContext';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import useNavigationCache from './useNavigationCache';
|
||||
import { Descriptor, NavigationHelpers, RouteConfig, RouteProp } from './types';
|
||||
import {
|
||||
Descriptor,
|
||||
NavigationHelpers,
|
||||
RouteConfig,
|
||||
RouteProp,
|
||||
EventMapBase,
|
||||
} from './types';
|
||||
|
||||
type Options<State extends NavigationState, ScreenOptions extends object> = {
|
||||
type Options<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
> = {
|
||||
state: State;
|
||||
screens: Record<string, RouteConfig<ParamListBase, string, ScreenOptions>>;
|
||||
screens: Record<
|
||||
string,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
>;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
screenOptions?:
|
||||
| ScreenOptions
|
||||
@@ -49,7 +62,8 @@ type Options<State extends NavigationState, ScreenOptions extends object> = {
|
||||
*/
|
||||
export default function useDescriptors<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
>({
|
||||
state,
|
||||
screens,
|
||||
@@ -64,7 +78,7 @@ export default function useDescriptors<
|
||||
onRouteFocus,
|
||||
router,
|
||||
emitter,
|
||||
}: Options<State, ScreenOptions>) {
|
||||
}: Options<State, ScreenOptions, EventMap>) {
|
||||
const [options, setOptions] = React.useState<Record<string, object>>({});
|
||||
const { trackAction } = React.useContext(NavigationBuilderContext);
|
||||
|
||||
@@ -133,6 +147,7 @@ export default function useDescriptors<
|
||||
: screen.options({
|
||||
// @ts-ignore
|
||||
route,
|
||||
// @ts-ignore
|
||||
navigation,
|
||||
})),
|
||||
// The options set via `navigation.setOptions`
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
|
||||
type Options = {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
reset: (state: NavigationState) => void;
|
||||
state: State;
|
||||
@@ -35,10 +36,11 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export default function useDevTools({ name, reset, state }: Options) {
|
||||
export default function useDevTools({ name, reset, state, enabled }: Options) {
|
||||
const devToolsRef = React.useRef<DevTools>();
|
||||
|
||||
if (
|
||||
enabled &&
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
global.__REDUX_DEVTOOLS_EXTENSION__ &&
|
||||
devToolsRef.current === undefined
|
||||
@@ -56,7 +58,7 @@ export default function useDevTools({ name, reset, state }: Options) {
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
devTools?.subscribe(message => {
|
||||
devTools?.subscribe((message) => {
|
||||
if (message.type === 'DISPATCH' && message.state) {
|
||||
reset(JSON.parse(message.state));
|
||||
}
|
||||
|
||||
@@ -5,12 +5,20 @@ export type NavigationEventEmitter = EventEmitter<Record<string, any>> & {
|
||||
create: (target: string) => EventConsumer<Record<string, any>>;
|
||||
};
|
||||
|
||||
type Listeners = ((data: any) => void)[];
|
||||
type Listeners = ((e: any) => void)[];
|
||||
|
||||
/**
|
||||
* Hook to manage the event system used by the navigator to notify screens of various events.
|
||||
*/
|
||||
export default function useEventEmitter(): NavigationEventEmitter {
|
||||
export default function useEventEmitter(
|
||||
listen?: (e: any) => void
|
||||
): NavigationEventEmitter {
|
||||
const listenRef = React.useRef(listen);
|
||||
|
||||
React.useEffect(() => {
|
||||
listenRef.current = listen;
|
||||
});
|
||||
|
||||
const listeners = React.useRef<Record<string, Record<string, Listeners>>>({});
|
||||
|
||||
const create = React.useCallback((target: string) => {
|
||||
@@ -60,7 +68,9 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
const callbacks =
|
||||
target !== undefined
|
||||
? items[target] && items[target].slice()
|
||||
: ([] as Listeners).concat(...Object.keys(items).map(t => items[t]));
|
||||
: ([] as Listeners)
|
||||
.concat(...Object.keys(items).map((t) => items[t]))
|
||||
.filter((cb, i, self) => self.lastIndexOf(cb) === i);
|
||||
|
||||
const event: EventArg<any, any, any> = {
|
||||
get type() {
|
||||
@@ -68,8 +78,18 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
},
|
||||
};
|
||||
|
||||
if (target !== undefined) {
|
||||
Object.defineProperty(event, 'target', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return target;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (data !== undefined) {
|
||||
Object.defineProperty(event, 'data', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return data;
|
||||
},
|
||||
@@ -81,11 +101,13 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
|
||||
Object.defineProperties(event, {
|
||||
defaultPrevented: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return defaultPrevented;
|
||||
},
|
||||
},
|
||||
preventDefault: {
|
||||
enumerable: true,
|
||||
value() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
@@ -93,7 +115,9 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
callbacks?.forEach(cb => cb(event));
|
||||
listenRef.current?.(event);
|
||||
|
||||
callbacks?.forEach((cb) => cb(event));
|
||||
|
||||
return event as any;
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function useFocusEffect(effect: EffectCallback) {
|
||||
' fetchData();\n' +
|
||||
' }, [someId])\n' +
|
||||
'};\n\n' +
|
||||
'See usage guide: https://reactnavigation.org/docs/en/use-focus-effect.html';
|
||||
'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
|
||||
} else {
|
||||
message += ` You returned: '${JSON.stringify(destroy)}'`;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
|
||||
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
||||
}
|
||||
|
||||
// We should only dispatch events when the focused key changed and navigator is focused
|
||||
// We should only emit events when the focused key changed and navigator is focused
|
||||
// When navigator is not focused, screens inside shouldn't receive focused status either
|
||||
if (
|
||||
lastFocusedKey === currentFocusedKey ||
|
||||
@@ -62,7 +62,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
||||
emitter.emit({ type: 'blur', target: lastFocusedKey });
|
||||
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
||||
}, [currentFocusedKey, emitter, navigation]);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function useNavigation<
|
||||
|
||||
if (navigation === undefined) {
|
||||
throw new Error(
|
||||
"We couldn't find a navigation object. Is your component inside a screen in a navigator?"
|
||||
"Couldn't find a navigation object. Is your component inside a screen in a navigator?"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
RouterFactory,
|
||||
PartialState,
|
||||
NavigationAction,
|
||||
Route,
|
||||
} from '@react-navigation/routers';
|
||||
import { NavigationStateContext } from './BaseNavigationContainer';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
@@ -27,9 +28,11 @@ import {
|
||||
DefaultNavigatorOptions,
|
||||
RouteConfig,
|
||||
PrivateValueStore,
|
||||
EventMapBase,
|
||||
} from './types';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import useOnGetState from './useOnGetState';
|
||||
import useScheduleUpdate from './useScheduleUpdate';
|
||||
|
||||
// This is to make TypeScript compiler happy
|
||||
// eslint-disable-next-line babel/no-unused-expressions
|
||||
@@ -40,6 +43,7 @@ type NavigatorRoute = {
|
||||
params?: {
|
||||
screen?: string;
|
||||
params?: object;
|
||||
initial?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -55,18 +59,28 @@ const isArrayEqual = (a: any[], b: any[]) =>
|
||||
*
|
||||
* @param children React Elements to extract the config from.
|
||||
*/
|
||||
const getRouteConfigsFromChildren = <ScreenOptions extends object>(
|
||||
const getRouteConfigsFromChildren = <
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends object,
|
||||
EventMap extends EventMapBase
|
||||
>(
|
||||
children: React.ReactNode
|
||||
) => {
|
||||
const configs = React.Children.toArray(children).reduce<
|
||||
RouteConfig<ParamListBase, string, ScreenOptions>[]
|
||||
RouteConfig<ParamListBase, string, 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(
|
||||
child.props as RouteConfig<ParamListBase, string, ScreenOptions>
|
||||
child.props as RouteConfig<
|
||||
ParamListBase,
|
||||
string,
|
||||
State,
|
||||
ScreenOptions,
|
||||
EventMap
|
||||
>
|
||||
);
|
||||
return acc;
|
||||
}
|
||||
@@ -75,7 +89,9 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
|
||||
// When we encounter a fragment, we need to dive into its children to extract the configs
|
||||
// This is handy to conditionally define a group of screens
|
||||
acc.push(
|
||||
...getRouteConfigsFromChildren<ScreenOptions>(child.props.children)
|
||||
...getRouteConfigsFromChildren<State, ScreenOptions, EventMap>(
|
||||
child.props.children
|
||||
)
|
||||
);
|
||||
return acc;
|
||||
}
|
||||
@@ -90,7 +106,7 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
|
||||
}, []);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
configs.forEach(config => {
|
||||
configs.forEach((config) => {
|
||||
const { name, children, component } = config as any;
|
||||
|
||||
if (typeof name !== 'string' || !name) {
|
||||
@@ -116,7 +132,7 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
|
||||
|
||||
if (component !== undefined && !isValidElementType(component)) {
|
||||
throw new Error(
|
||||
`Got an invalid value for 'component' prop for the screen '${name}'. It must be a a valid React Component.`
|
||||
`Got an invalid value for 'component' prop for the screen '${name}'. It must be a valid React Component.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,25 +177,35 @@ export default function useNavigationBuilder<
|
||||
| NavigatorRoute
|
||||
| undefined;
|
||||
|
||||
const previousRouteRef = React.useRef(route);
|
||||
const previousNestedParamsRef = React.useRef(route?.params);
|
||||
|
||||
React.useEffect(() => {
|
||||
previousRouteRef.current = route;
|
||||
previousNestedParamsRef.current = route?.params;
|
||||
}, [route]);
|
||||
|
||||
const { children, ...rest } = options;
|
||||
const { current: router } = React.useRef<Router<State, any>>(
|
||||
createRouter({
|
||||
...((rest as unknown) as RouterOptions),
|
||||
...(route?.params && typeof route.params.screen === 'string'
|
||||
...(route?.params &&
|
||||
route.params.initial !== false &&
|
||||
typeof route.params.screen === 'string'
|
||||
? { initialRouteName: route.params.screen }
|
||||
: null),
|
||||
})
|
||||
);
|
||||
|
||||
const routeConfigs = getRouteConfigsFromChildren<ScreenOptions>(children);
|
||||
const routeConfigs = getRouteConfigsFromChildren<
|
||||
State,
|
||||
ScreenOptions,
|
||||
EventMap
|
||||
>(children);
|
||||
|
||||
const screens = routeConfigs.reduce<
|
||||
Record<string, RouteConfig<ParamListBase, string, ScreenOptions>>
|
||||
Record<
|
||||
string,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
>
|
||||
>((acc, config) => {
|
||||
if (config.name in acc) {
|
||||
throw new Error(
|
||||
@@ -191,12 +217,12 @@ export default function useNavigationBuilder<
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const routeNames = routeConfigs.map(config => config.name);
|
||||
const routeNames = routeConfigs.map((config) => config.name);
|
||||
const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
|
||||
(acc, curr) => {
|
||||
const { initialParams } = screens[curr];
|
||||
const initialParamsFromParams =
|
||||
route?.params && route.params.screen === curr
|
||||
route?.params?.initial !== false && route?.params?.screen === curr
|
||||
? route.params.params
|
||||
: undefined;
|
||||
|
||||
@@ -220,12 +246,12 @@ export default function useNavigationBuilder<
|
||||
}
|
||||
|
||||
const isStateValid = React.useCallback(
|
||||
state => state.type === undefined || state.type === router.type,
|
||||
(state) => state.type === undefined || state.type === router.type,
|
||||
[router.type]
|
||||
);
|
||||
|
||||
const isStateInitialized = React.useCallback(
|
||||
state =>
|
||||
(state) =>
|
||||
state !== undefined && state.stale === false && isStateValid(state),
|
||||
[isStateValid]
|
||||
);
|
||||
@@ -243,6 +269,8 @@ export default function useNavigationBuilder<
|
||||
>();
|
||||
const initializedStateRef = React.useRef<State>();
|
||||
|
||||
let isFirstStateInitialization = false;
|
||||
|
||||
if (
|
||||
initializedStateRef.current === undefined ||
|
||||
currentState !== previousStateRef.current
|
||||
@@ -251,16 +279,21 @@ export default function useNavigationBuilder<
|
||||
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
|
||||
// Otherwise assume that the state was provided as initial state
|
||||
// So we need to rehydrate it to make it usable
|
||||
initializedStateRef.current =
|
||||
currentState === undefined || !isStateValid(currentState)
|
||||
? router.getInitialState({
|
||||
routeNames,
|
||||
routeParamList,
|
||||
})
|
||||
: router.getRehydratedState(currentState as PartialState<State>, {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
});
|
||||
if (currentState === undefined || !isStateValid(currentState)) {
|
||||
isFirstStateInitialization = true;
|
||||
initializedStateRef.current = router.getInitialState({
|
||||
routeNames,
|
||||
routeParamList,
|
||||
});
|
||||
} else {
|
||||
initializedStateRef.current = router.getRehydratedState(
|
||||
currentState as PartialState<State>,
|
||||
{
|
||||
routeNames,
|
||||
routeParamList,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -286,16 +319,14 @@ export default function useNavigationBuilder<
|
||||
}
|
||||
|
||||
if (
|
||||
previousRouteRef.current &&
|
||||
route &&
|
||||
route.params &&
|
||||
typeof route.params.screen === 'string' &&
|
||||
route.params !== previousRouteRef.current.params
|
||||
typeof route?.params?.screen === 'string' &&
|
||||
(route.params !== previousNestedParamsRef.current ||
|
||||
(route.params.initial === false && isFirstStateInitialization))
|
||||
) {
|
||||
// If the route was updated with new name and/or params, we should navigate there
|
||||
// The update should be limited to current navigator only, so we call the router manually
|
||||
const updatedState = router.getStateForAction(
|
||||
state,
|
||||
nextState,
|
||||
CommonActions.navigate(route.params.screen, route.params.params),
|
||||
{
|
||||
routeNames,
|
||||
@@ -309,15 +340,17 @@ export default function useNavigationBuilder<
|
||||
routeNames,
|
||||
routeParamList,
|
||||
})
|
||||
: state;
|
||||
: nextState;
|
||||
}
|
||||
|
||||
if (state !== nextState) {
|
||||
// If the state needs to be updated, we'll schedule an update with React
|
||||
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
|
||||
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
|
||||
setState(nextState);
|
||||
}
|
||||
const shouldUpdate = state !== nextState;
|
||||
|
||||
useScheduleUpdate(() => {
|
||||
if (shouldUpdate) {
|
||||
// If the state needs to be updated, we'll schedule an update
|
||||
setState(nextState);
|
||||
}
|
||||
});
|
||||
|
||||
// The up-to-date state will come in next render, but we don't need to wait for it
|
||||
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
|
||||
@@ -344,7 +377,50 @@ export default function useNavigationBuilder<
|
||||
: (initializedStateRef.current as State);
|
||||
}, [getCurrentState, isStateInitialized]);
|
||||
|
||||
const emitter = useEventEmitter();
|
||||
const emitter = useEventEmitter((e) => {
|
||||
let routeNames = [];
|
||||
|
||||
let route: Route<string> | undefined;
|
||||
|
||||
if (e.target) {
|
||||
route = state.routes.find((route) => route.key === e.target);
|
||||
|
||||
if (route?.name) {
|
||||
routeNames.push(route.name);
|
||||
}
|
||||
} else {
|
||||
route = state.routes[state.index];
|
||||
routeNames.push(
|
||||
...Object.keys(screens).filter((name) => route?.name === name)
|
||||
);
|
||||
}
|
||||
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navigation = descriptors[route.key].navigation;
|
||||
|
||||
const listeners = ([] as (((e: any) => void) | undefined)[])
|
||||
.concat(
|
||||
...routeNames.map((name) => {
|
||||
const { listeners } = screens[name];
|
||||
const map =
|
||||
typeof listeners === 'function'
|
||||
? listeners({ route: route as any, navigation })
|
||||
: listeners;
|
||||
|
||||
return map
|
||||
? Object.keys(map)
|
||||
.filter((type) => type === e.type)
|
||||
.map((type) => map?.[type])
|
||||
: undefined;
|
||||
})
|
||||
)
|
||||
.filter((cb, i, self) => cb && self.lastIndexOf(cb) === i);
|
||||
|
||||
listeners.forEach((listener) => listener?.(e));
|
||||
});
|
||||
|
||||
useFocusEvents({ state, emitter });
|
||||
|
||||
@@ -400,7 +476,7 @@ export default function useNavigationBuilder<
|
||||
getStateForRoute,
|
||||
});
|
||||
|
||||
const descriptors = useDescriptors<State, ScreenOptions>({
|
||||
const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
|
||||
state,
|
||||
screens,
|
||||
navigation,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Router,
|
||||
} from '@react-navigation/routers';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import NavigationContext from './NavigationContext';
|
||||
|
||||
import { NavigationHelpers, NavigationProp } from './types';
|
||||
|
||||
@@ -49,12 +48,10 @@ export default function useNavigationCache<
|
||||
// Cache object which holds navigation objects for each screen
|
||||
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
|
||||
// In reality, these deps will rarely change, if ever
|
||||
const parentNavigation = React.useContext(NavigationContext);
|
||||
|
||||
const cache = React.useMemo(
|
||||
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[getState, navigation, setOptions, router, emitter, parentNavigation]
|
||||
[getState, navigation, setOptions, router, emitter]
|
||||
);
|
||||
|
||||
const actions = {
|
||||
@@ -63,7 +60,7 @@ export default function useNavigationCache<
|
||||
};
|
||||
|
||||
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(
|
||||
(acc, route, index) => {
|
||||
(acc, route) => {
|
||||
const previous = cache.current[route.key];
|
||||
|
||||
if (previous) {
|
||||
@@ -99,18 +96,16 @@ export default function useNavigationCache<
|
||||
...rest,
|
||||
...helpers,
|
||||
...emitter.create(route.key),
|
||||
dangerouslyGetParent: () => parentNavigation as any,
|
||||
dangerouslyGetState: getState,
|
||||
dispatch,
|
||||
setOptions: (options: object) =>
|
||||
setOptions(o => ({
|
||||
setOptions((o) => ({
|
||||
...o,
|
||||
[route.key]: { ...o[route.key], ...options },
|
||||
})),
|
||||
isFocused: () => {
|
||||
const state = getState();
|
||||
|
||||
if (index !== state.index) {
|
||||
if (state.routes[state.index].key !== route.key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,18 +36,45 @@ export default function useNavigationHelpers<
|
||||
const parentNavigationHelpers = React.useContext(NavigationContext);
|
||||
|
||||
return React.useMemo(() => {
|
||||
const dispatch = (action: Action | ((state: State) => Action)) => {
|
||||
const payload =
|
||||
typeof action === 'function' ? action(getState()) : action;
|
||||
const dispatch = (op: Action | ((state: State) => Action)) => {
|
||||
const action = typeof op === 'function' ? op(getState()) : op;
|
||||
|
||||
const handled = onAction(payload);
|
||||
const handled = onAction(action);
|
||||
|
||||
if (!handled && process.env.NODE_ENV !== 'production') {
|
||||
console.error(
|
||||
`The action '${payload.type}' with payload '${JSON.stringify(
|
||||
payload.payload
|
||||
)}' was not handled by any navigator. If you are trying to navigate to a screen, check if the screen exists in your navigator. If you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/en/nesting-navigators.html#navigating-to-a-screen-in-a-nested-navigator.`
|
||||
);
|
||||
const payload: Record<string, any> | undefined = action.payload;
|
||||
|
||||
let message = `The action '${action.type}'${
|
||||
payload ? ` with payload ${JSON.stringify(action.payload)}` : ''
|
||||
} was not handled by any navigator.`;
|
||||
|
||||
switch (action.type) {
|
||||
case 'NAVIGATE':
|
||||
case 'PUSH':
|
||||
case 'REPLACE':
|
||||
case 'JUMP_TO':
|
||||
if (payload?.name) {
|
||||
message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`;
|
||||
} else {
|
||||
message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'GO_BACK':
|
||||
case 'POP':
|
||||
case 'POP_TO_TOP':
|
||||
message += `\n\nIs there any screen to go back to?`;
|
||||
break;
|
||||
case 'OPEN_DRAWER':
|
||||
case 'CLOSE_DRAWER':
|
||||
case 'TOGGLE_DRAWER':
|
||||
message += `\n\nIs your screen inside a Drawer navigator?`;
|
||||
break;
|
||||
}
|
||||
|
||||
message += `\n\nThis is a development-only warning and won't be shown in production.`;
|
||||
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,6 +112,8 @@ export default function useNavigationHelpers<
|
||||
false
|
||||
);
|
||||
},
|
||||
dangerouslyGetParent: () => parentNavigationHelpers as any,
|
||||
dangerouslyGetState: getState,
|
||||
} as NavigationHelpers<ParamListBase, EventMap> &
|
||||
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
|
||||
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function useNavigationState<T>(selector: Selector<T>): T {
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('state', e => {
|
||||
const unsubscribe = navigation.addListener('state', (e) => {
|
||||
setResult(selectorRef.current(e.data.state));
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function useOnGetState({
|
||||
const state = getState();
|
||||
return {
|
||||
...state,
|
||||
routes: state.routes.map(route => ({
|
||||
routes: state.routes.map((route) => ({
|
||||
...route,
|
||||
state: getStateForRoute(route.key),
|
||||
})),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import shortid from 'shortid';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { SingleNavigatorContext } from './EnsureSingleNavigator';
|
||||
|
||||
/**
|
||||
@@ -7,7 +7,7 @@ import { SingleNavigatorContext } from './EnsureSingleNavigator';
|
||||
* This is used to prevent multiple navigators under a single container or screen.
|
||||
*/
|
||||
export default function useRegisterNavigator() {
|
||||
const [key] = React.useState(() => shortid());
|
||||
const [key] = React.useState(() => nanoid());
|
||||
const container = React.useContext(SingleNavigatorContext);
|
||||
|
||||
if (container === undefined) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function useRoute<
|
||||
|
||||
if (route === undefined) {
|
||||
throw new Error(
|
||||
"We couldn't find a route object. Is your component inside a screen in a navigator?"
|
||||
"Couldn't find a route object. Is your component inside a screen in a navigator?"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user