Compare commits

...

67 Commits

Author SHA1 Message Date
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
Satyajit Sahoo
7754eb450f chore: publish
- @react-navigation/bottom-tabs@5.11.2
 - @react-navigation/compat@5.3.10
 - @react-navigation/core@5.14.4
 - @react-navigation/devtools@5.1.18
 - @react-navigation/drawer@5.11.4
 - @react-navigation/material-bottom-tabs@5.3.10
 - @react-navigation/material-top-tabs@5.3.10
 - @react-navigation/native@5.8.10
 - @react-navigation/stack@5.12.7
2020-11-20 18:07:07 +01:00
Satyajit Sahoo
95b2599877 fix: fix incorrect state change events in independent nested container
fixes #9080
2020-11-20 18:06:35 +01:00
Satyajit Sahoo
efcfa7121f chore: only match repo links for GitHub in action 2020-11-20 12:03:50 +01:00
Satyajit Sahoo
a8e27ef448 chore: fix typo in github workflow 2020-11-16 15:33:46 +01:00
Satyajit Sahoo
946d2923d7 chore: publish
- @react-navigation/drawer@5.11.3
2020-11-16 02:01:44 +01:00
Satyajit Sahoo
794339eeed fix: hide drawer's header by default 2020-11-16 02:00:05 +01:00
Satyajit Sahoo
53141a6436 chore: add action to check for repro 2020-11-14 20:53:56 +01:00
Satyajit Sahoo
a2337648bf chore: publish
- @react-navigation/bottom-tabs@5.11.1
 - @react-navigation/compat@5.3.9
 - @react-navigation/core@5.14.3
 - @react-navigation/devtools@5.1.17
 - @react-navigation/drawer@5.11.2
 - @react-navigation/material-bottom-tabs@5.3.9
 - @react-navigation/material-top-tabs@5.3.9
 - @react-navigation/native@5.8.9
 - @react-navigation/stack@5.12.6
2020-11-10 20:41:26 +01:00
Satyajit Sahoo
8f764d8b08 fix: improve the error message for incorrect screen configuration 2020-11-10 20:29:59 +01:00
Satyajit Sahoo
f8e998b10c refactor: simplify getStateFromPath 2020-11-10 19:44:00 +01:00
Satyajit Sahoo
da35085f1e fix: make sure inactive screen don't increase scroll area on web 2020-11-10 18:21:36 +01:00
Satyajit Sahoo
1f5fb5481a chore: publish
- @react-navigation/drawer@5.11.1
2020-11-09 20:40:11 +01:00
Satyajit Sahoo
18bbd177d9 fix: provide correct context to drawe header 2020-11-09 20:37:26 +01:00
Satyajit Sahoo
151055cf5a chore: publish
- @react-navigation/bottom-tabs@5.11.0
 - @react-navigation/compat@5.3.8
 - @react-navigation/core@5.14.2
 - @react-navigation/devtools@5.1.16
 - @react-navigation/drawer@5.11.0
 - @react-navigation/material-bottom-tabs@5.3.8
 - @react-navigation/material-top-tabs@5.3.8
 - @react-navigation/native@5.8.8
 - @react-navigation/routers@5.6.2
 - @react-navigation/stack@5.12.5
2020-11-09 20:17:39 +01:00
Satyajit Sahoo
52172453df fix: try fixing drawer blink on Android 2020-11-09 20:05:27 +01:00
Satyajit Sahoo
7bc385e4f3 chore: show header in drawer by default 2020-11-09 19:36:36 +01:00
Satyajit Sahoo
6ac4d40140 feat: add a tabBarBadgeStyle option to customize the badge 2020-11-09 19:28:49 +01:00
Satyajit Sahoo
dbe961ba5b feat: add option to show a header in drawer navigator screens
This commit adds new `header` and `headerShown` options in drawer navigator to be able to show a header, along with a bunch of header related options similar to stack navigator.

Historically, we have suggested to nest a stack navigator inside drawer navigator to render a header. While it works, it's not efficient to nest an entire navigator just for a header, considering it adds a lot of additional overhead from the code to handle animations, gestures etc. which won't ever be run in this case. It also increases the view hierarchy which has negative impacts on performance on Android, and could cause content not to render on older iOS devices.

Another issue with the approach is that since drawer navigator is at the root in this setup, it's possible to open drawer from every screen in the stack navigator, which usually isn't the expected behaviour. It's necessary to write additional code to disable the gesture to open drawer in all screens but first.

In addition, users also need to add a custom drawer icon to the header manually to be able to toggle the drawer

If drawer navigator could render its own header we'd avoid all these shortcomings as well as make the code simpler.

For now, I have implemented a new `Header` component in drawer since it's way simpler than stack navigator header. Though we may consider creating a shared UI package and add such components there which all our navigators could use.

The `Header` includes a button to toggle the drawer by default, and supports customization options such as showing custom left/right/title components. For this commit, I have kept `headerShown` to `false` by default coz I wasn't sure if it'd be a breaking change to start showing headers in drawers. Probably we can toggle it to `true` by default in next major.
2020-11-09 18:52:24 +01:00
Satyajit Sahoo
05d4e4d3be refactor: minor tweak 2020-11-09 02:02:43 +01:00
Satyajit Sahoo
48b2e77730 fix: throw if the same pattern resolves to multiple screens 2020-11-09 01:56:30 +01:00
Satyajit Sahoo
e08c91ff0a feat: add a hook to get bottom tab bar height
Usage:

```js
import { useBottomTabBarHeight } from '@react-navigation/stack';

// ...

const headerHeight = useBottomTabBarHeight();
```

closes #8037, closes #8536
2020-11-08 20:08:50 +01:00
Satyajit Sahoo
5bd682f0bf feat: add a getIsDrawerOpenFromState utility to drawer 2020-11-08 17:51:13 +01:00
Satyajit Sahoo
50a161dc3d chore: publish
- @react-navigation/bottom-tabs@5.10.7
 - @react-navigation/compat@5.3.7
 - @react-navigation/core@5.14.1
 - @react-navigation/devtools@5.1.15
 - @react-navigation/drawer@5.10.7
 - @react-navigation/material-bottom-tabs@5.3.7
 - @react-navigation/material-top-tabs@5.3.7
 - @react-navigation/native@5.8.7
 - @react-navigation/routers@5.6.1
 - @react-navigation/stack@5.12.4
2020-11-08 09:06:37 +01:00
Satyajit Sahoo
360b0e9958 fix: tweak error message when navigator has non-screen children 2020-11-07 16:43:45 +01:00
Satyajit Sahoo
e50c8aa942 refactor: use a regular action for 'resetRoot'
Previously, 'resetRoot' directly performed a 'setState' on the container instead of dispatching an action. This meant that hooks such as listener for 'preventRemove' won't be notified by it. This commit changes it to dispatch a regular 'RESET' action which will behave the same as other actions.
2020-11-07 15:55:48 +01:00
Satyajit Sahoo
8f0efc8db5 fix: don't hide child header automatically in stack 2020-11-07 14:39:23 +01:00
Satyajit Sahoo
7de6677e72 chore: fix statusbar height in modal example 2020-11-07 00:34:25 +01:00
Satyajit Sahoo
1dad338b7a chore: publish
- @react-navigation/bottom-tabs@5.10.6
 - @react-navigation/compat@5.3.6
 - @react-navigation/core@5.14.0
 - @react-navigation/devtools@5.1.14
 - @react-navigation/drawer@5.10.6
 - @react-navigation/material-bottom-tabs@5.3.6
 - @react-navigation/material-top-tabs@5.3.6
 - @react-navigation/native@5.8.6
 - @react-navigation/routers@5.6.0
 - @react-navigation/stack@5.12.3
2020-11-04 22:37:22 +01:00
Satyajit Sahoo
ce7d20e336 fix: disable react-native-screens on iOS for older versions 2020-11-04 22:36:43 +01:00
Satyajit Sahoo
e3e58c2d89 feat: add a NavigatorScreenParams type. closes #6931 2020-11-04 22:36:43 +01:00
Satyajit Sahoo
cb2e744dce fix: always respect key in the route object when generating action 2020-11-04 22:36:43 +01:00
Satyajit Sahoo
9beca3a802 feat: add a merge option to navigate to control merging params 2020-11-04 22:36:43 +01:00
Satyajit Sahoo
ec7b02af2c feat: add warning on accessing the state object on route prop 2020-11-04 22:36:43 +01:00
Satyajit Sahoo
4c2379cec1 fix: ignore any errors from deep linking 2020-11-04 22:36:43 +01:00
marhaupe
1169ed0946 fix: android textinput gets blurred after navigating back
When navigating from ScreenA to ScreenB and then back to ScreenA,
react-navigation unconditionally calls `Keyboard.dismiss()`.
If the user is fast enough and taps on a `TextInput` after coming
back from ScreenB, the keyboard opens, just to be dismissed again.
This would also happen if some logic automatically focuses the
`TextInput` in ScreenA. This behaviour can be seen observed in
https://snack.expo.io/lTDZhVNhV.
2020-11-04 22:35:57 +01:00
Satyajit Sahoo
bf464a8378 chore: tweak workflows for triage 2020-11-04 14:23:46 +01:00
Satyajit Sahoo
a495506e20 chore: publish
- @react-navigation/bottom-tabs@5.10.5
 - @react-navigation/compat@5.3.5
 - @react-navigation/core@5.13.5
 - @react-navigation/devtools@5.1.13
 - @react-navigation/drawer@5.10.5
 - @react-navigation/material-bottom-tabs@5.3.5
 - @react-navigation/material-top-tabs@5.3.5
 - @react-navigation/native@5.8.5
 - @react-navigation/stack@5.12.2
2020-11-04 13:24:15 +01:00
Satyajit Sahoo
b20f2d1f7c fix: use useDebugValue in more places 2020-11-04 13:21:36 +01:00
Satyajit Sahoo
66f3a4a0bb fix: don't use use-subscription to avoid peer dep related errors
The `use-subscription` package has a peer dep on latest React. This is problematic when using npm due to it's peer dependency algorithm which installs multiple versions of React when using an older version of React (Native).

This means that we'll need to use an ancient version of `use-subscription` to support older React versions with npm and make sure to never update it, or test with every version.

It's much lower maintenance to incporporate the same update in render logic that `use-subscription` has and not deal with dependencies. So this commit removes the `use-subscription` dependency.

See https://github.com/react-navigation/react-navigation/issues/9021#issuecomment-721679760 for more context.
2020-11-04 13:06:49 +01:00
110 changed files with 4507 additions and 972 deletions

View File

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

48
.github/workflows/check-repro.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Check for repro
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
jobs:
check-repro:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const user = context.payload.sender.login;
const body = context.payload.comment
? context.payload.comment.body
: context.payload.issue.body;
const regex = new RegExp(
`https?:\\/\\/((github\\.com\\/${user}\\/[^/]+\\/?[\\s\\n]+)|(snack\\.expo\\.io\\/.+))`,
'gm'
);
if (!regex.test(body)) {
return;
}
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['repro provided'],
});
try {
await github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'needs repro',
});
} catch (error) {
if (!/Label does not exist/.test(error.message)) {
throw error;
}
}

View File

@@ -45,7 +45,7 @@ jobs:
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
- name: Comment on PR
uses: actions/github-script@v2
uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
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
if: github.event.label.name == 'needs more info'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -16,14 +16,14 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help.\n\nMake sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
body: "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help.\n\nMake sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.). In addition, if you can provide a video or GIF demonstrating the issue, it will also be very helpful."
})
needs-repro:
runs-on: ubuntu-latest
if: github.event.label.name == 'needs repro'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'question'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -46,14 +46,14 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. This helps us prioritize fixing bugs in the library. Seems you have a usage question or an issue unrelated to this library. Please ask the question on [StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation) instead using the `react-navigation` label. You can also chat with other community members on [Reactiflux Discord server](https://www.reactiflux.com/) in the `#react-navigation` channel.\n\nIf you believe that this is actually a bug in the library, please open a new issue and fill the issue template with relevant information."
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:
runs-on: ubuntu-latest
if: github.event.label.name == 'feature-request'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -68,7 +68,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-screens'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -76,14 +76,14 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports in React Navigation. Seems you have an issue related to `native-stack` navigator or `react-native-screens` library. Please post your issue in [this repo](https://github.com/software-mansion/react-native-screens) so that it's notified to the maintainers of that library."
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-screens` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-screens) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
})
react-native-reanimated:
runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-reanimated'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -91,14 +91,14 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports in React Navigation. Seems you have an issue related to `react-native-reanimated` library. Please post your issue in [this repo](https://github.com/software-mansion/react-native-reanimated) so that it's notified to the maintainers of that library."
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-reanimated` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-reanimated) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
})
react-native-gesture-handler:
runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-gesture-handler'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -106,14 +106,14 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports in React Navigation. Seems you have an issue related to `react-native-gesture-handler` library. Please post your issue in [this repo](https://github.com/software-mansion/react-native-gesture-handler) so that it's notified to the maintainers of that library."
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-gesture-handler` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-gesture-handler) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
})
react-native-safe-area-context:
runs-on: ubuntu-latest
if: github.event.label.name == 'library:react-native-safe-area-context'
steps:
- uses: actions/github-script@v2
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -121,5 +121,5 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports in React Navigation. Seems you have an issue related to `react-native-safe-area-context` library. Please post your issue in [this repo](https://github.com/th3rdwave/react-native-safe-area-context) so that it's notified to the maintainers of that library."
body: "Hey! Thanks for opening the issue. Seems that this issue issue related to `react-native-safe-area-context` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/th3rdwave/react-native-safe-area-context) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it."
})

View File

@@ -8,12 +8,15 @@ const blacklist = require('metro-config/src/defaults/blacklist');
const root = path.resolve(__dirname, '..');
const packages = path.resolve(root, 'packages');
// List all packages under `packages/`
const workspaces = fs
// List all packages under `packages/`
.readdirSync(packages)
// Ignore hidden files such as .DS_Store
.filter((p) => !p.startsWith('.'))
.map((p) => path.join(packages, p));
.map((p) => path.join(packages, p))
.filter(
(p) =>
fs.statSync(p).isDirectory() &&
fs.existsSync(path.join(p, 'package.json'))
);
// Get the list of dependencies for all packages in the monorepo
const modules = ['@expo/vector-icons']
@@ -68,14 +71,9 @@ module.exports = {
enhanceMiddleware: (middleware) => {
return (req, res, next) => {
// When an asset is imported outside the project root, it has wrong path on Android
// This happens for the back button in stack, so we fix the path to correct one
const assets = '/packages/stack/src/views/assets';
if (req.url.startsWith(assets)) {
req.url = req.url.replace(
assets,
'/assets/../packages/stack/src/views/assets'
);
// So we fix the path to correct one
if (/\/packages\/.+\.png\?.+$/.test(req.url)) {
req.url = `/assets/../${req.url}`;
}
return middleware(req, res, next);

View File

@@ -5,6 +5,7 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI
import {
getFocusedRouteNameFromRoute,
ParamListBase,
NavigatorScreenParams,
} from '@react-navigation/native';
import type { StackScreenProps } from '@react-navigation/stack';
import {
@@ -15,7 +16,7 @@ import TouchableBounce from '../Shared/TouchableBounce';
import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts';
import Chat from '../Shared/Chat';
import SimpleStackScreen from './SimpleStack';
import SimpleStackScreen, { SimpleStackParams } from './SimpleStack';
const getTabBarIcon = (name: string) => ({
color,
@@ -26,7 +27,7 @@ const getTabBarIcon = (name: string) => ({
}) => <MaterialCommunityIcons name={name} color={color} size={size} />;
type BottomTabParams = {
Article: undefined;
Article: NavigatorScreenParams<SimpleStackParams>;
Albums: undefined;
Contacts: undefined;
Chat: undefined;
@@ -85,12 +86,18 @@ export default function BottomTabsScreen({
>
<BottomTabs.Screen
name="Article"
component={SimpleStackScreen}
options={{
title: 'Article',
tabBarIcon: getTabBarIcon('file-document-box'),
}}
/>
>
{(props) => (
<SimpleStackScreen
{...props}
screenOptions={{ headerShown: false }}
/>
)}
</BottomTabs.Screen>
<BottomTabs.Screen
name="Chat"
component={Chat}

View File

@@ -1,13 +1,14 @@
import * as React from 'react';
import { StyleSheet } from 'react-native';
import type { NavigatorScreenParams } from '@react-navigation/native';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts';
import Chat from '../Shared/Chat';
import SimpleStackScreen from './SimpleStack';
import SimpleStackScreen, { SimpleStackParams } from './SimpleStack';
type MaterialBottomTabParams = {
Article: undefined;
Article: NavigatorScreenParams<SimpleStackParams>;
Albums: undefined;
Contacts: undefined;
Chat: undefined;
@@ -22,13 +23,19 @@ export default function MaterialBottomTabsScreen() {
<MaterialBottomTabs.Navigator barStyle={styles.tabBar}>
<MaterialBottomTabs.Screen
name="Article"
component={SimpleStackScreen}
options={{
tabBarLabel: 'Article',
tabBarIcon: 'file-document-box',
tabBarColor: '#C9E7F8',
}}
/>
>
{(props) => (
<SimpleStackScreen
{...props}
screenOptions={{ headerShown: false }}
/>
)}
</MaterialBottomTabs.Screen>
<MaterialBottomTabs.Screen
name="Chat"
component={Chat}

View File

@@ -93,7 +93,9 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
cardOverlayEnabled: true,
gestureEnabled: true,
headerStatusBarHeight:
navigation.dangerouslyGetState().routes.indexOf(route) > 0
navigation
.dangerouslyGetState()
.routes.findIndex((r: any) => r.key === route.key) > 0
? 0
: undefined,
})}

View File

@@ -77,18 +77,28 @@ const InputScreen = ({
e.preventDefault();
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure to discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
onPress: () => navigation.dispatch(action),
},
]
);
if (Platform.OS === 'web') {
const discard = confirm(
'You have unsaved changes. Discard them and leave the screen?'
);
if (discard) {
navigation.dispatch(action);
}
} else {
Alert.alert(
'Discard changes?',
'You have unsaved changes. Discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
onPress: () => navigation.dispatch(action),
},
]
);
}
}),
[hasUnsavedChanges, navigation]
);

View File

@@ -4,13 +4,14 @@ import { Button } from 'react-native-paper';
import type { ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationOptions,
StackScreenProps,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type SimpleStackParams = {
export type SimpleStackParams = {
Article: { author: string } | undefined;
NewsFeed: { date: number };
Albums: undefined;
@@ -105,7 +106,10 @@ const SimpleStack = createStackNavigator<SimpleStackParams>();
export default function SimpleStackScreen({
navigation,
}: StackScreenProps<ParamListBase>) {
screenOptions,
}: StackScreenProps<ParamListBase> & {
screenOptions?: StackNavigationOptions;
}) {
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
@@ -113,7 +117,7 @@ export default function SimpleStackScreen({
}, [navigation]);
return (
<SimpleStack.Navigator>
<SimpleStack.Navigator screenOptions={screenOptions}>
<SimpleStack.Screen
name="Article"
component={ArticleScreen}

View File

@@ -9,13 +9,13 @@ import {
Linking,
LogBox,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { enableScreens } from 'react-native-screens';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {
Provider as PaperProvider,
DefaultTheme as PaperLightTheme,
DarkTheme as PaperDarkTheme,
Appbar,
List,
Divider,
Text,
@@ -28,10 +28,7 @@ import {
PathConfigMap,
NavigationContainerRef,
} from '@react-navigation/native';
import {
createDrawerNavigator,
DrawerScreenProps,
} from '@react-navigation/drawer';
import { createDrawerNavigator } from '@react-navigation/drawer';
import {
createStackNavigator,
StackScreenProps,
@@ -65,8 +62,7 @@ if (Platform.OS !== 'web') {
enableScreens();
type RootDrawerParamList = {
Root: undefined;
Another: undefined;
Examples: undefined;
};
const SCREENS = {
@@ -231,50 +227,49 @@ export default function App() {
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
prefixes: LinkingPrefixes,
config: {
screens: {
Root: {
path: '',
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<PathConfigMap>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
const path = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<PathConfigMap>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
const path = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
acc[name] = {
path,
screens: {
Article: {
path: 'article/:author?',
parse: {
author: (author) =>
author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '),
},
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
Dialog: 'dialog',
acc[name] = {
path,
screens: {
Article: {
path: 'article/:author?',
parse: {
author: (author) =>
author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '),
},
};
return acc;
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
Dialog: 'dialog',
},
{
Home: '',
NotFound: '*',
}
),
};
return acc;
},
},
{
Home: {
screens: {
Examples: '',
},
},
NotFound: '*',
}
),
},
}}
fallback={<Text>Loading</Text>}
@@ -283,95 +278,91 @@ export default function App() {
`${options?.title ?? route?.name} - React Navigation Example`,
}}
>
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen
name="Root"
<Stack.Navigator
screenOptions={{
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
}}
>
<Stack.Screen
name="Home"
options={{
title: 'Examples',
drawerIcon: ({ size, color }) => (
<MaterialIcons size={size} color={color} name="folder" />
),
headerShown: false,
}}
>
{({ navigation }: DrawerScreenProps<RootDrawerParamList>) => (
<Stack.Navigator
screenOptions={{
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
}}
{() => (
<Drawer.Navigator
drawerType={isLargeScreen ? 'permanent' : undefined}
screenOptions={{ headerShown: true }}
>
<Stack.Screen
name="Home"
<Drawer.Screen
name="Examples"
options={{
title: 'Examples',
headerLeft: isLargeScreen
? undefined
: () => (
<Appbar.Action
color={theme.colors.text}
icon="menu"
onPress={() => navigation.toggleDrawer()}
/>
),
drawerIcon: ({ size, color }) => (
<MaterialIcons size={size} color={color} name="folder" />
),
}}
>
{({ navigation }: StackScreenProps<RootStackParamList>) => (
<ScrollView
style={{ backgroundColor: theme.colors.background }}
>
<SettingsItem
label="Right to left"
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
restartApp();
}}
/>
<Divider />
<SettingsItem
label="Dark theme"
value={theme.dark}
onValueChange={() => {
AsyncStorage.setItem(
THEME_PERSISTENCE_KEY,
theme.dark ? 'light' : 'dark'
);
<SafeAreaView edges={['right', 'bottom', 'left']}>
<SettingsItem
label="Right to left"
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
restartApp();
}}
/>
<Divider />
<SettingsItem
label="Dark theme"
value={theme.dark}
onValueChange={() => {
AsyncStorage.setItem(
THEME_PERSISTENCE_KEY,
theme.dark ? 'light' : 'dark'
);
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme));
}}
/>
<Divider />
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
(name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
/>
)
)}
setTheme((t) =>
t.dark ? DefaultTheme : DarkTheme
);
}}
/>
<Divider />
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
(name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
/>
)
)}
</SafeAreaView>
</ScrollView>
)}
</Stack.Screen>
<Stack.Screen
name="NotFound"
component={NotFound}
options={{ title: 'Oops!' }}
/>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
(name) => (
<Stack.Screen
key={name}
name={name}
getComponent={() => SCREENS[name].component}
options={{ title: SCREENS[name].title }}
/>
)
)}
</Stack.Navigator>
</Drawer.Screen>
</Drawer.Navigator>
)}
</Drawer.Screen>
</Drawer.Navigator>
</Stack.Screen>
<Stack.Screen
name="NotFound"
component={NotFound}
options={{ title: 'Oops!' }}
/>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map((name) => (
<Stack.Screen
key={name}
name={name}
getComponent={() => SCREENS[name].component}
options={{ title: SCREENS[name].title }}
/>
))}
</Stack.Navigator>
</NavigationContainer>
</PaperProvider>
);

View File

@@ -3,6 +3,83 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.11.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.3...@react-navigation/bottom-tabs@5.11.4) (2021-01-21)
### Bug Fixes
* fix drawer and bottom tabs not being visible on web. closes [#9225](https://github.com/react-navigation/react-navigation/issues/9225) ([d88cbcb](https://github.com/react-navigation/react-navigation/commit/d88cbcb52d46de26edaa9ce6bfb06badb1b1de64))
## [5.11.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.2...@react-navigation/bottom-tabs@5.11.3) (2021-01-14)
### Bug Fixes
* enable detachInactiveScreens by default on web for better a11y ([dd87fa4](https://github.com/react-navigation/react-navigation/commit/dd87fa49a43ad8db105a62418243339e4150fadf))
## [5.11.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.1...@react-navigation/bottom-tabs@5.11.2) (2020-11-20)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.11.0...@react-navigation/bottom-tabs@5.11.1) (2020-11-10)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.7...@react-navigation/bottom-tabs@5.11.0) (2020-11-09)
### Features
* add a hook to get bottom tab bar height ([e08c91f](https://github.com/react-navigation/react-navigation/commit/e08c91ff0a3df13dc6e6096a3e95f60722e6946b)), closes [#8037](https://github.com/react-navigation/react-navigation/issues/8037) [#8536](https://github.com/react-navigation/react-navigation/issues/8536)
* add a tabBarBadgeStyle option to customize the badge ([6ac4d40](https://github.com/react-navigation/react-navigation/commit/6ac4d40140189a29d857c4d1203bced6929f7baf))
## [5.10.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.6...@react-navigation/bottom-tabs@5.10.7) (2020-11-08)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.10.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.5...@react-navigation/bottom-tabs@5.10.6) (2020-11-04)
### Bug Fixes
* disable react-native-screens on iOS for older versions ([ce7d20e](https://github.com/react-navigation/react-navigation/commit/ce7d20e3366415b07a537e01ee0b17ce7e72cad6))
## [5.10.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.4...@react-navigation/bottom-tabs@5.10.5) (2020-11-04)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.10.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.3...@react-navigation/bottom-tabs@5.10.4) (2020-11-03)
**Note:** Version bump only for package @react-navigation/bottom-tabs

View File

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

View File

@@ -9,6 +9,13 @@ export { default as createBottomTabNavigator } from './navigators/createBottomTa
export { default as BottomTabView } from './views/BottomTabView';
export { default as BottomTabBar } from './views/BottomTabBar';
/**
* Utilities
*/
export { default as BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
export { default as useBottomTabBarHeight } from './utils/useBottomTabBarHeight';
/**
* Types
*/

View File

@@ -109,6 +109,12 @@ export type BottomTabNavigationOptions = {
*/
tabBarBadge?: number | string;
/**
* Custom style for the tab bar badge.
* You can specify a background color or text color here.
*/
tabBarBadgeStyle?: StyleProp<TextStyle>;
/**
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
* It's recommended to set this if you don't have a label for the tab.
@@ -174,7 +180,7 @@ export type BottomTabNavigationConfig<T = BottomTabBarOptions> = {
/**
* Whether inactive screens should be detached from the view hierarchy to save memory.
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
* Defaults to `true`.
* Defaults to `true` on Android.
*/
detachInactiveScreens?: boolean;
/**

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
export default React.createContext<((height: number) => void) | undefined>(
undefined
);

View File

@@ -0,0 +1,3 @@
import * as React from 'react';
export default React.createContext<number | undefined>(undefined);

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import BottomTabBarHeightContext from './BottomTabBarHeightContext';
export default function useFloatingBottomTabBarHeight() {
const height = React.useContext(BottomTabBarHeightContext);
if (height === undefined) {
throw new Error(
"Couldn't find the bottom tab bar height. Are you inside a screen in Bottom Tab Navigator?"
);
}
return height;
}

View File

@@ -5,20 +5,25 @@ import {
StyleSheet,
Platform,
LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native';
import {
NavigationContext,
NavigationRouteContext,
TabNavigationState,
ParamListBase,
CommonActions,
useTheme,
useLinkBuilder,
} from '@react-navigation/native';
import { useSafeArea } from 'react-native-safe-area-context';
import { useSafeArea, EdgeInsets } from 'react-native-safe-area-context';
import BottomTabItem from './BottomTabItem';
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
import useWindowDimensions from '../utils/useWindowDimensions';
import useIsKeyboardShown from '../utils/useIsKeyboardShown';
import type { BottomTabBarProps } from '../types';
import type { BottomTabBarProps, LabelPosition } from '../types';
type Props = BottomTabBarProps & {
activeTintColor?: string;
@@ -31,13 +36,93 @@ const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
const useNativeDriver = Platform.OS !== 'web';
type Options = {
state: TabNavigationState<ParamListBase>;
layout: { height: number; width: number };
dimensions: { height: number; width: number };
tabStyle: StyleProp<ViewStyle>;
labelPosition: LabelPosition | undefined;
adaptive: boolean | undefined;
};
const shouldUseHorizontalLabels = ({
state,
layout,
dimensions,
adaptive = true,
labelPosition,
tabStyle,
}: Options) => {
if (labelPosition) {
return labelPosition === 'beside-icon';
}
if (!adaptive) {
return false;
}
if (layout.width >= 768) {
// Screen size matches a tablet
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
const flattenedStyle = StyleSheet.flatten(tabStyle);
if (flattenedStyle) {
if (typeof flattenedStyle.width === 'number') {
maxTabItemWidth = flattenedStyle.width;
} else if (typeof flattenedStyle.maxWidth === 'number') {
maxTabItemWidth = flattenedStyle.maxWidth;
}
}
return state.routes.length * maxTabItemWidth <= layout.width;
} else {
return dimensions.width > dimensions.height;
}
};
const getPaddingBottom = (insets: EdgeInsets) =>
Math.max(insets.bottom - Platform.select({ ios: 4, default: 0 }), 0);
export const getTabBarHeight = ({
dimensions,
insets,
style,
...rest
}: Options & {
insets: EdgeInsets;
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
}) => {
// @ts-ignore
const customHeight = StyleSheet.flatten(style)?.height;
if (typeof customHeight === 'number') {
return customHeight;
}
const isLandscape = dimensions.width > dimensions.height;
const horizontalLabels = shouldUseHorizontalLabels({ dimensions, ...rest });
const paddingBottom = getPaddingBottom(insets);
if (
Platform.OS === 'ios' &&
!Platform.isPad &&
isLandscape &&
horizontalLabels
) {
return COMPACT_TABBAR_HEIGHT + paddingBottom;
}
return DEFAULT_TABBAR_HEIGHT + paddingBottom;
};
export default function BottomTabBar({
state,
navigation,
descriptors,
activeBackgroundColor,
activeTintColor,
adaptive = true,
adaptive,
allowFontScaling,
inactiveBackgroundColor,
inactiveTintColor,
@@ -60,6 +145,8 @@ export default function BottomTabBar({
const dimensions = useWindowDimensions();
const isKeyboardShown = useIsKeyboardShown();
const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
const shouldShowTabBar =
focusedOptions.tabBarVisible !== false &&
!(keyboardHidesTabBar && isKeyboardShown);
@@ -120,11 +207,19 @@ export default function BottomTabBar({
width: dimensions.width,
});
const isLandscape = () => dimensions.width > dimensions.height;
const handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout;
const topBorderWidth =
// @ts-ignore
StyleSheet.flatten([styles.tabBar, style])?.borderTopWidth;
onHeightChange?.(
height +
paddingBottom +
(typeof topBorderWidth === 'number' ? topBorderWidth : 0)
);
setLayout((layout) => {
if (height === layout.height && width === layout.width) {
return layout;
@@ -138,34 +233,6 @@ export default function BottomTabBar({
};
const { routes } = state;
const shouldUseHorizontalLabels = () => {
if (labelPosition) {
return labelPosition === 'beside-icon';
}
if (!adaptive) {
return false;
}
if (layout.width >= 768) {
// Screen size matches a tablet
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
const flattenedStyle = StyleSheet.flatten(tabStyle);
if (flattenedStyle) {
if (typeof flattenedStyle.width === 'number') {
maxTabItemWidth = flattenedStyle.width;
} else if (typeof flattenedStyle.maxWidth === 'number') {
maxTabItemWidth = flattenedStyle.maxWidth;
}
}
return routes.length * maxTabItemWidth <= layout.width;
} else {
return isLandscape();
}
};
const defaultInsets = useSafeArea();
@@ -176,22 +243,26 @@ export default function BottomTabBar({
left: safeAreaInsets?.left ?? defaultInsets.left,
};
const paddingBottom = Math.max(
insets.bottom - Platform.select({ ios: 4, default: 0 }),
0
);
const paddingBottom = getPaddingBottom(insets);
const tabBarHeight = getTabBarHeight({
state,
insets,
dimensions,
layout,
adaptive,
labelPosition,
tabStyle,
style,
});
const getDefaultTabBarHeight = () => {
if (
Platform.OS === 'ios' &&
!Platform.isPad &&
isLandscape() &&
shouldUseHorizontalLabels()
) {
return COMPACT_TABBAR_HEIGHT;
}
return DEFAULT_TABBAR_HEIGHT;
};
const hasHorizontalLabels = shouldUseHorizontalLabels({
state,
dimensions,
layout,
adaptive,
labelPosition,
tabStyle,
});
return (
<Animated.View
@@ -218,7 +289,7 @@ export default function BottomTabBar({
position: isTabBarHidden ? 'absolute' : (null as any),
},
{
height: getDefaultTabBarHeight() + paddingBottom,
height: tabBarHeight,
paddingBottom,
paddingHorizontal: Math.max(insets.left, insets.right),
},
@@ -276,7 +347,7 @@ export default function BottomTabBar({
<BottomTabItem
route={route}
focused={focused}
horizontal={shouldUseHorizontalLabels()}
horizontal={hasHorizontalLabels}
onPress={onPress}
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
@@ -290,6 +361,7 @@ export default function BottomTabBar({
button={options.tabBarButton}
icon={options.tabBarIcon}
badge={options.tabBarBadge}
badgeStyle={options.tabBarBadgeStyle}
label={label}
showLabel={showLabel}
labelStyle={labelStyle}

View File

@@ -47,6 +47,10 @@ type Props = {
* Text to show in a badge on the tab icon.
*/
badge?: number | string;
/**
* Custom style for the badge.
*/
badgeStyle?: StyleProp<TextStyle>;
/**
* URL to use for the link to the tab.
*/
@@ -122,6 +126,7 @@ export default function BottomTabBarItem({
label,
icon,
badge,
badgeStyle,
to,
button = ({
children,
@@ -235,6 +240,7 @@ export default function BottomTabBarItem({
route={route}
horizontal={horizontal}
badge={badge}
badgeStyle={badgeStyle}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
activeTintColor={activeTintColor}

View File

@@ -1,5 +1,11 @@
import * as React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import {
View,
StyleSheet,
Dimensions,
StyleProp,
ViewStyle,
} from 'react-native';
import {
NavigationHelpersContext,
@@ -9,9 +15,13 @@ import {
} from '@react-navigation/native';
import { ScreenContainer } from 'react-native-screens';
import SafeAreaProviderCompat from './SafeAreaProviderCompat';
import SafeAreaProviderCompat, {
initialSafeAreaInsets,
} from './SafeAreaProviderCompat';
import ResourceSavingScene from './ResourceSavingScene';
import BottomTabBar from './BottomTabBar';
import BottomTabBar, { getTabBarHeight } from './BottomTabBar';
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
import BottomTabBarHeightContext from '../utils/BottomTabBarHeightContext';
import type {
BottomTabNavigationConfig,
BottomTabDescriptorMap,
@@ -27,6 +37,7 @@ type Props = BottomTabNavigationConfig & {
type State = {
loaded: string[];
tabBarHeight: number;
};
function SceneContent({
@@ -67,9 +78,28 @@ export default class BottomTabView extends React.Component<Props, State> {
};
}
state: State = {
loaded: [this.props.state.routes[this.props.state.index].key],
};
constructor(props: Props) {
super(props);
const { state, tabBarOptions } = this.props;
const dimensions = Dimensions.get('window');
const tabBarHeight = getTabBarHeight({
state,
dimensions,
layout: { width: dimensions.width, height: 0 },
insets: initialSafeAreaInsets,
adaptive: tabBarOptions?.adaptive,
labelPosition: tabBarOptions?.labelPosition,
tabStyle: tabBarOptions?.tabStyle,
style: tabBarOptions?.style,
});
this.state = {
loaded: [state.routes[state.index].key],
tabBarHeight: tabBarHeight,
};
}
private renderTabBar = () => {
const {
@@ -87,6 +117,16 @@ export default class BottomTabView extends React.Component<Props, State> {
});
};
private handleTabBarHeightChange = (height: number) => {
this.setState((state) => {
if (state.tabBarHeight !== height) {
return { tabBarHeight: height };
}
return null;
});
};
render() {
const {
state,
@@ -97,50 +137,54 @@ export default class BottomTabView extends React.Component<Props, State> {
sceneContainerStyle,
} = this.props;
const { routes } = state;
const { loaded } = this.state;
const { loaded, tabBarHeight } = this.state;
return (
<NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat>
<View style={styles.container}>
<ScreenContainer
// @ts-ignore
enabled={detachInactiveScreens}
style={styles.pages}
>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
<ScreenContainer
// @ts-ignore
enabled={detachInactiveScreens}
style={styles.container}
>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
if (unmountOnBlur && !isFocused) {
return null;
}
if (unmountOnBlur && !isFocused) {
return null;
}
if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a screen if we've never navigated to it
return null;
}
if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a screen if we've never navigated to it
return null;
}
return (
<ResourceSavingScene
key={route.key}
style={StyleSheet.absoluteFill}
isVisible={isFocused}
enabled={detachInactiveScreens}
return (
<ResourceSavingScene
key={route.key}
style={StyleSheet.absoluteFill}
isVisible={isFocused}
enabled={detachInactiveScreens}
>
<SceneContent
isFocused={isFocused}
style={sceneContainerStyle}
>
<SceneContent
isFocused={isFocused}
style={sceneContainerStyle}
>
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
{descriptor.render()}
</SceneContent>
</ResourceSavingScene>
);
})}
</ScreenContainer>
</BottomTabBarHeightContext.Provider>
</SceneContent>
</ResourceSavingScene>
);
})}
</ScreenContainer>
<BottomTabBarHeightCallbackContext.Provider
value={this.handleTabBarHeightChange}
>
{this.renderTabBar()}
</View>
</BottomTabBarHeightCallbackContext.Provider>
</SafeAreaProviderCompat>
</NavigationHelpersContext.Provider>
);
@@ -152,9 +196,6 @@ const styles = StyleSheet.create({
flex: 1,
overflow: 'hidden',
},
pages: {
flex: 1,
},
content: {
flex: 1,
},

View File

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

View File

@@ -12,7 +12,7 @@ import {
// The provider component for safe area initializes asynchornously
// Until the insets are available, there'll be blank screen
// To avoid the blank screen, we specify some initial values
const initialSafeAreaInsets = {
export const initialSafeAreaInsets = {
// Approximate values which are good enough for most cases
top: getStatusBarHeight(true),
bottom: getBottomSpace(),

View File

@@ -1,5 +1,11 @@
import React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import {
View,
StyleSheet,
StyleProp,
TextStyle,
ViewStyle,
} from 'react-native';
import type { Route } from '@react-navigation/native';
import Badge from './Badge';
@@ -7,6 +13,7 @@ type Props = {
route: Route<string>;
horizontal: boolean;
badge?: string | number;
badgeStyle?: StyleProp<TextStyle>;
activeOpacity: number;
inactiveOpacity: number;
activeTintColor: string;
@@ -22,6 +29,7 @@ type Props = {
export default function TabBarIcon({
horizontal,
badge,
badgeStyle,
activeOpacity,
inactiveOpacity,
activeTintColor,
@@ -56,6 +64,7 @@ export default function TabBarIcon({
style={[
styles.badge,
horizontal ? styles.badgeHorizontal : styles.badgeVertical,
badgeStyle,
]}
size={(size * 3) / 4}
>

View File

@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.11...@react-navigation/compat@5.3.12) (2021-01-21)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.10...@react-navigation/compat@5.3.11) (2021-01-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.9...@react-navigation/compat@5.3.10) (2020-11-20)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.8...@react-navigation/compat@5.3.9) (2020-11-10)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.7...@react-navigation/compat@5.3.8) (2020-11-09)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.6...@react-navigation/compat@5.3.7) (2020-11-08)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.5...@react-navigation/compat@5.3.6) (2020-11-04)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.4...@react-navigation/compat@5.3.5) (2020-11-04)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.3...@react-navigation/compat@5.3.4) (2020-11-03)
**Note:** Version bump only for package @react-navigation/compat

View File

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

View File

@@ -147,7 +147,6 @@ export default function createCompatNavigationProp<
}
},
state: {
// @ts-expect-error: these properties may actually exist
key: state.key,
// @ts-expect-error
routeName: state.name,
@@ -202,7 +201,6 @@ export default function createCompatNavigationProp<
const { routes } = navigation.dangerouslyGetState();
// @ts-expect-error
return routes[0].key === state.key;
},
dangerouslyGetParent() {

View File

@@ -3,6 +3,103 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.15.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.15.0...@react-navigation/core@5.15.1) (2021-01-21)
**Note:** Version bump only for package @react-navigation/core
# [5.15.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.4...@react-navigation/core@5.15.0) (2021-01-14)
### Bug Fixes
* print an error when passing a second argument to useFocusEffect ([2317633](https://github.com/react-navigation/react-navigation/commit/23176336528f98924d19f321d41cb70f13300edd))
### Features
* add a way to specify an unique ID for screens ([b19f76b](https://github.com/react-navigation/react-navigation/commit/b19f76bfffe623759e67d925bfd067c753a453bf))
## [5.14.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.3...@react-navigation/core@5.14.4) (2020-11-20)
### Bug Fixes
* fix incorrect state change events in independent nested container ([95b2599](https://github.com/react-navigation/react-navigation/commit/95b2599877f5ceedf753e399e0586bb4af54cb87)), closes [#9080](https://github.com/react-navigation/react-navigation/issues/9080)
## [5.14.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.2...@react-navigation/core@5.14.3) (2020-11-10)
### Bug Fixes
* improve the error message for incorrect screen configuration ([8f764d8](https://github.com/react-navigation/react-navigation/commit/8f764d8b0809604716d5d92ea33cc1beee02e804))
## [5.14.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.1...@react-navigation/core@5.14.2) (2020-11-09)
### Bug Fixes
* throw if the same pattern resolves to multiple screens ([48b2e77](https://github.com/react-navigation/react-navigation/commit/48b2e777307908e8b3fcb49d8555b610dc0e38f2))
## [5.14.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.14.0...@react-navigation/core@5.14.1) (2020-11-08)
### Bug Fixes
* tweak error message when navigator has non-screen children ([360b0e9](https://github.com/react-navigation/react-navigation/commit/360b0e995835990c55b75898757ebdd120d52446))
# [5.14.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.5...@react-navigation/core@5.14.0) (2020-11-04)
### Bug Fixes
* always respect key in the route object when generating action ([cb2e744](https://github.com/react-navigation/react-navigation/commit/cb2e744dcebf7f71ddaa5462d393a6dbfd971fcd))
### Features
* add a NavigatorScreenParams type. closes [#6931](https://github.com/react-navigation/react-navigation/issues/6931) ([e3e58c2](https://github.com/react-navigation/react-navigation/commit/e3e58c2d890e7fab75d78371e349aea55a402fcd))
* add warning on accessing the state object on route prop ([ec7b02a](https://github.com/react-navigation/react-navigation/commit/ec7b02af2ca835122b9000799e2366d7009da6e3))
## [5.13.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.4...@react-navigation/core@5.13.5) (2020-11-04)
### Bug Fixes
* don't use use-subscription to avoid peer dep related errors ([66f3a4a](https://github.com/react-navigation/react-navigation/commit/66f3a4a0bb39475434668bc94fb1750dbe618ee0)), closes [/github.com/react-navigation/react-navigation/issues/9021#issuecomment-721679760](https://github.com//github.com/react-navigation/react-navigation/issues/9021/issues/issuecomment-721679760)
* use useDebugValue in more places ([b20f2d1](https://github.com/react-navigation/react-navigation/commit/b20f2d1f7ccb82db70df9cddf5746557912daa99))
## [5.13.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.3...@react-navigation/core@5.13.4) (2020-11-03)

View File

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

View File

@@ -8,9 +8,11 @@ import {
NavigationAction,
} from '@react-navigation/routers';
import EnsureSingleNavigator from './EnsureSingleNavigator';
import UnhandledActionContext from './UnhandledActionContext';
import NavigationBuilderContext from './NavigationBuilderContext';
import NavigationStateContext from './NavigationStateContext';
import UnhandledActionContext from './UnhandledActionContext';
import NavigationRouteContext from './NavigationRouteContext';
import NavigationContext from './NavigationContext';
import { ScheduleUpdateContext } from './useScheduleUpdate';
import useChildListeners from './useChildListeners';
import useKeyedChildListeners from './useKeyedChildListeners';
@@ -160,9 +162,20 @@ const BaseNavigationContainer = React.forwardRef(
const resetRoot = React.useCallback(
(state?: PartialState<NavigationState> | NavigationState) => {
setState(state);
const target = state?.key ?? keyedListeners.getState.root?.().key;
if (target == null) {
throw new Error(NOT_INITIALIZED_ERROR);
}
listeners.focus[0]((navigation) =>
navigation.dispatch({
...CommonActions.reset(state),
target,
})
);
},
[setState]
[keyedListeners.getState, listeners.focus]
);
const getRootState = React.useCallback(() => {
@@ -386,7 +399,7 @@ const BaseNavigationContainer = React.forwardRef(
[]
);
return (
let element = (
<ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationBuilderContext.Provider value={builderContext}>
<NavigationStateContext.Provider value={context}>
@@ -399,6 +412,19 @@ const BaseNavigationContainer = React.forwardRef(
</NavigationBuilderContext.Provider>
</ScheduleUpdateContext.Provider>
);
if (independent) {
// We need to clear any existing contexts for nested independent container to work correctly
element = (
<NavigationRouteContext.Provider value={undefined}>
<NavigationContext.Provider value={undefined}>
{element}
</NavigationContext.Provider>
</NavigationRouteContext.Provider>
);
}
return element;
}
);

View File

@@ -18,9 +18,8 @@ type Props<
> = {
screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
route: Route<string> & {
state?: NavigationState | PartialState<NavigationState>;
};
route: Route<string>;
routeState: NavigationState | PartialState<NavigationState> | undefined;
getState: () => State;
setState: (state: State) => void;
options: object;
@@ -38,6 +37,7 @@ export default function SceneView<
screen,
route,
navigation,
routeState,
getState,
setState,
options,
@@ -86,7 +86,7 @@ export default function SceneView<
const context = React.useMemo(
() => ({
state: route.state,
state: routeState,
getState: getCurrentState,
setState: setCurrentState,
getKey,
@@ -95,7 +95,7 @@ export default function SceneView<
addOptionsGetter,
}),
[
route.state,
routeState,
getCurrentState,
setCurrentState,
getKey,

View File

@@ -757,3 +757,67 @@ it('invokes the unhandled action listener with the unhandled action', () => {
type: 'NAVIGATE',
});
});
it('works with state change events in independent nested container', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const ref = React.createRef<NavigationContainerRef>();
const onStateChange = jest.fn();
render(
<BaseNavigationContainer>
<TestNavigator>
<Screen name="foo">
{() => (
<BaseNavigationContainer
independent
ref={ref}
onStateChange={onStateChange}
>
<TestNavigator>
<Screen name="qux">{() => null}</Screen>
<Screen name="lex">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
)}
</Screen>
<Screen name="bar">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
act(() => ref.current?.navigate('lex'));
expect(onStateChange).toBeCalledWith({
index: 1,
key: '15',
routeNames: ['qux', 'lex'],
routes: [
{ key: 'qux', name: 'qux' },
{ key: 'lex', name: 'lex' },
],
stale: false,
type: 'test',
});
expect(ref.current?.getRootState()).toEqual({
index: 1,
key: '15',
routeNames: ['qux', 'lex'],
routes: [
{ key: 'qux', name: 'qux' },
{ key: 'lex', name: 'lex' },
],
stale: false,
type: 'test',
});
});

View File

@@ -64,7 +64,58 @@ it('gets navigate action from state for top-level screen', () => {
});
});
it('gets navigate action from state for top-level screen with 2 screens', () => {
it('gets reset action from state with 1 route with key at root', () => {
const state = {
routes: [
{
name: 'foo',
key: 'test',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
key: 'test',
name: 'qux',
params: { author: 'jane' },
},
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
routes: [
{
key: 'test',
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{ key: 'test', name: 'qux', params: { author: 'jane' } },
],
},
},
],
},
},
],
},
type: 'RESET',
});
});
it('gets reset action from state for top-level screen with 2 screens', () => {
const state = {
routes: [
{
@@ -95,37 +146,7 @@ it('gets navigate action from state for top-level screen with 2 screens', () =>
});
});
it('gets navigate action from state for top-level screen with 2 screens with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'bar',
params: { author: 'jane' },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state for top-level screen with more than 2 screens with config', () => {
it('gets reset action from state for top-level screen with more than 2 screens with config', () => {
const state = {
routes: [
{
@@ -165,6 +186,76 @@ it('gets navigate action from state for top-level screen with more than 2 screen
});
});
it('gets reset action from state for top-level screen with 2 screens with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
key: 'test',
params: { author: 'jane' },
},
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
key: 'test',
params: { author: 'jane' },
},
],
},
type: 'RESET',
});
});
it('gets navigate action from state for top-level screen with 2 screens with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'bar',
params: { author: 'jane' },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state for top-level screen with more than 2 screens with config with lower index', () => {
const state = {
index: 1,
@@ -550,7 +641,7 @@ it('gets navigate action from state with 2 screens without initial route and wit
});
});
it('gets navigate action from state with 2 screens including route with key and with config', () => {
it('gets navigate action from state with 2 screens including route with key on initial route and with config', () => {
const state = {
routes: [
{
@@ -615,6 +706,75 @@ it('gets navigate action from state with 2 screens including route with key and
});
});
it('gets navigate action from state with 2 screens including route with key on 2nd route and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{
key: 'test',
name: 'quz',
},
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{
key: 'test',
name: 'quz',
},
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with more than 2 screens and with config', () => {
const state = {
routes: [
@@ -743,7 +903,7 @@ it("doesn't return action if no routes are provided'", () => {
expect(getActionFromState({ routes: [] })).toBe(undefined);
});
it('gets reset action from state', () => {
it('gets undefined action from state', () => {
const state = {
routes: [
{

View File

@@ -2673,6 +2673,47 @@ it('uses nearest parent wildcard match for unmatched paths', () => {
);
});
it('throws if two screens map to the same pattern', () => {
const path = '/bar/42/baz/test';
expect(() =>
getStateFromPath(path, {
screens: {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: 'baz',
},
},
Bax: '/bar/:id/baz',
},
},
},
})
).toThrow(
"Found conflicting screens with the same pattern. The pattern 'bar/:id/baz' resolves to both 'Foo > Bax' and 'Foo > Bar > Baz'. Patterns must be unique and cannot resolve to more than one screen."
);
expect(() =>
getStateFromPath(path, {
screens: {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: '',
},
},
},
},
},
})
).not.toThrow();
});
it('throws if wildcard is specified with legacy config', () => {
const path = '/bar/42/baz/test';
const config = {

View File

@@ -1462,6 +1462,51 @@ it('throws when Screen is not the direct children', () => {
);
});
it('throws when undefined component is a direct children', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);
return null;
};
const Undefined = undefined;
const spy = jest.spyOn(console, 'error').mockImplementation();
const element = (
<BaseNavigationContainer>
<TestNavigator>
{/* @ts-ignore */}
<Undefined name="foo" component={jest.fn()} />
</TestNavigator>
</BaseNavigationContainer>
);
spy.mockRestore();
expect(() => render(element).update(element)).toThrowError(
"A navigator can only contain 'Screen' components as its direct children (found 'undefined' for the screen 'foo')"
);
});
it('throws when a tag is a direct children', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);
return null;
};
const element = (
<BaseNavigationContainer>
<TestNavigator>
{/* @ts-ignore */}
<screen name="foo" component={jest.fn()} />
</TestNavigator>
</BaseNavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
"A navigator can only contain 'Screen' components as its direct children (found 'screen' for the screen 'foo')"
);
});
it('throws when a React Element is not the direct children', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);

View File

@@ -239,3 +239,135 @@ it('runs cleanup when component is unmounted', () => {
expect(focusEffect).toBeCalledTimes(1);
expect(focusEffectCleanup).toBeCalledTimes(1);
});
it('prints error when a dependency array is passed', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const Test = () => {
// @ts-ignore
useFocusEffect(() => {}, []);
return null;
};
const App = () => (
<BaseNavigationContainer>
<TestNavigator>
<Screen name="test" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
const spy = jest.spyOn(console, 'error').mockImplementation();
render(<App />);
expect(spy.mock.calls[0][0]).toMatch(
"You passed a second argument to 'useFocusEffect', but it only accepts one argument."
);
spy.mockRestore();
});
it('prints error when the effect returns a value', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const Test = () => {
// @ts-ignore
useFocusEffect(() => 42);
return null;
};
const App = () => (
<BaseNavigationContainer>
<TestNavigator>
<Screen name="test" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
const spy = jest.spyOn(console, 'error').mockImplementation();
render(<App />);
expect(spy.mock.calls[0][0]).toMatch(
"An effect function must not return anything besides a function, which is used for clean-up. You returned '42'."
);
spy.mockRestore();
});
it('prints error when the effect returns null', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const Test = () => {
// @ts-ignore
useFocusEffect(() => null);
return null;
};
const App = () => (
<BaseNavigationContainer>
<TestNavigator>
<Screen name="test" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
const spy = jest.spyOn(console, 'error').mockImplementation();
render(<App />);
expect(spy.mock.calls[0][0]).toMatch(
"An effect function must not return anything besides a function, which is used for clean-up. You returned 'null'. If your effect does not require clean-up, return 'undefined' (or nothing)."
);
spy.mockRestore();
});
it('prints error when the effect is an async function', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const Test = () => {
// @ts-ignore
useFocusEffect(async () => {});
return null;
};
const App = () => (
<BaseNavigationContainer>
<TestNavigator>
<Screen name="test" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
const spy = jest.spyOn(console, 'error').mockImplementation();
render(<App />);
expect(spy.mock.calls[0][0]).toMatch(
"An effect function must not return anything besides a function, which is used for clean-up.\n\nIt looks like you wrote 'useFocusEffect(async () => ...)' or returned a Promise."
);
spy.mockRestore();
});

View File

@@ -1178,3 +1178,149 @@ it("prevents removing by multiple screens with 'beforeRemove' event", () => {
type: 'stack',
});
});
it("prevents removing a child screen with 'beforeRemove' event with 'resetRoot'", () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const onBeforeRemove = jest.fn();
let shouldPrevent = true;
let shouldContinue = false;
const TestScreen = (props: any) => {
React.useEffect(
() =>
props.navigation.addListener('beforeRemove', (e: any) => {
onBeforeRemove();
if (shouldPrevent) {
e.preventDefault();
if (shouldContinue) {
props.navigation.dispatch(e.data.action);
}
}
}),
[props.navigation]
);
return null;
};
const onStateChange = jest.fn();
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar">{() => null}</Screen>
<Screen name="baz">
{() => (
<TestNavigator>
<Screen name="qux" component={TestScreen} />
<Screen name="lex">{() => null}</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
act(() => ref.current?.navigate('baz'));
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).toBeCalledWith({
index: 1,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{
key: 'baz-4',
name: 'baz',
state: {
index: 0,
key: 'stack-6',
routeNames: ['qux', 'lex'],
routes: [{ key: 'qux-7', name: 'qux' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
});
act(() =>
ref.current?.resetRoot({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
})
);
expect(onStateChange).toBeCalledTimes(1);
expect(onBeforeRemove).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual({
index: 1,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{
key: 'baz-4',
name: 'baz',
state: {
index: 0,
key: 'stack-6',
routeNames: ['qux', 'lex'],
routes: [{ key: 'qux-7', name: 'qux' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
});
shouldPrevent = false;
act(() =>
ref.current?.resetRoot({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
})
);
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
});

View File

@@ -1,11 +1,12 @@
import type {
Route,
PartialRoute,
ParamListBase,
NavigationState,
PartialState,
CommonActions,
} from '@react-navigation/routers';
import type { PathConfig, PathConfigMap, NestedNavigateParams } from './types';
import type { PathConfig, PathConfigMap, NavigatorScreenParams } from './types';
type ConfigItem = {
initialRouteName?: string;
@@ -18,7 +19,7 @@ type NavigateAction<State extends NavigationState> = {
type: 'NAVIGATE';
payload: {
name: string;
params?: NestedNavigateParams<State>;
params?: NavigatorScreenParams<State>;
};
};
@@ -38,9 +39,11 @@ export default function getActionFromState(
if (
!(
routes.length === 1 ||
(routes.length === 1 && routes[0].key === undefined) ||
(routes.length === 2 &&
routes[0].name === normalizedConfig?.initialRouteName)
routes[0].key === undefined &&
routes[0].name === normalizedConfig?.initialRouteName &&
routes[1].key === undefined)
)
) {
return {
@@ -53,7 +56,10 @@ export default function getActionFromState(
let current: PartialState<NavigationState> | undefined = route?.state;
let config: ConfigItem | undefined = normalizedConfig?.screens?.[route?.name];
let params: NestedNavigateParams<NavigationState> = { ...route.params };
let params = { ...route.params } as NavigatorScreenParams<
ParamListBase,
NavigationState
>;
let payload = route ? { name: route.name, params } : undefined;
@@ -70,29 +76,36 @@ export default function getActionFromState(
const route: Route<string> | PartialRoute<Route<string>> =
routes[routes.length - 1];
if (routes.length === 1) {
// Explicitly set to override existing value when merging params
Object.assign(params, {
initial: undefined,
screen: undefined,
params: undefined,
state: undefined,
});
if (routes.length === 1 && routes[0].key === undefined) {
params.initial = true;
params.screen = route.name;
params.state = undefined; // Explicitly set to override existing value when merging params
} else if (
routes.length === 2 &&
routes[0].key === undefined &&
routes[0].name === config?.initialRouteName
routes[0].name === config?.initialRouteName &&
routes[1].key === undefined
) {
params.initial = false;
params.screen = route.name;
params.state = undefined;
} else {
params.initial = undefined;
params.screen = undefined;
params.params = undefined;
params.state = current;
break;
}
if (route.state) {
params.params = { ...route.params };
params = params.params;
params = params.params as NavigatorScreenParams<
ParamListBase,
NavigationState
>;
} else {
params.params = route.params;
}

View File

@@ -3,11 +3,17 @@ import type {
PartialState,
NavigationState,
} from '@react-navigation/routers';
import { SUPPRESS_STATE_ACCESS_WARNING } from './useRouteCache';
export default function getFocusedRouteNameFromRoute(
route: Partial<Route<string>> & { state?: PartialState<NavigationState> }
): string | undefined {
SUPPRESS_STATE_ACCESS_WARNING.value = true;
const state = route.state;
SUPPRESS_STATE_ACCESS_WARNING.value = false;
const params = route.params as { screen?: unknown } | undefined;
const routeName = state

View File

@@ -239,6 +239,10 @@ export default function getPathFromState(
// Object.fromEntries is not available in older iOS versions
const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
entries.reduce((acc, [k, v]) => {
if (acc.hasOwnProperty(k)) {
throw new Error(`A value for key '${k}' already exists in the object.`);
}
acc[k] = v;
return acc;
}, {} as Record<K, V>);

View File

@@ -33,6 +33,11 @@ type ResultState = PartialState<NavigationState> & {
state?: ResultState;
};
type ParsedRoute = {
name: string;
params?: Record<string, any> | undefined;
};
/**
* Utility to parse a path string to initial state object accepted by the container.
* This is useful for deep linking when we need to handle the incoming URL.
@@ -119,6 +124,12 @@ export default function getStateFromPath(
// - the most exhaustive ones are always at the beginning
// - patterns with wildcard are always at the end
// If 2 patterns are same, move the one with less route names up
// This is an error state, so it's only useful for consistent error messages
if (a.pattern === b.pattern) {
return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
}
// If one of the patterns starts with the other, it's more exhaustive
// So move it up
if (a.pattern.startsWith(b.pattern)) {
@@ -155,6 +166,35 @@ export default function getStateFromPath(
return bWildcardIndex - aWildcardIndex;
});
// Check for duplicate patterns in the config
configs.reduce<Record<string, RouteConfig>>((acc, config) => {
if (acc[config.pattern]) {
const a = acc[config.pattern].routeNames;
const b = config.routeNames;
// It's not a problem if the path string omitted from a inner most screen
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
const intersects =
a.length > b.length
? b.every((it, i) => a[i] === it)
: a.every((it, i) => b[i] === it);
if (!intersects) {
throw new Error(
`Found conflicting screens with the same pattern. The pattern '${
config.pattern
}' resolves to both '${a.join(' > ')}' and '${b.join(
' > '
)}'. Patterns must be unique and cannot resolve to more than one screen.`
);
}
}
return Object.assign(acc, {
[config.pattern]: config,
});
}, {});
if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works
// When handling empty path, we should only look at the root level config
@@ -189,7 +229,7 @@ export default function getStateFromPath(
if (legacy === false) {
// If we're not in legacy mode,, we match the whole path against the regex instead of segments
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
const { routeNames, allParams, remainingPath } = matchAgainstConfigs(
const { routes, remainingPath } = matchAgainstConfigs(
remaining,
configs.map((c) => ({
...c,
@@ -198,39 +238,30 @@ export default function getStateFromPath(
}))
);
if (routeNames !== undefined) {
if (routes !== undefined) {
// This will always be empty if full path matched
current = createNestedStateObject(routes, initialRoutes);
remaining = remainingPath;
current = createNestedStateObject(
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
result = current;
}
} else {
// In legacy mode, we divide the path into segments and match piece by piece
// This preserves the legacy behaviour, but we should remove it in next major
while (remaining) {
let { routeNames, allParams, remainingPath } = matchAgainstConfigs(
remaining,
configs
);
let { routes, remainingPath } = matchAgainstConfigs(remaining, configs);
remaining = remainingPath;
// If we hadn't matched any segments earlier, use the path as route name
if (routeNames === undefined) {
if (routes === undefined) {
const segments = remaining.split('/');
routeNames = [decodeURIComponent(segments[0])];
routes = [{ name: decodeURIComponent(segments[0]) }];
segments.shift();
remaining = segments.join('/');
}
const state = createNestedStateObject(
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
const state = createNestedStateObject(routes, initialRoutes);
if (current) {
// The state should be nested inside the deepest route we parsed before
@@ -274,8 +305,7 @@ const joinPaths = (...paths: string[]): string =>
.join('/');
const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
let routeNames: string[] | undefined;
let allParams: Record<string, any> | undefined;
let routes: ParsedRoute[] | undefined;
let remainingPath = remaining;
// Go through all configs, and see if the next path segment matches our regex
@@ -288,21 +318,40 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
// If our regex matches, we need to extract params from the path
if (match) {
routeNames = [...config.routeNames];
const matchedParams = config.pattern
?.split('/')
.filter((p) => p.startsWith(':'))
.reduce<Record<string, any>>(
(acc, p, i) =>
Object.assign(acc, {
// The param segments appear every second item starting from 2 in the regex match result
[p]: match![(i + 1) * 2].replace(/\//, ''),
}),
{}
);
const paramPatterns = config.pattern
.split('/')
.filter((p) => p.startsWith(':'));
routes = config.routeNames.map((name) => {
const config = configs.find((c) => c.screen === name);
const params = config?.path
?.split('/')
.filter((p) => p.startsWith(':'))
.reduce<Record<string, any>>((acc, p) => {
const value = matchedParams[p];
if (paramPatterns.length) {
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
if (value) {
const key = p.replace(/^:/, '').replace(/\?$/, '');
acc[key] = config.parse?.[key] ? config.parse[key](value) : value;
}
acc[p] = value;
return acc;
}, {});
return acc;
}, {});
}
if (params && Object.keys(params).length) {
return { name, params };
}
return { name };
});
remainingPath = remainingPath.replace(match[1], '');
@@ -310,7 +359,7 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
}
}
return { routeNames, allParams, remainingPath };
return { routes, remainingPath };
};
const createNormalizedConfigs = (
@@ -473,57 +522,48 @@ const findInitialRoute = (
// it is the end of state and if there is initialRoute for this level
const createStateObject = (
initialRoute: string | undefined,
routeName: string,
params: Record<string, any> | undefined,
route: ParsedRoute,
isEmpty: boolean
): InitialState => {
if (isEmpty) {
if (initialRoute) {
return {
index: 1,
routes: [{ name: initialRoute }, { name: routeName as string, params }],
routes: [{ name: initialRoute }, route],
};
} else {
return {
routes: [{ name: routeName as string, params }],
routes: [route],
};
}
} else {
if (initialRoute) {
return {
index: 1,
routes: [
{ name: initialRoute },
{ name: routeName as string, params, state: { routes: [] } },
],
routes: [{ name: initialRoute }, { ...route, state: { routes: [] } }],
};
} else {
return {
routes: [{ name: routeName as string, params, state: { routes: [] } }],
routes: [{ ...route, state: { routes: [] } }],
};
}
}
};
const createNestedStateObject = (
routes: { name: string; params?: object }[],
routes: ParsedRoute[],
initialRoutes: InitialRouteConfig[]
) => {
let state: InitialState;
let route = routes.shift() as { name: string; params?: object };
let route = routes.shift() as ParsedRoute;
let initialRoute = findInitialRoute(route.name, initialRoutes);
state = createStateObject(
initialRoute,
route.name,
route.params,
routes.length === 0
);
state = createStateObject(initialRoute, route, routes.length === 0);
if (routes.length > 0) {
let nestedState = state;
while ((route = routes.shift() as { name: string; params?: object })) {
while ((route = routes.shift() as ParsedRoute)) {
initialRoute = findInitialRoute(route.name, initialRoutes);
const nestedStateIndex =
@@ -531,8 +571,7 @@ const createNestedStateObject = (
nestedState.routes[nestedStateIndex].state = createStateObject(
initialRoute,
route.name,
route.params,
route,
routes.length === 0
);
@@ -546,46 +585,6 @@ const createNestedStateObject = (
return state;
};
const createRouteObjects = (
configs: RouteConfig[],
routeNames: string[],
allParams?: Record<string, any>
) =>
routeNames.map((name) => {
const config = configs.find((c) => c.screen === name);
let params: object | undefined;
if (allParams && config?.path) {
const pattern = config.path;
if (pattern) {
const paramPatterns = pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
const key = p.replace(/^:/, '').replace(/\?$/, '');
const value = allParams![p];
if (value) {
acc[key] = config.parse?.[key] ? config.parse[key](value) : value;
}
return acc;
}, {});
}
}
}
if (params && Object.keys(params).length) {
return { name, params };
}
return { name };
});
const findFocusedRoute = (state: InitialState) => {
let current: InitialState | undefined = state;

View File

@@ -388,6 +388,14 @@ export type RouteConfig<
navigation: any;
}) => ScreenListeners<State, EventMap>);
/**
* Function to return an unique ID for this screen.
* Receives an object with the route params.
* For a given screen name, there will always be only one screen corresponding to an ID.
* If `undefined` is returned, it acts same as no `getId` being specified.
*/
getId?: ({ params }: { params: ParamList[RouteName] }) => string | undefined;
/**
* Initial params object for the route.
*/
@@ -504,19 +512,31 @@ export type TypedNavigator<
) => null;
};
export type NestedNavigateParams<State extends NavigationState> =
| {
screen?: string;
params?: object;
initial?: boolean;
state?: never;
}
export type NavigatorScreenParams<
ParamList,
State extends NavigationState = NavigationState
> =
| {
screen?: never;
params?: never;
initial?: never;
state?: PartialState<State> | State;
};
state: PartialState<State> | State | undefined;
}
| {
[RouteName in keyof ParamList]: undefined extends ParamList[RouteName]
? {
screen: RouteName;
params?: ParamList[RouteName];
initial?: boolean;
state?: never;
}
: {
screen: RouteName;
params: ParamList[RouteName];
initial?: boolean;
state?: never;
};
}[keyof ParamList];
export type PathConfig = {
path?: string;

View File

@@ -12,6 +12,7 @@ import NavigationBuilderContext, {
} from './NavigationBuilderContext';
import type { NavigationEventEmitter } from './useEventEmitter';
import useNavigationCache from './useNavigationCache';
import useRouteCache from './useRouteCache';
import NavigationContext from './NavigationContext';
import NavigationRouteContext from './NavigationRouteContext';
import type {
@@ -113,9 +114,11 @@ export default function useDescriptors<
emitter,
});
return state.routes.reduce<
const routes = useRouteCache(state.routes);
return routes.reduce<
Record<string, Descriptor<ParamListBase, string, State, ScreenOptions>>
>((acc, route) => {
>((acc, route, i) => {
const screen = screens[route.name];
const navigation = navigations[route.key];
@@ -151,6 +154,7 @@ export default function useDescriptors<
navigation={navigation}
route={route}
screen={screen}
routeState={state.routes[i].state}
getState={getState}
setState={setState}
options={routeOptions}

View File

@@ -13,6 +13,20 @@ type EffectCallback = () => undefined | void | (() => void);
export default function useFocusEffect(effect: EffectCallback) {
const navigation = useNavigation();
if (arguments[1] !== undefined) {
const message =
"You passed a second argument to 'useFocusEffect', but it only accepts one argument. " +
"If you want to pass a dependency array, you can use 'React.useCallback':\n\n" +
'useFocusEffect(\n' +
' React.useCallback(() => {\n' +
' // Your code here\n' +
' }, [depA, depB])\n' +
');\n\n' +
'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
console.error(message);
}
React.useEffect(() => {
let isFocused = false;
let cleanup: undefined | void | (() => void);
@@ -45,10 +59,10 @@ export default function useFocusEffect(effect: EffectCallback) {
' }\n\n' +
' fetchData();\n' +
' }, [someId])\n' +
'};\n\n' +
');\n\n' +
'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
} else {
message += ` You returned: '${JSON.stringify(destroy)}'`;
message += ` You returned '${JSON.stringify(destroy)}'.`;
}
console.error(message);

View File

@@ -1,32 +1,42 @@
import * as React from 'react';
import { useSubscription } from 'use-subscription';
import { useState } from 'react';
import useNavigation from './useNavigation';
/**
* Hook to get the current focus state of the screen. Returns a `true` if screen is focused, otherwise `false`.
* This can be used if a component needs to render something based on the focus state.
* It uses `use-subscription` under the hood for safer use in concurrent mode.
*/
export default function useIsFocused(): boolean {
const navigation = useNavigation();
// eslint-disable-next-line react-hooks/exhaustive-deps
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
const subscribe = React.useCallback(
(callback: () => void) => {
const unsubscribeFocus = navigation.addListener('focus', callback);
const [isFocused, setIsFocused] = useState(navigation.isFocused);
const unsubscribeBlur = navigation.addListener('blur', callback);
const valueToReturn = navigation.isFocused();
return () => {
unsubscribeFocus();
unsubscribeBlur();
};
},
[navigation]
);
if (isFocused !== valueToReturn) {
// If the value has changed since the last render, we need to update it.
// This could happen if we missed an update from the event listeners during re-render.
// React will process this update immediately, so the old subscription value won't be committed.
// It is still nice to avoid returning a mismatched value though, so let's override the return value.
// This is the same logic as in https://github.com/facebook/react/tree/master/packages/use-subscription
setIsFocused(valueToReturn);
}
return useSubscription({
getCurrentValue,
subscribe,
});
React.useEffect(() => {
const unsubscribeFocus = navigation.addListener('focus', () =>
setIsFocused(true)
);
const unsubscribeBlur = navigation.addListener('blur', () =>
setIsFocused(false)
);
return () => {
unsubscribeFocus();
unsubscribeBlur();
};
}, [navigation]);
React.useDebugValue(valueToReturn);
return valueToReturn;
}

View File

@@ -7,6 +7,7 @@ import {
ParamListBase,
Router,
RouterFactory,
RouterConfigOptions,
PartialState,
NavigationAction,
Route,
@@ -34,7 +35,7 @@ import {
PrivateValueStore,
EventMapBase,
EventMapCore,
NestedNavigateParams,
NavigatorScreenParams,
} from './types';
// This is to make TypeScript compiler happy
@@ -43,7 +44,7 @@ PrivateValueStore;
type NavigatorRoute<State extends NavigationState> = {
key: string;
params?: NestedNavigateParams<State>;
params?: NavigatorScreenParams<ParamListBase, State>;
};
/**
@@ -90,10 +91,17 @@ const getRouteConfigsFromChildren = <
}
throw new Error(
`A navigator can only contain 'Screen' components as its direct children (found '${
// @ts-expect-error: child can be any type and we're accessing it safely, but TS doesn't understand it
child.type?.name ? child.type.name : String(child)
}')`
`A navigator can only contain 'Screen' components as its direct children (found ${
React.isValidElement(child)
? `'${
typeof child.type === 'string' ? child.type : child.type?.name
}'${
child.props?.name ? ` for the screen '${child.props.name}'` : ''
}`
: typeof child === 'object'
? JSON.stringify(child)
: `'${String(child)}'`
}). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.`
);
}, []);
@@ -250,6 +258,15 @@ export default function useNavigationBuilder<
},
{}
);
const routeGetIdList = routeNames.reduce<
RouterConfigOptions['routeGetIdList']
>(
(acc, curr) =>
Object.assign(acc, {
[curr]: screens[curr].getId,
}),
{}
);
if (!routeNames.length) {
throw new Error(
@@ -290,6 +307,7 @@ export default function useNavigationBuilder<
router.getInitialState({
routeNames,
routeParamList,
routeGetIdList,
}),
true,
];
@@ -300,6 +318,7 @@ export default function useNavigationBuilder<
{
routeNames,
routeParamList,
routeGetIdList,
}
),
false,
@@ -329,6 +348,7 @@ export default function useNavigationBuilder<
nextState = router.getStateForRouteNamesChange(state, {
routeNames,
routeParamList,
routeGetIdList,
});
}
@@ -365,6 +385,7 @@ export default function useNavigationBuilder<
? router.getStateForAction(nextState, action, {
routeNames,
routeParamList,
routeGetIdList,
})
: null;
@@ -373,6 +394,7 @@ export default function useNavigationBuilder<
? router.getRehydratedState(updatedState, {
routeNames,
routeParamList,
routeGetIdList,
})
: nextState;
}
@@ -494,6 +516,7 @@ export default function useNavigationBuilder<
routerConfigOptions: {
routeNames,
routeParamList,
routeGetIdList,
},
emitter,
});

View File

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

View File

@@ -90,18 +90,11 @@ export default function useOnAction({
onDispatchAction(action, state === result);
if (state !== result) {
const nextRouteKeys = (result.routes as any[]).map(
(route: { key?: string }) => route.key
);
const removedRoutes = state.routes.filter(
(route) => !nextRouteKeys.includes(route.key)
);
const isPrevented = shouldPreventRemove(
emitter,
beforeRemoveListeners,
removedRoutes,
state.routes,
result.routes,
action
);

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import type {
NavigationState,
Route,
NavigationAction,
} from '@react-navigation/routers';
import NavigationBuilderContext, {
@@ -22,11 +21,16 @@ const VISITED_ROUTE_KEYS = Symbol('VISITED_ROUTE_KEYS');
export const shouldPreventRemove = (
emitter: NavigationEventEmitter<EventMapCore<any>>,
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>,
routes: Route<string>[],
currentRoutes: { key: string }[],
nextRoutes: { key?: string | undefined }[],
action: NavigationAction
) => {
const nextRouteKeys = nextRoutes.map((route) => route.key);
// Call these in reverse order so last screens handle the event first
const reversedRoutes = [...routes].reverse();
const removedRoutes = currentRoutes
.filter((route) => !nextRouteKeys.includes(route.key))
.reverse();
const visitedRouteKeys: Set<string> =
// @ts-expect-error: add this property to mark that we've already emitted this action
@@ -37,7 +41,7 @@ export const shouldPreventRemove = (
[VISITED_ROUTE_KEYS]: visitedRouteKeys,
};
for (const route of reversedRoutes) {
for (const route of removedRoutes) {
if (visitedRouteKeys.has(route.key)) {
// Skip if we've already emitted this action for this screen
continue;
@@ -85,6 +89,7 @@ export default function useOnPreventRemove({
emitter,
beforeRemoveListeners,
state.routes,
[],
action
);
});

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import type {
ParamListBase,
NavigationState,
Route,
} from '@react-navigation/routers';
import type { RouteProp } from './types';
type RouteCache = Map<Route<string>, RouteProp<ParamListBase, string>>;
/**
* Utilites such as `getFocusedRouteNameFromRoute` need to access state.
* So we need a way to suppress the warning for those use cases.
* This is fine since they are internal utilities and this is not public API.
*/
export const SUPPRESS_STATE_ACCESS_WARNING = { value: false };
/**
* Hook to cache route props for each screen in the navigator.
* This lets add warnings and modifications to the route object but keep references between renders.
*/
export default function useRouteCache<State extends NavigationState>(
routes: State['routes']
) {
// Cache object which holds route objects for each screen
const cache = React.useMemo(() => ({ current: new Map() as RouteCache }), []);
if (process.env.NODE_ENV === 'production') {
// We don't want the overhead of creating extra maps every render in prod
return routes;
}
cache.current = routes.reduce((acc, route) => {
const previous = cache.current.get(route);
if (previous) {
// If a cached route object already exists, reuse it
acc.set(route, previous);
} else {
const proxy = { ...route };
Object.defineProperty(proxy, 'state', {
get() {
if (!SUPPRESS_STATE_ACCESS_WARNING.value) {
console.warn(
"Accessing the 'state' property of the 'route' object is not supported. If you want to get the focused route name, use the 'getFocusedRouteNameFromRoute' helper instead: https://reactnavigation.org/docs/screen-options-resolution/#setting-parent-screen-options-based-on-child-navigators-state"
);
}
return route.state;
},
});
acc.set(route, proxy);
}
return acc;
}, new Map() as RouteCache);
return Array.from(cache.current.values());
}

View File

@@ -68,5 +68,7 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
const state = stateRef.current;
React.useDebugValue(state);
return [state, getState, setState, scheduleUpdate, flushUpdates] as const;
}

View File

@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.20](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.19...@react-navigation/devtools@5.1.20) (2021-01-21)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.18...@react-navigation/devtools@5.1.19) (2021-01-14)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.18](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.17...@react-navigation/devtools@5.1.18) (2020-11-20)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.17](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.16...@react-navigation/devtools@5.1.17) (2020-11-10)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.16](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.15...@react-navigation/devtools@5.1.16) (2020-11-09)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.14...@react-navigation/devtools@5.1.15) (2020-11-08)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.13...@react-navigation/devtools@5.1.14) (2020-11-04)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.12...@react-navigation/devtools@5.1.13) (2020-11-04)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.11...@react-navigation/devtools@5.1.12) (2020-11-03)
**Note:** Version bump only for package @react-navigation/devtools

View File

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

View File

@@ -3,6 +3,112 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.12.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.5...@react-navigation/drawer@5.12.0) (2021-01-21)
### Bug Fixes
* fix drawer and bottom tabs not being visible on web. closes [#9225](https://github.com/react-navigation/react-navigation/issues/9225) ([d88cbcb](https://github.com/react-navigation/react-navigation/commit/d88cbcb52d46de26edaa9ce6bfb06badb1b1de64))
### Features
* add pressColor and pressOpacity props to drawerItem ([#8834](https://github.com/react-navigation/react-navigation/issues/8834)) ([bae4019](https://github.com/react-navigation/react-navigation/commit/bae4019995062c682f0213c121b7927ab8006c1e))
## [5.11.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.4...@react-navigation/drawer@5.11.5) (2021-01-14)
### Bug Fixes
* enable detachInactiveScreens by default on web for better a11y ([dd87fa4](https://github.com/react-navigation/react-navigation/commit/dd87fa49a43ad8db105a62418243339e4150fadf))
## [5.11.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.3...@react-navigation/drawer@5.11.4) (2020-11-20)
**Note:** Version bump only for package @react-navigation/drawer
## [5.11.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.2...@react-navigation/drawer@5.11.3) (2020-11-16)
### Bug Fixes
* hide drawer's header by default ([794339e](https://github.com/react-navigation/react-navigation/commit/794339eeed7c0d3b0e8b1752e494fbb4608ddfad))
## [5.11.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.1...@react-navigation/drawer@5.11.2) (2020-11-10)
**Note:** Version bump only for package @react-navigation/drawer
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.11.0...@react-navigation/drawer@5.11.1) (2020-11-09)
### Bug Fixes
* provide correct context to drawe header ([18bbd17](https://github.com/react-navigation/react-navigation/commit/18bbd177d91ccc4308516208a8b9f1a34ca5cc41))
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.7...@react-navigation/drawer@5.11.0) (2020-11-09)
### Bug Fixes
* try fixing drawer blink on Android ([5217245](https://github.com/react-navigation/react-navigation/commit/52172453dfb71822c2fb0f5947d00bac4a840d07))
### Features
* add a getIsDrawerOpenFromState utility to drawer ([5bd682f](https://github.com/react-navigation/react-navigation/commit/5bd682f0bf6b28a95fb3e7fc9e1974057a877cb0))
* add option to show a header in drawer navigator screens ([dbe961b](https://github.com/react-navigation/react-navigation/commit/dbe961ba5bb243e8da4d889c3c7dd6ed1de287c4))
## [5.10.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.6...@react-navigation/drawer@5.10.7) (2020-11-08)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.5...@react-navigation/drawer@5.10.6) (2020-11-04)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.4...@react-navigation/drawer@5.10.5) (2020-11-04)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.3...@react-navigation/drawer@5.10.4) (2020-11-03)
**Note:** Version bump only for package @react-navigation/drawer

View File

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

View File

@@ -17,6 +17,7 @@ export { default as DrawerContentScrollView } from './views/DrawerContentScrollV
*/
export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
export { default as getIsDrawerOpenFromState } from './utils/getIsDrawerOpenFromState';
export { default as useIsDrawerOpen } from './utils/useIsDrawerOpen';
/**

View File

@@ -18,6 +18,8 @@ export type Scene = {
color?: string;
};
export type Layout = { width: number; height: number };
export type DrawerNavigationConfig<T = DrawerContentOptions> = {
/**
* Position of the drawer on the screen. Defaults to `left`.
@@ -94,12 +96,95 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
detachInactiveScreens?: boolean;
};
export type DrawerNavigationOptions = {
export type DrawerHeaderOptions = {
/**
* String or a function that returns a React Element to be used by the header.
* Defaults to scene `title`.
* It receives `allowFontScaling`, `tintColor`, `style` and `children` in the options object as an argument.
* The title string is passed in `children`.
*/
headerTitle?:
| string
| ((props: {
/**
* Whether title font should scale to respect Text Size accessibility settings.
*/
allowFontScaling?: boolean;
/**
* Tint color for the header.
*/
tintColor?: string;
/**
* Content of the title element. Usually the title string.
*/
children?: string;
/**
* Style object for the title element.
*/
style?: StyleProp<TextStyle>;
}) => React.ReactNode);
/**
* How to align the the header title.
* Defaults to `center` on iOS and `left` on Android.
*/
headerTitleAlign?: 'left' | 'center';
/**
* Style object for the title component.
*/
headerTitleStyle?: StyleProp<TextStyle>;
/**
* Whether header title font should scale to respect Text Size accessibility settings. Defaults to `false`.
*/
headerTitleAllowFontScaling?: boolean;
/**
* Function which returns a React Element to display on the left side of the header.
*/
headerLeft?: (props: { tintColor?: string }) => React.ReactNode;
/**
* Accessibility label for the header left button.
*/
headerLeftAccessibilityLabel?: string;
/**
* Function which returns a React Element to display on the right side of the header.
*/
headerRight?: (props: { tintColor?: string }) => React.ReactNode;
/**
* Color for material ripple (Android >= 5.0 only).
*/
headerPressColorAndroid?: string;
/**
* Tint color for the header.
*/
headerTintColor?: string;
/**
* Style object for the header. You can specify a custom background color here, for example.
*/
headerStyle?: StyleProp<ViewStyle>;
/**
* Extra padding to add at the top of header to account for translucent status bar.
* By default, it uses the top value from the safe area insets of the device.
* Pass 0 or a custom value to disable the default behaviour, and customize the height.
*/
headerStatusBarHeight?: number;
};
export type DrawerNavigationOptions = DrawerHeaderOptions & {
/**
* Title text for the screen.
*/
title?: string;
/**
* Function that given `HeaderProps` returns a React Element to display as a header.
*/
header?: (props: DrawerHeaderProps) => React.ReactNode;
/**
* Whether to show the header. The header is not shown by default.
* Setting this to `true` shows the header.
*/
headerShown?: boolean;
/**
* Title string of a screen displayed in the drawer
* or a function that given { focused: boolean, color: string } returns a React.Node
@@ -187,6 +272,20 @@ export type DrawerContentOptions = {
style?: StyleProp<ViewStyle>;
};
export type DrawerHeaderProps = {
/**
* Layout of the screen.
*/
layout: Layout;
/**
* Object representing the current scene, such as the route object and descriptor.
*/
scene: {
route: Route<string>;
descriptor: DrawerDescriptor;
};
};
export type DrawerNavigationEventMap = {
/**
* Event which fires when the drawer opens.

View File

@@ -0,0 +1,16 @@
import type {
DrawerNavigationState,
ParamListBase,
} from '@react-navigation/native';
export default function getIsDrawerOpenFromState(
state: DrawerNavigationState<ParamListBase>
): boolean {
if (state.history == null) {
throw new Error(
"Couldn't find the drawer status in the state object. Is it a valid state object of drawer navigator?"
);
}
return state.history.some((it) => it.type === 'drawer');
}

View File

@@ -57,9 +57,10 @@ const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;
const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60;
const SWIPE_DISTANCE_MINIMUM = 5;
const DEFAULT_DRAWER_WIDTH = '80%';
const SPRING_CONFIG = {
stiffness: 1000,
damping: 500,
@@ -202,7 +203,8 @@ export default class DrawerView extends React.Component<Props> {
private getDrawerWidth = (): number => {
const { drawerStyle, dimensions } = this.props;
const { width } = StyleSheet.flatten(drawerStyle);
const { width = DEFAULT_DRAWER_WIDTH } =
StyleSheet.flatten(drawerStyle) || {};
if (typeof width === 'string' && width.endsWith('%')) {
// Try to calculate width if a percentage is given
@@ -246,7 +248,7 @@ export default class DrawerView extends React.Component<Props> {
private containerWidth = new Value<number>(this.props.dimensions.width);
private drawerWidth = new Value<number>(this.initialDrawerWidth);
private drawerOpacity = new Value<number>(
this.initialDrawerWidth || this.props.drawerType === 'permanent' ? 1 : 0
this.props.drawerType === 'permanent' ? 1 : 0
);
private drawerPosition = new Value<number>(
this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT
@@ -730,7 +732,7 @@ const styles = StyleSheet.create({
position: 'absolute',
top: 0,
bottom: 0,
width: '80%',
width: DEFAULT_DRAWER_WIDTH,
},
content: {
flex: 1,

View File

@@ -56,6 +56,20 @@ type Props = {
* Background color for item when its inactive.
*/
inactiveBackgroundColor?: string;
/**
* Color of the touchable effect on press.
* Only supported on Android.
*
* @platform android
*/
pressColor?: string;
/**
* Opacity of the touchable effect on press.
* Only supported on iOS.
*
* @platform ios
*/
pressOpacity?: string;
/**
* Style object for the label element.
*/
@@ -132,6 +146,8 @@ export default function DrawerItem(props: Props) {
inactiveBackgroundColor = 'transparent',
style,
onPress,
pressColor,
pressOpacity,
...rest
} = props;
@@ -159,6 +175,8 @@ export default function DrawerItem(props: Props) {
accessibilityState={{ selected: focused }}
// @ts-expect-error: keep for compatibility with older React Native versions
accessibilityStates={focused ? ['selected'] : []}
pressColor={pressColor}
pressOpacity={pressOpacity}
to={to}
>
<React.Fragment>

View File

@@ -10,6 +10,8 @@ import {
import { ScreenContainer } from 'react-native-screens';
import {
NavigationHelpersContext,
NavigationContext,
NavigationRouteContext,
DrawerNavigationState,
DrawerActions,
useTheme,
@@ -19,16 +21,19 @@ import {
import { GestureHandlerRootView } from './GestureHandler';
import SafeAreaProviderCompat from './SafeAreaProviderCompat';
import ResourceSavingScene from './ResourceSavingScene';
import Header from './Header';
import DrawerContent from './DrawerContent';
import Drawer from './Drawer';
import DrawerOpenContext from '../utils/DrawerOpenContext';
import DrawerPositionContext from '../utils/DrawerPositionContext';
import useWindowDimensions from '../utils/useWindowDimensions';
import getIsDrawerOpenFromState from '../utils/getIsDrawerOpenFromState';
import type {
DrawerDescriptorMap,
DrawerNavigationConfig,
DrawerNavigationHelpers,
DrawerContentComponentProps,
DrawerHeaderProps,
} from '../types';
type Props = DrawerNavigationConfig & {
@@ -90,7 +95,7 @@ export default function DrawerView({
const { colors } = useTheme();
const isDrawerOpen = state.history.some((it) => it.type === 'drawer');
const isDrawerOpen = getIsDrawerOpenFromState(state);
const handleDrawerOpen = React.useCallback(() => {
navigation.dispatch({
@@ -169,6 +174,11 @@ export default function DrawerView({
return null;
}
const {
header = (props: DrawerHeaderProps) => <Header {...props} />,
headerShown = false,
} = descriptor.options;
return (
<ResourceSavingScene
key={route.key}
@@ -176,6 +186,16 @@ export default function DrawerView({
isVisible={isFocused}
enabled={detachInactiveScreens}
>
{headerShown ? (
<NavigationContext.Provider value={descriptor.navigation}>
<NavigationRouteContext.Provider value={route}>
{header({
layout: dimensions,
scene: { route, descriptor },
})}
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
) : null}
{descriptor.render()}
</ResourceSavingScene>
);

View File

@@ -0,0 +1,240 @@
import * as React from 'react';
import { Text, View, Image, StyleSheet, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { DrawerActions, useTheme } from '@react-navigation/native';
import TouchableItem from './TouchableItem';
import type { Layout, DrawerHeaderProps } from '../types';
export const getDefaultHeaderHeight = (
layout: Layout,
statusBarHeight: number
): number => {
const isLandscape = layout.width > layout.height;
let headerHeight;
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
headerHeight = 32;
} else {
headerHeight = 44;
}
} else if (Platform.OS === 'android') {
headerHeight = 56;
} else {
headerHeight = 64;
}
return headerHeight + statusBarHeight;
};
export default function HeaderSegment({ scene, layout }: DrawerHeaderProps) {
const insets = useSafeAreaInsets();
const { colors } = useTheme();
const {
title,
headerTitle,
headerTitleAlign = Platform.select({
ios: 'center',
default: 'left',
}),
headerLeft,
headerLeftAccessibilityLabel,
headerRight,
headerTitleAllowFontScaling,
headerTitleStyle,
headerTintColor,
headerPressColorAndroid,
headerStyle,
headerStatusBarHeight = insets.top,
} = scene.descriptor.options;
const currentTitle =
typeof headerTitle !== 'function' && headerTitle !== undefined
? headerTitle
: title !== undefined
? title
: scene.route.name;
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
const leftButton = headerLeft ? (
headerLeft({ tintColor: headerTintColor })
) : (
<TouchableItem
accessible
accessibilityRole="button"
accessibilityComponentType="button"
accessibilityLabel={headerLeftAccessibilityLabel}
accessibilityTraits="button"
delayPressIn={0}
onPress={() =>
scene.descriptor.navigation.dispatch(DrawerActions.toggleDrawer())
}
style={styles.touchable}
pressColor={headerPressColorAndroid}
hitSlop={Platform.select({
ios: undefined,
default: { top: 16, right: 16, bottom: 16, left: 16 },
})}
borderless
>
<Image
style={[
styles.icon,
headerTintColor ? { tintColor: headerTintColor } : null,
]}
source={require('./assets/toggle-drawer-icon.png')}
fadeDuration={0}
/>
</TouchableItem>
);
const rightButton = headerRight
? headerRight({ tintColor: headerTintColor })
: null;
return (
<View
pointerEvents="box-none"
style={[
{
height: defaultHeight,
backgroundColor: colors.card,
borderBottomColor: colors.border,
shadowColor: colors.border,
},
styles.container,
headerStyle,
]}
>
<View pointerEvents="none" style={{ height: headerStatusBarHeight }} />
<View pointerEvents="box-none" style={styles.content}>
{leftButton ? (
<View
pointerEvents="box-none"
style={[styles.left, { left: insets.left }]}
>
{leftButton}
</View>
) : null}
<View
pointerEvents="box-none"
style={[
headerTitleAlign === 'left'
? {
position: 'absolute',
left: (leftButton ? 72 : 16) + insets.left,
right: (rightButton ? 72 : 16) + insets.right,
}
: {
marginHorizontal:
(leftButton ? 32 : 16) +
Math.max(insets.left, insets.right),
},
]}
>
{typeof headerTitle === 'function' ? (
headerTitle({
children: currentTitle,
allowFontScaling: headerTitleAllowFontScaling,
tintColor: headerTintColor,
style: headerTitleStyle,
})
) : (
<Text
accessibilityRole="header"
aria-level="1"
numberOfLines={1}
allowFontScaling={headerTitleAllowFontScaling}
style={[
styles.title,
{ color: headerTintColor ?? colors.text },
styles.title,
headerTitleStyle,
]}
>
{currentTitle}
</Text>
)}
</View>
{rightButton ? (
<View
pointerEvents="box-none"
style={[styles.right, { right: insets.right }]}
>
{rightButton}
</View>
) : null}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
...Platform.select({
android: {
elevation: 4,
},
ios: {
shadowOpacity: 0.85,
shadowRadius: 0,
shadowOffset: {
width: 0,
height: StyleSheet.hairlineWidth,
},
},
default: {
borderBottomWidth: StyleSheet.hairlineWidth,
},
}),
zIndex: 1,
},
content: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
title: Platform.select({
ios: {
fontSize: 17,
fontWeight: '600',
},
android: {
fontSize: 20,
fontFamily: 'sans-serif-medium',
fontWeight: 'normal',
},
default: {
fontSize: 18,
fontWeight: '500',
},
}),
icon: {
height: 24,
width: 24,
margin: 3,
resizeMode: 'contain',
},
touchable: {
marginHorizontal: 11,
},
left: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'flex-start',
},
right: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'flex-end',
},
});

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

View File

@@ -3,6 +3,73 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.11...@react-navigation/material-bottom-tabs@5.3.12) (2021-01-21)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.10...@react-navigation/material-bottom-tabs@5.3.11) (2021-01-14)
### Bug Fixes
* handle fallback for MaterialCommunityIcons better ([26074a2](https://github.com/react-navigation/react-navigation/commit/26074a28f768ba01743e2ca3b3cb9873a04c9d9c))
## [5.3.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.9...@react-navigation/material-bottom-tabs@5.3.10) (2020-11-20)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.8...@react-navigation/material-bottom-tabs@5.3.9) (2020-11-10)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.7...@react-navigation/material-bottom-tabs@5.3.8) (2020-11-09)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.6...@react-navigation/material-bottom-tabs@5.3.7) (2020-11-08)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.5...@react-navigation/material-bottom-tabs@5.3.6) (2020-11-04)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.4...@react-navigation/material-bottom-tabs@5.3.5) (2020-11-04)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.3...@react-navigation/material-bottom-tabs@5.3.4) (2020-11-03)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

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

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { StyleSheet, Platform } from 'react-native';
import { Text, StyleSheet, Platform } from 'react-native';
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import {
NavigationHelpersContext,
@@ -28,44 +28,48 @@ type Scene = { route: { key: string } };
// Optionally require vector-icons referenced from react-native-paper:
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
let MaterialCommunityIcons: any;
let MaterialCommunityIcons: React.ComponentType<React.ComponentProps<
typeof import('react-native-vector-icons/MaterialCommunityIcons').default
>>;
try {
// Optionally require vector-icons
MaterialCommunityIcons = require('react-native-vector-icons/MaterialCommunityIcons')
.default;
} catch (e) {
// @ts-expect-error
if (global.__expo?.Icon?.MaterialCommunityIcons) {
// Snack doesn't properly bundle vector icons from sub-path
// Use icons from the __expo global if available
// @ts-expect-error
MaterialCommunityIcons = global.__expo.Icon.MaterialCommunityIcons;
} else {
let isErrorLogged = false;
let isErrorLogged = false;
// Fallback component for icons
MaterialCommunityIcons = () => {
if (!isErrorLogged) {
if (
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
e.message
)
) {
console.error(e);
}
console.warn(
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons' could not be loaded.`,
`To remove this warning, try installing 'react-native-vector-icons' or use another method.`
);
isErrorLogged = true;
// Fallback component for icons
MaterialCommunityIcons = ({
name,
color,
size,
selectionColor: _,
...rest
}) => {
if (!isErrorLogged) {
if (
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
e.message
)
) {
console.error(e);
}
return null;
};
}
console.warn(
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons/MaterialCommunityIcons' could not be loaded.`,
`To remove this warning, try installing 'react-native-vector-icons' or use another method to specify icon: https://reactnavigation.org/docs/material-bottom-tab-navigator/#tabbaricon.`
);
isErrorLogged = true;
}
return (
<Text {...rest} style={[styles.icon, { color, fontSize: size }]}>
</Text>
);
};
}
function MaterialBottomTabViewInner({

View File

@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.11...@react-navigation/material-top-tabs@5.3.12) (2021-01-21)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.10...@react-navigation/material-top-tabs@5.3.11) (2021-01-14)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.9...@react-navigation/material-top-tabs@5.3.10) (2020-11-20)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.8...@react-navigation/material-top-tabs@5.3.9) (2020-11-10)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.7...@react-navigation/material-top-tabs@5.3.8) (2020-11-09)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.6...@react-navigation/material-top-tabs@5.3.7) (2020-11-08)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.5...@react-navigation/material-top-tabs@5.3.6) (2020-11-04)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.4...@react-navigation/material-top-tabs@5.3.5) (2020-11-04)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.3...@react-navigation/material-top-tabs@5.3.4) (2020-11-03)
**Note:** Version bump only for package @react-navigation/material-top-tabs

View File

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

View File

@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.9.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.9.0...@react-navigation/native@5.9.1) (2021-01-21)
**Note:** Version bump only for package @react-navigation/native
# [5.9.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.10...@react-navigation/native@5.9.0) (2021-01-14)
### Bug Fixes
* support sync getInitialURL in native useLinking ([b26b907](https://github.com/react-navigation/react-navigation/commit/b26b90706fe0a0d914d4a868df1310d2dc3a7623))
### Features
* expose getActionForState in linking ([c9a5d45](https://github.com/react-navigation/react-navigation/commit/c9a5d4532406c6bfdac0c675a3fe4db5430e9a55))
## [5.8.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.9...@react-navigation/native@5.8.10) (2020-11-20)
**Note:** Version bump only for package @react-navigation/native
## [5.8.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.8...@react-navigation/native@5.8.9) (2020-11-10)
**Note:** Version bump only for package @react-navigation/native
## [5.8.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.7...@react-navigation/native@5.8.8) (2020-11-09)
**Note:** Version bump only for package @react-navigation/native
## [5.8.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.6...@react-navigation/native@5.8.7) (2020-11-08)
**Note:** Version bump only for package @react-navigation/native
## [5.8.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.5...@react-navigation/native@5.8.6) (2020-11-04)
### Bug Fixes
* ignore any errors from deep linking ([4c2379c](https://github.com/react-navigation/react-navigation/commit/4c2379cec1e661aa132002fd1c50909ea64cb983))
## [5.8.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.4...@react-navigation/native@5.8.5) (2020-11-04)
**Note:** Version bump only for package @react-navigation/native
## [5.8.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.3...@react-navigation/native@5.8.4) (2020-11-03)
**Note:** Version bump only for package @react-navigation/native

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import type {
getStateFromPath as getStateFromPathDefault,
getPathFromState as getPathFromStateDefault,
getActionFromState as getActionFromStateDefault,
PathConfigMap,
Route,
} from '@react-navigation/core';
@@ -66,7 +67,11 @@ export type LinkingOptions = {
* }
* ```
*/
getInitialURL?: () => Promise<string | null | undefined>;
getInitialURL?: () =>
| string
| null
| undefined
| Promise<string | null | undefined>;
/**
* Custom function to get subscribe to URL updates.
* Uses `Linking.addEventListener('url', callback)` by default.
@@ -90,11 +95,18 @@ export type LinkingOptions = {
) => undefined | void | (() => void);
/**
* Custom function to parse the URL to a valid navigation state (advanced).
* This state object will be passed as `initialState` for initial URL,
* and converted to an action object to `dispatch` for subsequent URLs.
*/
getStateFromPath?: typeof getStateFromPathDefault;
/**
* Custom function to convert the state object to an action to dispatch (advanced).
* By default, the state is converted to a `NAVIGATE` action.
*/
getActionFromState?: typeof getActionFromStateDefault;
/**
* Custom function to convert the state object to a valid URL (advanced).
* Only applicable on Web.
* Used for creating links for navigation, primarily useful on Web.
*/
getPathFromState?: typeof getPathFromStateDefault;
};

View File

@@ -1,12 +1,14 @@
import * as React from 'react';
import { Linking, Platform } from 'react-native';
import {
getActionFromState,
getActionFromState as getActionFromStateDefault,
getStateFromPath as getStateFromPathDefault,
NavigationContainerRef,
} from '@react-navigation/core';
import type { LinkingOptions } from './types';
import escapeStringRegexp from 'escape-string-regexp';
import type { LinkingOptions } from './types';
type ResultState = ReturnType<typeof getStateFromPathDefault>;
let isUsingLinking = false;
@@ -33,6 +35,7 @@ export default function useLinking(
return () => Linking.removeEventListener('url', callback);
},
getStateFromPath = getStateFromPathDefault,
getActionFromState = getActionFromStateDefault,
}: LinkingOptions
) {
React.useEffect(() => {
@@ -66,6 +69,7 @@ export default function useLinking(
const configRef = React.useRef(config);
const getInitialURLRef = React.useRef(getInitialURL);
const getStateFromPathRef = React.useRef(getStateFromPath);
const getActionFromStateRef = React.useRef(getActionFromState);
React.useEffect(() => {
enabledRef.current = enabled;
@@ -73,7 +77,8 @@ export default function useLinking(
configRef.current = config;
getInitialURLRef.current = getInitialURL;
getStateFromPathRef.current = getStateFromPath;
}, [config, enabled, prefixes, getInitialURL, getStateFromPath]);
getActionFromStateRef.current = getActionFromState;
});
const extractPathFromURL = React.useCallback((url: string) => {
for (const prefix of prefixesRef.current) {
@@ -93,19 +98,39 @@ export default function useLinking(
return undefined;
}, []);
const getInitialState = React.useCallback(async () => {
if (!enabledRef.current) {
return undefined;
const getInitialState = React.useCallback(() => {
let state: ResultState | undefined;
if (enabledRef.current) {
const url = getInitialURLRef.current();
if (url != null && typeof url !== 'string') {
return url.then((url) => {
const path = url ? extractPathFromURL(url) : null;
return path
? getStateFromPathRef.current(path, configRef.current)
: undefined;
});
}
const path = url ? extractPathFromURL(url) : null;
state = path
? getStateFromPathRef.current(path, configRef.current)
: undefined;
}
const url = await getInitialURLRef.current();
const path = url ? extractPathFromURL(url) : null;
const thenable = {
then(onfulfilled?: (state: ResultState | undefined) => void) {
return Promise.resolve(onfulfilled ? onfulfilled(state) : state);
},
catch() {
return thenable;
},
};
if (path) {
return getStateFromPathRef.current(path, configRef.current);
} else {
return undefined;
}
return thenable as PromiseLike<ResultState | undefined>;
}, [extractPathFromURL]);
React.useEffect(() => {
@@ -134,10 +159,21 @@ export default function useLinking(
return;
}
const action = getActionFromState(state, configRef.current);
const action = getActionFromStateRef.current(
state,
configRef.current
);
if (action !== undefined) {
navigation.dispatch(action);
try {
navigation.dispatch(action);
} catch (e) {
// Ignore any errors from deep linking.
// This could happen in case of malformed links, navigation object not being initialized etc.
console.warn(
`An error occurred when trying to handle the link '${path}': ${e.message}`
);
}
} else {
navigation.resetRoot(state);
}

View File

@@ -2,9 +2,9 @@ import * as React from 'react';
import {
getStateFromPath as getStateFromPathDefault,
getPathFromState as getPathFromStateDefault,
getActionFromState as getActionFromStateDefault,
NavigationContainerRef,
NavigationState,
getActionFromState,
} from '@react-navigation/core';
import { nanoid } from 'nanoid/non-secure';
import ServerContext from './ServerContext';
@@ -134,7 +134,7 @@ const createMemoryHistory = () => {
// - There's history to go back, `history.go` is called, and `popstate` fires
// - `history.go` is called multiple times, we need to resolve on respective `popstate`
// - No history to go back, but `history.go` was called, browser has no API to detect it
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const done = (interrupted?: boolean) => {
clearTimeout(timer);
@@ -293,6 +293,7 @@ export default function useLinking(
config,
getStateFromPath = getStateFromPathDefault,
getPathFromState = getPathFromStateDefault,
getActionFromState = getActionFromStateDefault,
}: LinkingOptions
) {
React.useEffect(() => {
@@ -323,14 +324,16 @@ export default function useLinking(
const enabledRef = React.useRef(enabled);
const configRef = React.useRef(config);
const getStateFromPathRef = React.useRef(getStateFromPath);
const getActionFromStateRef = React.useRef(getActionFromState);
const getPathFromStateRef = React.useRef(getPathFromState);
React.useEffect(() => {
enabledRef.current = enabled;
configRef.current = config;
getStateFromPathRef.current = getStateFromPath;
getActionFromStateRef.current = getActionFromState;
getPathFromStateRef.current = getPathFromState;
}, [config, enabled, getPathFromState, getStateFromPath]);
});
const server = React.useContext(ServerContext);
@@ -349,7 +352,6 @@ export default function useLinking(
}
}
// Make it a thenable to keep consistent with the native impl
const thenable = {
then(onfulfilled?: (state: ResultState | undefined) => void) {
return Promise.resolve(onfulfilled ? onfulfilled(value) : value);
@@ -412,10 +414,21 @@ export default function useLinking(
}
if (index > previousIndex) {
const action = getActionFromState(state, configRef.current);
const action = getActionFromStateRef.current(
state,
configRef.current
);
if (action !== undefined) {
navigation.dispatch(action);
try {
navigation.dispatch(action);
} catch (e) {
// Ignore any errors from deep linking.
// This could happen in case of malformed links, navigation object not being initialized etc.
console.warn(
`An error occurred when trying to handle the link '${path}': ${e.message}`
);
}
} else {
navigation.resetRoot(state);
}

View File

@@ -3,6 +3,61 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.7.0...@react-navigation/routers@5.7.1) (2021-01-21)
### Bug Fixes
* fix StackRouter incorrectly handling invalid route if key is present ([d3a9639](https://github.com/react-navigation/react-navigation/commit/d3a9639060631b06551daf0eac191ec1a442e298))
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.6.2...@react-navigation/routers@5.7.0) (2021-01-14)
### Bug Fixes
* consider openByDefault prop when rehydrating drawer state ([#9099](https://github.com/react-navigation/react-navigation/issues/9099)) ([2ad61a6](https://github.com/react-navigation/react-navigation/commit/2ad61a67357242fc4663ecad62ab311facbaf1be))
### Features
* add a new backBehavior: firstRoute for TabRouter ([3c87419](https://github.com/react-navigation/react-navigation/commit/3c874191ffbd24b953ded5b62f606c4cc47e5651))
* add a way to specify an unique ID for screens ([b19f76b](https://github.com/react-navigation/react-navigation/commit/b19f76bfffe623759e67d925bfd067c753a453bf))
## [5.6.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.6.1...@react-navigation/routers@5.6.2) (2020-11-09)
**Note:** Version bump only for package @react-navigation/routers
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.6.0...@react-navigation/routers@5.6.1) (2020-11-08)
**Note:** Version bump only for package @react-navigation/routers
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.5.1...@react-navigation/routers@5.6.0) (2020-11-04)
### Features
* add a merge option to navigate to control merging params ([9beca3a](https://github.com/react-navigation/react-navigation/commit/9beca3a8027c6e2135dbef2abb8eede6b0b4bc44))
## [5.5.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.5.0...@react-navigation/routers@5.5.1) (2020-10-28)

View File

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

View File

@@ -16,14 +16,14 @@ export type Action =
| {
type: 'NAVIGATE';
payload:
| { key: string; name?: undefined; params?: object }
| { name: string; key?: string; params?: object };
| { key: string; name?: undefined; params?: object; merge?: boolean }
| { name: string; key?: string; params?: object; merge?: boolean };
source?: string;
target?: string;
}
| {
type: 'RESET';
payload: ResetState;
payload: ResetState | undefined;
source?: string;
target?: string;
}
@@ -62,7 +62,7 @@ export function navigate(...args: any): Action {
}
}
export function reset(state: ResetState): Action {
export function reset(state: ResetState | undefined): Action {
return { type: 'RESET', payload: state };
}

View File

@@ -75,7 +75,7 @@ const isDrawerOpen = (
state:
| DrawerNavigationState<ParamListBase>
| PartialState<DrawerNavigationState<ParamListBase>>
) => Boolean(state.history?.find((it) => it.type === 'drawer'));
) => Boolean(state.history?.some((it) => it.type === 'drawer'));
const openDrawer = (
state: DrawerNavigationState<ParamListBase>
@@ -120,8 +120,12 @@ export default function DrawerRouter({
type: 'drawer',
getInitialState({ routeNames, routeParamList }) {
let state = router.getInitialState({ routeNames, routeParamList });
getInitialState({ routeNames, routeParamList, routeGetIdList }) {
let state = router.getInitialState({
routeNames,
routeParamList,
routeGetIdList,
});
if (openByDefault) {
state = openDrawer(state);
@@ -135,7 +139,10 @@ export default function DrawerRouter({
};
},
getRehydratedState(partialState, { routeNames, routeParamList }) {
getRehydratedState(
partialState,
{ routeNames, routeParamList, routeGetIdList }
) {
if (partialState.stale === false) {
return partialState;
}
@@ -143,9 +150,10 @@ export default function DrawerRouter({
let state = router.getRehydratedState(partialState, {
routeNames,
routeParamList,
routeGetIdList,
});
if (isDrawerOpen(partialState)) {
if (partialState.history ? isDrawerOpen(partialState) : openByDefault) {
state = openDrawer(state);
}

View File

@@ -258,6 +258,9 @@ export default function StackRouter(options: StackRouterOptions) {
case 'PUSH':
if (state.routeNames.includes(action.payload.name)) {
const getId = options.routeGetIdList[action.payload.name];
const id = getId?.({ params: action.payload.params });
const route =
action.payload.name && action.payload.key
? state.routes.find(
@@ -265,34 +268,32 @@ export default function StackRouter(options: StackRouterOptions) {
route.name === action.payload.name &&
route.key === action.payload.key
)
: id
? state.routes.find(
(route) => id === getId?.({ params: route.params })
)
: undefined;
let routes: Route<string>[];
if (route) {
routes = state.routes.filter((r) => r.key !== route.key);
routes.push(
action.payload.params
? {
...route,
params:
action.payload.params !== undefined
? {
...route.params,
...action.payload.params,
}
: route.params,
}
: route
);
routes.push({
...route,
params:
action.payload.params !== undefined
? {
...route.params,
...action.payload.params,
}
: route.params,
});
} else {
routes = [
...state.routes,
{
key:
action.payload.key === undefined
? `${action.payload.name}-${nanoid()}`
: action.payload.key,
action.payload.key ?? `${action.payload.name}-${nanoid()}`,
name: action.payload.name,
params:
routeParamList[action.payload.name] !== undefined
@@ -348,14 +349,29 @@ export default function StackRouter(options: StackRouterOptions) {
case 'NAVIGATE':
if (
action.payload.key ||
(action.payload.name &&
state.routeNames.includes(action.payload.name))
action.payload.name !== undefined &&
!state.routeNames.includes(action.payload.name)
) {
return null;
}
if (action.payload.key || action.payload.name) {
// If the route already exists, navigate to that
let index = -1;
if (
const getId =
// `getId` and `key` can't be used together
action.payload.key === undefined &&
action.payload.name !== undefined
? options.routeGetIdList[action.payload.name]
: undefined;
const id = getId?.({ params: action.payload.params });
if (id) {
index = state.routes.findIndex(
(route) => id === getId?.({ params: route.params })
);
} else if (
(state.routes[state.index].name === action.payload.name &&
action.payload.key === undefined) ||
state.routes[state.index].key === action.payload.key
@@ -383,18 +399,48 @@ export default function StackRouter(options: StackRouterOptions) {
}
if (index === -1 && action.payload.name !== undefined) {
return router.getStateForAction(
state,
const routes = [
...state.routes,
{
type: 'PUSH',
payload: {
key: action.payload.key,
name: action.payload.name,
params: action.payload.params,
},
key:
action.payload.key ?? `${action.payload.name}-${nanoid()}`,
name: action.payload.name,
params:
routeParamList[action.payload.name] !== undefined
? {
...routeParamList[action.payload.name],
...action.payload.params,
}
: action.payload.params,
},
options
);
];
return {
...state,
routes,
index: routes.length - 1,
};
}
const route = state.routes[index];
let params;
if (action.payload.merge === false) {
params =
routeParamList[route.name] !== undefined
? {
...routeParamList[route.name],
...action.payload.params,
}
: action.payload.params;
} else {
params = action.payload.params
? {
...route.params,
...action.payload.params,
}
: route.params;
}
return {
@@ -402,14 +448,8 @@ export default function StackRouter(options: StackRouterOptions) {
index,
routes: [
...state.routes.slice(0, index),
action.payload.params !== undefined
? {
...state.routes[index],
params: {
...state.routes[index].params,
...action.payload.params,
},
}
params !== route.params
? { ...route, params }
: state.routes[index],
],
};

View File

@@ -17,7 +17,12 @@ export type TabActionType = {
target?: string;
};
export type BackBehavior = 'initialRoute' | 'order' | 'history' | 'none';
export type BackBehavior =
| 'initialRoute'
| 'firstRoute'
| 'history'
| 'order'
| 'none';
export type TabRouterOptions = DefaultRouterOptions & {
backBehavior?: BackBehavior;
@@ -74,13 +79,21 @@ const getRouteHistory = (
history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key });
}
break;
case 'firstRoute':
if (index !== 0) {
history.unshift({
type: TYPE_ROUTE,
key: routes[0].key,
});
}
break;
case 'initialRoute':
initialRouteIndex = routes.findIndex(
(route) => route.name === initialRouteName
);
initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
if (initialRouteIndex !== index) {
if (index !== initialRouteIndex) {
history.unshift({
type: TYPE_ROUTE,
key: routes[initialRouteIndex].key,
@@ -271,7 +284,7 @@ export default function TabRouter({
return changeIndex(state, index, backBehavior, initialRouteName);
},
getStateForAction(state, action) {
getStateForAction(state, action, { routeParamList }) {
switch (action.type) {
case 'JUMP_TO':
case 'NAVIGATE': {
@@ -296,17 +309,37 @@ export default function TabRouter({
...state,
routes:
action.payload.params !== undefined
? state.routes.map((route, i) =>
i === index
? {
...route,
params: {
? state.routes.map((route, i) => {
if (i !== index) {
return route;
}
let params;
if (
action.type === 'NAVIGATE' &&
action.payload.merge === false
) {
params =
routeParamList[route.name] !== undefined
? {
...routeParamList[route.name],
...action.payload.params,
}
: action.payload.params;
} else {
params = action.payload.params
? {
...route.params,
...action.payload.params,
},
}
: route
)
}
: route.params;
}
return params !== route.params
? { ...route, params }
: route;
})
: state.routes,
},
index,

View File

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

View File

@@ -1,4 +1,9 @@
import { CommonActions, StackRouter, StackActions } from '..';
import {
CommonActions,
StackRouter,
StackActions,
RouterConfigOptions,
} from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
@@ -12,6 +17,7 @@ it('gets initial state from route names and params with initialRouteName', () =>
baz: { answer: 42 },
qux: { name: 'Jane' },
},
routeGetIdList: {},
})
).toEqual({
index: 0,
@@ -33,6 +39,7 @@ it('gets initial state from route names and params without initialRouteName', ()
baz: { answer: 42 },
qux: { name: 'Jane' },
},
routeGetIdList: {},
})
).toEqual({
index: 0,
@@ -47,12 +54,13 @@ it('gets initial state from route names and params without initialRouteName', ()
it('gets rehydrated state from partial state', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
routeGetIdList: {},
};
expect(
@@ -136,6 +144,7 @@ it("doesn't rehydrate state if it's not stale", () => {
router.getRehydratedState(state, {
routeNames: [],
routeParamList: {},
routeGetIdList: {},
})
).toBe(state);
});
@@ -163,6 +172,7 @@ it('gets state on route names change', () => {
qux: { name: 'John' },
fiz: { fruit: 'apple' },
},
routeGetIdList: {},
}
)
).toEqual({
@@ -195,6 +205,7 @@ it('gets state on route names change', () => {
routeParamList: {
baz: { name: 'John' },
},
routeGetIdList: {},
}
)
).toEqual({
@@ -228,6 +239,7 @@ it('gets state on route names change with initialRouteName', () => {
routeParamList: {
baz: { name: 'John' },
},
routeGetIdList: {},
}
)
).toEqual({
@@ -242,9 +254,10 @@ it('gets state on route names change with initialRouteName', () => {
it('handles navigate action', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -427,11 +440,188 @@ it('handles navigate action', () => {
});
});
it('handles go back action', () => {
it("doesn't navigate to nonexistent screen", () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
},
CommonActions.navigate('far', { answer: 42 }),
options
)
).toBe(null);
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
},
CommonActions.navigate({
name: 'far',
key: 'test',
params: { answer: 42 },
}),
options
)
).toBe(null);
});
it('ensures unique ID for navigate', () => {
const router = StackRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {
bar: ({ params }) => params?.foo,
qux: ({ params }) => params?.fux,
},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'bar', name: 'bar' }],
},
CommonActions.navigate('bar', { foo: 'a' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
},
CommonActions.navigate('bar', { foo: 'a' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
},
CommonActions.navigate('bar', { foo: 'b' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
{ key: 'bar-test', name: 'bar', params: { foo: 'b' } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
},
CommonActions.navigate({
key: 'test',
name: 'bar',
params: { foo: 'a' },
}),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
{ key: 'test', name: 'bar', params: { foo: 'a' } },
],
});
});
it('handles go back action', () => {
const router = StackRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -477,9 +667,10 @@ it('handles go back action', () => {
it('handles pop action', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -650,9 +841,10 @@ it('handles pop action', () => {
it('handles pop to top action', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -684,9 +876,10 @@ it('handles pop to top action', () => {
it('replaces focused screen with replace', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -722,9 +915,10 @@ it('replaces focused screen with replace', () => {
it('replaces active screen with replace', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -763,9 +957,10 @@ it('replaces active screen with replace', () => {
it("doesn't handle replace if source key isn't present", () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -794,9 +989,10 @@ it("doesn't handle replace if source key isn't present", () => {
it("doesn't handle replace if screen to replace with isn't present", () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -824,11 +1020,12 @@ it("doesn't handle replace if screen to replace with isn't present", () => {
it('handles push action', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {
baz: { foo: 21 },
},
routeGetIdList: {},
};
expect(
@@ -895,6 +1092,152 @@ it('handles push action', () => {
options
)
).toBe(null);
});
it("doesn't push nonexistent screen", () => {
const router = StackRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
},
StackActions.push('far', { answer: 42 }),
options
)
).toBe(null);
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
},
{
type: 'PUSH',
payload: {
name: 'far',
key: 'test',
params: { answer: 42 },
},
},
options
)
).toBe(null);
});
it('ensures unique ID for push', () => {
const router = StackRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {
bar: ({ params }) => params?.foo,
qux: ({ params }) => params?.fux,
},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'bar', name: 'bar' }],
},
StackActions.push('bar', { foo: 'a' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
},
StackActions.push('bar', { foo: 'a' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
],
},
StackActions.push('bar', { foo: 'b' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar', name: 'bar' },
{ key: 'bar-test', name: 'bar', params: { foo: 'a' } },
{ key: 'bar-test', name: 'bar', params: { foo: 'b' } },
],
});
expect(
router.getStateForAction(
@@ -964,57 +1307,12 @@ it('handles push action', () => {
});
});
it('changes index on focus change', () => {
const router = StackRouter({});
expect(
router.getStateForRouteFocus(
{
index: 2,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
stale: false,
type: 'stack',
},
'baz-0'
)
).toEqual({
index: 1,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
],
stale: false,
type: 'stack',
});
const state = {
index: 0,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
],
stale: false as const,
type: 'stack' as const,
};
expect(router.getStateForRouteFocus(state, 'qux-0')).toEqual(state);
});
it('merges params on navigate to an existing screen', () => {
const router = StackRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -1074,3 +1372,116 @@ it('merges params on navigate to an existing screen', () => {
],
});
});
it("doesn't merge params on navigate to an existing screen if merge: false", () => {
const router = StackRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {
baz: { foo: 12 },
},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
},
{
type: 'NAVIGATE',
payload: {
name: 'bar',
merge: false,
},
},
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
],
},
{
type: 'NAVIGATE',
payload: {
name: 'bar',
params: { fruit: 'orange' },
merge: false,
},
},
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz', params: { test: 99 } },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
],
},
{
type: 'NAVIGATE',
payload: {
name: 'baz',
params: { color: 'black' },
merge: false,
},
},
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz', params: { foo: 12, color: 'black' } }],
});
});

View File

@@ -4,6 +4,7 @@ import {
TabActions,
TabNavigationState,
ParamListBase,
RouterConfigOptions,
} from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
@@ -18,6 +19,7 @@ it('gets initial state from route names and params with initialRouteName', () =>
baz: { answer: 42 },
qux: { name: 'Jane' },
},
routeGetIdList: {},
})
).toEqual({
index: 1,
@@ -44,6 +46,7 @@ it('gets initial state from route names and params without initialRouteName', ()
baz: { answer: 42 },
qux: { name: 'Jane' },
},
routeGetIdList: {},
})
).toEqual({
index: 0,
@@ -63,12 +66,13 @@ it('gets initial state from route names and params without initialRouteName', ()
it('gets rehydrated state from partial state', () => {
const router = TabRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
routeGetIdList: {},
};
expect(
@@ -241,6 +245,7 @@ it("doesn't rehydrate state if it's not stale", () => {
router.getRehydratedState(state, {
routeNames: [],
routeParamList: {},
routeGetIdList: {},
})
).toBe(state);
});
@@ -248,9 +253,10 @@ it("doesn't rehydrate state if it's not stale", () => {
it('restores correct history on rehydrating with backBehavior: order', () => {
const router = TabRouter({ backBehavior: 'order' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -289,9 +295,10 @@ it('restores correct history on rehydrating with backBehavior: order', () => {
it('restores correct history on rehydrating with backBehavior: history', () => {
const router = TabRouter({ backBehavior: 'history' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -323,12 +330,16 @@ it('restores correct history on rehydrating with backBehavior: history', () => {
});
});
it('restores correct history on rehydrating with backBehavior: initialRoute', () => {
const router = TabRouter({ backBehavior: 'initialRoute' });
it('restores correct history on rehydrating with backBehavior: firstRoute', () => {
const router = TabRouter({
backBehavior: 'firstRoute',
initialRouteName: 'bar',
});
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -363,12 +374,57 @@ it('restores correct history on rehydrating with backBehavior: initialRoute', ()
});
});
it('restores correct history on rehydrating with backBehavior: initialRoute', () => {
const router = TabRouter({
backBehavior: 'initialRoute',
initialRouteName: 'bar',
});
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
},
options
)
).toEqual({
key: 'tab-test',
index: 2,
routeNames: ['foo', 'bar', 'baz', 'qux'],
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
history: [
{ key: 'bar-0', type: 'route' },
{ key: 'baz-0', type: 'route' },
],
stale: false,
type: 'tab',
});
});
it('restores correct history on rehydrating with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -424,6 +480,7 @@ it('gets state on route names change', () => {
qux: { name: 'John' },
fiz: { fruit: 'apple' },
},
routeGetIdList: {},
}
)
).toEqual({
@@ -458,6 +515,7 @@ it('gets state on route names change', () => {
{
routeNames: ['foo', 'fiz'],
routeParamList: {},
routeGetIdList: {},
}
)
).toEqual({
@@ -498,6 +556,7 @@ it('preserves focused route on route names change', () => {
qux: { name: 'John' },
fiz: { fruit: 'apple' },
},
routeGetIdList: {},
}
)
).toEqual({
@@ -540,6 +599,7 @@ it('falls back to first route if route is removed on route names change', () =>
qux: { name: 'John' },
fiz: { fruit: 'apple' },
},
routeGetIdList: {},
}
)
).toEqual({
@@ -559,9 +619,10 @@ it('falls back to first route if route is removed on route names change', () =>
it('handles navigate action', () => {
const router = TabRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -647,11 +708,63 @@ it('handles navigate action', () => {
).toBe(null);
});
it("doesn't navigate to nonexistent screen", () => {
const router = TabRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'],
routeParamList: {},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
history: [{ type: 'route', key: 'bar' }],
},
CommonActions.navigate('foo', { answer: 42 }),
options
)
).toBe(null);
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
history: [{ type: 'route', key: 'bar' }],
},
CommonActions.navigate({
name: 'foo',
key: 'test',
params: { answer: 42 },
}),
options
)
).toBe(null);
});
it('handles jump to action', () => {
const router = TabRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -688,11 +801,40 @@ it('handles jump to action', () => {
});
});
it("doesn't jump to nonexistent screen", () => {
const router = TabRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar'],
routeParamList: {},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
],
history: [{ type: 'route', key: 'bar' }],
},
TabActions.jumpTo('foo', { answer: 42 }),
options
)
).toBe(null);
});
it('handles back action with backBehavior: history', () => {
const router = TabRouter({ backBehavior: 'history' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
let state = router.getInitialState(options);
@@ -776,9 +918,10 @@ it('handles back action with backBehavior: history', () => {
it('handles back action with backBehavior: order', () => {
const router = TabRouter({ backBehavior: 'order' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
let state = router.getInitialState(options);
@@ -847,9 +990,10 @@ it('handles back action with backBehavior: order', () => {
it('handles back action with backBehavior: initialRoute', () => {
const router = TabRouter({ backBehavior: 'initialRoute' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
let state = router.getInitialState(options);
@@ -919,9 +1063,10 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
initialRouteName: 'baz',
});
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
let state = router.getInitialState(options);
@@ -987,9 +1132,10 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
it('handles back action with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
let state = router.getInitialState(options);
@@ -1007,9 +1153,10 @@ it('handles back action with backBehavior: none', () => {
it('updates route key history on navigate and jump to', () => {
const router = TabRouter({ backBehavior: 'history' });
const options = {
const options: RouterConfigOptions = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
let state: TabNavigationState<ParamListBase> = {
@@ -1110,9 +1257,10 @@ it('updates route key history on focus change', () => {
it('merges params on navigate to an existing screen', () => {
const router = TabRouter({});
const options = {
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(
@@ -1186,11 +1334,159 @@ it('merges params on navigate to an existing screen', () => {
});
});
it('merges params on jump to an existing screen', () => {
it("doesn't merge params on navigate to an existing screen if merge: false", () => {
const router = TabRouter({});
const options = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {
qux: { color: 'indigo' },
},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
{
type: 'NAVIGATE',
payload: {
name: 'bar',
merge: false,
},
},
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'bar' },
],
});
});
it('merges params on navigate to an existing screen if merge: true', () => {
const router = TabRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {
qux: { color: 'indigo' },
},
routeGetIdList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
{
type: 'NAVIGATE',
payload: {
name: 'bar',
params: { fruit: 'orange' },
merge: false,
},
},
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
{ key: 'qux', name: 'qux' },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'bar' },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
{
type: 'NAVIGATE',
payload: {
name: 'qux',
params: { test: 12 },
merge: false,
},
},
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'qux', name: 'qux', params: { color: 'indigo', test: 12 } },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'qux' },
],
});
});
it('merges params on jump to an existing screen', () => {
const router = TabRouter({});
const options: RouterConfigOptions = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
routeGetIdList: {},
};
expect(

View File

@@ -56,12 +56,11 @@ export type PartialRoute<R extends Route<string>> = Omit<R, 'key'> & {
};
export type PartialState<State extends NavigationState> = Partial<
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
Omit<State, 'stale' | 'routes'>
> &
Readonly<{
stale?: true;
type?: string;
routes: PartialRoute<Route<string>>[];
routes: PartialRoute<Route<State['routeNames'][number]>>[];
}>;
export type Route<
@@ -133,6 +132,11 @@ export type RouterFactory<
export type RouterConfigOptions = {
routeNames: string[];
routeParamList: ParamListBase;
routeGetIdList: Record<
string,
| ((options: { params?: Record<string, any> }) => string | undefined)
| undefined
>;
};
export type Router<

View File

@@ -3,6 +3,102 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.14.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.13.0...@react-navigation/stack@5.14.0) (2021-01-21)
### Features
* add pressColor and pressOpacity props to drawerItem ([#8834](https://github.com/react-navigation/react-navigation/issues/8834)) ([bae4019](https://github.com/react-navigation/react-navigation/commit/bae4019995062c682f0213c121b7927ab8006c1e))
# [5.13.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.8...@react-navigation/stack@5.13.0) (2021-01-14)
### Bug Fixes
* enable detachInactiveScreens by default on web for better a11y ([dd87fa4](https://github.com/react-navigation/react-navigation/commit/dd87fa49a43ad8db105a62418243339e4150fadf))
### Features
* export TransitionPreset for custom TransitionPresets ([#9173](https://github.com/react-navigation/react-navigation/issues/9173)) ([9633c4d](https://github.com/react-navigation/react-navigation/commit/9633c4d35fe2f9cb4f37a7629872e436a4528238))
## [5.12.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.7...@react-navigation/stack@5.12.8) (2020-11-21)
### Bug Fixes
* force dismiss keyboard if there was no gesture ([3e069b7](https://github.com/react-navigation/react-navigation/commit/3e069b718d60f5381957f2d3838ee04ee9384779)), closes [#9078](https://github.com/react-navigation/react-navigation/issues/9078)
## [5.12.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.6...@react-navigation/stack@5.12.7) (2020-11-20)
**Note:** Version bump only for package @react-navigation/stack
## [5.12.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.5...@react-navigation/stack@5.12.6) (2020-11-10)
### Bug Fixes
* make sure inactive screen don't increase scroll area on web ([da35085](https://github.com/react-navigation/react-navigation/commit/da35085f1e3440f26eea800c892c88aec64d072f))
## [5.12.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.4...@react-navigation/stack@5.12.5) (2020-11-09)
**Note:** Version bump only for package @react-navigation/stack
## [5.12.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.3...@react-navigation/stack@5.12.4) (2020-11-08)
### Bug Fixes
* don't hide child header automatically in stack ([8f0efc8](https://github.com/react-navigation/react-navigation/commit/8f0efc8db534297a95ea8a2bcb6d2e387c1fea53))
## [5.12.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.2...@react-navigation/stack@5.12.3) (2020-11-04)
### Bug Fixes
* android textinput gets blurred after navigating back ([1169ed0](https://github.com/react-navigation/react-navigation/commit/1169ed0946df609cb7e5c52c4bdda0aa91b5737f))
* disable react-native-screens on iOS for older versions ([ce7d20e](https://github.com/react-navigation/react-navigation/commit/ce7d20e3366415b07a537e01ee0b17ce7e72cad6))
## [5.12.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.1...@react-navigation/stack@5.12.2) (2020-11-04)
**Note:** Version bump only for package @react-navigation/stack
## [5.12.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.12.0...@react-navigation/stack@5.12.1) (2020-11-03)
**Note:** Version bump only for package @react-navigation/stack

View File

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

Some files were not shown because too many files have changed in this diff Show More