Compare commits

..

47 Commits

Author SHA1 Message Date
Satyajit Sahoo
f274058b90 chore: publish
- @react-navigation/bottom-tabs@5.11.9
 - @react-navigation/compat@5.3.15
 - @react-navigation/core@5.15.3
 - @react-navigation/devtools@5.1.22
 - @react-navigation/drawer@5.12.5
 - @react-navigation/material-bottom-tabs@5.3.15
 - @react-navigation/material-top-tabs@5.3.15
 - @react-navigation/native@5.9.4
 - @react-navigation/stack@5.14.4
2021-04-04 02:14:25 +02:00
WoLewicki
976178d098 fix: properly resolve initialRouteNames 2021-04-04 02:09:52 +02:00
Satyajit Sahoo
493956ef71 fix: check for screens enabled in ScreenContainer 2021-04-04 01:58:32 +02:00
Satyajit Sahoo
699ea0cc50 fix: don't pass accessibilityState to link. closes #9418 2021-04-04 01:32:44 +02:00
Satyajit Sahoo
a63f9da8c1 fix: don't handle back button with permanent drawer 2021-04-04 01:25:20 +02:00
Satyajit Sahoo
cceaa6780d fix: only handle back button in drawer when focused 2021-04-04 01:18:10 +02:00
Satyajit Sahoo
4b8155386b chore: allow 5.x branch for publishing 2021-03-10 05:18:23 +01:00
Satyajit Sahoo
1a757fc30a chore: upgrade playwright 2021-03-05 02:17:34 +01:00
Satyajit Sahoo
7b353a4aea chore: publish
- @react-navigation/bottom-tabs@5.11.8
 - @react-navigation/compat@5.3.14
 - @react-navigation/core@5.15.2
 - @react-navigation/devtools@5.1.21
 - @react-navigation/drawer@5.12.4
 - @react-navigation/material-bottom-tabs@5.3.14
 - @react-navigation/material-top-tabs@5.3.14
 - @react-navigation/native@5.9.3
 - @react-navigation/routers@5.7.2
 - @react-navigation/stack@5.14.3
2021-02-21 16:09:01 +01:00
Satyajit Sahoo
3728390b60 fix: fix getId being called for incorrect routes. closes #9343 2021-02-21 15:55:03 +01:00
Satyajit Sahoo
a8342aaf3d fix: address breaking change in react-native for Linking 2021-02-21 15:49:58 +01:00
Satyajit Sahoo
860adbfd8b chore: publish
- @react-navigation/stack@5.14.2
2021-01-25 17:14:53 +01:00
Satyajit Sahoo
38d680833e fix: fix transparent modal on web 2021-01-25 17:13:24 +01:00
Satyajit Sahoo
cae115fc17 chore: publish
- @react-navigation/bottom-tabs@5.11.7
 - @react-navigation/drawer@5.12.3
2021-01-25 14:09:32 +01:00
Satyajit Sahoo
87b51476d0 fix: fix drawer screen content not being interactable on Android 2021-01-25 14:07:28 +01:00
Satyajit Sahoo
b1b211855f chore: publish
- @react-navigation/bottom-tabs@5.11.6
 - @react-navigation/drawer@5.12.2
2021-01-22 23:18:38 +01:00
Satyajit Sahoo
60fe0dbb0a fix: fix pointerEvents in ResourceSavingScene
fixes #9241, fixes #9242
2021-01-22 23:17:34 +01:00
Satyajit Sahoo
bb294b16f9 chore: publish
- @react-navigation/bottom-tabs@5.11.5
 - @react-navigation/compat@5.3.13
 - @react-navigation/drawer@5.12.1
 - @react-navigation/material-bottom-tabs@5.3.13
 - @react-navigation/material-top-tabs@5.3.13
 - @react-navigation/native@5.9.2
 - @react-navigation/stack@5.14.1
2021-01-22 13:45:06 +01:00
Satyajit Sahoo
4ca2d2d22b fix: normalize prefix when parsing. fixes #9081 2021-01-22 13:43:49 +01:00
Satyajit Sahoo
35747a6066 chore: publish
- @react-navigation/bottom-tabs@5.11.4
 - @react-navigation/compat@5.3.12
 - @react-navigation/core@5.15.1
 - @react-navigation/devtools@5.1.20
 - @react-navigation/drawer@5.12.0
 - @react-navigation/material-bottom-tabs@5.3.12
 - @react-navigation/material-top-tabs@5.3.12
 - @react-navigation/native@5.9.1
 - @react-navigation/routers@5.7.1
 - @react-navigation/stack@5.14.0
2021-01-21 20:55:27 +01:00
Petra Daneva
bae4019995 feat: add pressColor and pressOpacity props to drawerItem (#8834)
Co-authored-by: Petra Daneva <p.daneva@dineout.bg>
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2021-01-21 20:48:21 +01:00
sharifhh
d3a9639060 fix: fix StackRouter incorrectly handling invalid route if key is present 2021-01-21 20:38:19 +01:00
Satyajit Sahoo
d88cbcb52d fix: fix drawer and bottom tabs not being visible on web. closes #9225 2021-01-21 20:05:28 +01:00
Satyajit Sahoo
dc7e876b6f chore: publish
- @react-navigation/bottom-tabs@5.11.3
 - @react-navigation/compat@5.3.11
 - @react-navigation/core@5.15.0
 - @react-navigation/devtools@5.1.19
 - @react-navigation/drawer@5.11.5
 - @react-navigation/material-bottom-tabs@5.3.11
 - @react-navigation/material-top-tabs@5.3.11
 - @react-navigation/native@5.9.0
 - @react-navigation/routers@5.7.0
 - @react-navigation/stack@5.13.0
2021-01-14 14:40:13 +01:00
Satyajit Sahoo
1e215614d8 chore: wrap example list in SafeAreaView 2021-01-14 14:26:22 +01:00
Satyajit Sahoo
dd87fa49a4 fix: enable detachInactiveScreens by default on web for better a11y 2021-01-14 14:16:15 +01:00
Satyajit Sahoo
09f0ebbb0f chore: update Reactiflux instructions 2021-01-14 12:47:06 +01:00
youngjuning
9633c4d35f feat: export TransitionPreset for custom TransitionPresets (#9173) 2021-01-14 12:38:41 +01:00
Dulmandakh
28fac3e0b9 chore: set displayName for LinkingContext (#9202)
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2021-01-14 12:04:14 +01:00
Dulmandakh
a8b8c27174 chore: set displayName for ThemeContext (#9201)
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2021-01-14 12:03:55 +01:00
Satyajit Sahoo
b19f76bfff feat: add a way to specify an unique ID for screens
With this, the user will be able to specify a `getId` function for their screens which returns an unique ID to use for the screen:

```js
<Stack.Screen
  name="Profile"
  component={ProfileScreen}
  getId={({ params }) => params.userId}
/>
```

This is an alternative to the `key` option in `navigate` with several advantages:

- Often users specify a key that's dependent on data already in params, such as `userId`. So it's much easier to specify it one place rather than at every call site.
- Users won't need to deal with generating a unique key for routes manually.
- This will work with other actions such as `push`, and not just navigate.
- With this, it'll be possible to have multiple instances of the screen even if you use `navigate`, which may be desirable in many cases (such as profile screens).
2021-01-14 03:52:12 +01:00
Nick McCurdy
365a2ad28c chore: format MaterialBottomTabView.tsx 2021-01-14 03:47:35 +01:00
Satyajit Sahoo
b26b90706f fix: support sync getInitialURL in native useLinking 2021-01-13 22:04:16 +01:00
Satyajit Sahoo
47f28558d6 chore: fix listing packages in metro config 2020-12-17 15:30:36 +01:00
Satyajit Sahoo
26074a28f7 fix: handle fallback for MaterialCommunityIcons better 2020-12-17 15:30:14 +01:00
Satyajit Sahoo
6fe1d70c6c test: add tests for openByDefault in drawer 2020-12-17 15:29:16 +01:00
Satyajit Sahoo
77fa6fb683 chore: migrate to react-native-builder-bob 2020-12-17 15:23:54 +01:00
Sekonia Software Solutions
2ad61a6735 fix: consider openByDefault prop when rehydrating drawer state (#9099)
Co-authored-by: Johannes Huber <jh@sekonia.com>
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-12-17 15:23:28 +01:00
Satyajit Sahoo
c9a5d45324 feat: expose getActionForState in linking 2020-12-13 03:06:42 +01:00
Satyajit Sahoo
3c874191ff feat: add a new backBehavior: firstRoute for TabRouter 2020-12-03 02:11:05 +01:00
Satyajit Sahoo
2317633652 fix: print an error when passing a second argument to useFocusEffect 2020-12-02 20:25:25 +01:00
Satyajit Sahoo
74d368eb4d chore: fix typo in workflow 2020-11-28 19:02:51 +01:00
Satyajit Sahoo
d617ab82f9 chore: bump github scripts 2020-11-28 14:35:19 +01:00
Satyajit Sahoo
f5fd0e5be4 chore: add a label for first pull request 2020-11-28 14:33:19 +01:00
Satyajit Sahoo
7bef138e3d chore: only count repro from user's github repos 2020-11-28 14:24:54 +01:00
Satyajit Sahoo
1406eb83ed chore: publish
- @react-navigation/stack@5.12.8
2020-11-21 05:32:53 +01:00
Satyajit Sahoo
3e069b718d fix: force dismiss keyboard if there was no gesture
closes #9078
2020-11-21 05:32:00 +01:00
68 changed files with 3067 additions and 662 deletions

View File

@@ -8,6 +8,12 @@ executors:
environment: environment:
YARN_CACHE_FOLDER: "~/.cache/yarn" YARN_CACHE_FOLDER: "~/.cache/yarn"
playwright:
docker:
- image: mcr.microsoft.com/playwright:bionic
environment:
NODE_ENV: development
commands: commands:
attach_project: attach_project:
steps: steps:
@@ -61,18 +67,9 @@ jobs:
destination: coverage destination: coverage
integration-tests: integration-tests:
executor: default executor: playwright
steps: steps:
- attach_project - 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: - run:
name: Build example for web name: Build example for web
command: yarn example expo build:web --no-pwa command: yarn example expo build:web --no-pwa

View File

@@ -14,7 +14,7 @@ contact_links:
about: Ask and answer questions using the react-navigation label. about: Ask and answer questions using the react-navigation label.
- name: Reactiflux - name: Reactiflux
url: https://www.reactiflux.com/ url: https://www.reactiflux.com/
about: Chat with other community members in the react-navigation channel. about: Chat with other community members in the help-react-native channel.
- name: Write an RFC - name: Write an RFC
url: https://github.com/react-navigation/rfcs url: https://github.com/react-navigation/rfcs
about: Write a RFC if you have ideas for how to implement a feature request. about: Write a RFC if you have ideas for how to implement a feature request.

View File

@@ -9,16 +9,22 @@ jobs:
check-repro: check-repro:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
const user = context.payload.sender.login;
const body = context.payload.comment const body = context.payload.comment
? context.payload.comment.body ? context.payload.comment.body
: context.payload.issue.body : context.payload.issue.body;
if (!/https?:\/\/((github\.com\/[^/]+\/[^/]+\/?[\s\n]+)|(snack\.expo\.io\/.+))/gm.test(body)) { const regex = new RegExp(
return `https?:\\/\\/((github\\.com\\/${user}\\/[^/]+\\/?[\\s\\n]+)|(snack\\.expo\\.io\\/.+))`,
'gm'
);
if (!regex.test(body)) {
return;
} }
await github.issues.addLabels({ await github.issues.addLabels({
@@ -26,7 +32,7 @@ jobs:
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
labels: ['repro provided'], labels: ['repro provided'],
}) });
try { try {
await github.issues.removeLabel({ await github.issues.removeLabel({
@@ -34,9 +40,9 @@ jobs:
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
name: 'needs repro', name: 'needs repro',
}) });
} catch (error) { } catch (error) {
if (!/Label does not exist/.test(error.message)) { if (!/Label does not exist/.test(error.message)) {
throw error throw error;
} }
} }

View File

@@ -45,7 +45,7 @@ jobs:
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}" run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
- name: Comment on PR - name: Comment on PR
uses: actions/github-script@v2 uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |

View File

@@ -0,0 +1,45 @@
name: First pull request
on: pull_request
jobs:
welcome:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
// Get a list of all issues created by the PR opener
// See: https://octokit.github.io/rest.js/#pagination
const creator = context.payload.sender.login;
const opts = github.issues.listForRepo.endpoint.merge({
...context.issue,
creator,
state: 'all'
});
const issues = await github.paginate(opts);
for (const issue of issues) {
if (issue.number === context.issue.number) {
continue;
}
if (issue.pull_request) {
return ;// Creator is already a contributor.
}
}
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['first pull request'],
});
await github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey ${creator}! Thanks for opening the pull request. If you haven't already, make sure to read our [contribution guidelines](https://github.com/react-navigation/react-navigation/blob/main/CONTRIBUTING.md)."
});

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'needs more info' if: github.event.label.name == 'needs more info'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'needs repro' if: github.event.label.name == 'needs repro'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'question' if: github.event.label.name == 'question'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -46,14 +46,14 @@ jobs:
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, 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." 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 `#help-react-native` 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: feature-request:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'feature-request' if: github.event.label.name == 'feature-request'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -68,7 +68,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-screens' if: github.event.label.name == 'library:react-native-screens'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-reanimated' if: github.event.label.name == 'library:react-native-reanimated'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-gesture-handler' if: github.event.label.name == 'library:react-native-gesture-handler'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-safe-area-context' if: github.event.label.name == 'library:react-native-safe-area-context'
steps: steps:
- uses: actions/github-script@v2 - uses: actions/github-script@v3
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |

View File

@@ -8,12 +8,15 @@ const blacklist = require('metro-config/src/defaults/blacklist');
const root = path.resolve(__dirname, '..'); const root = path.resolve(__dirname, '..');
const packages = path.resolve(root, 'packages'); const packages = path.resolve(root, 'packages');
// List all packages under `packages/`
const workspaces = fs const workspaces = fs
// List all packages under `packages/`
.readdirSync(packages) .readdirSync(packages)
// Ignore hidden files such as .DS_Store .map((p) => path.join(packages, p))
.filter((p) => !p.startsWith('.')) .filter(
.map((p) => path.join(packages, p)); (p) =>
fs.statSync(p).isDirectory() &&
fs.existsSync(path.join(p, 'package.json'))
);
// Get the list of dependencies for all packages in the monorepo // Get the list of dependencies for all packages in the monorepo
const modules = ['@expo/vector-icons'] const modules = ['@expo/vector-icons']

View File

@@ -56,7 +56,7 @@
"mock-require-assets": "^0.0.1", "mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"nodemon": "^2.0.6", "nodemon": "^2.0.6",
"playwright": "^0.14.0", "playwright": "^1.9.1",
"serve": "^11.3.0", "serve": "^11.3.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
} }

View File

@@ -9,6 +9,7 @@ import {
Linking, Linking,
LogBox, LogBox,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { enableScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { import {
@@ -306,38 +307,42 @@ export default function App() {
<ScrollView <ScrollView
style={{ backgroundColor: theme.colors.background }} style={{ backgroundColor: theme.colors.background }}
> >
<SettingsItem <SafeAreaView edges={['right', 'bottom', 'left']}>
label="Right to left" <SettingsItem
value={I18nManager.isRTL} label="Right to left"
onValueChange={() => { value={I18nManager.isRTL}
I18nManager.forceRTL(!I18nManager.isRTL); onValueChange={() => {
restartApp(); I18nManager.forceRTL(!I18nManager.isRTL);
}} restartApp();
/> }}
<Divider /> />
<SettingsItem <Divider />
label="Dark theme" <SettingsItem
value={theme.dark} label="Dark theme"
onValueChange={() => { value={theme.dark}
AsyncStorage.setItem( onValueChange={() => {
THEME_PERSISTENCE_KEY, AsyncStorage.setItem(
theme.dark ? 'light' : 'dark' THEME_PERSISTENCE_KEY,
); theme.dark ? 'light' : 'dark'
);
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme)); setTheme((t) =>
}} t.dark ? DefaultTheme : DarkTheme
/> );
<Divider /> }}
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map( />
(name) => ( <Divider />
<List.Item {(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
key={name} (name) => (
testID={name} <List.Item
title={SCREENS[name].title} key={name}
onPress={() => navigation.navigate(name)} testID={name}
/> title={SCREENS[name].title}
) onPress={() => navigation.navigate(name)}
)} />
)
)}
</SafeAreaView>
</ScrollView> </ScrollView>
)} )}
</Drawer.Screen> </Drawer.Screen>

View File

@@ -8,7 +8,7 @@
"version": "independent", "version": "independent",
"command": { "command": {
"publish": { "publish": {
"allowBranch": "main", "allowBranch": "5.x",
"conventionalCommits": true, "conventionalCommits": true,
"createRelease": "github", "createRelease": "github",
"message": "chore: publish", "message": "chore: publish",

View File

@@ -3,6 +3,78 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/bottom-tabs", "name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines", "description": "Bottom tab navigator following iOS design guidelines",
"version": "5.11.2", "version": "5.11.9",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -40,8 +40,7 @@
"react-native-iphone-x-helper": "^1.3.0" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-navigation/native": "^5.9.4",
"@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
@@ -49,6 +48,7 @@
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-builder-bob": "^0.17.0",
"react-native-safe-area-context": "3.1.4", "react-native-safe-area-context": "3.1.4",
"react-native-screens": "~2.10.1", "react-native-screens": "~2.10.1",
"typescript": "^4.0.3" "typescript": "^4.0.3"
@@ -60,7 +60,7 @@
"react-native-safe-area-context": ">= 0.6.0", "react-native-safe-area-context": ">= 0.6.0",
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0" "react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -134,6 +134,7 @@ export default function BottomTabBarItem({
onPress, onPress,
to, to,
accessibilityRole, accessibilityRole,
accessibilityState,
...rest ...rest
}: BottomTabBarButtonProps) => { }: BottomTabBarButtonProps) => {
if (Platform.OS === 'web' && to) { if (Platform.OS === 'web' && to) {
@@ -162,6 +163,7 @@ export default function BottomTabBarItem({
<TouchableWithoutFeedback <TouchableWithoutFeedback
{...rest} {...rest}
accessibilityRole={accessibilityRole} accessibilityRole={accessibilityRole}
accessibilityState={accessibilityState}
onPress={onPress} onPress={onPress}
> >
<View style={style}>{children}</View> <View style={style}>{children}</View>

View File

@@ -13,7 +13,7 @@ import {
TabNavigationState, TabNavigationState,
useTheme, useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer, screensEnabled } from 'react-native-screens';
import SafeAreaProviderCompat, { import SafeAreaProviderCompat, {
initialSafeAreaInsets, initialSafeAreaInsets,
@@ -138,55 +138,54 @@ export default class BottomTabView extends React.Component<Props, State> {
} = this.props; } = this.props;
const { routes } = state; const { routes } = state;
const { loaded, tabBarHeight } = this.state; const { loaded, tabBarHeight } = this.state;
const isScreensEnabled = screensEnabled?.() && detachInactiveScreens;
return ( return (
<NavigationHelpersContext.Provider value={navigation}> <NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat> <SafeAreaProviderCompat>
<View style={styles.container}> <ScreenContainer
<ScreenContainer // @ts-ignore
// @ts-ignore enabled={isScreensEnabled}
enabled={detachInactiveScreens} style={styles.container}
style={styles.pages} >
> {routes.map((route, index) => {
{routes.map((route, index) => { const descriptor = descriptors[route.key];
const descriptor = descriptors[route.key]; const { unmountOnBlur } = descriptor.options;
const { unmountOnBlur } = descriptor.options; const isFocused = state.index === index;
const isFocused = state.index === index;
if (unmountOnBlur && !isFocused) { if (unmountOnBlur && !isFocused) {
return null; return null;
} }
if (lazy && !loaded.includes(route.key) && !isFocused) { if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a screen if we've never navigated to it // Don't render a screen if we've never navigated to it
return null; return null;
} }
return ( return (
<ResourceSavingScene <ResourceSavingScene
key={route.key} key={route.key}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
isVisible={isFocused} isVisible={isFocused}
enabled={detachInactiveScreens} enabled={isScreensEnabled}
>
<SceneContent
isFocused={isFocused}
style={sceneContainerStyle}
> >
<SceneContent <BottomTabBarHeightContext.Provider value={tabBarHeight}>
isFocused={isFocused} {descriptor.render()}
style={sceneContainerStyle} </BottomTabBarHeightContext.Provider>
> </SceneContent>
<BottomTabBarHeightContext.Provider value={tabBarHeight}> </ResourceSavingScene>
{descriptor.render()} );
</BottomTabBarHeightContext.Provider> })}
</SceneContent> </ScreenContainer>
</ResourceSavingScene> <BottomTabBarHeightCallbackContext.Provider
); value={this.handleTabBarHeightChange}
})} >
</ScreenContainer> {this.renderTabBar()}
<BottomTabBarHeightCallbackContext.Provider </BottomTabBarHeightCallbackContext.Provider>
value={this.handleTabBarHeightChange}
>
{this.renderTabBar()}
</BottomTabBarHeightCallbackContext.Provider>
</View>
</SafeAreaProviderCompat> </SafeAreaProviderCompat>
</NavigationHelpersContext.Provider> </NavigationHelpersContext.Provider>
); );
@@ -198,9 +197,6 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
overflow: 'hidden', overflow: 'hidden',
}, },
pages: {
flex: 1,
},
content: { content: {
flex: 1, flex: 1,
}, },

View File

@@ -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 const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container
export default class ResourceSavingScene extends React.Component<Props> { export default function ResourceSavingScene({
render() { isVisible,
// react-native-screens is buggy on web children,
if (screensEnabled?.() && Platform.OS !== 'web') { style,
const { isVisible, ...rest } = this.props; ...rest
}: Props) {
if (shouldUseActivityState) { // react-native-screens is buggy on web
return ( if (screensEnabled?.() && Platform.OS !== 'web') {
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required if (shouldUseActivityState) {
<Screen activityState={isVisible ? 2 : 0} {...rest} /> return (
); // @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
} else { <Screen activityState={isVisible ? 2 : 0} style={style} {...rest}>
return ( {children}
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required </Screen>
<Screen active={isVisible ? 1 : 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} style={style} {...rest}>
{children}
</Screen>
);
} }
}
const { isVisible, children, style, ...rest } = this.props; if (Platform.OS === 'web') {
return ( return (
<View <View
// @ts-expect-error: hidden exists on web, but not in React Native
hidden={!isVisible}
style={[ style={[
{ display: isVisible ? 'flex' : 'none' },
styles.container, styles.container,
Platform.OS === 'web'
? { display: isVisible ? 'flex' : 'none' }
: null,
style, 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} collapsable={false}
removeClippedSubviews={ removeClippedSubviews={
// On iOS, set removeClippedSubviews to true only when not focused // 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 Platform.OS === 'ios' ? !isVisible : true
} }
pointerEvents={isVisible ? 'auto' : 'none'} pointerEvents={isVisible ? 'auto' : 'none'}
{...rest} style={isVisible ? styles.attached : styles.detached}
> >
<View style={isVisible ? styles.attached : styles.detached}> {children}
{children}
</View>
</View> </View>
); </View>
} );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/compat", "name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format", "description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.3.10", "version": "5.3.15",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -31,17 +31,17 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-navigation/native": "^5.9.4",
"@react-navigation/native": "^5.8.10",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"react": "~16.13.1", "react": "~16.13.1",
"react-native-builder-bob": "^0.17.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.5", "@react-navigation/native": "^5.0.5",
"react": "*" "react": "*"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/core", "name": "@react-navigation/core",
"description": "Core utilities for building navigators", "description": "Core utilities for building navigators",
"version": "5.14.4", "version": "5.15.3",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -35,26 +35,26 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.6.2", "@react-navigation/routers": "^5.7.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.15", "nanoid": "^3.1.15",
"query-string": "^6.13.6", "query-string": "^6.13.6",
"react-is": "^16.13.0" "react-is": "^16.13.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-is": "^16.7.1", "@types/react-is": "^16.7.1",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native-builder-bob": "^0.17.0",
"react-test-renderer": "~16.13.1", "react-test-renderer": "~16.13.1",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -2821,3 +2821,117 @@ it("throws when using 'initialRouteName' or 'screens' with legacy config", () =>
}) })
).toThrow('Found invalid keys in the configuration object.'); ).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
);
});

View File

@@ -239,3 +239,135 @@ it('runs cleanup when component is unmounted', () => {
expect(focusEffect).toBeCalledTimes(1); expect(focusEffect).toBeCalledTimes(1);
expect(focusEffectCleanup).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();
});

View File

@@ -26,7 +26,7 @@ type RouteConfig = {
type InitialRouteConfig = { type InitialRouteConfig = {
initialRouteName: string; initialRouteName: string;
connectedRoutes: string[]; parentScreens: string[];
}; };
type ResultState = PartialState<NavigationState> & { type ResultState = PartialState<NavigationState> & {
@@ -70,7 +70,7 @@ export default function getStateFromPath(
if (compatOptions?.initialRouteName) { if (compatOptions?.initialRouteName) {
initialRoutes.push({ initialRoutes.push({
initialRouteName: compatOptions.initialRouteName, initialRouteName: compatOptions.initialRouteName,
connectedRoutes: Object.keys(compatOptions.screens), parentScreens: [],
}); });
} }
@@ -115,7 +115,8 @@ export default function getStateFromPath(
key, key,
screens as PathConfigMap, screens as PathConfigMap,
[], [],
initialRoutes initialRoutes,
[]
) )
) )
) )
@@ -368,12 +369,15 @@ const createNormalizedConfigs = (
routeConfig: PathConfigMap, routeConfig: PathConfigMap,
routeNames: string[] = [], routeNames: string[] = [],
initials: InitialRouteConfig[], initials: InitialRouteConfig[],
parentScreens: string[],
parentPattern?: string parentPattern?: string
): RouteConfig[] => { ): RouteConfig[] => {
const configs: RouteConfig[] = []; const configs: RouteConfig[] = [];
routeNames.push(screen); routeNames.push(screen);
parentScreens.push(screen);
const config = routeConfig[screen]; const config = routeConfig[screen];
if (typeof config === 'string') { if (typeof config === 'string') {
@@ -423,7 +427,7 @@ const createNormalizedConfigs = (
if (config.initialRouteName) { if (config.initialRouteName) {
initials.push({ initials.push({
initialRouteName: config.initialRouteName, initialRouteName: config.initialRouteName,
connectedRoutes: Object.keys(config.screens), parentScreens,
}); });
} }
@@ -434,6 +438,7 @@ const createNormalizedConfigs = (
config.screens as PathConfigMap, config.screens as PathConfigMap,
routeNames, routeNames,
initials, initials,
[...parentScreens],
pattern ?? parentPattern pattern ?? parentPattern
); );
@@ -506,13 +511,23 @@ const findParseConfigForRoute = (
// Try to find an initial route connected with the one passed // Try to find an initial route connected with the one passed
const findInitialRoute = ( const findInitialRoute = (
routeName: string, routeName: string,
parentScreens: string[],
initialRoutes: InitialRouteConfig[] initialRoutes: InitialRouteConfig[]
): string | undefined => { ): string | undefined => {
for (const config of initialRoutes) { for (const config of initialRoutes) {
if (config.connectedRoutes.includes(routeName)) { if (parentScreens.length === config.parentScreens.length) {
return config.initialRouteName === routeName let sameParents = true;
? undefined for (let i = 0; i < parentScreens.length; i++) {
: config.initialRouteName; if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
sameParents = false;
break;
}
}
if (sameParents) {
return routeName !== config.initialRouteName
? config.initialRouteName
: undefined;
}
} }
} }
return undefined; return undefined;
@@ -556,7 +571,11 @@ const createNestedStateObject = (
) => { ) => {
let state: InitialState; let state: InitialState;
let route = routes.shift() as ParsedRoute; let route = routes.shift() as ParsedRoute;
let initialRoute = findInitialRoute(route.name, initialRoutes); const parentScreens: string[] = [];
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
parentScreens.push(route.name);
state = createStateObject(initialRoute, route, routes.length === 0); state = createStateObject(initialRoute, route, routes.length === 0);
@@ -564,7 +583,7 @@ const createNestedStateObject = (
let nestedState = state; let nestedState = state;
while ((route = routes.shift() as ParsedRoute)) { while ((route = routes.shift() as ParsedRoute)) {
initialRoute = findInitialRoute(route.name, initialRoutes); initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
const nestedStateIndex = const nestedStateIndex =
nestedState.index || nestedState.routes.length - 1; nestedState.index || nestedState.routes.length - 1;
@@ -579,6 +598,8 @@ const createNestedStateObject = (
nestedState = nestedState.routes[nestedStateIndex] nestedState = nestedState.routes[nestedStateIndex]
.state as InitialState; .state as InitialState;
} }
parentScreens.push(route.name);
} }
} }

View File

@@ -388,6 +388,14 @@ export type RouteConfig<
navigation: any; navigation: any;
}) => ScreenListeners<State, EventMap>); }) => 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. * Initial params object for the route.
*/ */

View File

@@ -13,6 +13,20 @@ type EffectCallback = () => undefined | void | (() => void);
export default function useFocusEffect(effect: EffectCallback) { export default function useFocusEffect(effect: EffectCallback) {
const navigation = useNavigation(); 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(() => { React.useEffect(() => {
let isFocused = false; let isFocused = false;
let cleanup: undefined | void | (() => void); let cleanup: undefined | void | (() => void);
@@ -45,10 +59,10 @@ export default function useFocusEffect(effect: EffectCallback) {
' }\n\n' + ' }\n\n' +
' fetchData();\n' + ' fetchData();\n' +
' }, [someId])\n' + ' }, [someId])\n' +
'};\n\n' + ');\n\n' +
'See usage guide: https://reactnavigation.org/docs/use-focus-effect'; 'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
} else { } else {
message += ` You returned: '${JSON.stringify(destroy)}'`; message += ` You returned '${JSON.stringify(destroy)}'.`;
} }
console.error(message); console.error(message);

View File

@@ -7,6 +7,7 @@ import {
ParamListBase, ParamListBase,
Router, Router,
RouterFactory, RouterFactory,
RouterConfigOptions,
PartialState, PartialState,
NavigationAction, NavigationAction,
Route, Route,
@@ -257,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) { if (!routeNames.length) {
throw new Error( throw new Error(
@@ -297,6 +307,7 @@ export default function useNavigationBuilder<
router.getInitialState({ router.getInitialState({
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
}), }),
true, true,
]; ];
@@ -307,6 +318,7 @@ export default function useNavigationBuilder<
{ {
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
} }
), ),
false, false,
@@ -336,6 +348,7 @@ export default function useNavigationBuilder<
nextState = router.getStateForRouteNamesChange(state, { nextState = router.getStateForRouteNamesChange(state, {
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
}); });
} }
@@ -372,6 +385,7 @@ export default function useNavigationBuilder<
? router.getStateForAction(nextState, action, { ? router.getStateForAction(nextState, action, {
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
}) })
: null; : null;
@@ -380,6 +394,7 @@ export default function useNavigationBuilder<
? router.getRehydratedState(updatedState, { ? router.getRehydratedState(updatedState, {
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
}) })
: nextState; : nextState;
} }
@@ -501,6 +516,7 @@ export default function useNavigationBuilder<
routerConfigOptions: { routerConfigOptions: {
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
}, },
emitter, emitter,
}); });

View File

@@ -78,6 +78,7 @@ export default function useNavigationHelpers<
router.getStateForAction(state, CommonActions.goBack() as Action, { router.getStateForAction(state, CommonActions.goBack() as Action, {
routeNames: state.routeNames, routeNames: state.routeNames,
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}) !== null || }) !== null ||
parentNavigationHelpers?.canGoBack() || parentNavigationHelpers?.canGoBack() ||
false false

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.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) ## [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 **Note:** Version bump only for package @react-navigation/devtools

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/devtools", "name": "@react-navigation/devtools",
"description": "Developer tools for React Navigation", "description": "Developer tools for React Navigation",
"version": "5.1.18", "version": "5.1.22",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -36,22 +36,22 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.14.4", "@react-navigation/core": "^5.15.3",
"deep-equal": "^2.0.4" "deep-equal": "^2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/deep-equal": "^1.0.1", "@types/deep-equal": "^1.0.1",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native-builder-bob": "^0.17.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -3,6 +3,85 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/drawer

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/drawer", "name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess", "description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.11.4", "version": "5.12.5",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -45,14 +45,14 @@
"react-native-iphone-x-helper": "^1.3.0" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-navigation/native": "^5.9.4",
"@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.30", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-builder-bob": "^0.17.0",
"react-native-gesture-handler": "~1.7.0", "react-native-gesture-handler": "~1.7.0",
"react-native-reanimated": "~1.13.0", "react-native-reanimated": "~1.13.0",
"react-native-safe-area-context": "3.1.4", "react-native-safe-area-context": "3.1.4",
@@ -68,7 +68,7 @@
"react-native-safe-area-context": ">= 0.6.0", "react-native-safe-area-context": ">= 0.6.0",
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0" "react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -116,12 +116,6 @@ export default class DrawerView extends React.Component<Props> {
statusBarAnimation: 'slide', statusBarAnimation: 'slide',
}; };
componentDidMount() {
if (Platform.OS === 'web') {
document?.body?.addEventListener?.('keyup', this.handleEscape);
}
}
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
const { const {
open, open,
@@ -172,22 +166,8 @@ export default class DrawerView extends React.Component<Props> {
componentWillUnmount() { componentWillUnmount() {
this.toggleStatusBar(false); this.toggleStatusBar(false);
this.handleEndInteraction(); 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 = () => { private handleEndInteraction = () => {
if (this.interactionHandle !== undefined) { if (this.interactionHandle !== undefined) {
InteractionManager.clearInteractionHandle(this.interactionHandle); InteractionManager.clearInteractionHandle(this.interactionHandle);

View File

@@ -56,6 +56,20 @@ type Props = {
* Background color for item when its inactive. * Background color for item when its inactive.
*/ */
inactiveBackgroundColor?: string; 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. * Style object for the label element.
*/ */
@@ -72,6 +86,7 @@ const Touchable = ({
onPress, onPress,
to, to,
accessibilityRole, accessibilityRole,
accessibilityState,
delayPressIn, delayPressIn,
...rest ...rest
}: TouchableWithoutFeedbackProps & { }: TouchableWithoutFeedbackProps & {
@@ -105,6 +120,7 @@ const Touchable = ({
<TouchableItem <TouchableItem
{...rest} {...rest}
accessibilityRole={accessibilityRole} accessibilityRole={accessibilityRole}
accessibilityState={accessibilityState}
delayPressIn={delayPressIn} delayPressIn={delayPressIn}
onPress={onPress} onPress={onPress}
> >
@@ -132,6 +148,8 @@ export default function DrawerItem(props: Props) {
inactiveBackgroundColor = 'transparent', inactiveBackgroundColor = 'transparent',
style, style,
onPress, onPress,
pressColor,
pressOpacity,
...rest ...rest
} = props; } = props;
@@ -159,6 +177,8 @@ export default function DrawerItem(props: Props) {
accessibilityState={{ selected: focused }} accessibilityState={{ selected: focused }}
// @ts-expect-error: keep for compatibility with older React Native versions // @ts-expect-error: keep for compatibility with older React Native versions
accessibilityStates={focused ? ['selected'] : []} accessibilityStates={focused ? ['selected'] : []}
pressColor={pressColor}
pressOpacity={pressOpacity}
to={to} to={to}
> >
<React.Fragment> <React.Fragment>

View File

@@ -5,9 +5,8 @@ import {
I18nManager, I18nManager,
Platform, Platform,
BackHandler, BackHandler,
NativeEventSubscription,
} from 'react-native'; } from 'react-native';
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer, screensEnabled } from 'react-native-screens';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
NavigationContext, NavigationContext,
@@ -112,29 +111,48 @@ export default function DrawerView({
}, [navigation, state.key]); }, [navigation, state.key]);
React.useEffect(() => { React.useEffect(() => {
if (isDrawerOpen) { if (!isDrawerOpen || drawerType === 'permanent') {
navigation.emit({ type: 'drawerOpen' }); return;
} 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;
});
} }
return () => subscription?.remove(); const handleClose = () => {
}, [handleDrawerClose, isDrawerOpen, navigation, state.key]); // 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; const focusedRouteKey = state.routes[state.index].key;
@@ -157,9 +175,11 @@ export default function DrawerView({
}; };
const renderContent = () => { const renderContent = () => {
const isScreensEnabled = screensEnabled?.() && detachInactiveScreens;
return ( return (
// @ts-ignore // @ts-ignore
<ScreenContainer enabled={detachInactiveScreens} style={styles.content}> <ScreenContainer enabled={isScreensEnabled} style={styles.content}>
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
const descriptor = descriptors[route.key]; const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options; const { unmountOnBlur } = descriptor.options;
@@ -184,7 +204,7 @@ export default function DrawerView({
key={route.key} key={route.key}
style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]} style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]}
isVisible={isFocused} isVisible={isFocused}
enabled={detachInactiveScreens} enabled={isScreensEnabled}
> >
{headerShown ? ( {headerShown ? (
<NavigationContext.Provider value={descriptor.navigation}> <NavigationContext.Provider value={descriptor.navigation}>

View File

@@ -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 const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container
export default class ResourceSavingScene extends React.Component<Props> { export default function ResourceSavingScene({
render() { isVisible,
// react-native-screens is buggy on web children,
if (screensEnabled?.() && Platform.OS !== 'web') { style,
const { isVisible, ...rest } = this.props; ...rest
}: Props) {
if (shouldUseActivityState) { // react-native-screens is buggy on web
return ( if (screensEnabled?.() && Platform.OS !== 'web') {
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required if (shouldUseActivityState) {
<Screen activityState={isVisible ? 2 : 0} {...rest} /> return (
); // @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
} else { <Screen activityState={isVisible ? 2 : 0} style={style} {...rest}>
return ( {children}
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required </Screen>
<Screen active={isVisible ? 1 : 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} style={style} {...rest}>
{children}
</Screen>
);
} }
}
const { isVisible, children, style, ...rest } = this.props; if (Platform.OS === 'web') {
return ( return (
<View <View
// @ts-expect-error: hidden exists on web, but not in React Native
hidden={!isVisible}
style={[ style={[
{ display: isVisible ? 'flex' : 'none' },
styles.container, styles.container,
Platform.OS === 'web'
? { display: isVisible ? 'flex' : 'none' }
: { overflow: 'hidden' },
style, 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} collapsable={false}
removeClippedSubviews={ removeClippedSubviews={
// On iOS, set removeClippedSubviews to true only when not focused // 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 Platform.OS === 'ios' ? !isVisible : true
} }
pointerEvents={isVisible ? 'auto' : 'none'} pointerEvents={isVisible ? 'auto' : 'none'}
{...rest} style={isVisible ? styles.attached : styles.detached}
> >
<View style={isVisible ? styles.attached : styles.detached}> {children}
{children}
</View>
</View> </View>
); </View>
} );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -5,14 +5,14 @@ import { BaseButton } from 'react-native-gesture-handler';
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton); const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
type Props = React.ComponentProps<typeof BaseButton> & { type Props = React.ComponentProps<typeof BaseButton> & {
activeOpacity: number; pressOpacity: number;
}; };
const useNativeDriver = Platform.OS !== 'web'; const useNativeDriver = Platform.OS !== 'web';
export default class TouchableItem extends React.Component<Props> { export default class TouchableItem extends React.Component<Props> {
static defaultProps = { static defaultProps = {
activeOpacity: 0.3, pressOpacity: 0.3,
borderless: true, borderless: true,
enabled: true, enabled: true,
}; };
@@ -27,7 +27,7 @@ export default class TouchableItem extends React.Component<Props> {
overshootClamping: true, overshootClamping: true,
restDisplacementThreshold: 0.01, restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01, restSpeedThreshold: 0.01,
toValue: active ? this.props.activeOpacity : 1, toValue: active ? this.props.pressOpacity : 1,
useNativeDriver, useNativeDriver,
}).start(); }).start();

View File

@@ -3,6 +3,52 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-bottom-tabs", "name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper", "description": "Integration for bottom navigation component from react-native-paper",
"version": "5.3.10", "version": "5.3.15",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -41,8 +41,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-navigation/native": "^5.9.4",
"@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.30", "@types/react-native": "^0.63.30",
@@ -50,6 +49,7 @@
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-builder-bob": "^0.17.0",
"react-native-paper": "^4.2.0", "react-native-paper": "^4.2.0",
"react-native-vector-icons": "^7.0.0", "react-native-vector-icons": "^7.0.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
@@ -61,7 +61,7 @@
"react-native-paper": ">= 3.0.0", "react-native-paper": ">= 3.0.0",
"react-native-vector-icons": ">= 6.0.0" "react-native-vector-icons": ">= 6.0.0"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; 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 { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
@@ -28,44 +28,48 @@ type Scene = { route: { key: string } };
// Optionally require vector-icons referenced from react-native-paper: // Optionally require vector-icons referenced from react-native-paper:
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14 // 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 { try {
// Optionally require vector-icons // Optionally require vector-icons
MaterialCommunityIcons = require('react-native-vector-icons/MaterialCommunityIcons') MaterialCommunityIcons = require('react-native-vector-icons/MaterialCommunityIcons')
.default; .default;
} catch (e) { } catch (e) {
// @ts-expect-error let isErrorLogged = false;
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;
// Fallback component for icons // Fallback component for icons
MaterialCommunityIcons = () => { MaterialCommunityIcons = ({
if (!isErrorLogged) { name,
if ( color,
!/(Cannot find module|Module not found|Cannot resolve module)/.test( size,
e.message selectionColor: _,
) ...rest
) { }) => {
console.error(e); if (!isErrorLogged) {
} if (
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
console.warn( e.message
`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.` ) {
); console.error(e);
isErrorLogged = true;
} }
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({ function MaterialBottomTabViewInner({
@@ -107,10 +111,13 @@ function MaterialBottomTabViewInner({
? ({ ? ({
onPress, onPress,
route, route,
accessibilityRole: _0, /* eslint-disable @typescript-eslint/no-unused-vars */
borderless: _1, accessibilityRole,
centered: _2, accessibilityState,
rippleColor: _3, borderless,
centered,
rippleColor,
/* eslint-enable @typescript-eslint/no-unused-vars */
style, style,
...rest ...rest
}) => { }) => {

View File

@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-top-tabs", "name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view", "description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.3.10", "version": "5.3.15",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -44,14 +44,14 @@
"color": "^3.1.3" "color": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-navigation/native": "^5.9.4",
"@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.30", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-builder-bob": "^0.17.0",
"react-native-gesture-handler": "~1.7.0", "react-native-gesture-handler": "~1.7.0",
"react-native-reanimated": "~1.13.0", "react-native-reanimated": "~1.13.0",
"react-native-tab-view": "^2.15.2", "react-native-tab-view": "^2.15.2",
@@ -65,7 +65,7 @@
"react-native-reanimated": ">= 1.0.0", "react-native-reanimated": ">= 1.0.0",
"react-native-tab-view": ">= 2.0.0" "react-native-tab-view": ">= 2.0.0"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/native

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/native", "name": "@react-navigation/native",
"description": "React Native integration for React Navigation", "description": "React Native integration for React Navigation",
"version": "5.8.10", "version": "5.9.4",
"keywords": [ "keywords": [
"react-native", "react-native",
"react-navigation", "react-navigation",
@@ -37,12 +37,11 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.14.4", "@react-navigation/core": "^5.15.3",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.15" "nanoid": "^3.1.15"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
@@ -51,13 +50,14 @@
"react": "~16.13.1", "react": "~16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-builder-bob": "^0.17.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
"react-native": "*" "react-native": "*"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -5,4 +5,6 @@ const LinkingContext = React.createContext<{
options: LinkingOptions | undefined; options: LinkingOptions | undefined;
}>({ options: undefined }); }>({ options: undefined });
LinkingContext.displayName = 'LinkingContext';
export default LinkingContext; export default LinkingContext;

View 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);
});

View 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;
}

View File

@@ -4,4 +4,6 @@ import type { Theme } from '../types';
const ThemeContext = React.createContext<Theme>(DefaultTheme); const ThemeContext = React.createContext<Theme>(DefaultTheme);
ThemeContext.displayName = 'ThemeContext';
export default ThemeContext; export default ThemeContext;

View File

@@ -1,6 +1,7 @@
import type { import type {
getStateFromPath as getStateFromPathDefault, getStateFromPath as getStateFromPathDefault,
getPathFromState as getPathFromStateDefault, getPathFromState as getPathFromStateDefault,
getActionFromState as getActionFromStateDefault,
PathConfigMap, PathConfigMap,
Route, Route,
} from '@react-navigation/core'; } 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. * Custom function to get subscribe to URL updates.
* Uses `Linking.addEventListener('url', callback)` by default. * Uses `Linking.addEventListener('url', callback)` by default.
@@ -90,11 +95,18 @@ export type LinkingOptions = {
) => undefined | void | (() => void); ) => undefined | void | (() => void);
/** /**
* Custom function to parse the URL to a valid navigation state (advanced). * 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; 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). * 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; getPathFromState?: typeof getPathFromStateDefault;
}; };

View File

@@ -1,12 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { Linking, Platform } from 'react-native'; import { Linking, Platform } from 'react-native';
import { import {
getActionFromState, getActionFromState as getActionFromStateDefault,
getStateFromPath as getStateFromPathDefault, getStateFromPath as getStateFromPathDefault,
NavigationContainerRef, NavigationContainerRef,
} from '@react-navigation/core'; } from '@react-navigation/core';
import extractPathFromURL from './extractPathFromURL';
import type { LinkingOptions } from './types'; import type { LinkingOptions } from './types';
import escapeStringRegexp from 'escape-string-regexp';
type ResultState = ReturnType<typeof getStateFromPathDefault>;
let isUsingLinking = false; let isUsingLinking = false;
@@ -28,11 +30,21 @@ export default function useLinking(
subscribe = (listener) => { subscribe = (listener) => {
const callback = ({ url }: { url: string }) => listener(url); 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, getStateFromPath = getStateFromPathDefault,
getActionFromState = getActionFromStateDefault,
}: LinkingOptions }: LinkingOptions
) { ) {
React.useEffect(() => { React.useEffect(() => {
@@ -66,6 +78,7 @@ export default function useLinking(
const configRef = React.useRef(config); const configRef = React.useRef(config);
const getInitialURLRef = React.useRef(getInitialURL); const getInitialURLRef = React.useRef(getInitialURL);
const getStateFromPathRef = React.useRef(getStateFromPath); const getStateFromPathRef = React.useRef(getStateFromPath);
const getActionFromStateRef = React.useRef(getActionFromState);
React.useEffect(() => { React.useEffect(() => {
enabledRef.current = enabled; enabledRef.current = enabled;
@@ -73,48 +86,53 @@ export default function useLinking(
configRef.current = config; configRef.current = config;
getInitialURLRef.current = getInitialURL; getInitialURLRef.current = getInitialURL;
getStateFromPathRef.current = getStateFromPath; getStateFromPathRef.current = getStateFromPath;
}, [config, enabled, prefixes, getInitialURL, getStateFromPath]); getActionFromStateRef.current = getActionFromState;
});
const extractPathFromURL = React.useCallback((url: string) => { const getInitialState = React.useCallback(() => {
for (const prefix of prefixesRef.current) { let state: ResultState | undefined;
const protocol = prefix.match(/^[^:]+:\/\//)?.[0] ?? '';
const host = prefix.replace(protocol, ''); if (enabledRef.current) {
const prefixRegex = new RegExp( const url = getInitialURLRef.current();
`^${escapeStringRegexp(protocol)}${host
.split('.') if (url != null && typeof url !== 'string') {
.map((it) => (it === '*' ? '[^/]+' : escapeStringRegexp(it))) return url.then((url) => {
.join('\\.')}` const path = url
); ? extractPathFromURL(prefixesRef.current, url)
if (prefixRegex.test(url)) { : null;
return url.replace(prefixRegex, '');
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(() => { React.useEffect(() => {
const listener = (url: string) => { const listener = (url: string) => {
if (!enabled) { if (!enabled) {
return; return;
} }
const path = extractPathFromURL(url); const path = extractPathFromURL(prefixesRef.current, url);
const navigation = ref.current; const navigation = ref.current;
if (navigation && path) { if (navigation && path) {
@@ -134,7 +152,10 @@ export default function useLinking(
return; return;
} }
const action = getActionFromState(state, configRef.current); const action = getActionFromStateRef.current(
state,
configRef.current
);
if (action !== undefined) { if (action !== undefined) {
try { try {
@@ -154,7 +175,7 @@ export default function useLinking(
}; };
return subscribe(listener); return subscribe(listener);
}, [enabled, ref, subscribe, extractPathFromURL]); }, [enabled, ref, subscribe]);
return { return {
getInitialState, getInitialState,

View File

@@ -2,9 +2,9 @@ import * as React from 'react';
import { import {
getStateFromPath as getStateFromPathDefault, getStateFromPath as getStateFromPathDefault,
getPathFromState as getPathFromStateDefault, getPathFromState as getPathFromStateDefault,
getActionFromState as getActionFromStateDefault,
NavigationContainerRef, NavigationContainerRef,
NavigationState, NavigationState,
getActionFromState,
} from '@react-navigation/core'; } from '@react-navigation/core';
import { nanoid } from 'nanoid/non-secure'; import { nanoid } from 'nanoid/non-secure';
import ServerContext from './ServerContext'; import ServerContext from './ServerContext';
@@ -134,7 +134,7 @@ const createMemoryHistory = () => {
// - There's history to go back, `history.go` is called, and `popstate` fires // - 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` // - `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 // - 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) => { const done = (interrupted?: boolean) => {
clearTimeout(timer); clearTimeout(timer);
@@ -293,6 +293,7 @@ export default function useLinking(
config, config,
getStateFromPath = getStateFromPathDefault, getStateFromPath = getStateFromPathDefault,
getPathFromState = getPathFromStateDefault, getPathFromState = getPathFromStateDefault,
getActionFromState = getActionFromStateDefault,
}: LinkingOptions }: LinkingOptions
) { ) {
React.useEffect(() => { React.useEffect(() => {
@@ -323,14 +324,16 @@ export default function useLinking(
const enabledRef = React.useRef(enabled); const enabledRef = React.useRef(enabled);
const configRef = React.useRef(config); const configRef = React.useRef(config);
const getStateFromPathRef = React.useRef(getStateFromPath); const getStateFromPathRef = React.useRef(getStateFromPath);
const getActionFromStateRef = React.useRef(getActionFromState);
const getPathFromStateRef = React.useRef(getPathFromState); const getPathFromStateRef = React.useRef(getPathFromState);
React.useEffect(() => { React.useEffect(() => {
enabledRef.current = enabled; enabledRef.current = enabled;
configRef.current = config; configRef.current = config;
getStateFromPathRef.current = getStateFromPath; getStateFromPathRef.current = getStateFromPath;
getActionFromStateRef.current = getActionFromState;
getPathFromStateRef.current = getPathFromState; getPathFromStateRef.current = getPathFromState;
}, [config, enabled, getPathFromState, getStateFromPath]); });
const server = React.useContext(ServerContext); 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 = { const thenable = {
then(onfulfilled?: (state: ResultState | undefined) => void) { then(onfulfilled?: (state: ResultState | undefined) => void) {
return Promise.resolve(onfulfilled ? onfulfilled(value) : value); return Promise.resolve(onfulfilled ? onfulfilled(value) : value);
@@ -412,7 +414,10 @@ export default function useLinking(
} }
if (index > previousIndex) { if (index > previousIndex) {
const action = getActionFromState(state, configRef.current); const action = getActionFromStateRef.current(
state,
configRef.current
);
if (action !== undefined) { if (action !== undefined) {
try { try {

View File

@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.7.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) ## [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 **Note:** Version bump only for package @react-navigation/routers

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/routers", "name": "@react-navigation/routers",
"description": "Routers to help build custom navigators", "description": "Routers to help build custom navigators",
"version": "5.6.2", "version": "5.7.2",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -39,11 +39,11 @@
"nanoid": "^3.1.15" "nanoid": "^3.1.15"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react-native-builder-bob": "^0.17.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -120,8 +120,12 @@ export default function DrawerRouter({
type: 'drawer', type: 'drawer',
getInitialState({ routeNames, routeParamList }) { getInitialState({ routeNames, routeParamList, routeGetIdList }) {
let state = router.getInitialState({ routeNames, routeParamList }); let state = router.getInitialState({
routeNames,
routeParamList,
routeGetIdList,
});
if (openByDefault) { if (openByDefault) {
state = openDrawer(state); state = openDrawer(state);
@@ -135,7 +139,10 @@ export default function DrawerRouter({
}; };
}, },
getRehydratedState(partialState, { routeNames, routeParamList }) { getRehydratedState(
partialState,
{ routeNames, routeParamList, routeGetIdList }
) {
if (partialState.stale === false) { if (partialState.stale === false) {
return partialState; return partialState;
} }
@@ -143,9 +150,10 @@ export default function DrawerRouter({
let state = router.getRehydratedState(partialState, { let state = router.getRehydratedState(partialState, {
routeNames, routeNames,
routeParamList, routeParamList,
routeGetIdList,
}); });
if (isDrawerOpen(partialState)) { if (partialState.history ? isDrawerOpen(partialState) : openByDefault) {
state = openDrawer(state); state = openDrawer(state);
} }

View File

@@ -258,6 +258,9 @@ export default function StackRouter(options: StackRouterOptions) {
case 'PUSH': case 'PUSH':
if (state.routeNames.includes(action.payload.name)) { if (state.routeNames.includes(action.payload.name)) {
const getId = options.routeGetIdList[action.payload.name];
const id = getId?.({ params: action.payload.params });
const route = const route =
action.payload.name && action.payload.key action.payload.name && action.payload.key
? state.routes.find( ? state.routes.find(
@@ -265,34 +268,34 @@ export default function StackRouter(options: StackRouterOptions) {
route.name === action.payload.name && route.name === action.payload.name &&
route.key === action.payload.key route.key === action.payload.key
) )
: id
? state.routes.find(
(route) =>
route.name === action.payload.name &&
id === getId?.({ params: route.params })
)
: undefined; : undefined;
let routes: Route<string>[]; let routes: Route<string>[];
if (route) { if (route) {
routes = state.routes.filter((r) => r.key !== route.key); routes = state.routes.filter((r) => r.key !== route.key);
routes.push( routes.push({
action.payload.params ...route,
? { params:
...route, action.payload.params !== undefined
params: ? {
action.payload.params !== undefined ...route.params,
? { ...action.payload.params,
...route.params, }
...action.payload.params, : route.params,
} });
: route.params,
}
: route
);
} else { } else {
routes = [ routes = [
...state.routes, ...state.routes,
{ {
key: key:
action.payload.key === undefined action.payload.key ?? `${action.payload.name}-${nanoid()}`,
? `${action.payload.name}-${nanoid()}`
: action.payload.key,
name: action.payload.name, name: action.payload.name,
params: params:
routeParamList[action.payload.name] !== undefined routeParamList[action.payload.name] !== undefined
@@ -348,14 +351,31 @@ export default function StackRouter(options: StackRouterOptions) {
case 'NAVIGATE': case 'NAVIGATE':
if ( if (
action.payload.key || action.payload.name !== undefined &&
(action.payload.name && !state.routeNames.includes(action.payload.name)
state.routeNames.includes(action.payload.name))
) { ) {
return null;
}
if (action.payload.key || action.payload.name) {
// If the route already exists, navigate to that // If the route already exists, navigate to that
let index = -1; 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 && (state.routes[state.index].name === action.payload.name &&
action.payload.key === undefined) || action.payload.key === undefined) ||
state.routes[state.index].key === action.payload.key 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) { if (index === -1 && action.payload.name !== undefined) {
return router.getStateForAction( const routes = [
state, ...state.routes,
{ {
type: 'PUSH', key:
payload: { action.payload.key ?? `${action.payload.name}-${nanoid()}`,
key: action.payload.key, name: action.payload.name,
name: action.payload.name, params:
params: action.payload.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]; const route = state.routes[index];

View File

@@ -17,7 +17,12 @@ export type TabActionType = {
target?: string; target?: string;
}; };
export type BackBehavior = 'initialRoute' | 'order' | 'history' | 'none'; export type BackBehavior =
| 'initialRoute'
| 'firstRoute'
| 'history'
| 'order'
| 'none';
export type TabRouterOptions = DefaultRouterOptions & { export type TabRouterOptions = DefaultRouterOptions & {
backBehavior?: BackBehavior; backBehavior?: BackBehavior;
@@ -74,13 +79,21 @@ const getRouteHistory = (
history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key }); history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key });
} }
break; break;
case 'firstRoute':
if (index !== 0) {
history.unshift({
type: TYPE_ROUTE,
key: routes[0].key,
});
}
break;
case 'initialRoute': case 'initialRoute':
initialRouteIndex = routes.findIndex( initialRouteIndex = routes.findIndex(
(route) => route.name === initialRouteName (route) => route.name === initialRouteName
); );
initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex; initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
if (initialRouteIndex !== index) { if (index !== initialRouteIndex) {
history.unshift({ history.unshift({
type: TYPE_ROUTE, type: TYPE_ROUTE,
key: routes[initialRouteIndex].key, key: routes[initialRouteIndex].key,

View File

@@ -4,6 +4,7 @@ import {
DrawerActions, DrawerActions,
DrawerNavigationState, DrawerNavigationState,
ParamListBase, ParamListBase,
RouterConfigOptions,
} from '..'; } from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' })); 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 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}) })
).toEqual({ ).toEqual({
index: 1, index: 1,
@@ -44,6 +46,7 @@ it('gets initial state from route names and params without initialRouteName', ()
baz: { answer: 42 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}) })
).toEqual({ ).toEqual({
index: 0, 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', () => { it('gets rehydrated state from partial state', () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: { routeParamList: {
baz: { answer: 42 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}; };
expect( expect(
@@ -218,15 +249,87 @@ it("doesn't rehydrate state if it's not stale", () => {
router.getRehydratedState(state, { router.getRehydratedState(state, {
routeNames: [], routeNames: [],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}) })
).toBe(state); ).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', () => { it('handles navigate action', () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'], routeNames: ['baz', 'bar'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -265,9 +368,10 @@ it('handles navigate action', () => {
it('handles navigate action with open drawer', () => { it('handles navigate action with open drawer', () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'], routeNames: ['baz', 'bar'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -306,9 +410,10 @@ it('handles navigate action with open drawer', () => {
it('handles open drawer action', () => { it('handles open drawer action', () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'], routeNames: ['baz', 'bar'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -361,9 +466,10 @@ it('handles open drawer action', () => {
it('handles close drawer action', () => { it('handles close drawer action', () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'], routeNames: ['baz', 'bar'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -419,9 +525,10 @@ it('handles close drawer action', () => {
it('handles toggle drawer action', () => { it('handles toggle drawer action', () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'], routeNames: ['baz', 'bar'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(

View File

@@ -1,4 +1,9 @@
import { CommonActions, StackRouter, StackActions } from '..'; import {
CommonActions,
StackRouter,
StackActions,
RouterConfigOptions,
} from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' })); 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 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}) })
).toEqual({ ).toEqual({
index: 0, index: 0,
@@ -33,6 +39,7 @@ it('gets initial state from route names and params without initialRouteName', ()
baz: { answer: 42 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}) })
).toEqual({ ).toEqual({
index: 0, index: 0,
@@ -47,12 +54,13 @@ it('gets initial state from route names and params without initialRouteName', ()
it('gets rehydrated state from partial state', () => { it('gets rehydrated state from partial state', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: { routeParamList: {
baz: { answer: 42 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}; };
expect( expect(
@@ -136,6 +144,7 @@ it("doesn't rehydrate state if it's not stale", () => {
router.getRehydratedState(state, { router.getRehydratedState(state, {
routeNames: [], routeNames: [],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}) })
).toBe(state); ).toBe(state);
}); });
@@ -163,6 +172,7 @@ it('gets state on route names change', () => {
qux: { name: 'John' }, qux: { name: 'John' },
fiz: { fruit: 'apple' }, fiz: { fruit: 'apple' },
}, },
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -195,6 +205,7 @@ it('gets state on route names change', () => {
routeParamList: { routeParamList: {
baz: { name: 'John' }, baz: { name: 'John' },
}, },
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -228,6 +239,7 @@ it('gets state on route names change with initialRouteName', () => {
routeParamList: { routeParamList: {
baz: { name: 'John' }, baz: { name: 'John' },
}, },
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -242,9 +254,10 @@ it('gets state on route names change with initialRouteName', () => {
it('handles navigate action', () => { it('handles navigate action', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( 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 router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, 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( expect(
@@ -477,9 +709,10 @@ it('handles go back action', () => {
it('handles pop action', () => { it('handles pop action', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -650,9 +883,10 @@ it('handles pop action', () => {
it('handles pop to top action', () => { it('handles pop to top action', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -684,9 +918,10 @@ it('handles pop to top action', () => {
it('replaces focused screen with replace', () => { it('replaces focused screen with replace', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -722,9 +957,10 @@ it('replaces focused screen with replace', () => {
it('replaces active screen with replace', () => { it('replaces active screen with replace', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -763,9 +999,10 @@ it('replaces active screen with replace', () => {
it("doesn't handle replace if source key isn't present", () => { it("doesn't handle replace if source key isn't present", () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( 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", () => { it("doesn't handle replace if screen to replace with isn't present", () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -824,11 +1062,12 @@ it("doesn't handle replace if screen to replace with isn't present", () => {
it('handles push action', () => { it('handles push action', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: { routeParamList: {
baz: { foo: 21 }, baz: { foo: 21 },
}, },
routeGetIdList: {},
}; };
expect( expect(
@@ -895,6 +1134,152 @@ it('handles push action', () => {
options options
) )
).toBe(null); ).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( expect(
router.getStateForAction( 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({}); const router = StackRouter({});
const options: RouterConfigOptions = {
expect( routeNames: ['baz', 'bar', 'qux'],
router.getStateForRouteFocus( routeParamList: {},
{ routeGetIdList: {
index: 2, baz: ({ params }) => params?.foo,
key: 'stack-test', bar: ({ params }) => params?.foo,
routeNames: ['bar', 'baz', 'qux'], qux: ({ params }) => params?.test,
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,
}; };
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', () => { it('merges params on navigate to an existing screen', () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( 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", () => { it("doesn't merge params on navigate to an existing screen if merge: false", () => {
const router = StackRouter({}); const router = StackRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: { routeParamList: {
baz: { foo: 12 }, baz: { foo: 12 },
}, },
routeGetIdList: {},
}; };
expect( expect(

View File

@@ -4,6 +4,7 @@ import {
TabActions, TabActions,
TabNavigationState, TabNavigationState,
ParamListBase, ParamListBase,
RouterConfigOptions,
} from '..'; } from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' })); 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 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}) })
).toEqual({ ).toEqual({
index: 1, index: 1,
@@ -44,6 +46,7 @@ it('gets initial state from route names and params without initialRouteName', ()
baz: { answer: 42 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}) })
).toEqual({ ).toEqual({
index: 0, index: 0,
@@ -63,12 +66,13 @@ it('gets initial state from route names and params without initialRouteName', ()
it('gets rehydrated state from partial state', () => { it('gets rehydrated state from partial state', () => {
const router = TabRouter({}); const router = TabRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: { routeParamList: {
baz: { answer: 42 }, baz: { answer: 42 },
qux: { name: 'Jane' }, qux: { name: 'Jane' },
}, },
routeGetIdList: {},
}; };
expect( expect(
@@ -241,6 +245,7 @@ it("doesn't rehydrate state if it's not stale", () => {
router.getRehydratedState(state, { router.getRehydratedState(state, {
routeNames: [], routeNames: [],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}) })
).toBe(state); ).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', () => { it('restores correct history on rehydrating with backBehavior: order', () => {
const router = TabRouter({ backBehavior: 'order' }); const router = TabRouter({ backBehavior: 'order' });
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -289,9 +295,10 @@ it('restores correct history on rehydrating with backBehavior: order', () => {
it('restores correct history on rehydrating with backBehavior: history', () => { it('restores correct history on rehydrating with backBehavior: history', () => {
const router = TabRouter({ backBehavior: 'history' }); const router = TabRouter({ backBehavior: 'history' });
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -323,12 +330,16 @@ it('restores correct history on rehydrating with backBehavior: history', () => {
}); });
}); });
it('restores correct history on rehydrating with backBehavior: initialRoute', () => { it('restores correct history on rehydrating with backBehavior: firstRoute', () => {
const router = TabRouter({ backBehavior: 'initialRoute' }); const router = TabRouter({
backBehavior: 'firstRoute',
initialRouteName: 'bar',
});
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( 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', () => { it('restores correct history on rehydrating with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' }); const router = TabRouter({ backBehavior: 'none' });
const options = { const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'], routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -424,6 +480,7 @@ it('gets state on route names change', () => {
qux: { name: 'John' }, qux: { name: 'John' },
fiz: { fruit: 'apple' }, fiz: { fruit: 'apple' },
}, },
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -458,6 +515,7 @@ it('gets state on route names change', () => {
{ {
routeNames: ['foo', 'fiz'], routeNames: ['foo', 'fiz'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -498,6 +556,7 @@ it('preserves focused route on route names change', () => {
qux: { name: 'John' }, qux: { name: 'John' },
fiz: { fruit: 'apple' }, fiz: { fruit: 'apple' },
}, },
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -540,6 +599,7 @@ it('falls back to first route if route is removed on route names change', () =>
qux: { name: 'John' }, qux: { name: 'John' },
fiz: { fruit: 'apple' }, fiz: { fruit: 'apple' },
}, },
routeGetIdList: {},
} }
) )
).toEqual({ ).toEqual({
@@ -559,9 +619,10 @@ it('falls back to first route if route is removed on route names change', () =>
it('handles navigate action', () => { it('handles navigate action', () => {
const router = TabRouter({}); const router = TabRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz'], routeNames: ['bar', 'baz'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -647,11 +708,63 @@ it('handles navigate action', () => {
).toBe(null); ).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', () => { it('handles jump to action', () => {
const router = TabRouter({}); const router = TabRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz'], routeNames: ['bar', 'baz'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( 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', () => { it('handles back action with backBehavior: history', () => {
const router = TabRouter({ backBehavior: 'history' }); const router = TabRouter({ backBehavior: 'history' });
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
let state = router.getInitialState(options); let state = router.getInitialState(options);
@@ -776,9 +918,10 @@ it('handles back action with backBehavior: history', () => {
it('handles back action with backBehavior: order', () => { it('handles back action with backBehavior: order', () => {
const router = TabRouter({ backBehavior: 'order' }); const router = TabRouter({ backBehavior: 'order' });
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
let state = router.getInitialState(options); let state = router.getInitialState(options);
@@ -847,9 +990,10 @@ it('handles back action with backBehavior: order', () => {
it('handles back action with backBehavior: initialRoute', () => { it('handles back action with backBehavior: initialRoute', () => {
const router = TabRouter({ backBehavior: 'initialRoute' }); const router = TabRouter({ backBehavior: 'initialRoute' });
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
let state = router.getInitialState(options); let state = router.getInitialState(options);
@@ -919,9 +1063,10 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
initialRouteName: 'baz', initialRouteName: 'baz',
}); });
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
let state = router.getInitialState(options); 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', () => { it('handles back action with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' }); const router = TabRouter({ backBehavior: 'none' });
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
let state = router.getInitialState(options); 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', () => { it('updates route key history on navigate and jump to', () => {
const router = TabRouter({ backBehavior: 'history' }); const router = TabRouter({ backBehavior: 'history' });
const options = { const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
let state: TabNavigationState<ParamListBase> = { 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', () => { it('merges params on navigate to an existing screen', () => {
const router = TabRouter({}); const router = TabRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(
@@ -1193,6 +1341,7 @@ it("doesn't merge params on navigate to an existing screen if merge: false", ()
routeParamList: { routeParamList: {
qux: { color: 'indigo' }, qux: { color: 'indigo' },
}, },
routeGetIdList: {},
}; };
expect( expect(
@@ -1235,6 +1384,17 @@ it("doesn't merge params on navigate to an existing screen if merge: false", ()
{ type: 'route', key: 'bar' }, { 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( expect(
router.getStateForAction( 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', () => { it('merges params on jump to an existing screen', () => {
const router = TabRouter({}); const router = TabRouter({});
const options = { const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routeParamList: {}, routeParamList: {},
routeGetIdList: {},
}; };
expect( expect(

View File

@@ -132,6 +132,11 @@ export type RouterFactory<
export type RouterConfigOptions = { export type RouterConfigOptions = {
routeNames: string[]; routeNames: string[];
routeParamList: ParamListBase; routeParamList: ParamListBase;
routeGetIdList: Record<
string,
| ((options: { params?: Record<string, any> }) => string | undefined)
| undefined
>;
}; };
export type Router< export type Router<

View File

@@ -3,6 +3,82 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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) ## [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 **Note:** Version bump only for package @react-navigation/stack

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/stack", "name": "@react-navigation/stack",
"description": "Stack navigator component for iOS and Android with animated transitions and gestures", "description": "Stack navigator component for iOS and Android with animated transitions and gestures",
"version": "5.12.7", "version": "5.14.4",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -44,9 +44,8 @@
"react-native-iphone-x-helper": "^1.3.0" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2",
"@react-native-community/masked-view": "^0.1.10", "@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.8.10", "@react-navigation/native": "^5.9.4",
"@testing-library/react-native": "^7.1.0", "@testing-library/react-native": "^7.1.0",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
@@ -54,6 +53,7 @@
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-builder-bob": "^0.17.0",
"react-native-gesture-handler": "~1.7.0", "react-native-gesture-handler": "~1.7.0",
"react-native-safe-area-context": "3.1.4", "react-native-safe-area-context": "3.1.4",
"react-native-screens": "~2.10.1", "react-native-screens": "~2.10.1",
@@ -68,7 +68,7 @@
"react-native-safe-area-context": ">= 0.6.0", "react-native-safe-area-context": ">= 0.6.0",
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0" "react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
}, },
"@react-native-community/bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",
"targets": [ "targets": [

View File

@@ -61,4 +61,5 @@ export type {
StackHeaderInterpolatedStyle, StackHeaderInterpolatedStyle,
StackHeaderInterpolationProps, StackHeaderInterpolationProps,
StackHeaderStyleInterpolator, StackHeaderStyleInterpolator,
TransitionPreset,
} from './types'; } from './types';

View File

@@ -5,7 +5,7 @@ import { BaseButton } from 'react-native-gesture-handler';
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton); const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
type Props = React.ComponentProps<typeof BaseButton> & { type Props = React.ComponentProps<typeof BaseButton> & {
activeOpacity: number; pressOpacity: number;
}; };
const useNativeDriver = Platform.OS !== 'web'; const useNativeDriver = Platform.OS !== 'web';
@@ -27,7 +27,7 @@ export default class BorderlessButton extends React.Component<Props> {
overshootClamping: true, overshootClamping: true,
restDisplacementThreshold: 0.01, restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01, restSpeedThreshold: 0.01,
toValue: active ? this.props.activeOpacity : 1, toValue: active ? this.props.pressOpacity : 1,
useNativeDriver, useNativeDriver,
}).start(); }).start();
} }

View File

@@ -1,15 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { TextInput, Platform, Keyboard } from 'react-native'; import { TextInput, Keyboard, HostComponent } from 'react-native';
type Props = { type Props = {
enabled: boolean; enabled: boolean;
children: (props: { children: (props: {
onPageChangeStart: () => void; onPageChangeStart: () => void;
onPageChangeConfirm: () => void; onPageChangeConfirm: (force: boolean) => void;
onPageChangeCancel: () => void; onPageChangeCancel: () => void;
}) => React.ReactNode; }) => React.ReactNode;
}; };
type InputRef = React.ElementRef<HostComponent<unknown>> | undefined;
export default class KeyboardManager extends React.Component<Props> { export default class KeyboardManager extends React.Component<Props> {
componentWillUnmount() { componentWillUnmount() {
this.clearKeyboardTimeout(); this.clearKeyboardTimeout();
@@ -17,7 +19,7 @@ export default class KeyboardManager extends React.Component<Props> {
// Numeric id of the previously focused text input // Numeric id of the previously focused text input
// When a gesture didn't change the tab, we can restore the focused input with this // 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 startTimestamp: number = 0;
private keyboardTimeout: any; private keyboardTimeout: any;
@@ -35,7 +37,8 @@ export default class KeyboardManager extends React.Component<Props> {
this.clearKeyboardTimeout(); 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.currentlyFocusedInput()
: TextInput.State.currentlyFocusedField(); : TextInput.State.currentlyFocusedField();
@@ -49,25 +52,30 @@ export default class KeyboardManager extends React.Component<Props> {
this.startTimestamp = Date.now(); this.startTimestamp = Date.now();
}; };
private handlePageChangeConfirm = () => { private handlePageChangeConfirm = (force: boolean) => {
if (!this.props.enabled) { if (!this.props.enabled) {
return; return;
} }
this.clearKeyboardTimeout(); 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 (input) {
if (Platform.OS === 'android') { // Dismiss the keyboard only if an input was a focused before
Keyboard.dismiss(); // This makes sure we don't dismiss input on going back and focusing an input
} else {
TextInput.State.blurTextInput(input); TextInput.State.blurTextInput(input);
} }
} }
// Cleanup the ID on successful page change // Cleanup the ID on successful page change
this.previouslyFocusedTextInput = null; this.previouslyFocusedTextInput = undefined;
}; };
private handlePageChangeCancel = () => { private handlePageChangeCancel = () => {
@@ -91,11 +99,11 @@ export default class KeyboardManager extends React.Component<Props> {
if (Date.now() - this.startTimestamp < 100) { if (Date.now() - this.startTimestamp < 100) {
this.keyboardTimeout = setTimeout(() => { this.keyboardTimeout = setTimeout(() => {
TextInput.State.focusTextInput(input); TextInput.State.focusTextInput(input);
this.previouslyFocusedTextInput = null; this.previouslyFocusedTextInput = undefined;
}, 100); }, 100);
} else { } else {
TextInput.State.focusTextInput(input); TextInput.State.focusTextInput(input);
this.previouslyFocusedTextInput = null; this.previouslyFocusedTextInput = undefined;
} }
} }
}; };

View File

@@ -41,7 +41,7 @@ type Props = ViewProps & {
gestureDirection: GestureDirection; gestureDirection: GestureDirection;
onOpen: () => void; onOpen: () => void;
onClose: () => void; onClose: () => void;
onTransitionStart?: (props: { closing: boolean }) => void; onTransition?: (props: { closing: boolean; gesture: boolean }) => void;
onGestureBegin?: () => void; onGestureBegin?: () => void;
onGestureCanceled?: () => void; onGestureCanceled?: () => void;
onGestureEnd?: () => void; onGestureEnd?: () => void;
@@ -178,7 +178,7 @@ export default class Card extends React.Component<Props> {
transitionSpec, transitionSpec,
onOpen, onOpen,
onClose, onClose,
onTransitionStart, onTransition,
} = this.props; } = this.props;
const toValue = this.getAnimateToValue({ const toValue = this.getAnimateToValue({
@@ -198,7 +198,7 @@ export default class Card extends React.Component<Props> {
clearTimeout(this.pendingGestureCallback); clearTimeout(this.pendingGestureCallback);
onTransitionStart?.({ closing }); onTransition?.({ closing, gesture: velocity !== undefined });
animation(gesture, { animation(gesture, {
...spec.config, ...spec.config,
velocity, velocity,

View File

@@ -46,7 +46,7 @@ type Props = TransitionPreset & {
) => void; ) => void;
onTransitionEnd?: (props: { route: Route<string> }, closing: boolean) => void; onTransitionEnd?: (props: { route: Route<string> }, closing: boolean) => void;
onPageChangeStart?: () => void; onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void; onPageChangeConfirm?: (force: boolean) => void;
onPageChangeCancel?: () => void; onPageChangeCancel?: () => void;
onGestureStart?: (props: { route: Route<string> }) => void; onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void; onGestureEnd?: (props: { route: Route<string> }) => void;
@@ -116,42 +116,58 @@ function CardContainer({
scene, scene,
transitionSpec, transitionSpec,
}: Props) { }: Props) {
React.useEffect(() => {
onPageChangeConfirm?.();
}, [active, onPageChangeConfirm]);
const handleOpen = () => { const handleOpen = () => {
onTransitionEnd?.({ route: scene.route }, false); const { route } = scene;
onOpenRoute({ route: scene.route });
onTransitionEnd?.({ route }, false);
onOpenRoute({ route });
}; };
const handleClose = () => { const handleClose = () => {
onTransitionEnd?.({ route: scene.route }, true); const { route } = scene;
onCloseRoute({ route: scene.route });
onTransitionEnd?.({ route }, true);
onCloseRoute({ route });
}; };
const handleGestureBegin = () => { const handleGestureBegin = () => {
const { route } = scene;
onPageChangeStart?.(); onPageChangeStart?.();
onGestureStart?.({ route: scene.route }); onGestureStart?.({ route });
}; };
const handleGestureCanceled = () => { const handleGestureCanceled = () => {
const { route } = scene;
onPageChangeCancel?.(); onPageChangeCancel?.();
onGestureCancel?.({ route: scene.route }); onGestureCancel?.({ route });
}; };
const handleGestureEnd = () => { const handleGestureEnd = () => {
onGestureEnd?.({ route: scene.route }); const { route } = scene;
onGestureEnd?.({ route });
}; };
const handleTransitionStart = ({ closing }: { closing: boolean }) => { const handleTransition = ({
if (active && closing) { closing,
onPageChangeConfirm?.(); gesture,
}: {
closing: boolean;
gesture: boolean;
}) => {
const { route } = scene;
if (!gesture) {
onPageChangeConfirm?.(true);
} else if (active && closing) {
onPageChangeConfirm?.(false);
} else { } else {
onPageChangeCancel?.(); onPageChangeCancel?.();
} }
onTransitionStart?.({ route: scene.route }, closing); onTransitionStart?.({ route }, closing);
}; };
const insets = { const insets = {
@@ -201,7 +217,7 @@ function CardContainer({
overlay={cardOverlay} overlay={cardOverlay}
overlayEnabled={cardOverlayEnabled} overlayEnabled={cardOverlayEnabled}
shadowEnabled={cardShadowEnabled} shadowEnabled={cardShadowEnabled}
onTransitionStart={handleTransitionStart} onTransition={handleTransition}
onGestureBegin={handleGestureBegin} onGestureBegin={handleGestureBegin}
onGestureCanceled={handleGestureCanceled} onGestureCanceled={handleGestureCanceled}
onGestureEnd={handleGestureEnd} onGestureEnd={handleGestureEnd}

View File

@@ -4,6 +4,7 @@ import {
StyleSheet, StyleSheet,
LayoutChangeEvent, LayoutChangeEvent,
Dimensions, Dimensions,
Platform,
} from 'react-native'; } from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context'; import type { EdgeInsets } from 'react-native-safe-area-context';
import type { import type {
@@ -11,6 +12,7 @@ import type {
Route, Route,
StackNavigationState, StackNavigationState,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { screensEnabled } from 'react-native-screens';
import { import {
MaybeScreenContainer, MaybeScreenContainer,
@@ -65,7 +67,7 @@ type Props = {
) => void; ) => void;
onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void; onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void;
onPageChangeStart?: () => void; onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void; onPageChangeConfirm?: (force: boolean) => void;
onPageChangeCancel?: () => void; onPageChangeCancel?: () => void;
onGestureStart?: (props: { route: Route<string> }) => void; onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void; onGestureEnd?: (props: { route: Route<string> }) => void;
@@ -398,7 +400,7 @@ export default class CardStack extends React.Component<Props, State> {
onGestureCancel, onGestureCancel,
// Enable on new versions of `react-native-screens` // Enable on new versions of `react-native-screens`
// On older versions of `react-native-screens`, there's an issue with screens not being responsive to user interaction. // On older versions of `react-native-screens`, there's an issue with screens not being responsive to user interaction.
detachInactiveScreens = shouldUseActivityState, detachInactiveScreens = Platform.OS === 'web' || shouldUseActivityState,
} = this.props; } = this.props;
const { scenes, layout, gestures, headerHeights } = this.state; const { scenes, layout, gestures, headerHeights } = this.state;
@@ -488,11 +490,13 @@ export default class CardStack extends React.Component<Props, State> {
</React.Fragment> </React.Fragment>
) : null; ) : null;
const isScreensEnabled = screensEnabled?.() && detachInactiveScreens;
return ( return (
<React.Fragment> <React.Fragment>
{isFloatHeaderAbsolute ? null : floatingHeader} {isFloatHeaderAbsolute ? null : floatingHeader}
<MaybeScreenContainer <MaybeScreenContainer
enabled={detachInactiveScreens} enabled={isScreensEnabled}
style={styles.container} style={styles.container}
onLayout={this.handleLayout} onLayout={this.handleLayout}
> >
@@ -507,7 +511,7 @@ export default class CardStack extends React.Component<Props, State> {
// For the old implementation, it stays the same it was // For the old implementation, it stays the same it was
let isScreenActive: Animated.AnimatedInterpolation | 2 | 1 | 0 = 1; let isScreenActive: Animated.AnimatedInterpolation | 2 | 1 | 0 = 1;
if (shouldUseActivityState) { if (shouldUseActivityState || Platform.OS === 'web') {
if (index < self.length - activeScreensLimit - 1) { if (index < self.length - activeScreensLimit - 1) {
// screen should be inactive because it is too deep in the stack // screen should be inactive because it is too deep in the stack
isScreenActive = STATE_INACTIVE; isScreenActive = STATE_INACTIVE;
@@ -612,7 +616,7 @@ export default class CardStack extends React.Component<Props, State> {
<MaybeScreen <MaybeScreen
key={route.key} key={route.key}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
enabled={detachInactiveScreens} enabled={isScreensEnabled}
active={isScreenActive} active={isScreenActive}
pointerEvents="box-none" pointerEvents="box-none"
> >

View File

@@ -5,7 +5,7 @@ import { BaseButton } from 'react-native-gesture-handler';
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton); const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
type Props = React.ComponentProps<typeof BaseButton> & { type Props = React.ComponentProps<typeof BaseButton> & {
activeOpacity: number; pressOpacity: number;
}; };
const useNativeDriver = Platform.OS !== 'web'; const useNativeDriver = Platform.OS !== 'web';
@@ -27,7 +27,7 @@ export default class TouchableItem extends React.Component<Props> {
overshootClamping: true, overshootClamping: true,
restDisplacementThreshold: 0.01, restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01, restSpeedThreshold: 0.01,
toValue: active ? this.props.activeOpacity : 1, toValue: active ? this.props.pressOpacity : 1,
useNativeDriver, useNativeDriver,
}).start(); }).start();

666
yarn.lock

File diff suppressed because it is too large Load Diff