Compare commits
74 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4c1b9767c | ||
|
|
9fb4202597 | ||
|
|
ae484782a6 | ||
|
|
12398ce98c | ||
|
|
24c0753107 | ||
|
|
f274058b90 | ||
|
|
976178d098 | ||
|
|
493956ef71 | ||
|
|
699ea0cc50 | ||
|
|
a63f9da8c1 | ||
|
|
cceaa6780d | ||
|
|
4b8155386b | ||
|
|
1a757fc30a | ||
|
|
7b353a4aea | ||
|
|
3728390b60 | ||
|
|
a8342aaf3d | ||
|
|
860adbfd8b | ||
|
|
38d680833e | ||
|
|
cae115fc17 | ||
|
|
87b51476d0 | ||
|
|
b1b211855f | ||
|
|
60fe0dbb0a | ||
|
|
bb294b16f9 | ||
|
|
4ca2d2d22b | ||
|
|
35747a6066 | ||
|
|
bae4019995 | ||
|
|
d3a9639060 | ||
|
|
d88cbcb52d | ||
|
|
dc7e876b6f | ||
|
|
1e215614d8 | ||
|
|
dd87fa49a4 | ||
|
|
09f0ebbb0f | ||
|
|
9633c4d35f | ||
|
|
28fac3e0b9 | ||
|
|
a8b8c27174 | ||
|
|
b19f76bfff | ||
|
|
365a2ad28c | ||
|
|
b26b90706f | ||
|
|
47f28558d6 | ||
|
|
26074a28f7 | ||
|
|
6fe1d70c6c | ||
|
|
77fa6fb683 | ||
|
|
2ad61a6735 | ||
|
|
c9a5d45324 | ||
|
|
3c874191ff | ||
|
|
2317633652 | ||
|
|
74d368eb4d | ||
|
|
d617ab82f9 | ||
|
|
f5fd0e5be4 | ||
|
|
7bef138e3d | ||
|
|
1406eb83ed | ||
|
|
3e069b718d | ||
|
|
7754eb450f | ||
|
|
95b2599877 | ||
|
|
efcfa7121f | ||
|
|
a8e27ef448 | ||
|
|
946d2923d7 | ||
|
|
794339eeed | ||
|
|
53141a6436 | ||
|
|
a2337648bf | ||
|
|
8f764d8b08 | ||
|
|
f8e998b10c | ||
|
|
da35085f1e | ||
|
|
1f5fb5481a | ||
|
|
18bbd177d9 | ||
|
|
151055cf5a | ||
|
|
52172453df | ||
|
|
7bc385e4f3 | ||
|
|
6ac4d40140 | ||
|
|
dbe961ba5b | ||
|
|
05d4e4d3be | ||
|
|
48b2e77730 | ||
|
|
e08c91ff0a | ||
|
|
5bd682f0bf |
@@ -8,6 +8,12 @@ executors:
|
||||
environment:
|
||||
YARN_CACHE_FOLDER: "~/.cache/yarn"
|
||||
|
||||
playwright:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:bionic
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
|
||||
commands:
|
||||
attach_project:
|
||||
steps:
|
||||
@@ -61,18 +67,9 @@ jobs:
|
||||
destination: coverage
|
||||
|
||||
integration-tests:
|
||||
executor: default
|
||||
executor: playwright
|
||||
steps:
|
||||
- attach_project
|
||||
- run:
|
||||
name: Install Headless Chrome dependencies
|
||||
command: |
|
||||
sudo apt-get install -yq \
|
||||
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
||||
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
|
||||
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
|
||||
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
- run:
|
||||
name: Build example for web
|
||||
command: yarn example expo build:web --no-pwa
|
||||
|
||||
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
github: react-navigation
|
||||
38
.github/ISSUE_TEMPLATE/bug-report-bottom-tabs.md
vendored
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bottom Tab Navigator
|
||||
about: Report an issue with Bottom Tab Navigator (@react-navigation/bottom-tabs)
|
||||
title: ''
|
||||
labels: bug, package:bottom-tabs
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| ----------------------------- | ------- |
|
||||
| iOS or Android |
|
||||
| @react-navigation/native |
|
||||
| @react-navigation/bottom-tabs |
|
||||
| react-native-screens |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
41
.github/ISSUE_TEMPLATE/bug-report-drawer.md
vendored
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Drawer Navigator
|
||||
about: Report an issue with Drawer Navigator (@react-navigation/drawer)
|
||||
title: ''
|
||||
labels: bug, package:drawer
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| ------------------------------ | ------- |
|
||||
| iOS or Android |
|
||||
| @react-navigation/native |
|
||||
| @react-navigation/drawer |
|
||||
| react-native-reanimated |
|
||||
| react-native-gesture-handler |
|
||||
| react-native-safe-area-context |
|
||||
| react-native-screens |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Material Bottom Tab Navigator
|
||||
about: Report an issue with Material Bottom Tab Navigator (@react-navigation/material-bottom-tabs)
|
||||
title: ''
|
||||
labels: bug, package:material-bottom-tabs
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| -------------------------------------- | ------- |
|
||||
| iOS or Android |
|
||||
| @react-navigation/native |
|
||||
| @react-navigation/material-bottom-tabs |
|
||||
| react-native-paper |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Material Top Tab Navigator
|
||||
about: Report an issue with Material Top Tab Navigator (@react-navigation/material-top-tabs)
|
||||
title: ''
|
||||
labels: bug, package:material-top-tabs
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| ----------------------------------- | ------- |
|
||||
| iOS or Android |
|
||||
| @react-navigation/native |
|
||||
| @react-navigation/material-top-tabs |
|
||||
| react-native-tab-view |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
40
.github/ISSUE_TEMPLATE/bug-report-stack.md
vendored
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: Stack Navigator
|
||||
about: Report an issue with Stack Navigator (@react-navigation/stack)
|
||||
title: ''
|
||||
labels: bug, package:stack
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| ------------------------------ | ------- |
|
||||
| iOS or Android |
|
||||
| @react-navigation/native |
|
||||
| @react-navigation/stack |
|
||||
| react-native-gesture-handler |
|
||||
| react-native-safe-area-context |
|
||||
| react-native-screens |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
43
.github/ISSUE_TEMPLATE/bug-report-version-4.md
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: React Navigation 4
|
||||
about: Report an issue with React Navigation 4
|
||||
title: ''
|
||||
labels: bug, version-4
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| ------------------------------ | ------- |
|
||||
| iOS or Android |
|
||||
| react-navigation |
|
||||
| react-navigation-stack |
|
||||
| react-navigation-tabs |
|
||||
| react-navigation-drawer |
|
||||
| react-native-reanimated |
|
||||
| react-native-gesture-handler |
|
||||
| react-native-safe-area-context |
|
||||
| react-native-screens |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
36
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Other bugs
|
||||
about: Report an issue which is not about a specific navigator.
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
- What code are you running and what is happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
- What do you expect should be happening?
|
||||
- Include a screenshot or video if it makes sense.
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
|
||||
- Before reporting an issue, make sure you are on latest version of the package.
|
||||
|
||||
**Your Environment**
|
||||
|
||||
| software | version |
|
||||
| ------------------------------ | ------- |
|
||||
| iOS or Android |
|
||||
| @react-navigation/native |
|
||||
| react-native |
|
||||
| expo |
|
||||
| node |
|
||||
| npm or yarn |
|
||||
20
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,20 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Troubleshooting
|
||||
url: https://reactnavigation.org/docs/troubleshooting.html
|
||||
about: Read how to troubleshoot and fix common issues and mistakes.
|
||||
- name: Documentation
|
||||
url: https://reactnavigation.org
|
||||
about: Read the official documentation.
|
||||
- name: Feature requests
|
||||
url: https://react-navigation.canny.io/feature-requests
|
||||
about: Post a feature request on Canny.
|
||||
- name: StackOverflow
|
||||
url: https://stackoverflow.com/questions/tagged/react-navigation
|
||||
about: Ask and answer questions using the react-navigation label.
|
||||
- name: Reactiflux
|
||||
url: https://www.reactiflux.com/
|
||||
about: Chat with other community members in the react-navigation channel.
|
||||
- name: Write an RFC
|
||||
url: https://github.com/react-navigation/rfcs
|
||||
about: Write a RFC if you have ideas for how to implement a feature request.
|
||||
17
.github/PULL_REQUEST.md
vendored
@@ -1,17 +0,0 @@
|
||||
Please provide enough information so that others can review your pull request:
|
||||
|
||||
**Motivation**
|
||||
|
||||
Explain the **motivation** for making this change. What existing problem does the pull request solve?
|
||||
|
||||
**Test plan**
|
||||
|
||||
Demonstrate the code is solid. Example: the exact commands you ran and their output, screenshots / videos if the pull request changes UI.
|
||||
|
||||
Make sure you test on both platforms if your change affects both platforms.
|
||||
|
||||
The code must pass tests.
|
||||
|
||||
**Code formatting**
|
||||
|
||||
Look around. Match the style of the rest of the codebase. Run `yarn lint --fix` before committing.
|
||||
69
.github/workflows/expo-preview.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: Expo Preview
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Install and publish
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.owner.login == 'react-navigation'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@v5
|
||||
with:
|
||||
expo-version: 3.x
|
||||
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||
expo-cache: true
|
||||
|
||||
- name: Restore yarn cache
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Publish Expo app
|
||||
working-directory: ./example
|
||||
run: expo publish --release-channel=pr-${{ github.event.number }}
|
||||
env:
|
||||
EXPO_USE_DEV_SERVER: true
|
||||
|
||||
- name: Get expo link
|
||||
id: expo
|
||||
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
|
||||
|
||||
- name: Comment on PR
|
||||
uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const body = 'The Expo app for the example from this branch is ready!\n\n[expo.io/${{ steps.expo.outputs.path }}](https://expo.io/${{ steps.expo.outputs.path }})\n\n<a href="https://exp.host/${{ steps.expo.outputs.path }}"><img src="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=exp://exp.host/${{ steps.expo.outputs.path }}" height="200px" width="200px"></a>';
|
||||
|
||||
const comments = await github.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
if (comments.data.some(comment => comment.body === body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body
|
||||
})
|
||||
42
.github/workflows/expo.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Expo Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Install and publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@v5
|
||||
with:
|
||||
expo-version: 3.x
|
||||
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||
expo-cache: true
|
||||
|
||||
- name: Restore yarn cache
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Publish Expo app
|
||||
working-directory: ./example
|
||||
run: expo publish
|
||||
28
.github/workflows/rebase.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Automatic Rebase
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# https://github.community/t5/GitHub-Actions/Workflow-is-failing-if-no-job-can-be-ran-due-to-condition/m-p/38186#M3250
|
||||
always_job:
|
||||
name: Always run job
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Always run
|
||||
run: echo "This job is used to prevent the workflow to fail when all other jobs are skipped."
|
||||
125
.github/workflows/triage.yml
vendored
@@ -1,125 +0,0 @@
|
||||
name: Triage
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
needs-more-info:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'needs more info'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "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.\n\nMake 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.). In addition, if you can provide a video or GIF demonstrating the issue, it will also be very helpful."
|
||||
})
|
||||
|
||||
needs-repro:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'needs repro'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible.\n\nThe easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then please provide the repro in a GitHub repository."
|
||||
})
|
||||
|
||||
question:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'question'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "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 or an issue unrelated to this library. 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.\n\nIf you believe that this is actually a bug in the library, please open a new issue and fill the issue template with relevant information."
|
||||
})
|
||||
|
||||
feature-request:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'feature-request'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "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.\n\nYou can also open a detailed proposal in our [RFC repo](https://github.com/react-navigation/rfcs) for discussion."
|
||||
})
|
||||
|
||||
react-native-screens:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'library:react-native-screens'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-screens` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-screens) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
|
||||
})
|
||||
|
||||
react-native-reanimated:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'library:react-native-reanimated'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-reanimated` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-reanimated) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
|
||||
})
|
||||
|
||||
react-native-gesture-handler:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'library:react-native-gesture-handler'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-gesture-handler` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-gesture-handler) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
|
||||
})
|
||||
|
||||
react-native-safe-area-context:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'library:react-native-safe-area-context'
|
||||
steps:
|
||||
- uses: actions/github-script@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-safe-area-context` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/th3rdwave/react-native-safe-area-context) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
|
||||
})
|
||||
38
.github/workflows/versions.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Check versions
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
check-versions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: react-navigation/check-versions-action@v1.0.0
|
||||
if: contains(github.event.issue.labels.*.name, 'version-4') != true
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
required-packages: |
|
||||
@react-navigation/native
|
||||
optional-packages: |
|
||||
@react-navigation/bottom-tabs
|
||||
@react-navigation/compat
|
||||
@react-navigation/core
|
||||
@react-navigation/devtools
|
||||
@react-navigation/drawer
|
||||
@react-navigation/material-bottom-tabs
|
||||
@react-navigation/material-top-tabs
|
||||
@react-navigation/routers
|
||||
@react-navigation/stack
|
||||
|
||||
- uses: react-navigation/check-versions-action@v1.0.0
|
||||
if: contains(github.event.issue.labels.*.name, 'version-4')
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
required-packages: |
|
||||
react-navigation
|
||||
optional-packages: |
|
||||
react-navigation-animated-switch
|
||||
react-navigation-drawer
|
||||
react-navigation-material-bottom-tabs
|
||||
react-navigation-stack
|
||||
react-navigation-tabs
|
||||
@@ -8,12 +8,15 @@ const blacklist = require('metro-config/src/defaults/blacklist');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const packages = path.resolve(root, 'packages');
|
||||
|
||||
// List all packages under `packages/`
|
||||
const workspaces = fs
|
||||
// List all packages under `packages/`
|
||||
.readdirSync(packages)
|
||||
// Ignore hidden files such as .DS_Store
|
||||
.filter((p) => !p.startsWith('.'))
|
||||
.map((p) => path.join(packages, p));
|
||||
.map((p) => path.join(packages, p))
|
||||
.filter(
|
||||
(p) =>
|
||||
fs.statSync(p).isDirectory() &&
|
||||
fs.existsSync(path.join(p, 'package.json'))
|
||||
);
|
||||
|
||||
// Get the list of dependencies for all packages in the monorepo
|
||||
const modules = ['@expo/vector-icons']
|
||||
@@ -68,14 +71,9 @@ module.exports = {
|
||||
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
|
||||
const assets = '/packages/stack/src/views/assets';
|
||||
|
||||
if (req.url.startsWith(assets)) {
|
||||
req.url = req.url.replace(
|
||||
assets,
|
||||
'/assets/../packages/stack/src/views/assets'
|
||||
);
|
||||
// So we fix the path to correct one
|
||||
if (/\/packages\/.+\.png\?.+$/.test(req.url)) {
|
||||
req.url = `/assets/../${req.url}`;
|
||||
}
|
||||
|
||||
return middleware(req, res, next);
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"mock-require-assets": "^0.0.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"playwright": "^0.14.0",
|
||||
"playwright": "^1.9.1",
|
||||
"serve": "^11.3.0",
|
||||
"typescript": "^4.0.3"
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
Linking,
|
||||
LogBox,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import {
|
||||
Provider as PaperProvider,
|
||||
DefaultTheme as PaperLightTheme,
|
||||
DarkTheme as PaperDarkTheme,
|
||||
Appbar,
|
||||
List,
|
||||
Divider,
|
||||
Text,
|
||||
@@ -28,10 +28,7 @@ import {
|
||||
PathConfigMap,
|
||||
NavigationContainerRef,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createDrawerNavigator,
|
||||
DrawerScreenProps,
|
||||
} from '@react-navigation/drawer';
|
||||
import { createDrawerNavigator } from '@react-navigation/drawer';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackScreenProps,
|
||||
@@ -65,8 +62,7 @@ if (Platform.OS !== 'web') {
|
||||
enableScreens();
|
||||
|
||||
type RootDrawerParamList = {
|
||||
Root: undefined;
|
||||
Another: undefined;
|
||||
Examples: undefined;
|
||||
};
|
||||
|
||||
const SCREENS = {
|
||||
@@ -231,50 +227,49 @@ export default function App() {
|
||||
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
||||
prefixes: LinkingPrefixes,
|
||||
config: {
|
||||
screens: {
|
||||
Root: {
|
||||
path: '',
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<PathConfigMap>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
const path = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<PathConfigMap>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
const path = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
|
||||
acc[name] = {
|
||||
path,
|
||||
screens: {
|
||||
Article: {
|
||||
path: 'article/:author?',
|
||||
parse: {
|
||||
author: (author) =>
|
||||
author.charAt(0).toUpperCase() +
|
||||
author.slice(1).replace(/-/g, ' '),
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.toLowerCase().replace(/\s/g, '-'),
|
||||
},
|
||||
},
|
||||
Albums: 'music',
|
||||
Chat: 'chat',
|
||||
Contacts: 'people',
|
||||
NewsFeed: 'feed',
|
||||
Dialog: 'dialog',
|
||||
acc[name] = {
|
||||
path,
|
||||
screens: {
|
||||
Article: {
|
||||
path: 'article/:author?',
|
||||
parse: {
|
||||
author: (author) =>
|
||||
author.charAt(0).toUpperCase() +
|
||||
author.slice(1).replace(/-/g, ' '),
|
||||
},
|
||||
};
|
||||
|
||||
return acc;
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.toLowerCase().replace(/\s/g, '-'),
|
||||
},
|
||||
},
|
||||
Albums: 'music',
|
||||
Chat: 'chat',
|
||||
Contacts: 'people',
|
||||
NewsFeed: 'feed',
|
||||
Dialog: 'dialog',
|
||||
},
|
||||
{
|
||||
Home: '',
|
||||
NotFound: '*',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
},
|
||||
{
|
||||
Home: {
|
||||
screens: {
|
||||
Examples: '',
|
||||
},
|
||||
},
|
||||
NotFound: '*',
|
||||
}
|
||||
),
|
||||
},
|
||||
}}
|
||||
fallback={<Text>Loading…</Text>}
|
||||
@@ -283,95 +278,91 @@ export default function App() {
|
||||
`${options?.title ?? route?.name} - React Navigation Example`,
|
||||
}}
|
||||
>
|
||||
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||
<Drawer.Screen
|
||||
name="Root"
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Home"
|
||||
options={{
|
||||
title: 'Examples',
|
||||
drawerIcon: ({ size, color }) => (
|
||||
<MaterialIcons size={size} color={color} name="folder" />
|
||||
),
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
{({ navigation }: DrawerScreenProps<RootDrawerParamList>) => (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||
}}
|
||||
{() => (
|
||||
<Drawer.Navigator
|
||||
drawerType={isLargeScreen ? 'permanent' : undefined}
|
||||
screenOptions={{ headerShown: true }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Home"
|
||||
<Drawer.Screen
|
||||
name="Examples"
|
||||
options={{
|
||||
title: 'Examples',
|
||||
headerLeft: isLargeScreen
|
||||
? undefined
|
||||
: () => (
|
||||
<Appbar.Action
|
||||
color={theme.colors.text}
|
||||
icon="menu"
|
||||
onPress={() => navigation.toggleDrawer()}
|
||||
/>
|
||||
),
|
||||
drawerIcon: ({ size, color }) => (
|
||||
<MaterialIcons size={size} color={color} name="folder" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{({ navigation }: StackScreenProps<RootStackParamList>) => (
|
||||
<ScrollView
|
||||
style={{ backgroundColor: theme.colors.background }}
|
||||
>
|
||||
<SettingsItem
|
||||
label="Right to left"
|
||||
value={I18nManager.isRTL}
|
||||
onValueChange={() => {
|
||||
I18nManager.forceRTL(!I18nManager.isRTL);
|
||||
restartApp();
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<SettingsItem
|
||||
label="Dark theme"
|
||||
value={theme.dark}
|
||||
onValueChange={() => {
|
||||
AsyncStorage.setItem(
|
||||
THEME_PERSISTENCE_KEY,
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
<SafeAreaView edges={['right', 'bottom', 'left']}>
|
||||
<SettingsItem
|
||||
label="Right to left"
|
||||
value={I18nManager.isRTL}
|
||||
onValueChange={() => {
|
||||
I18nManager.forceRTL(!I18nManager.isRTL);
|
||||
restartApp();
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<SettingsItem
|
||||
label="Dark theme"
|
||||
value={theme.dark}
|
||||
onValueChange={() => {
|
||||
AsyncStorage.setItem(
|
||||
THEME_PERSISTENCE_KEY,
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
|
||||
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme));
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
(name) => (
|
||||
<List.Item
|
||||
key={name}
|
||||
testID={name}
|
||||
title={SCREENS[name].title}
|
||||
onPress={() => navigation.navigate(name)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
setTheme((t) =>
|
||||
t.dark ? DefaultTheme : DarkTheme
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
(name) => (
|
||||
<List.Item
|
||||
key={name}
|
||||
testID={name}
|
||||
title={SCREENS[name].title}
|
||||
onPress={() => navigation.navigate(name)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name="NotFound"
|
||||
component={NotFound}
|
||||
options={{ title: 'Oops!' }}
|
||||
/>
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
(name) => (
|
||||
<Stack.Screen
|
||||
key={name}
|
||||
name={name}
|
||||
getComponent={() => SCREENS[name].component}
|
||||
options={{ title: SCREENS[name].title }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack.Navigator>
|
||||
</Drawer.Screen>
|
||||
</Drawer.Navigator>
|
||||
)}
|
||||
</Drawer.Screen>
|
||||
</Drawer.Navigator>
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name="NotFound"
|
||||
component={NotFound}
|
||||
options={{ title: 'Oops!' }}
|
||||
/>
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map((name) => (
|
||||
<Stack.Screen
|
||||
key={name}
|
||||
name={name}
|
||||
getComponent={() => SCREENS[name].component}
|
||||
options={{ title: SCREENS[name].title }}
|
||||
/>
|
||||
))}
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"version": "independent",
|
||||
"command": {
|
||||
"publish": {
|
||||
"allowBranch": "main",
|
||||
"allowBranch": "5.x",
|
||||
"conventionalCommits": true,
|
||||
"createRelease": "github",
|
||||
"message": "chore: publish",
|
||||
|
||||
@@ -3,6 +3,117 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.11.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.9...@react-navigation/bottom-tabs@5.11.10) (2021-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update tab bar height correctly. fixes [#9296](https://github.com/react-navigation/react-navigation/issues/9296) ([12398ce](https://github.com/react-navigation/react-navigation/commit/12398ce98c28a1b5710a144e6138b91031bffa91))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.8...@react-navigation/bottom-tabs@5.11.9) (2021-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check for screens enabled in ScreenContainer ([493956e](https://github.com/react-navigation/react-navigation/commit/493956ef717a03bd8c3533a2949434e83718c5e4))
|
||||
* don't pass accessibilityState to link. closes [#9418](https://github.com/react-navigation/react-navigation/issues/9418) ([699ea0c](https://github.com/react-navigation/react-navigation/commit/699ea0cc5052f190acc7ce8bc0328bb052d7cf26))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.7...@react-navigation/bottom-tabs@5.11.8) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.6...@react-navigation/bottom-tabs@5.11.7) (2021-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix drawer screen content not being interactable on Android ([87b5147](https://github.com/react-navigation/react-navigation/commit/87b51476d0bce8f2dae793416c2976da30a1a5f7))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.5...@react-navigation/bottom-tabs@5.11.6) (2021-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix pointerEvents in ResourceSavingScene ([60fe0db](https://github.com/react-navigation/react-navigation/commit/60fe0dbb0ae443fdb21016d368c919b933cb64e7)), closes [#9241](https://github.com/react-navigation/react-navigation/issues/9241) [#9242](https://github.com/react-navigation/react-navigation/issues/9242)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.4...@react-navigation/bottom-tabs@5.11.5) (2021-01-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.3...@react-navigation/bottom-tabs@5.11.4) (2021-01-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix drawer and bottom tabs not being visible on web. closes [#9225](https://github.com/react-navigation/react-navigation/issues/9225) ([d88cbcb](https://github.com/react-navigation/react-navigation/commit/d88cbcb52d46de26edaa9ce6bfb06badb1b1de64))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.2...@react-navigation/bottom-tabs@5.11.3) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable detachInactiveScreens by default on web for better a11y ([dd87fa4](https://github.com/react-navigation/react-navigation/commit/dd87fa49a43ad8db105a62418243339e4150fadf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.1...@react-navigation/bottom-tabs@5.11.2) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.0...@react-navigation/bottom-tabs@5.11.1) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.7...@react-navigation/bottom-tabs@5.11.0) (2020-11-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a hook to get bottom tab bar height ([e08c91f](https://github.com/react-navigation/react-navigation/commit/e08c91ff0a3df13dc6e6096a3e95f60722e6946b)), closes [#8037](https://github.com/react-navigation/react-navigation/issues/8037) [#8536](https://github.com/react-navigation/react-navigation/issues/8536)
|
||||
* add a tabBarBadgeStyle option to customize the badge ([6ac4d40](https://github.com/react-navigation/react-navigation/commit/6ac4d40140189a29d857c4d1203bced6929f7baf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.10.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.6...@react-navigation/bottom-tabs@5.10.7) (2020-11-08)
|
||||
|
||||
**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.10.7",
|
||||
"version": "5.11.10",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -40,8 +40,7 @@
|
||||
"react-native-iphone-x-helper": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.53",
|
||||
@@ -49,6 +48,7 @@
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"react-native-safe-area-context": "3.1.4",
|
||||
"react-native-screens": "~2.10.1",
|
||||
"typescript": "^4.0.3"
|
||||
@@ -60,7 +60,7 @@
|
||||
"react-native-safe-area-context": ">= 0.6.0",
|
||||
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -9,6 +9,13 @@ export { default as createBottomTabNavigator } from './navigators/createBottomTa
|
||||
export { default as BottomTabView } from './views/BottomTabView';
|
||||
export { default as BottomTabBar } from './views/BottomTabBar';
|
||||
|
||||
/**
|
||||
* Utilities
|
||||
*/
|
||||
export { default as BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
|
||||
|
||||
export { default as useBottomTabBarHeight } from './utils/useBottomTabBarHeight';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
|
||||
@@ -109,6 +109,12 @@ export type BottomTabNavigationOptions = {
|
||||
*/
|
||||
tabBarBadge?: number | string;
|
||||
|
||||
/**
|
||||
* Custom style for the tab bar badge.
|
||||
* You can specify a background color or text color here.
|
||||
*/
|
||||
tabBarBadgeStyle?: StyleProp<TextStyle>;
|
||||
|
||||
/**
|
||||
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
||||
* It's recommended to set this if you don't have a label for the tab.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default React.createContext<((height: number) => void) | undefined>(
|
||||
undefined
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default React.createContext<number | undefined>(undefined);
|
||||
14
packages/bottom-tabs/src/utils/useBottomTabBarHeight.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import BottomTabBarHeightContext from './BottomTabBarHeightContext';
|
||||
|
||||
export default function useFloatingBottomTabBarHeight() {
|
||||
const height = React.useContext(BottomTabBarHeightContext);
|
||||
|
||||
if (height === undefined) {
|
||||
throw new Error(
|
||||
"Couldn't find the bottom tab bar height. Are you inside a screen in Bottom Tab Navigator?"
|
||||
);
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
@@ -5,20 +5,25 @@ import {
|
||||
StyleSheet,
|
||||
Platform,
|
||||
LayoutChangeEvent,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import {
|
||||
NavigationContext,
|
||||
NavigationRouteContext,
|
||||
TabNavigationState,
|
||||
ParamListBase,
|
||||
CommonActions,
|
||||
useTheme,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import { useSafeArea } from 'react-native-safe-area-context';
|
||||
import { useSafeArea, EdgeInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import BottomTabItem from './BottomTabItem';
|
||||
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
|
||||
import useWindowDimensions from '../utils/useWindowDimensions';
|
||||
import useIsKeyboardShown from '../utils/useIsKeyboardShown';
|
||||
import type { BottomTabBarProps } from '../types';
|
||||
import type { BottomTabBarProps, LabelPosition } from '../types';
|
||||
|
||||
type Props = BottomTabBarProps & {
|
||||
activeTintColor?: string;
|
||||
@@ -31,13 +36,93 @@ const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
type Options = {
|
||||
state: TabNavigationState<ParamListBase>;
|
||||
layout: { height: number; width: number };
|
||||
dimensions: { height: number; width: number };
|
||||
tabStyle: StyleProp<ViewStyle>;
|
||||
labelPosition: LabelPosition | undefined;
|
||||
adaptive: boolean | undefined;
|
||||
};
|
||||
|
||||
const shouldUseHorizontalLabels = ({
|
||||
state,
|
||||
layout,
|
||||
dimensions,
|
||||
adaptive = true,
|
||||
labelPosition,
|
||||
tabStyle,
|
||||
}: Options) => {
|
||||
if (labelPosition) {
|
||||
return labelPosition === 'beside-icon';
|
||||
}
|
||||
|
||||
if (!adaptive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layout.width >= 768) {
|
||||
// Screen size matches a tablet
|
||||
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
|
||||
|
||||
const flattenedStyle = StyleSheet.flatten(tabStyle);
|
||||
|
||||
if (flattenedStyle) {
|
||||
if (typeof flattenedStyle.width === 'number') {
|
||||
maxTabItemWidth = flattenedStyle.width;
|
||||
} else if (typeof flattenedStyle.maxWidth === 'number') {
|
||||
maxTabItemWidth = flattenedStyle.maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return state.routes.length * maxTabItemWidth <= layout.width;
|
||||
} else {
|
||||
return dimensions.width > dimensions.height;
|
||||
}
|
||||
};
|
||||
|
||||
const getPaddingBottom = (insets: EdgeInsets) =>
|
||||
Math.max(insets.bottom - Platform.select({ ios: 4, default: 0 }), 0);
|
||||
|
||||
export const getTabBarHeight = ({
|
||||
dimensions,
|
||||
insets,
|
||||
style,
|
||||
...rest
|
||||
}: Options & {
|
||||
insets: EdgeInsets;
|
||||
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
}) => {
|
||||
// @ts-ignore
|
||||
const customHeight = StyleSheet.flatten(style)?.height;
|
||||
|
||||
if (typeof customHeight === 'number') {
|
||||
return customHeight;
|
||||
}
|
||||
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
const horizontalLabels = shouldUseHorizontalLabels({ dimensions, ...rest });
|
||||
const paddingBottom = getPaddingBottom(insets);
|
||||
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
!Platform.isPad &&
|
||||
isLandscape &&
|
||||
horizontalLabels
|
||||
) {
|
||||
return COMPACT_TABBAR_HEIGHT + paddingBottom;
|
||||
}
|
||||
|
||||
return DEFAULT_TABBAR_HEIGHT + paddingBottom;
|
||||
};
|
||||
|
||||
export default function BottomTabBar({
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
activeBackgroundColor,
|
||||
activeTintColor,
|
||||
adaptive = true,
|
||||
adaptive,
|
||||
allowFontScaling,
|
||||
inactiveBackgroundColor,
|
||||
inactiveTintColor,
|
||||
@@ -60,6 +145,8 @@ export default function BottomTabBar({
|
||||
const dimensions = useWindowDimensions();
|
||||
const isKeyboardShown = useIsKeyboardShown();
|
||||
|
||||
const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
|
||||
|
||||
const shouldShowTabBar =
|
||||
focusedOptions.tabBarVisible !== false &&
|
||||
!(keyboardHidesTabBar && isKeyboardShown);
|
||||
@@ -120,11 +207,19 @@ export default function BottomTabBar({
|
||||
width: dimensions.width,
|
||||
});
|
||||
|
||||
const isLandscape = () => dimensions.width > dimensions.height;
|
||||
|
||||
const handleLayout = (e: LayoutChangeEvent) => {
|
||||
const { height, width } = e.nativeEvent.layout;
|
||||
|
||||
const topBorderWidth =
|
||||
// @ts-ignore
|
||||
StyleSheet.flatten([styles.tabBar, style])?.borderTopWidth;
|
||||
|
||||
onHeightChange?.(
|
||||
height +
|
||||
paddingBottom +
|
||||
(typeof topBorderWidth === 'number' ? topBorderWidth : 0)
|
||||
);
|
||||
|
||||
setLayout((layout) => {
|
||||
if (height === layout.height && width === layout.width) {
|
||||
return layout;
|
||||
@@ -138,34 +233,6 @@ export default function BottomTabBar({
|
||||
};
|
||||
|
||||
const { routes } = state;
|
||||
const shouldUseHorizontalLabels = () => {
|
||||
if (labelPosition) {
|
||||
return labelPosition === 'beside-icon';
|
||||
}
|
||||
|
||||
if (!adaptive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layout.width >= 768) {
|
||||
// Screen size matches a tablet
|
||||
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
|
||||
|
||||
const flattenedStyle = StyleSheet.flatten(tabStyle);
|
||||
|
||||
if (flattenedStyle) {
|
||||
if (typeof flattenedStyle.width === 'number') {
|
||||
maxTabItemWidth = flattenedStyle.width;
|
||||
} else if (typeof flattenedStyle.maxWidth === 'number') {
|
||||
maxTabItemWidth = flattenedStyle.maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return routes.length * maxTabItemWidth <= layout.width;
|
||||
} else {
|
||||
return isLandscape();
|
||||
}
|
||||
};
|
||||
|
||||
const defaultInsets = useSafeArea();
|
||||
|
||||
@@ -176,22 +243,26 @@ export default function BottomTabBar({
|
||||
left: safeAreaInsets?.left ?? defaultInsets.left,
|
||||
};
|
||||
|
||||
const paddingBottom = Math.max(
|
||||
insets.bottom - Platform.select({ ios: 4, default: 0 }),
|
||||
0
|
||||
);
|
||||
const paddingBottom = getPaddingBottom(insets);
|
||||
const tabBarHeight = getTabBarHeight({
|
||||
state,
|
||||
insets,
|
||||
dimensions,
|
||||
layout,
|
||||
adaptive,
|
||||
labelPosition,
|
||||
tabStyle,
|
||||
style,
|
||||
});
|
||||
|
||||
const getDefaultTabBarHeight = () => {
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
!Platform.isPad &&
|
||||
isLandscape() &&
|
||||
shouldUseHorizontalLabels()
|
||||
) {
|
||||
return COMPACT_TABBAR_HEIGHT;
|
||||
}
|
||||
return DEFAULT_TABBAR_HEIGHT;
|
||||
};
|
||||
const hasHorizontalLabels = shouldUseHorizontalLabels({
|
||||
state,
|
||||
dimensions,
|
||||
layout,
|
||||
adaptive,
|
||||
labelPosition,
|
||||
tabStyle,
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
@@ -218,15 +289,16 @@ export default function BottomTabBar({
|
||||
position: isTabBarHidden ? 'absolute' : (null as any),
|
||||
},
|
||||
{
|
||||
height: getDefaultTabBarHeight() + paddingBottom,
|
||||
height: tabBarHeight,
|
||||
paddingBottom,
|
||||
paddingHorizontal: Math.max(insets.left, insets.right),
|
||||
},
|
||||
style,
|
||||
]}
|
||||
pointerEvents={isTabBarHidden ? 'none' : 'auto'}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
<View style={styles.content} onLayout={handleLayout}>
|
||||
<View style={styles.content}>
|
||||
{routes.map((route, index) => {
|
||||
const focused = index === state.index;
|
||||
const { options } = descriptors[route.key];
|
||||
@@ -276,7 +348,7 @@ export default function BottomTabBar({
|
||||
<BottomTabItem
|
||||
route={route}
|
||||
focused={focused}
|
||||
horizontal={shouldUseHorizontalLabels()}
|
||||
horizontal={hasHorizontalLabels}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
@@ -290,6 +362,7 @@ export default function BottomTabBar({
|
||||
button={options.tabBarButton}
|
||||
icon={options.tabBarIcon}
|
||||
badge={options.tabBarBadge}
|
||||
badgeStyle={options.tabBarBadgeStyle}
|
||||
label={label}
|
||||
showLabel={showLabel}
|
||||
labelStyle={labelStyle}
|
||||
|
||||
@@ -47,6 +47,10 @@ type Props = {
|
||||
* Text to show in a badge on the tab icon.
|
||||
*/
|
||||
badge?: number | string;
|
||||
/**
|
||||
* Custom style for the badge.
|
||||
*/
|
||||
badgeStyle?: StyleProp<TextStyle>;
|
||||
/**
|
||||
* URL to use for the link to the tab.
|
||||
*/
|
||||
@@ -122,6 +126,7 @@ export default function BottomTabBarItem({
|
||||
label,
|
||||
icon,
|
||||
badge,
|
||||
badgeStyle,
|
||||
to,
|
||||
button = ({
|
||||
children,
|
||||
@@ -129,6 +134,7 @@ export default function BottomTabBarItem({
|
||||
onPress,
|
||||
to,
|
||||
accessibilityRole,
|
||||
accessibilityState,
|
||||
...rest
|
||||
}: BottomTabBarButtonProps) => {
|
||||
if (Platform.OS === 'web' && to) {
|
||||
@@ -157,6 +163,7 @@ export default function BottomTabBarItem({
|
||||
<TouchableWithoutFeedback
|
||||
{...rest}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityState={accessibilityState}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={style}>{children}</View>
|
||||
@@ -235,6 +242,7 @@ export default function BottomTabBarItem({
|
||||
route={route}
|
||||
horizontal={horizontal}
|
||||
badge={badge}
|
||||
badgeStyle={badgeStyle}
|
||||
activeOpacity={activeOpacity}
|
||||
inactiveOpacity={inactiveOpacity}
|
||||
activeTintColor={activeTintColor}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
@@ -7,11 +13,15 @@ import {
|
||||
TabNavigationState,
|
||||
useTheme,
|
||||
} from '@react-navigation/native';
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import { ScreenContainer, screensEnabled } from 'react-native-screens';
|
||||
|
||||
import SafeAreaProviderCompat from './SafeAreaProviderCompat';
|
||||
import SafeAreaProviderCompat, {
|
||||
initialSafeAreaInsets,
|
||||
} from './SafeAreaProviderCompat';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
import BottomTabBar from './BottomTabBar';
|
||||
import BottomTabBar, { getTabBarHeight } from './BottomTabBar';
|
||||
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
|
||||
import BottomTabBarHeightContext from '../utils/BottomTabBarHeightContext';
|
||||
import type {
|
||||
BottomTabNavigationConfig,
|
||||
BottomTabDescriptorMap,
|
||||
@@ -27,6 +37,7 @@ type Props = BottomTabNavigationConfig & {
|
||||
|
||||
type State = {
|
||||
loaded: string[];
|
||||
tabBarHeight: number;
|
||||
};
|
||||
|
||||
function SceneContent({
|
||||
@@ -67,9 +78,28 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
state: State = {
|
||||
loaded: [this.props.state.routes[this.props.state.index].key],
|
||||
};
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { state, tabBarOptions } = this.props;
|
||||
|
||||
const dimensions = Dimensions.get('window');
|
||||
const tabBarHeight = getTabBarHeight({
|
||||
state,
|
||||
dimensions,
|
||||
layout: { width: dimensions.width, height: 0 },
|
||||
insets: initialSafeAreaInsets,
|
||||
adaptive: tabBarOptions?.adaptive,
|
||||
labelPosition: tabBarOptions?.labelPosition,
|
||||
tabStyle: tabBarOptions?.tabStyle,
|
||||
style: tabBarOptions?.style,
|
||||
});
|
||||
|
||||
this.state = {
|
||||
loaded: [state.routes[state.index].key],
|
||||
tabBarHeight: tabBarHeight,
|
||||
};
|
||||
}
|
||||
|
||||
private renderTabBar = () => {
|
||||
const {
|
||||
@@ -87,6 +117,16 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
private handleTabBarHeightChange = (height: number) => {
|
||||
this.setState((state) => {
|
||||
if (state.tabBarHeight !== height) {
|
||||
return { tabBarHeight: height };
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
state,
|
||||
@@ -97,50 +137,55 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
sceneContainerStyle,
|
||||
} = this.props;
|
||||
const { routes } = state;
|
||||
const { loaded } = this.state;
|
||||
const { loaded, tabBarHeight } = this.state;
|
||||
const isScreensEnabled = screensEnabled?.() && detachInactiveScreens;
|
||||
|
||||
return (
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
<SafeAreaProviderCompat>
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer
|
||||
// @ts-ignore
|
||||
enabled={detachInactiveScreens}
|
||||
style={styles.pages}
|
||||
>
|
||||
{routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { unmountOnBlur } = descriptor.options;
|
||||
const isFocused = state.index === index;
|
||||
<ScreenContainer
|
||||
// @ts-ignore
|
||||
enabled={isScreensEnabled}
|
||||
style={styles.container}
|
||||
>
|
||||
{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(route.key) && !isFocused) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
if (lazy && !loaded.includes(route.key) && !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}
|
||||
enabled={detachInactiveScreens}
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
enabled={isScreensEnabled}
|
||||
>
|
||||
<SceneContent
|
||||
isFocused={isFocused}
|
||||
style={sceneContainerStyle}
|
||||
>
|
||||
<SceneContent
|
||||
isFocused={isFocused}
|
||||
style={sceneContainerStyle}
|
||||
>
|
||||
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
|
||||
{descriptor.render()}
|
||||
</SceneContent>
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
</BottomTabBarHeightContext.Provider>
|
||||
</SceneContent>
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
<BottomTabBarHeightCallbackContext.Provider
|
||||
value={this.handleTabBarHeightChange}
|
||||
>
|
||||
{this.renderTabBar()}
|
||||
</View>
|
||||
</BottomTabBarHeightCallbackContext.Provider>
|
||||
</SafeAreaProviderCompat>
|
||||
</NavigationHelpersContext.Provider>
|
||||
);
|
||||
@@ -152,9 +197,6 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pages: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -16,36 +16,56 @@ type Props = {
|
||||
|
||||
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() {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
const { isVisible, ...rest } = this.props;
|
||||
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen activityState={isVisible ? 2 : 0} {...rest} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen active={isVisible ? 1 : 0} {...rest} />
|
||||
);
|
||||
}
|
||||
export default function ResourceSavingScene({
|
||||
isVisible,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: Props) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen activityState={isVisible ? 2 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen active={isVisible ? 1 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { isVisible, children, style, ...rest } = this.props;
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
return (
|
||||
<View
|
||||
// @ts-expect-error: hidden exists on web, but not in React Native
|
||||
hidden={!isVisible}
|
||||
style={[
|
||||
{ display: isVisible ? 'flex' : 'none' },
|
||||
styles.container,
|
||||
Platform.OS === 'web'
|
||||
? { display: isVisible ? 'flex' : 'none' }
|
||||
: null,
|
||||
style,
|
||||
]}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, style]}
|
||||
// box-none doesn't seem to work properly on Android
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={
|
||||
// On iOS, set removeClippedSubviews to true only when not focused
|
||||
@@ -53,14 +73,12 @@ export default class ResourceSavingScene extends React.Component<Props> {
|
||||
Platform.OS === 'ios' ? !isVisible : true
|
||||
}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
{...rest}
|
||||
style={isVisible ? styles.attached : styles.detached}
|
||||
>
|
||||
<View style={isVisible ? styles.attached : styles.detached}>
|
||||
{children}
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
// The provider component for safe area initializes asynchornously
|
||||
// Until the insets are available, there'll be blank screen
|
||||
// To avoid the blank screen, we specify some initial values
|
||||
const initialSafeAreaInsets = {
|
||||
export const initialSafeAreaInsets = {
|
||||
// Approximate values which are good enough for most cases
|
||||
top: getStatusBarHeight(true),
|
||||
bottom: getBottomSpace(),
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
StyleProp,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import type { Route } from '@react-navigation/native';
|
||||
import Badge from './Badge';
|
||||
|
||||
@@ -7,6 +13,7 @@ type Props = {
|
||||
route: Route<string>;
|
||||
horizontal: boolean;
|
||||
badge?: string | number;
|
||||
badgeStyle?: StyleProp<TextStyle>;
|
||||
activeOpacity: number;
|
||||
inactiveOpacity: number;
|
||||
activeTintColor: string;
|
||||
@@ -22,6 +29,7 @@ type Props = {
|
||||
export default function TabBarIcon({
|
||||
horizontal,
|
||||
badge,
|
||||
badgeStyle,
|
||||
activeOpacity,
|
||||
inactiveOpacity,
|
||||
activeTintColor,
|
||||
@@ -56,6 +64,7 @@ export default function TabBarIcon({
|
||||
style={[
|
||||
styles.badge,
|
||||
horizontal ? styles.badgeHorizontal : styles.badgeVertical,
|
||||
badgeStyle,
|
||||
]}
|
||||
size={(size * 3) / 4}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.3.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.14...@react-navigation/compat@5.3.15) (2021-04-04)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.13...@react-navigation/compat@5.3.14) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.12...@react-navigation/compat@5.3.13) (2021-01-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.11...@react-navigation/compat@5.3.12) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.10...@react-navigation/compat@5.3.11) (2021-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.9...@react-navigation/compat@5.3.10) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.8...@react-navigation/compat@5.3.9) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.7...@react-navigation/compat@5.3.8) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.6...@react-navigation/compat@5.3.7) (2020-11-08)
|
||||
|
||||
**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.3.7",
|
||||
"version": "5.3.15",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -31,17 +31,17 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@types/react": "^16.9.53",
|
||||
"react": "~16.13.1",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^5.0.5",
|
||||
"react": "*"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -3,6 +3,82 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.15.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.15.2...@react-navigation/core@5.15.3) (2021-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly resolve initialRouteNames ([976178d](https://github.com/react-navigation/react-navigation/commit/976178d0986a90697931ab9cc2c297eb7938e28b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.15.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.15.1...@react-navigation/core@5.15.2) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.15.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.15.0...@react-navigation/core@5.15.1) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.15.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.4...@react-navigation/core@5.15.0) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* print an error when passing a second argument to useFocusEffect ([2317633](https://github.com/react-navigation/react-navigation/commit/23176336528f98924d19f321d41cb70f13300edd))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a way to specify an unique ID for screens ([b19f76b](https://github.com/react-navigation/react-navigation/commit/b19f76bfffe623759e67d925bfd067c753a453bf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.3...@react-navigation/core@5.14.4) (2020-11-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix incorrect state change events in independent nested container ([95b2599](https://github.com/react-navigation/react-navigation/commit/95b2599877f5ceedf753e399e0586bb4af54cb87)), closes [#9080](https://github.com/react-navigation/react-navigation/issues/9080)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.2...@react-navigation/core@5.14.3) (2020-11-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve the error message for incorrect screen configuration ([8f764d8](https://github.com/react-navigation/react-navigation/commit/8f764d8b0809604716d5d92ea33cc1beee02e804))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.1...@react-navigation/core@5.14.2) (2020-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* throw if the same pattern resolves to multiple screens ([48b2e77](https://github.com/react-navigation/react-navigation/commit/48b2e777307908e8b3fcb49d8555b610dc0e38f2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.0...@react-navigation/core@5.14.1) (2020-11-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "5.14.1",
|
||||
"version": "5.15.3",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -35,26 +35,26 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.6.1",
|
||||
"@react-navigation/routers": "^5.7.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"nanoid": "^3.1.15",
|
||||
"query-string": "^6.13.6",
|
||||
"react-is": "^16.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-is": "^16.7.1",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"react-test-renderer": "~16.13.1",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
NavigationAction,
|
||||
} from '@react-navigation/routers';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import UnhandledActionContext from './UnhandledActionContext';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import NavigationStateContext from './NavigationStateContext';
|
||||
import UnhandledActionContext from './UnhandledActionContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
||||
import useChildListeners from './useChildListeners';
|
||||
import useKeyedChildListeners from './useKeyedChildListeners';
|
||||
@@ -397,7 +399,7 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
let element = (
|
||||
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
@@ -410,6 +412,19 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
</NavigationBuilderContext.Provider>
|
||||
</ScheduleUpdateContext.Provider>
|
||||
);
|
||||
|
||||
if (independent) {
|
||||
// We need to clear any existing contexts for nested independent container to work correctly
|
||||
element = (
|
||||
<NavigationRouteContext.Provider value={undefined}>
|
||||
<NavigationContext.Provider value={undefined}>
|
||||
{element}
|
||||
</NavigationContext.Provider>
|
||||
</NavigationRouteContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -757,3 +757,67 @@ it('invokes the unhandled action listener with the unhandled action', () => {
|
||||
type: 'NAVIGATE',
|
||||
});
|
||||
});
|
||||
|
||||
it('works with state change events in independent nested container', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map((route) => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
render(
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo">
|
||||
{() => (
|
||||
<BaseNavigationContainer
|
||||
independent
|
||||
ref={ref}
|
||||
onStateChange={onStateChange}
|
||||
>
|
||||
<TestNavigator>
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="bar">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
act(() => ref.current?.navigate('lex'));
|
||||
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
index: 1,
|
||||
key: '15',
|
||||
routeNames: ['qux', 'lex'],
|
||||
routes: [
|
||||
{ key: 'qux', name: 'qux' },
|
||||
{ key: 'lex', name: 'lex' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
expect(ref.current?.getRootState()).toEqual({
|
||||
index: 1,
|
||||
key: '15',
|
||||
routeNames: ['qux', 'lex'],
|
||||
routes: [
|
||||
{ key: 'qux', name: 'qux' },
|
||||
{ key: 'lex', name: 'lex' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2673,6 +2673,47 @@ it('uses nearest parent wildcard match for unmatched paths', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if two screens map to the same pattern', () => {
|
||||
const path = '/bar/42/baz/test';
|
||||
|
||||
expect(() =>
|
||||
getStateFromPath(path, {
|
||||
screens: {
|
||||
Foo: {
|
||||
screens: {
|
||||
Bar: {
|
||||
path: '/bar/:id/',
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
Bax: '/bar/:id/baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toThrow(
|
||||
"Found conflicting screens with the same pattern. The pattern 'bar/:id/baz' resolves to both 'Foo > Bax' and 'Foo > Bar > Baz'. Patterns must be unique and cannot resolve to more than one screen."
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
getStateFromPath(path, {
|
||||
screens: {
|
||||
Foo: {
|
||||
screens: {
|
||||
Bar: {
|
||||
path: '/bar/:id/',
|
||||
screens: {
|
||||
Baz: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if wildcard is specified with legacy config', () => {
|
||||
const path = '/bar/42/baz/test';
|
||||
const config = {
|
||||
@@ -2780,3 +2821,117 @@ it("throws when using 'initialRouteName' or 'screens' with legacy config", () =>
|
||||
})
|
||||
).toThrow('Found invalid keys in the configuration object.');
|
||||
});
|
||||
|
||||
it('correctly applies initialRouteName for config with similar route names', () => {
|
||||
const path = '/weekly-earnings';
|
||||
|
||||
const config = {
|
||||
screens: {
|
||||
RootTabs: {
|
||||
screens: {
|
||||
HomeTab: {
|
||||
screens: {
|
||||
Home: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
EventDetails: 'event-details/:eventId',
|
||||
},
|
||||
},
|
||||
EarningsTab: {
|
||||
initialRouteName: 'Earnings',
|
||||
path: 'earnings',
|
||||
screens: {
|
||||
Earnings: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'RootTabs',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'HomeTab',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'WeeklyEarnings',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly applies initialRouteName for config with similar route names v2', () => {
|
||||
const path = '/earnings/weekly-earnings';
|
||||
|
||||
const config = {
|
||||
screens: {
|
||||
RootTabs: {
|
||||
screens: {
|
||||
HomeTab: {
|
||||
initialRouteName: 'Home',
|
||||
screens: {
|
||||
Home: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
},
|
||||
},
|
||||
EarningsTab: {
|
||||
initialRouteName: 'Earnings',
|
||||
path: 'earnings',
|
||||
screens: {
|
||||
Earnings: '',
|
||||
WeeklyEarnings: 'weekly-earnings',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'RootTabs',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'EarningsTab',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Earnings',
|
||||
},
|
||||
{
|
||||
name: 'WeeklyEarnings',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1462,6 +1462,51 @@ it('throws when Screen is not the direct children', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when undefined component is a direct children', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const Undefined = undefined;
|
||||
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
{/* @ts-ignore */}
|
||||
<Undefined name="foo" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'undefined' for the screen 'foo')"
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when a tag is a direct children', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
{/* @ts-ignore */}
|
||||
<screen name="foo" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'screen' for the screen 'foo')"
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when a React Element is not the direct children', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
|
||||
@@ -239,3 +239,135 @@ it('runs cleanup when component is unmounted', () => {
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('prints error when a dependency array is passed', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
// @ts-ignore
|
||||
useFocusEffect(() => {}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="test" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(spy.mock.calls[0][0]).toMatch(
|
||||
"You passed a second argument to 'useFocusEffect', but it only accepts one argument."
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('prints error when the effect returns a value', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
// @ts-ignore
|
||||
useFocusEffect(() => 42);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="test" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(spy.mock.calls[0][0]).toMatch(
|
||||
"An effect function must not return anything besides a function, which is used for clean-up. You returned '42'."
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('prints error when the effect returns null', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
// @ts-ignore
|
||||
useFocusEffect(() => null);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="test" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(spy.mock.calls[0][0]).toMatch(
|
||||
"An effect function must not return anything besides a function, which is used for clean-up. You returned 'null'. If your effect does not require clean-up, return 'undefined' (or nothing)."
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('prints error when the effect is an async function', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const Test = () => {
|
||||
// @ts-ignore
|
||||
useFocusEffect(async () => {});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="test" component={Test} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(spy.mock.calls[0][0]).toMatch(
|
||||
"An effect function must not return anything besides a function, which is used for clean-up.\n\nIt looks like you wrote 'useFocusEffect(async () => ...)' or returned a Promise."
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -239,6 +239,10 @@ export default function getPathFromState(
|
||||
// Object.fromEntries is not available in older iOS versions
|
||||
const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
|
||||
entries.reduce((acc, [k, v]) => {
|
||||
if (acc.hasOwnProperty(k)) {
|
||||
throw new Error(`A value for key '${k}' already exists in the object.`);
|
||||
}
|
||||
|
||||
acc[k] = v;
|
||||
return acc;
|
||||
}, {} as Record<K, V>);
|
||||
|
||||
@@ -26,13 +26,18 @@ type RouteConfig = {
|
||||
|
||||
type InitialRouteConfig = {
|
||||
initialRouteName: string;
|
||||
connectedRoutes: string[];
|
||||
parentScreens: string[];
|
||||
};
|
||||
|
||||
type ResultState = PartialState<NavigationState> & {
|
||||
state?: ResultState;
|
||||
};
|
||||
|
||||
type ParsedRoute = {
|
||||
name: string;
|
||||
params?: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to parse a path string to initial state object accepted by the container.
|
||||
* This is useful for deep linking when we need to handle the incoming URL.
|
||||
@@ -65,7 +70,7 @@ export default function getStateFromPath(
|
||||
if (compatOptions?.initialRouteName) {
|
||||
initialRoutes.push({
|
||||
initialRouteName: compatOptions.initialRouteName,
|
||||
connectedRoutes: Object.keys(compatOptions.screens),
|
||||
parentScreens: [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +115,8 @@ export default function getStateFromPath(
|
||||
key,
|
||||
screens as PathConfigMap,
|
||||
[],
|
||||
initialRoutes
|
||||
initialRoutes,
|
||||
[]
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -119,6 +125,12 @@ export default function getStateFromPath(
|
||||
// - the most exhaustive ones are always at the beginning
|
||||
// - patterns with wildcard are always at the end
|
||||
|
||||
// If 2 patterns are same, move the one with less route names up
|
||||
// This is an error state, so it's only useful for consistent error messages
|
||||
if (a.pattern === b.pattern) {
|
||||
return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
|
||||
}
|
||||
|
||||
// If one of the patterns starts with the other, it's more exhaustive
|
||||
// So move it up
|
||||
if (a.pattern.startsWith(b.pattern)) {
|
||||
@@ -155,6 +167,35 @@ export default function getStateFromPath(
|
||||
return bWildcardIndex - aWildcardIndex;
|
||||
});
|
||||
|
||||
// Check for duplicate patterns in the config
|
||||
configs.reduce<Record<string, RouteConfig>>((acc, config) => {
|
||||
if (acc[config.pattern]) {
|
||||
const a = acc[config.pattern].routeNames;
|
||||
const b = config.routeNames;
|
||||
|
||||
// It's not a problem if the path string omitted from a inner most screen
|
||||
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
||||
const intersects =
|
||||
a.length > b.length
|
||||
? b.every((it, i) => a[i] === it)
|
||||
: a.every((it, i) => b[i] === it);
|
||||
|
||||
if (!intersects) {
|
||||
throw new Error(
|
||||
`Found conflicting screens with the same pattern. The pattern '${
|
||||
config.pattern
|
||||
}' resolves to both '${a.join(' > ')}' and '${b.join(
|
||||
' > '
|
||||
)}'. Patterns must be unique and cannot resolve to more than one screen.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign(acc, {
|
||||
[config.pattern]: config,
|
||||
});
|
||||
}, {});
|
||||
|
||||
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
|
||||
@@ -189,7 +230,7 @@ export default function getStateFromPath(
|
||||
if (legacy === false) {
|
||||
// If we're not in legacy mode,, we match the whole path against the regex instead of segments
|
||||
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
||||
const { routeNames, allParams, remainingPath } = matchAgainstConfigs(
|
||||
const { routes, remainingPath } = matchAgainstConfigs(
|
||||
remaining,
|
||||
configs.map((c) => ({
|
||||
...c,
|
||||
@@ -198,39 +239,30 @@ export default function getStateFromPath(
|
||||
}))
|
||||
);
|
||||
|
||||
if (routeNames !== undefined) {
|
||||
if (routes !== undefined) {
|
||||
// This will always be empty if full path matched
|
||||
current = createNestedStateObject(routes, initialRoutes);
|
||||
remaining = remainingPath;
|
||||
current = createNestedStateObject(
|
||||
createRouteObjects(configs, routeNames, allParams),
|
||||
initialRoutes
|
||||
);
|
||||
result = current;
|
||||
}
|
||||
} else {
|
||||
// In legacy mode, we divide the path into segments and match piece by piece
|
||||
// This preserves the legacy behaviour, but we should remove it in next major
|
||||
while (remaining) {
|
||||
let { routeNames, allParams, remainingPath } = matchAgainstConfigs(
|
||||
remaining,
|
||||
configs
|
||||
);
|
||||
let { routes, remainingPath } = matchAgainstConfigs(remaining, configs);
|
||||
|
||||
remaining = remainingPath;
|
||||
|
||||
// If we hadn't matched any segments earlier, use the path as route name
|
||||
if (routeNames === undefined) {
|
||||
if (routes === undefined) {
|
||||
const segments = remaining.split('/');
|
||||
|
||||
routeNames = [decodeURIComponent(segments[0])];
|
||||
routes = [{ name: decodeURIComponent(segments[0]) }];
|
||||
segments.shift();
|
||||
remaining = segments.join('/');
|
||||
}
|
||||
|
||||
const state = createNestedStateObject(
|
||||
createRouteObjects(configs, routeNames, allParams),
|
||||
initialRoutes
|
||||
);
|
||||
const state = createNestedStateObject(routes, initialRoutes);
|
||||
|
||||
if (current) {
|
||||
// The state should be nested inside the deepest route we parsed before
|
||||
@@ -274,8 +306,7 @@ const joinPaths = (...paths: string[]): string =>
|
||||
.join('/');
|
||||
|
||||
const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
|
||||
let routeNames: string[] | undefined;
|
||||
let allParams: Record<string, any> | undefined;
|
||||
let routes: ParsedRoute[] | undefined;
|
||||
let remainingPath = remaining;
|
||||
|
||||
// Go through all configs, and see if the next path segment matches our regex
|
||||
@@ -288,21 +319,40 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
|
||||
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
routeNames = [...config.routeNames];
|
||||
const matchedParams = config.pattern
|
||||
?.split('/')
|
||||
.filter((p) => p.startsWith(':'))
|
||||
.reduce<Record<string, any>>(
|
||||
(acc, p, i) =>
|
||||
Object.assign(acc, {
|
||||
// The param segments appear every second item starting from 2 in the regex match result
|
||||
[p]: match![(i + 1) * 2].replace(/\//, ''),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
.split('/')
|
||||
.filter((p) => p.startsWith(':'));
|
||||
routes = config.routeNames.map((name) => {
|
||||
const config = configs.find((c) => c.screen === name);
|
||||
const params = config?.path
|
||||
?.split('/')
|
||||
.filter((p) => p.startsWith(':'))
|
||||
.reduce<Record<string, any>>((acc, p) => {
|
||||
const value = matchedParams[p];
|
||||
|
||||
if (paramPatterns.length) {
|
||||
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
|
||||
if (value) {
|
||||
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
||||
acc[key] = config.parse?.[key] ? config.parse[key](value) : value;
|
||||
}
|
||||
|
||||
acc[p] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
if (params && Object.keys(params).length) {
|
||||
return { name, params };
|
||||
}
|
||||
|
||||
return { name };
|
||||
});
|
||||
|
||||
remainingPath = remainingPath.replace(match[1], '');
|
||||
|
||||
@@ -310,7 +360,7 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
return { routeNames, allParams, remainingPath };
|
||||
return { routes, remainingPath };
|
||||
};
|
||||
|
||||
const createNormalizedConfigs = (
|
||||
@@ -319,12 +369,15 @@ const createNormalizedConfigs = (
|
||||
routeConfig: PathConfigMap,
|
||||
routeNames: string[] = [],
|
||||
initials: InitialRouteConfig[],
|
||||
parentScreens: string[],
|
||||
parentPattern?: string
|
||||
): RouteConfig[] => {
|
||||
const configs: RouteConfig[] = [];
|
||||
|
||||
routeNames.push(screen);
|
||||
|
||||
parentScreens.push(screen);
|
||||
|
||||
const config = routeConfig[screen];
|
||||
|
||||
if (typeof config === 'string') {
|
||||
@@ -374,7 +427,7 @@ const createNormalizedConfigs = (
|
||||
if (config.initialRouteName) {
|
||||
initials.push({
|
||||
initialRouteName: config.initialRouteName,
|
||||
connectedRoutes: Object.keys(config.screens),
|
||||
parentScreens,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,6 +438,7 @@ const createNormalizedConfigs = (
|
||||
config.screens as PathConfigMap,
|
||||
routeNames,
|
||||
initials,
|
||||
[...parentScreens],
|
||||
pattern ?? parentPattern
|
||||
);
|
||||
|
||||
@@ -457,13 +511,23 @@ const findParseConfigForRoute = (
|
||||
// Try to find an initial route connected with the one passed
|
||||
const findInitialRoute = (
|
||||
routeName: string,
|
||||
parentScreens: string[],
|
||||
initialRoutes: InitialRouteConfig[]
|
||||
): string | undefined => {
|
||||
for (const config of initialRoutes) {
|
||||
if (config.connectedRoutes.includes(routeName)) {
|
||||
return config.initialRouteName === routeName
|
||||
? undefined
|
||||
: config.initialRouteName;
|
||||
if (parentScreens.length === config.parentScreens.length) {
|
||||
let sameParents = true;
|
||||
for (let i = 0; i < parentScreens.length; i++) {
|
||||
if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
|
||||
sameParents = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sameParents) {
|
||||
return routeName !== config.initialRouteName
|
||||
? config.initialRouteName
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -473,66 +537,60 @@ const findInitialRoute = (
|
||||
// it is the end of state and if there is initialRoute for this level
|
||||
const createStateObject = (
|
||||
initialRoute: string | undefined,
|
||||
routeName: string,
|
||||
params: Record<string, any> | undefined,
|
||||
route: ParsedRoute,
|
||||
isEmpty: boolean
|
||||
): InitialState => {
|
||||
if (isEmpty) {
|
||||
if (initialRoute) {
|
||||
return {
|
||||
index: 1,
|
||||
routes: [{ name: initialRoute }, { name: routeName as string, params }],
|
||||
routes: [{ name: initialRoute }, route],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
routes: [{ name: routeName as string, params }],
|
||||
routes: [route],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (initialRoute) {
|
||||
return {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: initialRoute },
|
||||
{ name: routeName as string, params, state: { routes: [] } },
|
||||
],
|
||||
routes: [{ name: initialRoute }, { ...route, state: { routes: [] } }],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
routes: [{ name: routeName as string, params, state: { routes: [] } }],
|
||||
routes: [{ ...route, state: { routes: [] } }],
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createNestedStateObject = (
|
||||
routes: { name: string; params?: object }[],
|
||||
routes: ParsedRoute[],
|
||||
initialRoutes: InitialRouteConfig[]
|
||||
) => {
|
||||
let state: InitialState;
|
||||
let route = routes.shift() as { name: string; params?: object };
|
||||
let initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||
let route = routes.shift() as ParsedRoute;
|
||||
const parentScreens: string[] = [];
|
||||
|
||||
state = createStateObject(
|
||||
initialRoute,
|
||||
route.name,
|
||||
route.params,
|
||||
routes.length === 0
|
||||
);
|
||||
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
||||
|
||||
parentScreens.push(route.name);
|
||||
|
||||
state = createStateObject(initialRoute, route, routes.length === 0);
|
||||
|
||||
if (routes.length > 0) {
|
||||
let nestedState = state;
|
||||
|
||||
while ((route = routes.shift() as { name: string; params?: object })) {
|
||||
initialRoute = findInitialRoute(route.name, initialRoutes);
|
||||
while ((route = routes.shift() as ParsedRoute)) {
|
||||
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
||||
|
||||
const nestedStateIndex =
|
||||
nestedState.index || nestedState.routes.length - 1;
|
||||
|
||||
nestedState.routes[nestedStateIndex].state = createStateObject(
|
||||
initialRoute,
|
||||
route.name,
|
||||
route.params,
|
||||
route,
|
||||
routes.length === 0
|
||||
);
|
||||
|
||||
@@ -540,52 +598,14 @@ const createNestedStateObject = (
|
||||
nestedState = nestedState.routes[nestedStateIndex]
|
||||
.state as InitialState;
|
||||
}
|
||||
|
||||
parentScreens.push(route.name);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const createRouteObjects = (
|
||||
configs: RouteConfig[],
|
||||
routeNames: string[],
|
||||
allParams?: Record<string, any>
|
||||
) =>
|
||||
routeNames.map((name) => {
|
||||
const config = configs.find((c) => c.screen === name);
|
||||
|
||||
let params: object | undefined;
|
||||
|
||||
if (allParams && config?.path) {
|
||||
const pattern = config.path;
|
||||
|
||||
if (pattern) {
|
||||
const paramPatterns = pattern
|
||||
.split('/')
|
||||
.filter((p) => p.startsWith(':'));
|
||||
|
||||
if (paramPatterns.length) {
|
||||
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
|
||||
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
||||
const value = allParams![p];
|
||||
|
||||
if (value) {
|
||||
acc[key] = config.parse?.[key] ? config.parse[key](value) : value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (params && Object.keys(params).length) {
|
||||
return { name, params };
|
||||
}
|
||||
|
||||
return { name };
|
||||
});
|
||||
|
||||
const findFocusedRoute = (state: InitialState) => {
|
||||
let current: InitialState | undefined = state;
|
||||
|
||||
|
||||
@@ -388,6 +388,14 @@ export type RouteConfig<
|
||||
navigation: any;
|
||||
}) => ScreenListeners<State, EventMap>);
|
||||
|
||||
/**
|
||||
* Function to return an unique ID for this screen.
|
||||
* Receives an object with the route params.
|
||||
* For a given screen name, there will always be only one screen corresponding to an ID.
|
||||
* If `undefined` is returned, it acts same as no `getId` being specified.
|
||||
*/
|
||||
getId?: ({ params }: { params: ParamList[RouteName] }) => string | undefined;
|
||||
|
||||
/**
|
||||
* Initial params object for the route.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,20 @@ type EffectCallback = () => undefined | void | (() => void);
|
||||
export default function useFocusEffect(effect: EffectCallback) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
if (arguments[1] !== undefined) {
|
||||
const message =
|
||||
"You passed a second argument to 'useFocusEffect', but it only accepts one argument. " +
|
||||
"If you want to pass a dependency array, you can use 'React.useCallback':\n\n" +
|
||||
'useFocusEffect(\n' +
|
||||
' React.useCallback(() => {\n' +
|
||||
' // Your code here\n' +
|
||||
' }, [depA, depB])\n' +
|
||||
');\n\n' +
|
||||
'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
|
||||
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
let isFocused = false;
|
||||
let cleanup: undefined | void | (() => void);
|
||||
@@ -45,10 +59,10 @@ export default function useFocusEffect(effect: EffectCallback) {
|
||||
' }\n\n' +
|
||||
' fetchData();\n' +
|
||||
' }, [someId])\n' +
|
||||
'};\n\n' +
|
||||
');\n\n' +
|
||||
'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
|
||||
} else {
|
||||
message += ` You returned: '${JSON.stringify(destroy)}'`;
|
||||
message += ` You returned '${JSON.stringify(destroy)}'.`;
|
||||
}
|
||||
|
||||
console.error(message);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ParamListBase,
|
||||
Router,
|
||||
RouterFactory,
|
||||
RouterConfigOptions,
|
||||
PartialState,
|
||||
NavigationAction,
|
||||
Route,
|
||||
@@ -90,10 +91,17 @@ const getRouteConfigsFromChildren = <
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`A navigator can only contain 'Screen' components as its direct children (found '${
|
||||
// @ts-expect-error: child can be any type and we're accessing it safely, but TS doesn't understand it
|
||||
child.type?.name ? child.type.name : String(child)
|
||||
}'). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.`
|
||||
`A navigator can only contain 'Screen' components as its direct children (found ${
|
||||
React.isValidElement(child)
|
||||
? `'${
|
||||
typeof child.type === 'string' ? child.type : child.type?.name
|
||||
}'${
|
||||
child.props?.name ? ` for the screen '${child.props.name}'` : ''
|
||||
}`
|
||||
: typeof child === 'object'
|
||||
? JSON.stringify(child)
|
||||
: `'${String(child)}'`
|
||||
}). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.`
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -250,6 +258,15 @@ export default function useNavigationBuilder<
|
||||
},
|
||||
{}
|
||||
);
|
||||
const routeGetIdList = routeNames.reduce<
|
||||
RouterConfigOptions['routeGetIdList']
|
||||
>(
|
||||
(acc, curr) =>
|
||||
Object.assign(acc, {
|
||||
[curr]: screens[curr].getId,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
if (!routeNames.length) {
|
||||
throw new Error(
|
||||
@@ -290,6 +307,7 @@ export default function useNavigationBuilder<
|
||||
router.getInitialState({
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
}),
|
||||
true,
|
||||
];
|
||||
@@ -300,6 +318,7 @@ export default function useNavigationBuilder<
|
||||
{
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
}
|
||||
),
|
||||
false,
|
||||
@@ -329,6 +348,7 @@ export default function useNavigationBuilder<
|
||||
nextState = router.getStateForRouteNamesChange(state, {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -365,6 +385,7 @@ export default function useNavigationBuilder<
|
||||
? router.getStateForAction(nextState, action, {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -373,6 +394,7 @@ export default function useNavigationBuilder<
|
||||
? router.getRehydratedState(updatedState, {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
})
|
||||
: nextState;
|
||||
}
|
||||
@@ -494,6 +516,7 @@ export default function useNavigationBuilder<
|
||||
routerConfigOptions: {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
},
|
||||
emitter,
|
||||
});
|
||||
|
||||
@@ -78,6 +78,7 @@ export default function useNavigationHelpers<
|
||||
router.getStateForAction(state, CommonActions.goBack() as Action, {
|
||||
routeNames: state.routeNames,
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
}) !== null ||
|
||||
parentNavigationHelpers?.canGoBack() ||
|
||||
false
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.22](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.21...@react-navigation/devtools@5.1.22) (2021-04-04)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.21](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.20...@react-navigation/devtools@5.1.21) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.20](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.19...@react-navigation/devtools@5.1.20) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.18...@react-navigation/devtools@5.1.19) (2021-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.18](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.17...@react-navigation/devtools@5.1.18) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.17](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.16...@react-navigation/devtools@5.1.17) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.16](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.15...@react-navigation/devtools@5.1.16) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.14...@react-navigation/devtools@5.1.15) (2020-11-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/devtools
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/devtools",
|
||||
"description": "Developer tools for React Navigation",
|
||||
"version": "5.1.15",
|
||||
"version": "5.1.22",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -36,22 +36,22 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.14.1",
|
||||
"@react-navigation/core": "^5.15.3",
|
||||
"deep-equal": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/react": "^16.9.53",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -3,6 +3,140 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.12.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.12.4...@react-navigation/drawer@5.12.5) (2021-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check for screens enabled in ScreenContainer ([493956e](https://github.com/react-navigation/react-navigation/commit/493956ef717a03bd8c3533a2949434e83718c5e4))
|
||||
* don't handle back button with permanent drawer ([a63f9da](https://github.com/react-navigation/react-navigation/commit/a63f9da8c1efe5d34567517ac2653608c6bbdeba))
|
||||
* don't pass accessibilityState to link. closes [#9418](https://github.com/react-navigation/react-navigation/issues/9418) ([699ea0c](https://github.com/react-navigation/react-navigation/commit/699ea0cc5052f190acc7ce8bc0328bb052d7cf26))
|
||||
* only handle back button in drawer when focused ([cceaa67](https://github.com/react-navigation/react-navigation/commit/cceaa6780d588b2a2ffa3a2039f65f9e60a33bf9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.12.3...@react-navigation/drawer@5.12.4) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.12.2...@react-navigation/drawer@5.12.3) (2021-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix drawer screen content not being interactable on Android ([87b5147](https://github.com/react-navigation/react-navigation/commit/87b51476d0bce8f2dae793416c2976da30a1a5f7))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.12.1...@react-navigation/drawer@5.12.2) (2021-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix pointerEvents in ResourceSavingScene ([60fe0db](https://github.com/react-navigation/react-navigation/commit/60fe0dbb0ae443fdb21016d368c919b933cb64e7)), closes [#9241](https://github.com/react-navigation/react-navigation/issues/9241) [#9242](https://github.com/react-navigation/react-navigation/issues/9242)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.12.0...@react-navigation/drawer@5.12.1) (2021-01-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.12.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.5...@react-navigation/drawer@5.12.0) (2021-01-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix drawer and bottom tabs not being visible on web. closes [#9225](https://github.com/react-navigation/react-navigation/issues/9225) ([d88cbcb](https://github.com/react-navigation/react-navigation/commit/d88cbcb52d46de26edaa9ce6bfb06badb1b1de64))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add pressColor and pressOpacity props to drawerItem ([#8834](https://github.com/react-navigation/react-navigation/issues/8834)) ([bae4019](https://github.com/react-navigation/react-navigation/commit/bae4019995062c682f0213c121b7927ab8006c1e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.4...@react-navigation/drawer@5.11.5) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable detachInactiveScreens by default on web for better a11y ([dd87fa4](https://github.com/react-navigation/react-navigation/commit/dd87fa49a43ad8db105a62418243339e4150fadf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.3...@react-navigation/drawer@5.11.4) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.2...@react-navigation/drawer@5.11.3) (2020-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* hide drawer's header by default ([794339e](https://github.com/react-navigation/react-navigation/commit/794339eeed7c0d3b0e8b1752e494fbb4608ddfad))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.1...@react-navigation/drawer@5.11.2) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.0...@react-navigation/drawer@5.11.1) (2020-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* provide correct context to drawe header ([18bbd17](https://github.com/react-navigation/react-navigation/commit/18bbd177d91ccc4308516208a8b9f1a34ca5cc41))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.7...@react-navigation/drawer@5.11.0) (2020-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* try fixing drawer blink on Android ([5217245](https://github.com/react-navigation/react-navigation/commit/52172453dfb71822c2fb0f5947d00bac4a840d07))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a getIsDrawerOpenFromState utility to drawer ([5bd682f](https://github.com/react-navigation/react-navigation/commit/5bd682f0bf6b28a95fb3e7fc9e1974057a877cb0))
|
||||
* add option to show a header in drawer navigator screens ([dbe961b](https://github.com/react-navigation/react-navigation/commit/dbe961ba5bb243e8da4d889c3c7dd6ed1de287c4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.10.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.6...@react-navigation/drawer@5.10.7) (2020-11-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/drawer",
|
||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||
"version": "5.10.7",
|
||||
"version": "5.12.5",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -45,14 +45,14 @@
|
||||
"react-native-iphone-x-helper": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "^0.63.30",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"react-native-gesture-handler": "~1.7.0",
|
||||
"react-native-reanimated": "~1.13.0",
|
||||
"react-native-safe-area-context": "3.1.4",
|
||||
@@ -68,7 +68,7 @@
|
||||
"react-native-safe-area-context": ">= 0.6.0",
|
||||
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -17,6 +17,7 @@ export { default as DrawerContentScrollView } from './views/DrawerContentScrollV
|
||||
*/
|
||||
export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
|
||||
|
||||
export { default as getIsDrawerOpenFromState } from './utils/getIsDrawerOpenFromState';
|
||||
export { default as useIsDrawerOpen } from './utils/useIsDrawerOpen';
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,8 @@ export type Scene = {
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export type Layout = { width: number; height: number };
|
||||
|
||||
export type DrawerNavigationConfig<T = DrawerContentOptions> = {
|
||||
/**
|
||||
* Position of the drawer on the screen. Defaults to `left`.
|
||||
@@ -94,12 +96,95 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
|
||||
detachInactiveScreens?: boolean;
|
||||
};
|
||||
|
||||
export type DrawerNavigationOptions = {
|
||||
export type DrawerHeaderOptions = {
|
||||
/**
|
||||
* String or a function that returns a React Element to be used by the header.
|
||||
* Defaults to scene `title`.
|
||||
* It receives `allowFontScaling`, `tintColor`, `style` and `children` in the options object as an argument.
|
||||
* The title string is passed in `children`.
|
||||
*/
|
||||
headerTitle?:
|
||||
| string
|
||||
| ((props: {
|
||||
/**
|
||||
* Whether title font should scale to respect Text Size accessibility settings.
|
||||
*/
|
||||
allowFontScaling?: boolean;
|
||||
/**
|
||||
* Tint color for the header.
|
||||
*/
|
||||
tintColor?: string;
|
||||
/**
|
||||
* Content of the title element. Usually the title string.
|
||||
*/
|
||||
children?: string;
|
||||
/**
|
||||
* Style object for the title element.
|
||||
*/
|
||||
style?: StyleProp<TextStyle>;
|
||||
}) => React.ReactNode);
|
||||
/**
|
||||
* How to align the the header title.
|
||||
* Defaults to `center` on iOS and `left` on Android.
|
||||
*/
|
||||
headerTitleAlign?: 'left' | 'center';
|
||||
/**
|
||||
* Style object for the title component.
|
||||
*/
|
||||
headerTitleStyle?: StyleProp<TextStyle>;
|
||||
/**
|
||||
* Whether header title font should scale to respect Text Size accessibility settings. Defaults to `false`.
|
||||
*/
|
||||
headerTitleAllowFontScaling?: boolean;
|
||||
/**
|
||||
* Function which returns a React Element to display on the left side of the header.
|
||||
*/
|
||||
headerLeft?: (props: { tintColor?: string }) => React.ReactNode;
|
||||
/**
|
||||
* Accessibility label for the header left button.
|
||||
*/
|
||||
headerLeftAccessibilityLabel?: string;
|
||||
/**
|
||||
* Function which returns a React Element to display on the right side of the header.
|
||||
*/
|
||||
headerRight?: (props: { tintColor?: string }) => React.ReactNode;
|
||||
/**
|
||||
* Color for material ripple (Android >= 5.0 only).
|
||||
*/
|
||||
headerPressColorAndroid?: string;
|
||||
/**
|
||||
* Tint color for the header.
|
||||
*/
|
||||
headerTintColor?: string;
|
||||
/**
|
||||
* Style object for the header. You can specify a custom background color here, for example.
|
||||
*/
|
||||
headerStyle?: StyleProp<ViewStyle>;
|
||||
/**
|
||||
* Extra padding to add at the top of header to account for translucent status bar.
|
||||
* By default, it uses the top value from the safe area insets of the device.
|
||||
* Pass 0 or a custom value to disable the default behaviour, and customize the height.
|
||||
*/
|
||||
headerStatusBarHeight?: number;
|
||||
};
|
||||
|
||||
export type DrawerNavigationOptions = DrawerHeaderOptions & {
|
||||
/**
|
||||
* Title text for the screen.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Function that given `HeaderProps` returns a React Element to display as a header.
|
||||
*/
|
||||
header?: (props: DrawerHeaderProps) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Whether to show the header. The header is not shown by default.
|
||||
* Setting this to `true` shows the header.
|
||||
*/
|
||||
headerShown?: boolean;
|
||||
|
||||
/**
|
||||
* Title string of a screen displayed in the drawer
|
||||
* or a function that given { focused: boolean, color: string } returns a React.Node
|
||||
@@ -187,6 +272,20 @@ export type DrawerContentOptions = {
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type DrawerHeaderProps = {
|
||||
/**
|
||||
* Layout of the screen.
|
||||
*/
|
||||
layout: Layout;
|
||||
/**
|
||||
* Object representing the current scene, such as the route object and descriptor.
|
||||
*/
|
||||
scene: {
|
||||
route: Route<string>;
|
||||
descriptor: DrawerDescriptor;
|
||||
};
|
||||
};
|
||||
|
||||
export type DrawerNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires when the drawer opens.
|
||||
|
||||
16
packages/drawer/src/utils/getIsDrawerOpenFromState.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type {
|
||||
DrawerNavigationState,
|
||||
ParamListBase,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
export default function getIsDrawerOpenFromState(
|
||||
state: DrawerNavigationState<ParamListBase>
|
||||
): boolean {
|
||||
if (state.history == null) {
|
||||
throw new Error(
|
||||
"Couldn't find the drawer status in the state object. Is it a valid state object of drawer navigator?"
|
||||
);
|
||||
}
|
||||
|
||||
return state.history.some((it) => it.type === 'drawer');
|
||||
}
|
||||
@@ -57,9 +57,10 @@ const DIRECTION_LEFT = 1;
|
||||
const DIRECTION_RIGHT = -1;
|
||||
|
||||
const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60;
|
||||
|
||||
const SWIPE_DISTANCE_MINIMUM = 5;
|
||||
|
||||
const DEFAULT_DRAWER_WIDTH = '80%';
|
||||
|
||||
const SPRING_CONFIG = {
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
@@ -115,12 +116,6 @@ export default class DrawerView extends React.Component<Props> {
|
||||
statusBarAnimation: 'slide',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.addEventListener?.('keyup', this.handleEscape);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const {
|
||||
open,
|
||||
@@ -171,22 +166,8 @@ export default class DrawerView extends React.Component<Props> {
|
||||
componentWillUnmount() {
|
||||
this.toggleStatusBar(false);
|
||||
this.handleEndInteraction();
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.removeEventListener?.('keyup', this.handleEscape);
|
||||
}
|
||||
}
|
||||
|
||||
private handleEscape = (e: KeyboardEvent) => {
|
||||
const { open, onClose } = this.props;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
if (open) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleEndInteraction = () => {
|
||||
if (this.interactionHandle !== undefined) {
|
||||
InteractionManager.clearInteractionHandle(this.interactionHandle);
|
||||
@@ -202,7 +183,8 @@ export default class DrawerView extends React.Component<Props> {
|
||||
|
||||
private getDrawerWidth = (): number => {
|
||||
const { drawerStyle, dimensions } = this.props;
|
||||
const { width } = StyleSheet.flatten(drawerStyle);
|
||||
const { width = DEFAULT_DRAWER_WIDTH } =
|
||||
StyleSheet.flatten(drawerStyle) || {};
|
||||
|
||||
if (typeof width === 'string' && width.endsWith('%')) {
|
||||
// Try to calculate width if a percentage is given
|
||||
@@ -246,7 +228,7 @@ export default class DrawerView extends React.Component<Props> {
|
||||
private containerWidth = new Value<number>(this.props.dimensions.width);
|
||||
private drawerWidth = new Value<number>(this.initialDrawerWidth);
|
||||
private drawerOpacity = new Value<number>(
|
||||
this.initialDrawerWidth || this.props.drawerType === 'permanent' ? 1 : 0
|
||||
this.props.drawerType === 'permanent' ? 1 : 0
|
||||
);
|
||||
private drawerPosition = new Value<number>(
|
||||
this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT
|
||||
@@ -730,7 +712,7 @@ const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '80%',
|
||||
width: DEFAULT_DRAWER_WIDTH,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
|
||||
@@ -56,6 +56,20 @@ type Props = {
|
||||
* Background color for item when its inactive.
|
||||
*/
|
||||
inactiveBackgroundColor?: string;
|
||||
/**
|
||||
* Color of the touchable effect on press.
|
||||
* Only supported on Android.
|
||||
*
|
||||
* @platform android
|
||||
*/
|
||||
pressColor?: string;
|
||||
/**
|
||||
* Opacity of the touchable effect on press.
|
||||
* Only supported on iOS.
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
pressOpacity?: string;
|
||||
/**
|
||||
* Style object for the label element.
|
||||
*/
|
||||
@@ -72,6 +86,7 @@ const Touchable = ({
|
||||
onPress,
|
||||
to,
|
||||
accessibilityRole,
|
||||
accessibilityState,
|
||||
delayPressIn,
|
||||
...rest
|
||||
}: TouchableWithoutFeedbackProps & {
|
||||
@@ -105,6 +120,7 @@ const Touchable = ({
|
||||
<TouchableItem
|
||||
{...rest}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityState={accessibilityState}
|
||||
delayPressIn={delayPressIn}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -132,6 +148,8 @@ export default function DrawerItem(props: Props) {
|
||||
inactiveBackgroundColor = 'transparent',
|
||||
style,
|
||||
onPress,
|
||||
pressColor,
|
||||
pressOpacity,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -159,6 +177,8 @@ export default function DrawerItem(props: Props) {
|
||||
accessibilityState={{ selected: focused }}
|
||||
// @ts-expect-error: keep for compatibility with older React Native versions
|
||||
accessibilityStates={focused ? ['selected'] : []}
|
||||
pressColor={pressColor}
|
||||
pressOpacity={pressOpacity}
|
||||
to={to}
|
||||
>
|
||||
<React.Fragment>
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
I18nManager,
|
||||
Platform,
|
||||
BackHandler,
|
||||
NativeEventSubscription,
|
||||
} from 'react-native';
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import { ScreenContainer, screensEnabled } from 'react-native-screens';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
NavigationContext,
|
||||
NavigationRouteContext,
|
||||
DrawerNavigationState,
|
||||
DrawerActions,
|
||||
useTheme,
|
||||
@@ -19,16 +20,19 @@ import {
|
||||
import { GestureHandlerRootView } from './GestureHandler';
|
||||
import SafeAreaProviderCompat from './SafeAreaProviderCompat';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
import Header from './Header';
|
||||
import DrawerContent from './DrawerContent';
|
||||
import Drawer from './Drawer';
|
||||
import DrawerOpenContext from '../utils/DrawerOpenContext';
|
||||
import DrawerPositionContext from '../utils/DrawerPositionContext';
|
||||
import useWindowDimensions from '../utils/useWindowDimensions';
|
||||
import getIsDrawerOpenFromState from '../utils/getIsDrawerOpenFromState';
|
||||
import type {
|
||||
DrawerDescriptorMap,
|
||||
DrawerNavigationConfig,
|
||||
DrawerNavigationHelpers,
|
||||
DrawerContentComponentProps,
|
||||
DrawerHeaderProps,
|
||||
} from '../types';
|
||||
|
||||
type Props = DrawerNavigationConfig & {
|
||||
@@ -90,7 +94,7 @@ export default function DrawerView({
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
const isDrawerOpen = state.history.some((it) => it.type === 'drawer');
|
||||
const isDrawerOpen = getIsDrawerOpenFromState(state);
|
||||
|
||||
const handleDrawerOpen = React.useCallback(() => {
|
||||
navigation.dispatch({
|
||||
@@ -107,29 +111,48 @@ export default function DrawerView({
|
||||
}, [navigation, state.key]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDrawerOpen) {
|
||||
navigation.emit({ type: 'drawerOpen' });
|
||||
} else {
|
||||
navigation.emit({ type: 'drawerClose' });
|
||||
}
|
||||
}, [isDrawerOpen, navigation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let subscription: NativeEventSubscription | undefined;
|
||||
|
||||
if (isDrawerOpen) {
|
||||
// We only add the subscription when drawer opens
|
||||
// This way we can make sure that the subscription is added as late as possible
|
||||
// This will make sure that our handler will run first when back button is pressed
|
||||
subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
handleDrawerClose();
|
||||
|
||||
return true;
|
||||
});
|
||||
if (!isDrawerOpen || drawerType === 'permanent') {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => subscription?.remove();
|
||||
}, [handleDrawerClose, isDrawerOpen, navigation, state.key]);
|
||||
const handleClose = () => {
|
||||
// We shouldn't handle the back button if the parent screen isn't focused
|
||||
// This will avoid the drawer overriding event listeners from a focused screen
|
||||
if (!navigation.isFocused()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
handleDrawerClose();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
// We only add the listeners when drawer opens
|
||||
// This way we can make sure that the listener is added as late as possible
|
||||
// This will make sure that our handler will run first when back button is pressed
|
||||
const subscription = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
handleClose
|
||||
);
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.addEventListener?.('keyup', handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
document?.body?.removeEventListener?.('keyup', handleEscape);
|
||||
}
|
||||
};
|
||||
}, [drawerType, handleDrawerClose, isDrawerOpen, navigation]);
|
||||
|
||||
const focusedRouteKey = state.routes[state.index].key;
|
||||
|
||||
@@ -152,9 +175,11 @@ export default function DrawerView({
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
const isScreensEnabled = screensEnabled?.() && detachInactiveScreens;
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ScreenContainer enabled={detachInactiveScreens} style={styles.content}>
|
||||
<ScreenContainer enabled={isScreensEnabled} style={styles.content}>
|
||||
{state.routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { unmountOnBlur } = descriptor.options;
|
||||
@@ -169,13 +194,28 @@ export default function DrawerView({
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
header = (props: DrawerHeaderProps) => <Header {...props} />,
|
||||
headerShown = false,
|
||||
} = descriptor.options;
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]}
|
||||
isVisible={isFocused}
|
||||
enabled={detachInactiveScreens}
|
||||
enabled={isScreensEnabled}
|
||||
>
|
||||
{headerShown ? (
|
||||
<NavigationContext.Provider value={descriptor.navigation}>
|
||||
<NavigationRouteContext.Provider value={route}>
|
||||
{header({
|
||||
layout: dimensions,
|
||||
scene: { route, descriptor },
|
||||
})}
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
) : null}
|
||||
{descriptor.render()}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
|
||||
240
packages/drawer/src/views/Header.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import * as React from 'react';
|
||||
import { Text, View, Image, StyleSheet, Platform } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { DrawerActions, useTheme } from '@react-navigation/native';
|
||||
import TouchableItem from './TouchableItem';
|
||||
import type { Layout, DrawerHeaderProps } from '../types';
|
||||
|
||||
export const getDefaultHeaderHeight = (
|
||||
layout: Layout,
|
||||
statusBarHeight: number
|
||||
): number => {
|
||||
const isLandscape = layout.width > layout.height;
|
||||
|
||||
let headerHeight;
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
if (isLandscape && !Platform.isPad) {
|
||||
headerHeight = 32;
|
||||
} else {
|
||||
headerHeight = 44;
|
||||
}
|
||||
} else if (Platform.OS === 'android') {
|
||||
headerHeight = 56;
|
||||
} else {
|
||||
headerHeight = 64;
|
||||
}
|
||||
|
||||
return headerHeight + statusBarHeight;
|
||||
};
|
||||
|
||||
export default function HeaderSegment({ scene, layout }: DrawerHeaderProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { colors } = useTheme();
|
||||
|
||||
const {
|
||||
title,
|
||||
headerTitle,
|
||||
headerTitleAlign = Platform.select({
|
||||
ios: 'center',
|
||||
default: 'left',
|
||||
}),
|
||||
headerLeft,
|
||||
headerLeftAccessibilityLabel,
|
||||
headerRight,
|
||||
headerTitleAllowFontScaling,
|
||||
headerTitleStyle,
|
||||
headerTintColor,
|
||||
headerPressColorAndroid,
|
||||
headerStyle,
|
||||
headerStatusBarHeight = insets.top,
|
||||
} = scene.descriptor.options;
|
||||
|
||||
const currentTitle =
|
||||
typeof headerTitle !== 'function' && headerTitle !== undefined
|
||||
? headerTitle
|
||||
: title !== undefined
|
||||
? title
|
||||
: scene.route.name;
|
||||
|
||||
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
|
||||
|
||||
const leftButton = headerLeft ? (
|
||||
headerLeft({ tintColor: headerTintColor })
|
||||
) : (
|
||||
<TouchableItem
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityComponentType="button"
|
||||
accessibilityLabel={headerLeftAccessibilityLabel}
|
||||
accessibilityTraits="button"
|
||||
delayPressIn={0}
|
||||
onPress={() =>
|
||||
scene.descriptor.navigation.dispatch(DrawerActions.toggleDrawer())
|
||||
}
|
||||
style={styles.touchable}
|
||||
pressColor={headerPressColorAndroid}
|
||||
hitSlop={Platform.select({
|
||||
ios: undefined,
|
||||
default: { top: 16, right: 16, bottom: 16, left: 16 },
|
||||
})}
|
||||
borderless
|
||||
>
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
headerTintColor ? { tintColor: headerTintColor } : null,
|
||||
]}
|
||||
source={require('./assets/toggle-drawer-icon.png')}
|
||||
fadeDuration={0}
|
||||
/>
|
||||
</TouchableItem>
|
||||
);
|
||||
const rightButton = headerRight
|
||||
? headerRight({ tintColor: headerTintColor })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
{
|
||||
height: defaultHeight,
|
||||
backgroundColor: colors.card,
|
||||
borderBottomColor: colors.border,
|
||||
shadowColor: colors.border,
|
||||
},
|
||||
styles.container,
|
||||
headerStyle,
|
||||
]}
|
||||
>
|
||||
<View pointerEvents="none" style={{ height: headerStatusBarHeight }} />
|
||||
<View pointerEvents="box-none" style={styles.content}>
|
||||
{leftButton ? (
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[styles.left, { left: insets.left }]}
|
||||
>
|
||||
{leftButton}
|
||||
</View>
|
||||
) : null}
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
headerTitleAlign === 'left'
|
||||
? {
|
||||
position: 'absolute',
|
||||
left: (leftButton ? 72 : 16) + insets.left,
|
||||
right: (rightButton ? 72 : 16) + insets.right,
|
||||
}
|
||||
: {
|
||||
marginHorizontal:
|
||||
(leftButton ? 32 : 16) +
|
||||
Math.max(insets.left, insets.right),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{typeof headerTitle === 'function' ? (
|
||||
headerTitle({
|
||||
children: currentTitle,
|
||||
allowFontScaling: headerTitleAllowFontScaling,
|
||||
tintColor: headerTintColor,
|
||||
style: headerTitleStyle,
|
||||
})
|
||||
) : (
|
||||
<Text
|
||||
accessibilityRole="header"
|
||||
aria-level="1"
|
||||
numberOfLines={1}
|
||||
allowFontScaling={headerTitleAllowFontScaling}
|
||||
style={[
|
||||
styles.title,
|
||||
{ color: headerTintColor ?? colors.text },
|
||||
styles.title,
|
||||
headerTitleStyle,
|
||||
]}
|
||||
>
|
||||
{currentTitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{rightButton ? (
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[styles.right, { right: insets.right }]}
|
||||
>
|
||||
{rightButton}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 4,
|
||||
},
|
||||
ios: {
|
||||
shadowOpacity: 0.85,
|
||||
shadowRadius: 0,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: StyleSheet.hairlineWidth,
|
||||
},
|
||||
},
|
||||
default: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
}),
|
||||
zIndex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: Platform.select({
|
||||
ios: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
},
|
||||
android: {
|
||||
fontSize: 20,
|
||||
fontFamily: 'sans-serif-medium',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
default: {
|
||||
fontSize: 18,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}),
|
||||
icon: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
margin: 3,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
touchable: {
|
||||
marginHorizontal: 11,
|
||||
},
|
||||
left: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
right: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
});
|
||||
@@ -16,36 +16,56 @@ type Props = {
|
||||
|
||||
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() {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
const { isVisible, ...rest } = this.props;
|
||||
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen activityState={isVisible ? 2 : 0} {...rest} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen active={isVisible ? 1 : 0} {...rest} />
|
||||
);
|
||||
}
|
||||
export default function ResourceSavingScene({
|
||||
isVisible,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: Props) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen activityState={isVisible ? 2 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
|
||||
<Screen active={isVisible ? 1 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { isVisible, children, style, ...rest } = this.props;
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
return (
|
||||
<View
|
||||
// @ts-expect-error: hidden exists on web, but not in React Native
|
||||
hidden={!isVisible}
|
||||
style={[
|
||||
{ display: isVisible ? 'flex' : 'none' },
|
||||
styles.container,
|
||||
Platform.OS === 'web'
|
||||
? { display: isVisible ? 'flex' : 'none' }
|
||||
: { overflow: 'hidden' },
|
||||
style,
|
||||
]}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, style]}
|
||||
// box-none doesn't seem to work properly on Android
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={
|
||||
// On iOS, set removeClippedSubviews to true only when not focused
|
||||
@@ -53,14 +73,12 @@ export default class ResourceSavingScene extends React.Component<Props> {
|
||||
Platform.OS === 'ios' ? !isVisible : true
|
||||
}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
{...rest}
|
||||
style={isVisible ? styles.attached : styles.detached}
|
||||
>
|
||||
<View style={isVisible ? styles.attached : styles.detached}>
|
||||
{children}
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -5,14 +5,14 @@ import { BaseButton } from 'react-native-gesture-handler';
|
||||
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
|
||||
|
||||
type Props = React.ComponentProps<typeof BaseButton> & {
|
||||
activeOpacity: number;
|
||||
pressOpacity: number;
|
||||
};
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
export default class TouchableItem extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
activeOpacity: 0.3,
|
||||
pressOpacity: 0.3,
|
||||
borderless: true,
|
||||
enabled: true,
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export default class TouchableItem extends React.Component<Props> {
|
||||
overshootClamping: true,
|
||||
restDisplacementThreshold: 0.01,
|
||||
restSpeedThreshold: 0.01,
|
||||
toValue: active ? this.props.activeOpacity : 1,
|
||||
toValue: active ? this.props.pressOpacity : 1,
|
||||
useNativeDriver,
|
||||
}).start();
|
||||
|
||||
|
||||
BIN
packages/drawer/src/views/assets/toggle-drawer-icon.png
Normal file
|
After Width: | Height: | Size: 116 B |
|
After Width: | Height: | Size: 106 B |
BIN
packages/drawer/src/views/assets/toggle-drawer-icon@1.5x.ios.png
Normal file
|
After Width: | Height: | Size: 159 B |
|
After Width: | Height: | Size: 84 B |
BIN
packages/drawer/src/views/assets/toggle-drawer-icon@1x.ios.png
Normal file
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 100 B |
BIN
packages/drawer/src/views/assets/toggle-drawer-icon@2x.ios.png
Normal file
|
After Width: | Height: | Size: 163 B |
|
After Width: | Height: | Size: 126 B |
BIN
packages/drawer/src/views/assets/toggle-drawer-icon@3x.ios.png
Normal file
|
After Width: | Height: | Size: 212 B |
|
After Width: | Height: | Size: 116 B |
BIN
packages/drawer/src/views/assets/toggle-drawer-icon@4x.ios.png
Normal file
|
After Width: | Height: | Size: 219 B |
@@ -3,6 +3,76 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.3.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.14...@react-navigation/material-bottom-tabs@5.3.15) (2021-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't pass accessibilityState to link. closes [#9418](https://github.com/react-navigation/react-navigation/issues/9418) ([699ea0c](https://github.com/react-navigation/react-navigation/commit/699ea0cc5052f190acc7ce8bc0328bb052d7cf26))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.13...@react-navigation/material-bottom-tabs@5.3.14) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.12...@react-navigation/material-bottom-tabs@5.3.13) (2021-01-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.11...@react-navigation/material-bottom-tabs@5.3.12) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.10...@react-navigation/material-bottom-tabs@5.3.11) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle fallback for MaterialCommunityIcons better ([26074a2](https://github.com/react-navigation/react-navigation/commit/26074a28f768ba01743e2ca3b3cb9873a04c9d9c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.9...@react-navigation/material-bottom-tabs@5.3.10) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.8...@react-navigation/material-bottom-tabs@5.3.9) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.7...@react-navigation/material-bottom-tabs@5.3.8) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.6...@react-navigation/material-bottom-tabs@5.3.7) (2020-11-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-bottom-tabs",
|
||||
"description": "Integration for bottom navigation component from react-native-paper",
|
||||
"version": "5.3.7",
|
||||
"version": "5.3.15",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -41,8 +41,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "^0.63.30",
|
||||
@@ -50,6 +49,7 @@
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"react-native-paper": "^4.2.0",
|
||||
"react-native-vector-icons": "^7.0.0",
|
||||
"typescript": "^4.0.3"
|
||||
@@ -61,7 +61,7 @@
|
||||
"react-native-paper": ">= 3.0.0",
|
||||
"react-native-vector-icons": ">= 6.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, Platform } from 'react-native';
|
||||
import { Text, StyleSheet, Platform } from 'react-native';
|
||||
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
@@ -28,44 +28,48 @@ type Scene = { route: { key: string } };
|
||||
|
||||
// Optionally require vector-icons referenced from react-native-paper:
|
||||
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
|
||||
let MaterialCommunityIcons: any;
|
||||
let MaterialCommunityIcons: React.ComponentType<React.ComponentProps<
|
||||
typeof import('react-native-vector-icons/MaterialCommunityIcons').default
|
||||
>>;
|
||||
|
||||
try {
|
||||
// Optionally require vector-icons
|
||||
MaterialCommunityIcons = require('react-native-vector-icons/MaterialCommunityIcons')
|
||||
.default;
|
||||
} catch (e) {
|
||||
// @ts-expect-error
|
||||
if (global.__expo?.Icon?.MaterialCommunityIcons) {
|
||||
// Snack doesn't properly bundle vector icons from sub-path
|
||||
// Use icons from the __expo global if available
|
||||
// @ts-expect-error
|
||||
MaterialCommunityIcons = global.__expo.Icon.MaterialCommunityIcons;
|
||||
} else {
|
||||
let isErrorLogged = false;
|
||||
let isErrorLogged = false;
|
||||
|
||||
// Fallback component for icons
|
||||
MaterialCommunityIcons = () => {
|
||||
if (!isErrorLogged) {
|
||||
if (
|
||||
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
|
||||
e.message
|
||||
)
|
||||
) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons' could not be loaded.`,
|
||||
`To remove this warning, try installing 'react-native-vector-icons' or use another method.`
|
||||
);
|
||||
|
||||
isErrorLogged = true;
|
||||
// Fallback component for icons
|
||||
MaterialCommunityIcons = ({
|
||||
name,
|
||||
color,
|
||||
size,
|
||||
selectionColor: _,
|
||||
...rest
|
||||
}) => {
|
||||
if (!isErrorLogged) {
|
||||
if (
|
||||
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
|
||||
e.message
|
||||
)
|
||||
) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
console.warn(
|
||||
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons/MaterialCommunityIcons' could not be loaded.`,
|
||||
`To remove this warning, try installing 'react-native-vector-icons' or use another method to specify icon: https://reactnavigation.org/docs/material-bottom-tab-navigator/#tabbaricon.`
|
||||
);
|
||||
|
||||
isErrorLogged = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text {...rest} style={[styles.icon, { color, fontSize: size }]}>
|
||||
□
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function MaterialBottomTabViewInner({
|
||||
@@ -107,10 +111,13 @@ function MaterialBottomTabViewInner({
|
||||
? ({
|
||||
onPress,
|
||||
route,
|
||||
accessibilityRole: _0,
|
||||
borderless: _1,
|
||||
centered: _2,
|
||||
rippleColor: _3,
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
accessibilityRole,
|
||||
accessibilityState,
|
||||
borderless,
|
||||
centered,
|
||||
rippleColor,
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
style,
|
||||
...rest
|
||||
}) => {
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.3.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.14...@react-navigation/material-top-tabs@5.3.15) (2021-04-04)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.13...@react-navigation/material-top-tabs@5.3.14) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.12...@react-navigation/material-top-tabs@5.3.13) (2021-01-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.11...@react-navigation/material-top-tabs@5.3.12) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.10...@react-navigation/material-top-tabs@5.3.11) (2021-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.9...@react-navigation/material-top-tabs@5.3.10) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.8...@react-navigation/material-top-tabs@5.3.9) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.7...@react-navigation/material-top-tabs@5.3.8) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.6...@react-navigation/material-top-tabs@5.3.7) (2020-11-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-top-tabs",
|
||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||
"version": "5.3.7",
|
||||
"version": "5.3.15",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -44,14 +44,14 @@
|
||||
"color": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-native": "^0.63.30",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"react-native-gesture-handler": "~1.7.0",
|
||||
"react-native-reanimated": "~1.13.0",
|
||||
"react-native-tab-view": "^2.15.2",
|
||||
@@ -65,7 +65,7 @@
|
||||
"react-native-reanimated": ">= 1.0.0",
|
||||
"react-native-tab-view": ">= 2.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -3,6 +3,84 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.9.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.9.3...@react-navigation/native@5.9.4) (2021-04-04)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.9.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.9.2...@react-navigation/native@5.9.3) (2021-02-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* address breaking change in react-native for Linking ([a8342aa](https://github.com/react-navigation/react-navigation/commit/a8342aaf3d1ba8fb29faa91c7b63ed25f11745e5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.9.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.9.1...@react-navigation/native@5.9.2) (2021-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* normalize prefix when parsing. fixes [#9081](https://github.com/react-navigation/react-navigation/issues/9081) ([4ca2d2d](https://github.com/react-navigation/react-navigation/commit/4ca2d2d22bc9eccf87451b15c823174d98cbd0a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.9.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.9.0...@react-navigation/native@5.9.1) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.9.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.10...@react-navigation/native@5.9.0) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* support sync getInitialURL in native useLinking ([b26b907](https://github.com/react-navigation/react-navigation/commit/b26b90706fe0a0d914d4a868df1310d2dc3a7623))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose getActionForState in linking ([c9a5d45](https://github.com/react-navigation/react-navigation/commit/c9a5d4532406c6bfdac0c675a3fe4db5430e9a55))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.8.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.9...@react-navigation/native@5.8.10) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.8.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.8...@react-navigation/native@5.8.9) (2020-11-10)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.8.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.7...@react-navigation/native@5.8.8) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.8.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.6...@react-navigation/native@5.8.7) (2020-11-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/native",
|
||||
"description": "React Native integration for React Navigation",
|
||||
"version": "5.8.7",
|
||||
"version": "5.9.4",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
@@ -37,12 +37,11 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.14.1",
|
||||
"@react-navigation/core": "^5.15.3",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"nanoid": "^3.1.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
@@ -51,13 +50,14 @@
|
||||
"react": "~16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -5,4 +5,6 @@ const LinkingContext = React.createContext<{
|
||||
options: LinkingOptions | undefined;
|
||||
}>({ options: undefined });
|
||||
|
||||
LinkingContext.displayName = 'LinkingContext';
|
||||
|
||||
export default LinkingContext;
|
||||
|
||||
318
packages/native/src/__tests__/extractPathFromURL.test.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import extractPathFromURL from '../extractPathFromURL';
|
||||
|
||||
it('extracts path from URL with protocol', () => {
|
||||
expect(extractPathFromURL(['scheme://'], 'scheme://some/path')).toBe(
|
||||
'some/path'
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme://'], 'scheme:some/path')).toBe(
|
||||
'some/path'
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme://'], 'scheme:///some/path')).toBe(
|
||||
'some/path'
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:///'], 'scheme:some/path')).toBe(
|
||||
'some/path'
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:'], 'scheme:some/path')).toBe('some/path');
|
||||
|
||||
expect(extractPathFromURL(['scheme:'], 'scheme://some/path')).toBe(
|
||||
'some/path'
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:'], 'scheme:///some/path')).toBe(
|
||||
'some/path'
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts path from URL with protocol and host', () => {
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://example.com'],
|
||||
'scheme://example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme://example.com'], 'scheme:example.com/some/path')
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://example.com'],
|
||||
'scheme:///example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:///example.com'],
|
||||
'scheme:example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme:example.com'], 'scheme:example.com/some/path')
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme:example.com'], 'scheme://example.com/some/path')
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com'],
|
||||
'scheme:///example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
});
|
||||
|
||||
it('extracts path from URL with protocol and host with wildcard', () => {
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://*.example.com'],
|
||||
'scheme://test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://*.example.com'],
|
||||
'scheme:test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://*.example.com'],
|
||||
'scheme:///test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:///*.example.com'],
|
||||
'scheme:test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:*.example.com'],
|
||||
'scheme:test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:*.example.com'],
|
||||
'scheme://test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:*.example.com'],
|
||||
'scheme:///test.example.com/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
});
|
||||
|
||||
it('extracts path from URL with protocol, host and path', () => {
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://example.com/test'],
|
||||
'scheme://example.com/test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme://example.com'], 'scheme:example.com/some/path')
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://example.com/test'],
|
||||
'scheme:///example.com/test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:///example.com/test'],
|
||||
'scheme:example.com/test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com/test'],
|
||||
'scheme:example.com/test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com/test'],
|
||||
'scheme://example.com/test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com/test'],
|
||||
'scheme:///example.com/test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com/test'],
|
||||
'scheme:///example.com//test/some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com/test'],
|
||||
'scheme:///example.com/test//some/path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:example.com/test'],
|
||||
'scheme:///example.com/test/some//path'
|
||||
)
|
||||
).toBe('/some/path');
|
||||
});
|
||||
|
||||
it('returns undefined for non-matching protocol', () => {
|
||||
expect(extractPathFromURL(['scheme://'], 'foo://some/path')).toBe(undefined);
|
||||
|
||||
expect(extractPathFromURL(['scheme://'], 'foo:some/path')).toBe(undefined);
|
||||
|
||||
expect(extractPathFromURL(['scheme://'], 'foo:///some/path')).toBe(undefined);
|
||||
|
||||
expect(extractPathFromURL(['scheme:///'], 'foo:some/path')).toBe(undefined);
|
||||
|
||||
expect(extractPathFromURL(['scheme:'], 'foo:some/path')).toBe(undefined);
|
||||
|
||||
expect(extractPathFromURL(['scheme:'], 'foo://some/path')).toBe(undefined);
|
||||
|
||||
expect(extractPathFromURL(['scheme:'], 'foo:///some/path')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined for non-matching path', () => {
|
||||
expect(extractPathFromURL(['scheme://foo'], 'scheme://some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme://foo'], 'scheme:some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme://foo'], 'scheme:///some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:///foo'], 'scheme:some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:foo'], 'scheme:some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:foo'], 'scheme://some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(extractPathFromURL(['scheme:foo'], 'scheme:///some/path')).toBe(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for non-matching host', () => {
|
||||
expect(
|
||||
extractPathFromURL(['scheme://example.com'], 'scheme://foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme://example.com'], 'scheme:foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme://example.com'], 'scheme:///foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme:///example.com'], 'scheme:foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme:example.com'], 'scheme:foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme:example.com'], 'scheme://foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(['scheme:example.com'], 'scheme:///foo.com/some/path')
|
||||
).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined for non-matching host with wildcard', () => {
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://*.example.com'],
|
||||
'scheme://test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://*.example.com'],
|
||||
'scheme:test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme://*.example.com'],
|
||||
'scheme:///test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:///*.example.com'],
|
||||
'scheme:test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:*.example.com'],
|
||||
'scheme:test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:*.example.com'],
|
||||
'scheme://test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
|
||||
expect(
|
||||
extractPathFromURL(
|
||||
['scheme:*.example.com'],
|
||||
'scheme:///test.foo.com/some/path'
|
||||
)
|
||||
).toBe(undefined);
|
||||
});
|
||||
26
packages/native/src/extractPathFromURL.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
|
||||
export default function extractPathFromURL(prefixes: string[], url: string) {
|
||||
for (const prefix of prefixes) {
|
||||
const protocol = prefix.match(/^[^:]+:/)?.[0] ?? '';
|
||||
const host = prefix
|
||||
.replace(new RegExp(`^${escapeStringRegexp(protocol)}`), '')
|
||||
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
||||
.replace(/^\//, ''); // Remove extra leading slash
|
||||
|
||||
const prefixRegex = new RegExp(
|
||||
`^${escapeStringRegexp(protocol)}(/)*${host
|
||||
.split('.')
|
||||
.map((it) => (it === '*' ? '[^/]+' : escapeStringRegexp(it)))
|
||||
.join('\\.')}`
|
||||
);
|
||||
|
||||
const normalizedURL = url.replace(/\/+/g, '/');
|
||||
|
||||
if (prefixRegex.test(normalizedURL)) {
|
||||
return normalizedURL.replace(prefixRegex, '');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -4,4 +4,6 @@ import type { Theme } from '../types';
|
||||
|
||||
const ThemeContext = React.createContext<Theme>(DefaultTheme);
|
||||
|
||||
ThemeContext.displayName = 'ThemeContext';
|
||||
|
||||
export default ThemeContext;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
getStateFromPath as getStateFromPathDefault,
|
||||
getPathFromState as getPathFromStateDefault,
|
||||
getActionFromState as getActionFromStateDefault,
|
||||
PathConfigMap,
|
||||
Route,
|
||||
} from '@react-navigation/core';
|
||||
@@ -66,7 +67,11 @@ export type LinkingOptions = {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getInitialURL?: () => Promise<string | null | undefined>;
|
||||
getInitialURL?: () =>
|
||||
| string
|
||||
| null
|
||||
| undefined
|
||||
| Promise<string | null | undefined>;
|
||||
/**
|
||||
* Custom function to get subscribe to URL updates.
|
||||
* Uses `Linking.addEventListener('url', callback)` by default.
|
||||
@@ -90,11 +95,18 @@ export type LinkingOptions = {
|
||||
) => undefined | void | (() => void);
|
||||
/**
|
||||
* Custom function to parse the URL to a valid navigation state (advanced).
|
||||
* This state object will be passed as `initialState` for initial URL,
|
||||
* and converted to an action object to `dispatch` for subsequent URLs.
|
||||
*/
|
||||
getStateFromPath?: typeof getStateFromPathDefault;
|
||||
/**
|
||||
* Custom function to convert the state object to an action to dispatch (advanced).
|
||||
* By default, the state is converted to a `NAVIGATE` action.
|
||||
*/
|
||||
getActionFromState?: typeof getActionFromStateDefault;
|
||||
/**
|
||||
* Custom function to convert the state object to a valid URL (advanced).
|
||||
* Only applicable on Web.
|
||||
* Used for creating links for navigation, primarily useful on Web.
|
||||
*/
|
||||
getPathFromState?: typeof getPathFromStateDefault;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { Linking, Platform } from 'react-native';
|
||||
import {
|
||||
getActionFromState,
|
||||
getActionFromState as getActionFromStateDefault,
|
||||
getStateFromPath as getStateFromPathDefault,
|
||||
NavigationContainerRef,
|
||||
} from '@react-navigation/core';
|
||||
import extractPathFromURL from './extractPathFromURL';
|
||||
import type { LinkingOptions } from './types';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
|
||||
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||
|
||||
let isUsingLinking = false;
|
||||
|
||||
@@ -28,11 +30,21 @@ export default function useLinking(
|
||||
subscribe = (listener) => {
|
||||
const callback = ({ url }: { url: string }) => listener(url);
|
||||
|
||||
Linking.addEventListener('url', callback);
|
||||
const subscription = Linking.addEventListener('url', callback) as
|
||||
| { remove(): void }
|
||||
| undefined;
|
||||
|
||||
return () => Linking.removeEventListener('url', callback);
|
||||
return () => {
|
||||
// https://github.com/facebook/react-native/commit/6d1aca806cee86ad76de771ed3a1cc62982ebcd7
|
||||
if (subscription?.remove) {
|
||||
subscription.remove();
|
||||
} else {
|
||||
Linking.removeEventListener('url', callback);
|
||||
}
|
||||
};
|
||||
},
|
||||
getStateFromPath = getStateFromPathDefault,
|
||||
getActionFromState = getActionFromStateDefault,
|
||||
}: LinkingOptions
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
@@ -66,6 +78,7 @@ export default function useLinking(
|
||||
const configRef = React.useRef(config);
|
||||
const getInitialURLRef = React.useRef(getInitialURL);
|
||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||
const getActionFromStateRef = React.useRef(getActionFromState);
|
||||
|
||||
React.useEffect(() => {
|
||||
enabledRef.current = enabled;
|
||||
@@ -73,48 +86,53 @@ export default function useLinking(
|
||||
configRef.current = config;
|
||||
getInitialURLRef.current = getInitialURL;
|
||||
getStateFromPathRef.current = getStateFromPath;
|
||||
}, [config, enabled, prefixes, getInitialURL, getStateFromPath]);
|
||||
getActionFromStateRef.current = getActionFromState;
|
||||
});
|
||||
|
||||
const extractPathFromURL = React.useCallback((url: string) => {
|
||||
for (const prefix of prefixesRef.current) {
|
||||
const protocol = prefix.match(/^[^:]+:\/\//)?.[0] ?? '';
|
||||
const host = prefix.replace(protocol, '');
|
||||
const prefixRegex = new RegExp(
|
||||
`^${escapeStringRegexp(protocol)}${host
|
||||
.split('.')
|
||||
.map((it) => (it === '*' ? '[^/]+' : escapeStringRegexp(it)))
|
||||
.join('\\.')}`
|
||||
);
|
||||
if (prefixRegex.test(url)) {
|
||||
return url.replace(prefixRegex, '');
|
||||
const getInitialState = React.useCallback(() => {
|
||||
let state: ResultState | undefined;
|
||||
|
||||
if (enabledRef.current) {
|
||||
const url = getInitialURLRef.current();
|
||||
|
||||
if (url != null && typeof url !== 'string') {
|
||||
return url.then((url) => {
|
||||
const path = url
|
||||
? extractPathFromURL(prefixesRef.current, url)
|
||||
: null;
|
||||
|
||||
return path
|
||||
? getStateFromPathRef.current(path, configRef.current)
|
||||
: undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const path = url ? extractPathFromURL(prefixesRef.current, url) : null;
|
||||
|
||||
state = path
|
||||
? getStateFromPathRef.current(path, configRef.current)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const thenable = {
|
||||
then(onfulfilled?: (state: ResultState | undefined) => void) {
|
||||
return Promise.resolve(onfulfilled ? onfulfilled(state) : state);
|
||||
},
|
||||
catch() {
|
||||
return thenable;
|
||||
},
|
||||
};
|
||||
|
||||
return thenable as PromiseLike<ResultState | undefined>;
|
||||
}, []);
|
||||
|
||||
const getInitialState = React.useCallback(async () => {
|
||||
if (!enabledRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = await getInitialURLRef.current();
|
||||
const path = url ? extractPathFromURL(url) : null;
|
||||
|
||||
if (path) {
|
||||
return getStateFromPathRef.current(path, configRef.current);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [extractPathFromURL]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = (url: string) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = extractPathFromURL(url);
|
||||
const path = extractPathFromURL(prefixesRef.current, url);
|
||||
const navigation = ref.current;
|
||||
|
||||
if (navigation && path) {
|
||||
@@ -134,7 +152,10 @@ export default function useLinking(
|
||||
return;
|
||||
}
|
||||
|
||||
const action = getActionFromState(state, configRef.current);
|
||||
const action = getActionFromStateRef.current(
|
||||
state,
|
||||
configRef.current
|
||||
);
|
||||
|
||||
if (action !== undefined) {
|
||||
try {
|
||||
@@ -154,7 +175,7 @@ export default function useLinking(
|
||||
};
|
||||
|
||||
return subscribe(listener);
|
||||
}, [enabled, ref, subscribe, extractPathFromURL]);
|
||||
}, [enabled, ref, subscribe]);
|
||||
|
||||
return {
|
||||
getInitialState,
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from 'react';
|
||||
import {
|
||||
getStateFromPath as getStateFromPathDefault,
|
||||
getPathFromState as getPathFromStateDefault,
|
||||
getActionFromState as getActionFromStateDefault,
|
||||
NavigationContainerRef,
|
||||
NavigationState,
|
||||
getActionFromState,
|
||||
} from '@react-navigation/core';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import ServerContext from './ServerContext';
|
||||
@@ -134,7 +134,7 @@ const createMemoryHistory = () => {
|
||||
// - There's history to go back, `history.go` is called, and `popstate` fires
|
||||
// - `history.go` is called multiple times, we need to resolve on respective `popstate`
|
||||
// - No history to go back, but `history.go` was called, browser has no API to detect it
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const done = (interrupted?: boolean) => {
|
||||
clearTimeout(timer);
|
||||
|
||||
@@ -293,6 +293,7 @@ export default function useLinking(
|
||||
config,
|
||||
getStateFromPath = getStateFromPathDefault,
|
||||
getPathFromState = getPathFromStateDefault,
|
||||
getActionFromState = getActionFromStateDefault,
|
||||
}: LinkingOptions
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
@@ -323,14 +324,16 @@ export default function useLinking(
|
||||
const enabledRef = React.useRef(enabled);
|
||||
const configRef = React.useRef(config);
|
||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||
const getActionFromStateRef = React.useRef(getActionFromState);
|
||||
const getPathFromStateRef = React.useRef(getPathFromState);
|
||||
|
||||
React.useEffect(() => {
|
||||
enabledRef.current = enabled;
|
||||
configRef.current = config;
|
||||
getStateFromPathRef.current = getStateFromPath;
|
||||
getActionFromStateRef.current = getActionFromState;
|
||||
getPathFromStateRef.current = getPathFromState;
|
||||
}, [config, enabled, getPathFromState, getStateFromPath]);
|
||||
});
|
||||
|
||||
const server = React.useContext(ServerContext);
|
||||
|
||||
@@ -349,7 +352,6 @@ export default function useLinking(
|
||||
}
|
||||
}
|
||||
|
||||
// Make it a thenable to keep consistent with the native impl
|
||||
const thenable = {
|
||||
then(onfulfilled?: (state: ResultState | undefined) => void) {
|
||||
return Promise.resolve(onfulfilled ? onfulfilled(value) : value);
|
||||
@@ -412,7 +414,10 @@ export default function useLinking(
|
||||
}
|
||||
|
||||
if (index > previousIndex) {
|
||||
const action = getActionFromState(state, configRef.current);
|
||||
const action = getActionFromStateRef.current(
|
||||
state,
|
||||
configRef.current
|
||||
);
|
||||
|
||||
if (action !== undefined) {
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,53 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.7.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.7.1...@react-navigation/routers@5.7.2) (2021-02-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix getId being called for incorrect routes. closes [#9343](https://github.com/react-navigation/react-navigation/issues/9343) ([3728390](https://github.com/react-navigation/react-navigation/commit/3728390b60814ba414bd15cc5b7e5b51baa1f026))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.7.0...@react-navigation/routers@5.7.1) (2021-01-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix StackRouter incorrectly handling invalid route if key is present ([d3a9639](https://github.com/react-navigation/react-navigation/commit/d3a9639060631b06551daf0eac191ec1a442e298))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.6.2...@react-navigation/routers@5.7.0) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* consider openByDefault prop when rehydrating drawer state ([#9099](https://github.com/react-navigation/react-navigation/issues/9099)) ([2ad61a6](https://github.com/react-navigation/react-navigation/commit/2ad61a67357242fc4663ecad62ab311facbaf1be))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a new backBehavior: firstRoute for TabRouter ([3c87419](https://github.com/react-navigation/react-navigation/commit/3c874191ffbd24b953ded5b62f606c4cc47e5651))
|
||||
* add a way to specify an unique ID for screens ([b19f76b](https://github.com/react-navigation/react-navigation/commit/b19f76bfffe623759e67d925bfd067c753a453bf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.6.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.6.1...@react-navigation/routers@5.6.2) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/routers
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.6.0...@react-navigation/routers@5.6.1) (2020-11-08)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/routers
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/routers",
|
||||
"description": "Routers to help build custom navigators",
|
||||
"version": "5.6.1",
|
||||
"version": "5.7.2",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -39,11 +39,11 @@
|
||||
"nanoid": "^3.1.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"del-cli": "^3.0.1",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -75,7 +75,7 @@ const isDrawerOpen = (
|
||||
state:
|
||||
| DrawerNavigationState<ParamListBase>
|
||||
| PartialState<DrawerNavigationState<ParamListBase>>
|
||||
) => Boolean(state.history?.find((it) => it.type === 'drawer'));
|
||||
) => Boolean(state.history?.some((it) => it.type === 'drawer'));
|
||||
|
||||
const openDrawer = (
|
||||
state: DrawerNavigationState<ParamListBase>
|
||||
@@ -120,8 +120,12 @@ export default function DrawerRouter({
|
||||
|
||||
type: 'drawer',
|
||||
|
||||
getInitialState({ routeNames, routeParamList }) {
|
||||
let state = router.getInitialState({ routeNames, routeParamList });
|
||||
getInitialState({ routeNames, routeParamList, routeGetIdList }) {
|
||||
let state = router.getInitialState({
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
});
|
||||
|
||||
if (openByDefault) {
|
||||
state = openDrawer(state);
|
||||
@@ -135,7 +139,10 @@ export default function DrawerRouter({
|
||||
};
|
||||
},
|
||||
|
||||
getRehydratedState(partialState, { routeNames, routeParamList }) {
|
||||
getRehydratedState(
|
||||
partialState,
|
||||
{ routeNames, routeParamList, routeGetIdList }
|
||||
) {
|
||||
if (partialState.stale === false) {
|
||||
return partialState;
|
||||
}
|
||||
@@ -143,9 +150,10 @@ export default function DrawerRouter({
|
||||
let state = router.getRehydratedState(partialState, {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
routeGetIdList,
|
||||
});
|
||||
|
||||
if (isDrawerOpen(partialState)) {
|
||||
if (partialState.history ? isDrawerOpen(partialState) : openByDefault) {
|
||||
state = openDrawer(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,9 @@ export default function StackRouter(options: StackRouterOptions) {
|
||||
|
||||
case 'PUSH':
|
||||
if (state.routeNames.includes(action.payload.name)) {
|
||||
const getId = options.routeGetIdList[action.payload.name];
|
||||
const id = getId?.({ params: action.payload.params });
|
||||
|
||||
const route =
|
||||
action.payload.name && action.payload.key
|
||||
? state.routes.find(
|
||||
@@ -265,34 +268,34 @@ export default function StackRouter(options: StackRouterOptions) {
|
||||
route.name === action.payload.name &&
|
||||
route.key === action.payload.key
|
||||
)
|
||||
: id
|
||||
? state.routes.find(
|
||||
(route) =>
|
||||
route.name === action.payload.name &&
|
||||
id === getId?.({ params: route.params })
|
||||
)
|
||||
: undefined;
|
||||
|
||||
let routes: Route<string>[];
|
||||
|
||||
if (route) {
|
||||
routes = state.routes.filter((r) => r.key !== route.key);
|
||||
routes.push(
|
||||
action.payload.params
|
||||
? {
|
||||
...route,
|
||||
params:
|
||||
action.payload.params !== undefined
|
||||
? {
|
||||
...route.params,
|
||||
...action.payload.params,
|
||||
}
|
||||
: route.params,
|
||||
}
|
||||
: route
|
||||
);
|
||||
routes.push({
|
||||
...route,
|
||||
params:
|
||||
action.payload.params !== undefined
|
||||
? {
|
||||
...route.params,
|
||||
...action.payload.params,
|
||||
}
|
||||
: route.params,
|
||||
});
|
||||
} else {
|
||||
routes = [
|
||||
...state.routes,
|
||||
{
|
||||
key:
|
||||
action.payload.key === undefined
|
||||
? `${action.payload.name}-${nanoid()}`
|
||||
: action.payload.key,
|
||||
action.payload.key ?? `${action.payload.name}-${nanoid()}`,
|
||||
name: action.payload.name,
|
||||
params:
|
||||
routeParamList[action.payload.name] !== undefined
|
||||
@@ -348,14 +351,31 @@ export default function StackRouter(options: StackRouterOptions) {
|
||||
|
||||
case 'NAVIGATE':
|
||||
if (
|
||||
action.payload.key ||
|
||||
(action.payload.name &&
|
||||
state.routeNames.includes(action.payload.name))
|
||||
action.payload.name !== undefined &&
|
||||
!state.routeNames.includes(action.payload.name)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action.payload.key || action.payload.name) {
|
||||
// If the route already exists, navigate to that
|
||||
let index = -1;
|
||||
|
||||
if (
|
||||
const getId =
|
||||
// `getId` and `key` can't be used together
|
||||
action.payload.key === undefined &&
|
||||
action.payload.name !== undefined
|
||||
? options.routeGetIdList[action.payload.name]
|
||||
: undefined;
|
||||
const id = getId?.({ params: action.payload.params });
|
||||
|
||||
if (id) {
|
||||
index = state.routes.findIndex(
|
||||
(route) =>
|
||||
route.name === action.payload.name &&
|
||||
id === getId?.({ params: route.params })
|
||||
);
|
||||
} else if (
|
||||
(state.routes[state.index].name === action.payload.name &&
|
||||
action.payload.key === undefined) ||
|
||||
state.routes[state.index].key === action.payload.key
|
||||
@@ -383,18 +403,27 @@ export default function StackRouter(options: StackRouterOptions) {
|
||||
}
|
||||
|
||||
if (index === -1 && action.payload.name !== undefined) {
|
||||
return router.getStateForAction(
|
||||
state,
|
||||
const routes = [
|
||||
...state.routes,
|
||||
{
|
||||
type: 'PUSH',
|
||||
payload: {
|
||||
key: action.payload.key,
|
||||
name: action.payload.name,
|
||||
params: action.payload.params,
|
||||
},
|
||||
key:
|
||||
action.payload.key ?? `${action.payload.name}-${nanoid()}`,
|
||||
name: action.payload.name,
|
||||
params:
|
||||
routeParamList[action.payload.name] !== undefined
|
||||
? {
|
||||
...routeParamList[action.payload.name],
|
||||
...action.payload.params,
|
||||
}
|
||||
: action.payload.params,
|
||||
},
|
||||
options
|
||||
);
|
||||
];
|
||||
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
index: routes.length - 1,
|
||||
};
|
||||
}
|
||||
|
||||
const route = state.routes[index];
|
||||
|
||||
@@ -17,7 +17,12 @@ export type TabActionType = {
|
||||
target?: string;
|
||||
};
|
||||
|
||||
export type BackBehavior = 'initialRoute' | 'order' | 'history' | 'none';
|
||||
export type BackBehavior =
|
||||
| 'initialRoute'
|
||||
| 'firstRoute'
|
||||
| 'history'
|
||||
| 'order'
|
||||
| 'none';
|
||||
|
||||
export type TabRouterOptions = DefaultRouterOptions & {
|
||||
backBehavior?: BackBehavior;
|
||||
@@ -74,13 +79,21 @@ const getRouteHistory = (
|
||||
history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key });
|
||||
}
|
||||
break;
|
||||
case 'firstRoute':
|
||||
if (index !== 0) {
|
||||
history.unshift({
|
||||
type: TYPE_ROUTE,
|
||||
key: routes[0].key,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'initialRoute':
|
||||
initialRouteIndex = routes.findIndex(
|
||||
(route) => route.name === initialRouteName
|
||||
);
|
||||
initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
|
||||
|
||||
if (initialRouteIndex !== index) {
|
||||
if (index !== initialRouteIndex) {
|
||||
history.unshift({
|
||||
type: TYPE_ROUTE,
|
||||
key: routes[initialRouteIndex].key,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DrawerActions,
|
||||
DrawerNavigationState,
|
||||
ParamListBase,
|
||||
RouterConfigOptions,
|
||||
} from '..';
|
||||
|
||||
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
|
||||
@@ -18,6 +19,7 @@ it('gets initial state from route names and params with initialRouteName', () =>
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 1,
|
||||
@@ -44,6 +46,7 @@ it('gets initial state from route names and params without initialRouteName', ()
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 0,
|
||||
@@ -60,15 +63,43 @@ it('gets initial state from route names and params without initialRouteName', ()
|
||||
});
|
||||
});
|
||||
|
||||
it('gets initial state from route names and params with openByDefault', () => {
|
||||
const router = DrawerRouter({ openByDefault: true });
|
||||
|
||||
expect(
|
||||
router.getInitialState({
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 0,
|
||||
key: 'drawer-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||
],
|
||||
history: [{ type: 'route', key: 'bar-test' }, { type: 'drawer' }],
|
||||
stale: false,
|
||||
type: 'drawer',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets rehydrated state from partial state', () => {
|
||||
const router = DrawerRouter({});
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -218,15 +249,87 @@ it("doesn't rehydrate state if it's not stale", () => {
|
||||
router.getRehydratedState(state, {
|
||||
routeNames: [],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toBe(state);
|
||||
});
|
||||
|
||||
it('respects openByDefault when rehydrating', () => {
|
||||
const router = DrawerRouter({ openByDefault: true });
|
||||
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getRehydratedState(
|
||||
{
|
||||
index: 0,
|
||||
key: 'drawer-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||
],
|
||||
},
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
index: 0,
|
||||
key: 'drawer-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||
],
|
||||
history: [{ key: 'bar-test', type: 'route' }, { type: 'drawer' }],
|
||||
stale: false,
|
||||
type: 'drawer',
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getRehydratedState(
|
||||
{
|
||||
index: 0,
|
||||
key: 'drawer-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||
],
|
||||
history: [{ type: 'route', key: 'bar-test' }],
|
||||
},
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
index: 0,
|
||||
key: 'drawer-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||
],
|
||||
history: [{ type: 'route', key: 'bar-test' }],
|
||||
stale: false,
|
||||
type: 'drawer',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles navigate action', () => {
|
||||
const router = DrawerRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -265,9 +368,10 @@ it('handles navigate action', () => {
|
||||
|
||||
it('handles navigate action with open drawer', () => {
|
||||
const router = DrawerRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -306,9 +410,10 @@ it('handles navigate action with open drawer', () => {
|
||||
|
||||
it('handles open drawer action', () => {
|
||||
const router = DrawerRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -361,9 +466,10 @@ it('handles open drawer action', () => {
|
||||
|
||||
it('handles close drawer action', () => {
|
||||
const router = DrawerRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -419,9 +525,10 @@ it('handles close drawer action', () => {
|
||||
|
||||
it('handles toggle drawer action', () => {
|
||||
const router = DrawerRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { CommonActions, StackRouter, StackActions } from '..';
|
||||
import {
|
||||
CommonActions,
|
||||
StackRouter,
|
||||
StackActions,
|
||||
RouterConfigOptions,
|
||||
} from '..';
|
||||
|
||||
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
|
||||
|
||||
@@ -12,6 +17,7 @@ it('gets initial state from route names and params with initialRouteName', () =>
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 0,
|
||||
@@ -33,6 +39,7 @@ it('gets initial state from route names and params without initialRouteName', ()
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 0,
|
||||
@@ -47,12 +54,13 @@ it('gets initial state from route names and params without initialRouteName', ()
|
||||
it('gets rehydrated state from partial state', () => {
|
||||
const router = StackRouter({});
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -136,6 +144,7 @@ it("doesn't rehydrate state if it's not stale", () => {
|
||||
router.getRehydratedState(state, {
|
||||
routeNames: [],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toBe(state);
|
||||
});
|
||||
@@ -163,6 +172,7 @@ it('gets state on route names change', () => {
|
||||
qux: { name: 'John' },
|
||||
fiz: { fruit: 'apple' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -195,6 +205,7 @@ it('gets state on route names change', () => {
|
||||
routeParamList: {
|
||||
baz: { name: 'John' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -228,6 +239,7 @@ it('gets state on route names change with initialRouteName', () => {
|
||||
routeParamList: {
|
||||
baz: { name: 'John' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -242,9 +254,10 @@ it('gets state on route names change with initialRouteName', () => {
|
||||
|
||||
it('handles navigate action', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -427,11 +440,230 @@ it('handles navigate action', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles go back action', () => {
|
||||
it("doesn't navigate to nonexistent screen", () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
},
|
||||
CommonActions.navigate('far', { answer: 42 }),
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
},
|
||||
CommonActions.navigate({
|
||||
name: 'far',
|
||||
key: 'test',
|
||||
params: { answer: 42 },
|
||||
}),
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('ensures unique ID for navigate', () => {
|
||||
const router = StackRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {
|
||||
bar: ({ params }) => params?.foo,
|
||||
qux: ({ params }) => params?.fux,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 0,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [{ key: 'bar', name: 'bar' }],
|
||||
},
|
||||
CommonActions.navigate('bar', { foo: 'a' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
CommonActions.navigate('bar', { foo: 'a' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
CommonActions.navigate('bar', { foo: 'b' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 2,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'b' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
CommonActions.navigate({
|
||||
key: 'test',
|
||||
name: 'bar',
|
||||
params: { foo: 'a' },
|
||||
}),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 2,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
{ key: 'test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('ensure unique ID is only per route name for navigate', () => {
|
||||
const router = StackRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {
|
||||
baz: ({ params }) => params?.foo,
|
||||
bar: ({ params }) => params?.foo,
|
||||
qux: ({ params }) => params?.test,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'qux-test', name: 'qux', params: { test: 'a' } },
|
||||
{ key: 'baz-test', name: 'baz', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
CommonActions.navigate('bar', { foo: 'a' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 2,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'qux-test', name: 'qux', params: { test: 'a' } },
|
||||
{ key: 'baz-test', name: 'baz', params: { foo: 'a' } },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles go back action', () => {
|
||||
const router = StackRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -477,9 +709,10 @@ it('handles go back action', () => {
|
||||
|
||||
it('handles pop action', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -650,9 +883,10 @@ it('handles pop action', () => {
|
||||
|
||||
it('handles pop to top action', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -684,9 +918,10 @@ it('handles pop to top action', () => {
|
||||
|
||||
it('replaces focused screen with replace', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -722,9 +957,10 @@ it('replaces focused screen with replace', () => {
|
||||
|
||||
it('replaces active screen with replace', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -763,9 +999,10 @@ it('replaces active screen with replace', () => {
|
||||
|
||||
it("doesn't handle replace if source key isn't present", () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -794,9 +1031,10 @@ it("doesn't handle replace if source key isn't present", () => {
|
||||
|
||||
it("doesn't handle replace if screen to replace with isn't present", () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -824,11 +1062,12 @@ it("doesn't handle replace if screen to replace with isn't present", () => {
|
||||
|
||||
it('handles push action', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { foo: 21 },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -895,6 +1134,152 @@ it('handles push action', () => {
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("doesn't push nonexistent screen", () => {
|
||||
const router = StackRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
},
|
||||
StackActions.push('far', { answer: 42 }),
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'PUSH',
|
||||
payload: {
|
||||
name: 'far',
|
||||
key: 'test',
|
||||
params: { answer: 42 },
|
||||
},
|
||||
},
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('ensures unique ID for push', () => {
|
||||
const router = StackRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {
|
||||
bar: ({ params }) => params?.foo,
|
||||
qux: ({ params }) => params?.fux,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 0,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [{ key: 'bar', name: 'bar' }],
|
||||
},
|
||||
StackActions.push('bar', { foo: 'a' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
StackActions.push('bar', { foo: 'a' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
StackActions.push('bar', { foo: 'b' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 2,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'b' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
@@ -964,57 +1349,54 @@ it('handles push action', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('changes index on focus change', () => {
|
||||
it('ensure unique ID is only per route name for push', () => {
|
||||
const router = StackRouter({});
|
||||
|
||||
expect(
|
||||
router.getStateForRouteFocus(
|
||||
{
|
||||
index: 2,
|
||||
key: 'stack-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-0', name: 'bar' },
|
||||
{ key: 'baz-0', name: 'baz' },
|
||||
{ key: 'qux-0', name: 'qux' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
},
|
||||
'baz-0'
|
||||
)
|
||||
).toEqual({
|
||||
index: 1,
|
||||
key: 'stack-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-0', name: 'bar' },
|
||||
{ key: 'baz-0', name: 'baz' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
key: 'stack-test',
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-0', name: 'bar' },
|
||||
{ key: 'baz-0', name: 'baz' },
|
||||
],
|
||||
stale: false as const,
|
||||
type: 'stack' as const,
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {
|
||||
baz: ({ params }) => params?.foo,
|
||||
bar: ({ params }) => params?.foo,
|
||||
qux: ({ params }) => params?.test,
|
||||
},
|
||||
};
|
||||
|
||||
expect(router.getStateForRouteFocus(state, 'qux-0')).toEqual(state);
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'qux-test', name: 'qux', params: { test: 'a' } },
|
||||
{ key: 'baz-test', name: 'baz', params: { foo: 'a' } },
|
||||
],
|
||||
},
|
||||
StackActions.push('bar', { foo: 'a' }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 2,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'qux-test', name: 'qux', params: { test: 'a' } },
|
||||
{ key: 'baz-test', name: 'baz', params: { foo: 'a' } },
|
||||
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('merges params on navigate to an existing screen', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -1077,11 +1459,12 @@ it('merges params on navigate to an existing screen', () => {
|
||||
|
||||
it("doesn't merge params on navigate to an existing screen if merge: false", () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { foo: 12 },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
TabActions,
|
||||
TabNavigationState,
|
||||
ParamListBase,
|
||||
RouterConfigOptions,
|
||||
} from '..';
|
||||
|
||||
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
|
||||
@@ -18,6 +19,7 @@ it('gets initial state from route names and params with initialRouteName', () =>
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 1,
|
||||
@@ -44,6 +46,7 @@ it('gets initial state from route names and params without initialRouteName', ()
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toEqual({
|
||||
index: 0,
|
||||
@@ -63,12 +66,13 @@ it('gets initial state from route names and params without initialRouteName', ()
|
||||
it('gets rehydrated state from partial state', () => {
|
||||
const router = TabRouter({});
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {
|
||||
baz: { answer: 42 },
|
||||
qux: { name: 'Jane' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -241,6 +245,7 @@ it("doesn't rehydrate state if it's not stale", () => {
|
||||
router.getRehydratedState(state, {
|
||||
routeNames: [],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
})
|
||||
).toBe(state);
|
||||
});
|
||||
@@ -248,9 +253,10 @@ it("doesn't rehydrate state if it's not stale", () => {
|
||||
it('restores correct history on rehydrating with backBehavior: order', () => {
|
||||
const router = TabRouter({ backBehavior: 'order' });
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -289,9 +295,10 @@ it('restores correct history on rehydrating with backBehavior: order', () => {
|
||||
it('restores correct history on rehydrating with backBehavior: history', () => {
|
||||
const router = TabRouter({ backBehavior: 'history' });
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -323,12 +330,16 @@ it('restores correct history on rehydrating with backBehavior: history', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('restores correct history on rehydrating with backBehavior: initialRoute', () => {
|
||||
const router = TabRouter({ backBehavior: 'initialRoute' });
|
||||
it('restores correct history on rehydrating with backBehavior: firstRoute', () => {
|
||||
const router = TabRouter({
|
||||
backBehavior: 'firstRoute',
|
||||
initialRouteName: 'bar',
|
||||
});
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -363,12 +374,57 @@ it('restores correct history on rehydrating with backBehavior: initialRoute', ()
|
||||
});
|
||||
});
|
||||
|
||||
it('restores correct history on rehydrating with backBehavior: initialRoute', () => {
|
||||
const router = TabRouter({
|
||||
backBehavior: 'initialRoute',
|
||||
initialRouteName: 'bar',
|
||||
});
|
||||
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getRehydratedState(
|
||||
{
|
||||
index: 2,
|
||||
routes: [
|
||||
{ key: 'foo-0', name: 'foo' },
|
||||
{ key: 'bar-0', name: 'bar' },
|
||||
{ key: 'baz-0', name: 'baz' },
|
||||
{ key: 'qux-0', name: 'qux' },
|
||||
],
|
||||
},
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
key: 'tab-test',
|
||||
index: 2,
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'foo-0', name: 'foo' },
|
||||
{ key: 'bar-0', name: 'bar' },
|
||||
{ key: 'baz-0', name: 'baz' },
|
||||
{ key: 'qux-0', name: 'qux' },
|
||||
],
|
||||
history: [
|
||||
{ key: 'bar-0', type: 'route' },
|
||||
{ key: 'baz-0', type: 'route' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
});
|
||||
});
|
||||
|
||||
it('restores correct history on rehydrating with backBehavior: none', () => {
|
||||
const router = TabRouter({ backBehavior: 'none' });
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -424,6 +480,7 @@ it('gets state on route names change', () => {
|
||||
qux: { name: 'John' },
|
||||
fiz: { fruit: 'apple' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -458,6 +515,7 @@ it('gets state on route names change', () => {
|
||||
{
|
||||
routeNames: ['foo', 'fiz'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -498,6 +556,7 @@ it('preserves focused route on route names change', () => {
|
||||
qux: { name: 'John' },
|
||||
fiz: { fruit: 'apple' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -540,6 +599,7 @@ it('falls back to first route if route is removed on route names change', () =>
|
||||
qux: { name: 'John' },
|
||||
fiz: { fruit: 'apple' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
@@ -559,9 +619,10 @@ it('falls back to first route if route is removed on route names change', () =>
|
||||
|
||||
it('handles navigate action', () => {
|
||||
const router = TabRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -647,11 +708,63 @@ it('handles navigate action', () => {
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("doesn't navigate to nonexistent screen", () => {
|
||||
const router = TabRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
history: [{ type: 'route', key: 'bar' }],
|
||||
},
|
||||
CommonActions.navigate('foo', { answer: 42 }),
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
history: [{ type: 'route', key: 'bar' }],
|
||||
},
|
||||
CommonActions.navigate({
|
||||
name: 'foo',
|
||||
key: 'test',
|
||||
params: { answer: 42 },
|
||||
}),
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('handles jump to action', () => {
|
||||
const router = TabRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -688,11 +801,40 @@ it('handles jump to action', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't jump to nonexistent screen", () => {
|
||||
const router = TabRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routeNames: ['baz', 'bar'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
history: [{ type: 'route', key: 'bar' }],
|
||||
},
|
||||
TabActions.jumpTo('foo', { answer: 42 }),
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('handles back action with backBehavior: history', () => {
|
||||
const router = TabRouter({ backBehavior: 'history' });
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
let state = router.getInitialState(options);
|
||||
@@ -776,9 +918,10 @@ it('handles back action with backBehavior: history', () => {
|
||||
|
||||
it('handles back action with backBehavior: order', () => {
|
||||
const router = TabRouter({ backBehavior: 'order' });
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
let state = router.getInitialState(options);
|
||||
@@ -847,9 +990,10 @@ it('handles back action with backBehavior: order', () => {
|
||||
|
||||
it('handles back action with backBehavior: initialRoute', () => {
|
||||
const router = TabRouter({ backBehavior: 'initialRoute' });
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
let state = router.getInitialState(options);
|
||||
@@ -919,9 +1063,10 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
|
||||
initialRouteName: 'baz',
|
||||
});
|
||||
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
let state = router.getInitialState(options);
|
||||
@@ -987,9 +1132,10 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
|
||||
|
||||
it('handles back action with backBehavior: none', () => {
|
||||
const router = TabRouter({ backBehavior: 'none' });
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
let state = router.getInitialState(options);
|
||||
@@ -1007,9 +1153,10 @@ it('handles back action with backBehavior: none', () => {
|
||||
|
||||
it('updates route key history on navigate and jump to', () => {
|
||||
const router = TabRouter({ backBehavior: 'history' });
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
let state: TabNavigationState<ParamListBase> = {
|
||||
@@ -1110,9 +1257,10 @@ it('updates route key history on focus change', () => {
|
||||
|
||||
it('merges params on navigate to an existing screen', () => {
|
||||
const router = TabRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -1193,6 +1341,7 @@ it("doesn't merge params on navigate to an existing screen if merge: false", ()
|
||||
routeParamList: {
|
||||
qux: { color: 'indigo' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -1235,6 +1384,17 @@ it("doesn't merge params on navigate to an existing screen if merge: false", ()
|
||||
{ type: 'route', key: 'bar' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('merges params on navigate to an existing screen if merge: true', () => {
|
||||
const router = TabRouter({});
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {
|
||||
qux: { color: 'indigo' },
|
||||
},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
@@ -1323,9 +1483,10 @@ it("doesn't merge params on navigate to an existing screen if merge: false", ()
|
||||
|
||||
it('merges params on jump to an existing screen', () => {
|
||||
const router = TabRouter({});
|
||||
const options = {
|
||||
const options: RouterConfigOptions = {
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routeParamList: {},
|
||||
routeGetIdList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
|
||||
@@ -132,6 +132,11 @@ export type RouterFactory<
|
||||
export type RouterConfigOptions = {
|
||||
routeNames: string[];
|
||||
routeParamList: ParamListBase;
|
||||
routeGetIdList: Record<
|
||||
string,
|
||||
| ((options: { params?: Record<string, any> }) => string | undefined)
|
||||
| undefined
|
||||
>;
|
||||
};
|
||||
|
||||
export type Router<
|
||||
|
||||
@@ -3,6 +3,120 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.14.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.14.4...@react-navigation/stack@5.14.5) (2021-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pressOpacity for ios Touchable ([9fb4202](https://github.com/react-navigation/react-navigation/commit/9fb420259758405efa819bf6d01636f46f7f5033))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.14.3...@react-navigation/stack@5.14.4) (2021-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check for screens enabled in ScreenContainer ([493956e](https://github.com/react-navigation/react-navigation/commit/493956ef717a03bd8c3533a2949434e83718c5e4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.14.2...@react-navigation/stack@5.14.3) (2021-02-21)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.14.1...@react-navigation/stack@5.14.2) (2021-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix transparent modal on web ([38d6808](https://github.com/react-navigation/react-navigation/commit/38d680833e31e62736da19f79328aec553ced814))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.14.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.14.0...@react-navigation/stack@5.14.1) (2021-01-22)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.14.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.13.0...@react-navigation/stack@5.14.0) (2021-01-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add pressColor and pressOpacity props to drawerItem ([#8834](https://github.com/react-navigation/react-navigation/issues/8834)) ([bae4019](https://github.com/react-navigation/react-navigation/commit/bae4019995062c682f0213c121b7927ab8006c1e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.13.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.8...@react-navigation/stack@5.13.0) (2021-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable detachInactiveScreens by default on web for better a11y ([dd87fa4](https://github.com/react-navigation/react-navigation/commit/dd87fa49a43ad8db105a62418243339e4150fadf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* export TransitionPreset for custom TransitionPresets ([#9173](https://github.com/react-navigation/react-navigation/issues/9173)) ([9633c4d](https://github.com/react-navigation/react-navigation/commit/9633c4d35fe2f9cb4f37a7629872e436a4528238))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.7...@react-navigation/stack@5.12.8) (2020-11-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* force dismiss keyboard if there was no gesture ([3e069b7](https://github.com/react-navigation/react-navigation/commit/3e069b718d60f5381957f2d3838ee04ee9384779)), closes [#9078](https://github.com/react-navigation/react-navigation/issues/9078)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.6...@react-navigation/stack@5.12.7) (2020-11-20)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.5...@react-navigation/stack@5.12.6) (2020-11-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure inactive screen don't increase scroll area on web ([da35085](https://github.com/react-navigation/react-navigation/commit/da35085f1e3440f26eea800c892c88aec64d072f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.4...@react-navigation/stack@5.12.5) (2020-11-09)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.12.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.3...@react-navigation/stack@5.12.4) (2020-11-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/stack",
|
||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||
"version": "5.12.4",
|
||||
"version": "5.14.5",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -44,9 +44,8 @@
|
||||
"react-native-iphone-x-helper": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.16.2",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@testing-library/react-native": "^7.1.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.53",
|
||||
@@ -54,6 +53,7 @@
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native": "~0.63.2",
|
||||
"react-native-builder-bob": "^0.17.0",
|
||||
"react-native-gesture-handler": "~1.7.0",
|
||||
"react-native-safe-area-context": "3.1.4",
|
||||
"react-native-screens": "~2.10.1",
|
||||
@@ -68,7 +68,7 @@
|
||||
"react-native-safe-area-context": ">= 0.6.0",
|
||||
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
|
||||
@@ -61,4 +61,5 @@ export type {
|
||||
StackHeaderInterpolatedStyle,
|
||||
StackHeaderInterpolationProps,
|
||||
StackHeaderStyleInterpolator,
|
||||
TransitionPreset,
|
||||
} from './types';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseButton } from 'react-native-gesture-handler';
|
||||
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
|
||||
|
||||
type Props = React.ComponentProps<typeof BaseButton> & {
|
||||
activeOpacity: number;
|
||||
pressOpacity: number;
|
||||
};
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
@@ -27,7 +27,7 @@ export default class BorderlessButton extends React.Component<Props> {
|
||||
overshootClamping: true,
|
||||
restDisplacementThreshold: 0.01,
|
||||
restSpeedThreshold: 0.01,
|
||||
toValue: active ? this.props.activeOpacity : 1,
|
||||
toValue: active ? this.props.pressOpacity : 1,
|
||||
useNativeDriver,
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { TextInput, Platform, Keyboard } from 'react-native';
|
||||
import { TextInput, Keyboard, HostComponent } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
enabled: boolean;
|
||||
children: (props: {
|
||||
onPageChangeStart: () => void;
|
||||
onPageChangeConfirm: () => void;
|
||||
onPageChangeConfirm: (force: boolean) => void;
|
||||
onPageChangeCancel: () => void;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
|
||||
type InputRef = React.ElementRef<HostComponent<unknown>> | undefined;
|
||||
|
||||
export default class KeyboardManager extends React.Component<Props> {
|
||||
componentWillUnmount() {
|
||||
this.clearKeyboardTimeout();
|
||||
@@ -17,7 +19,7 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
|
||||
// Numeric id of the previously focused text input
|
||||
// When a gesture didn't change the tab, we can restore the focused input with this
|
||||
private previouslyFocusedTextInput: any | null = null;
|
||||
private previouslyFocusedTextInput: InputRef = undefined;
|
||||
private startTimestamp: number = 0;
|
||||
private keyboardTimeout: any;
|
||||
|
||||
@@ -35,7 +37,8 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
|
||||
this.clearKeyboardTimeout();
|
||||
|
||||
const input: any = TextInput.State.currentlyFocusedInput
|
||||
// @ts-expect-error: blurTextInput accepts both number and ref, but types say only ref
|
||||
const input: InputRef = TextInput.State.currentlyFocusedInput
|
||||
? TextInput.State.currentlyFocusedInput()
|
||||
: TextInput.State.currentlyFocusedField();
|
||||
|
||||
@@ -49,25 +52,30 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
this.startTimestamp = Date.now();
|
||||
};
|
||||
|
||||
private handlePageChangeConfirm = () => {
|
||||
private handlePageChangeConfirm = (force: boolean) => {
|
||||
if (!this.props.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearKeyboardTimeout();
|
||||
|
||||
const input = this.previouslyFocusedTextInput;
|
||||
if (force) {
|
||||
// Always dismiss input, even if we don't have a ref to it
|
||||
// We might not have the ref if onPageChangeStart was never called
|
||||
// This can happen if page change was not from a gesture
|
||||
Keyboard.dismiss();
|
||||
} else {
|
||||
const input = this.previouslyFocusedTextInput;
|
||||
|
||||
if (input) {
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.dismiss();
|
||||
} else {
|
||||
if (input) {
|
||||
// Dismiss the keyboard only if an input was a focused before
|
||||
// This makes sure we don't dismiss input on going back and focusing an input
|
||||
TextInput.State.blurTextInput(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup the ID on successful page change
|
||||
this.previouslyFocusedTextInput = null;
|
||||
this.previouslyFocusedTextInput = undefined;
|
||||
};
|
||||
|
||||
private handlePageChangeCancel = () => {
|
||||
@@ -91,11 +99,11 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
if (Date.now() - this.startTimestamp < 100) {
|
||||
this.keyboardTimeout = setTimeout(() => {
|
||||
TextInput.State.focusTextInput(input);
|
||||
this.previouslyFocusedTextInput = null;
|
||||
this.previouslyFocusedTextInput = undefined;
|
||||
}, 100);
|
||||
} else {
|
||||
TextInput.State.focusTextInput(input);
|
||||
this.previouslyFocusedTextInput = null;
|
||||
this.previouslyFocusedTextInput = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||