Compare commits

...

83 Commits

Author SHA1 Message Date
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
Satyajit Sahoo
84cc0d758a chore: publish
- @react-navigation/bottom-tabs@5.10.4
 - @react-navigation/compat@5.3.4
 - @react-navigation/core@5.13.4
 - @react-navigation/devtools@5.1.12
 - @react-navigation/drawer@5.10.4
 - @react-navigation/material-bottom-tabs@5.3.4
 - @react-navigation/material-top-tabs@5.3.4
 - @react-navigation/native@5.8.4
 - @react-navigation/stack@5.12.1
2020-11-03 07:04:08 +01:00
Satyajit Sahoo
ebc7f9ea75 fix: fix nested navigation not working the first time 2020-11-03 06:59:42 +01:00
Satyajit Sahoo
bd9f0ad5f6 chore: publish
- @react-navigation/bottom-tabs@5.10.3
 - @react-navigation/compat@5.3.3
 - @react-navigation/core@5.13.3
 - @react-navigation/devtools@5.1.11
 - @react-navigation/drawer@5.10.3
 - @react-navigation/material-bottom-tabs@5.3.3
 - @react-navigation/material-top-tabs@5.3.3
 - @react-navigation/native@5.8.3
 - @react-navigation/stack@5.12.0
2020-11-03 06:31:58 +01:00
Satyajit Sahoo
c326c106f9 feat: add a headerBackAccessibilityLabel option in stack
closes #9016
2020-11-03 06:22:51 +01:00
Satyajit Sahoo
52451d1109 fix: make sure that invalid linking config doesn't work if app is open 2020-11-03 06:15:44 +01:00
Satyajit Sahoo
0945689b70 fix: handle navigating to same screen again for nested screens 2020-11-03 05:51:52 +01:00
Satyajit Sahoo
37b9454f3e chore: publish
- @react-navigation/bottom-tabs@5.10.2
 - @react-navigation/compat@5.3.2
 - @react-navigation/core@5.13.2
 - @react-navigation/devtools@5.1.10
 - @react-navigation/drawer@5.10.2
 - @react-navigation/material-bottom-tabs@5.3.2
 - @react-navigation/material-top-tabs@5.3.2
 - @react-navigation/native@5.8.2
 - @react-navigation/stack@5.11.1
2020-10-30 13:42:48 +01:00
Satyajit Sahoo
fb7ac960c8 fix: trim routes if an index is specified in state 2020-10-30 13:41:28 +01:00
Satyajit Sahoo
e8515f9cd9 fix: fix params from for the root screen when creating action
closes #9006
2020-10-30 13:26:52 +01:00
Satyajit Sahoo
5eee804e7f chore: publish
- @react-navigation/bottom-tabs@5.10.1
 - @react-navigation/compat@5.3.1
 - @react-navigation/core@5.13.1
 - @react-navigation/devtools@5.1.9
 - @react-navigation/drawer@5.10.1
 - @react-navigation/material-bottom-tabs@5.3.1
 - @react-navigation/material-top-tabs@5.3.1
 - @react-navigation/native@5.8.1
 - @react-navigation/routers@5.5.1
 - @react-navigation/stack@5.11.0
2020-10-28 22:21:16 +01:00
Satyajit Sahoo
45dbe5c40e feat: enable react-native-screens in Stack by default on iOS 2020-10-28 22:15:37 +01:00
Satyajit Sahoo
d26bcc057e fix: improve types for route prop in screenOptions 2020-10-28 22:06:52 +01:00
Satyajit Sahoo
836ca4482e chore: fix loading indicator not visible in auth example 2020-10-27 01:37:53 +01:00
Satyajit Sahoo
fdd549a536 chore: migrate example to community async-storage 2020-10-27 00:44:30 +01:00
Satyajit Sahoo
128bbbe62a chore: add ability to manually run expo publish workflow 2020-10-25 02:05:18 +02:00
Satyajit Sahoo
a186b445b4 chore: fix slug for example app 2020-10-25 01:58:38 +02:00
Satyajit Sahoo
ac11a3bded chore: publish
- @react-navigation/bottom-tabs@5.10.0
 - @react-navigation/compat@5.3.0
 - @react-navigation/core@5.13.0
 - @react-navigation/devtools@5.1.8
 - @react-navigation/drawer@5.10.0
 - @react-navigation/material-bottom-tabs@5.3.0
 - @react-navigation/material-top-tabs@5.3.0
 - @react-navigation/native@5.8.0
 - @react-navigation/routers@5.5.0
 - @react-navigation/stack@5.10.0
2020-10-25 01:38:02 +02:00
Satyajit Sahoo
55d635f53e chore: fix custom link button example on web 2020-10-25 01:32:36 +02:00
Satyajit Sahoo
95600500a4 chore: upgrade depenendecies 2020-10-25 01:28:19 +02:00
Satyajit Sahoo
6cf124a190 docs: improve jsdoc for linking 2020-10-24 23:51:59 +02:00
Satyajit Sahoo
bfd0d94985 docs: fix incorrect comment 2020-10-24 23:35:50 +02:00
Satyajit Sahoo
748e92f120 feat: add getInitialURL and subscribe options to linking config
For apps with push notifications linking to screens inside the app, currently we need to handle them separately (e.g. [instructions for firebase](https://rnfirebase.io/messaging/notifications#handling-interaction), [instructions for expo notifications](https://docs.expo.io/push-notifications/receiving-notifications/)). But if we add a link in the notification to use for deep linking, we can instead reuse the same deep linking logic instead.

This commit adds the `getInitialURL` and `subscribe` options which internally used `Linking` API to allow more advanced implementations by combining it with other sources such as push notifications.

Example usage with Firebase notifications could look like this:

```js
const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  async getInitialURL() {
    // Check if app was opened from a deep link
    const url = await Linking.getInitialURL();

    if (url != null) {
      return url;
    }

    // Check if there is an initial firebase notification
    const message = await messaging().getInitialNotification();

    // Get the `url` property from the notification which corresponds to a screen
    // This property needs to be set on the notification payload when sending it
    return message?.notification.url;
  },
  subscribe(listener) {
    const onReceiveURL = ({ url }: { url: string }) => listener(url);

    // Listen to incoming links from deep linking
    Linking.addEventListener('url', onReceiveURL);

    // Listen to firebase push notifications
    const unsubscribeNotification = messaging().onNotificationOpenedApp(
      (message) => {
        const url = message.notification.url;

        if (url) {
          // If the notification has a `url` property, use it for linking
          listener(url);
        }
      }
    );

    return () => {
      // Clean up the event listeners
      Linking.removeEventListener('url', onReceiveURL);
      unsubscribeNotification();
    };
  },
  config,
};
```
2020-10-24 23:32:51 +02:00
Satyajit Sahoo
7f3b27a9ec feat: allow deep linking to reset state (#8973)
Currently when we receive a deep link after the app is rendered, it always results in a `navigate` action. While it's ok with the default configuration, it may result in incorrect behaviour when a custom `getStateForPath` function is provided and it returns a routes array different than the initial route and new route pair.

The commit changes 2 things:

1. Add ability to reset state via params of `navigate` by specifying a `state` property instead of `screen`
2. Update `getStateForAction` to return an action for reset when necessary according to the deep linking configuration

Closes #8952
2020-10-24 15:27:06 +02:00
Satyajit Sahoo
f51086edea feat: update helper types to have navigator specific methods 2020-10-23 18:12:36 +02:00
Wojciech Lewicki
7196889bf1 feat: add optional screens per navigator (#8805)
Changes done here will work properly with https://github.com/software-mansion/react-native-screens/pull/624 merged and released. The documentation of `screensEnabled` and `activeLimit` props should also be added. It also enabled `Screens` in iOS stack-navigator by default.

New things:
- `detachInactiveScreens` - prop for navigators with `react-native-screens` integration that can be set by user. It controls if the `react-native-screens` are used by the navigator.
- `detachPreviousScreen` - option that tells to keep the previous screen active. It can be set by user, defaults to `true` for normal mode and `false` for the last screen for mode = “modal”.

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-10-23 17:59:26 +02:00
Satyajit Sahoo
7dc2f5832e feat: improve types for navigation state (#8980)
The commit improves the navigation state object to have more specific types.
e.g. The `routeNames` array will now have proper type instead of `string[]`
2020-10-23 17:06:31 +02:00
Satyajit Sahoo
8ec6c1a603 chore: remove test code from example app 2020-10-23 16:51:44 +02:00
Satyajit Sahoo
960f0a5281 refactor: make sure height set on header container is focused header height 2020-10-23 03:32:40 +02:00
Satyajit Sahoo
da91cec941 fix: add missing check for parent header when calculating height 2020-10-23 02:37:36 +02:00
Satyajit Sahoo
38e17aae93 fix: don't set statusbar height on nested header by default 2020-10-23 02:27:50 +02:00
Satyajit Sahoo
0f60b4617f refactor: refactor HeaderSegment to function component 2020-10-23 02:17:04 +02:00
Drew Miller
f01bb4834b feat: add sceneContainerStyle prop to bottom-tabs navigator (#8947)
This feature adds the sceneContainerStyle prop to the bottom-tabs navigator, to allow setting styles on the container's view. It's already implemented in the material-top-tabs and drawer navigators, I've simply ported it into the bottom-tabs navigator.

It also fixes this issue:

https://github.com/react-navigation/react-navigation/issues/8076

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-10-23 02:16:09 +02:00
Satyajit Sahoo
261a33a0d0 fix: fix imports from query-string. closes #8971 (#8976) 2020-10-23 02:15:47 +02:00
Listi
d6cac6713a fix: fix header buttons not pressable when headerTransparent=true & headerMode=float (#8804)
Motivation
--
Previously when using `headerMode="float"` with headerTransparent set to true, we cant press header buttons in Android. This PR fixes this. (resolves #8731 )
Been doing some debugging and found out that this is caused by `HeaderContainer` being set as `absolute`. Initially it didn't have width & height in Android when it's set to default, that's why we can't access the children.
So, the solution in this PR is to define the height by using headerHeight. But, since we can't access headerHeight from header, Im using local state for keeping up with the headerHeight. Or should I move the HeaderHeight provider out of StackContainer? I'm not sure, since I think it was intended to be kept inside the StackContainer

Test Plan
--
With this config, now the header button can be clicked. Tested in both platform
```typescript
  <Stack.Navigator
    headerMode="float"
    screenOptions={{
      headerTransparent: true
    }}
  >
     <Stack.Screen  
      name="Home Screen"
      component={Home}
      />
      <Stack.Screen  
      name="Details Screen"
      component={Details}
      />
  </Stack.Navigator>
```

Android:
-
![Kapture 2020-09-30 at 19 01 21](https://user-images.githubusercontent.com/24470609/94682575-5b0fe480-034f-11eb-880a-318643d4eb00.gif)

iOS:
--
<img width="300" src="https://user-images.githubusercontent.com/24470609/94682743-9a3e3580-034f-11eb-8e90-2d31748bde5c.gif" />
2020-10-23 01:52:34 +02:00
Satyajit Sahoo
8ee0dda155 fix: set needsOffscreenAlphaCompositing and update default android animation
closes #8696
2020-10-23 01:38:14 +02:00
David Pett
8585f97226 docs: fix comments for gestureResponseDistance (#8954)
gestureResponseDistance vertical and horizontal documentations were swapped
2020-10-21 13:12:54 +02:00
Hossein Mohammadi
23ab350492 feat: support wildcard string prefixes (#8942)
Prefixes should be more flexible for situations like wild card subdomain. On android and IOS we can define wild cards by * but react-navigation does not work, In this PR I added support for RegExp Prefixes.

For Example
```js
{
  prefixes: [
    /^[^.s]+.example.com/g
 ],
}
```
I tested this work well.

Closes #8941 

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-10-20 12:01:49 +02:00
ahmadj-levelbenefits
80ff5a9c54 feat: add an unhandled action listener (#8895)
Often developers miss these console messages: this allows missed routes to be emitted to whatever event logger users prefer.
2020-10-09 16:49:20 +02:00
Kaushil Ruparelia
90ebfc40b3 feat: make react-native-vector-icons optional (#8936)
Referenced from 4b26429c49/src/components/MaterialCommunityIcon.tsx (L14)

https://callstack.github.io/react-native-paper/getting-started.html
> To get smaller bundle size by excluding modules you don't use, you can use our optional babel plugin. The plugin automatically rewrites the import statements so that only the modules you use are imported instead of the whole library. Add react-native-paper/babel to the plugins section in your babel.config.js for production environment. It should look like this:
> ```
> module.exports = {
>   presets: ['module:metro-react-native-babel-preset'],
>   env: {
>     production: {
>       plugins: ['react-native-paper/babel'],
>     },
>   },
> };
> ```
> If you created your project using Expo, it'll look something like this:
> ```
> module.exports = function(api) {
>   api.cache(true);
>   return {
>     presets: ['babel-preset-expo'],
>     env: {
>       production: {
>         plugins: ['react-native-paper/babel'],
>       },
>     },
>   };
> };
> ```

Closes #8821
2020-10-09 13:41:37 +02:00
Satyajit Sahoo
091b2a2038 fix: handle pushing a route with duplicate key
Currently, stack router adds a duplicate route when pushing a new route with a key that already exists. This is a buggy behaviour since keys need to be unique in the stack.

This commit fixes the behaviour to bring the existing route with the same key to focus (and merge new params if any) instead of adding a duplicate route.
2020-10-09 00:28:45 +02:00
Satyajit Sahoo
01f86d2ac6 chore: publish
- @react-navigation/bottom-tabs@5.9.2
 - @react-navigation/compat@5.2.8
 - @react-navigation/core@5.12.5
 - @react-navigation/devtools@5.1.7
 - @react-navigation/drawer@5.9.3
 - @react-navigation/material-bottom-tabs@5.2.19
 - @react-navigation/material-top-tabs@5.2.19
 - @react-navigation/native@5.7.6
 - @react-navigation/stack@5.9.3
2020-10-07 11:39:56 +02:00
Satyajit Sahoo
c49dab31b2 fix: use route keys instead of index for lazy load 2020-10-07 11:36:02 +02:00
Satyajit Sahoo
16e7ac131f chore: upgrade depenendecies 2020-10-07 11:18:38 +02:00
Satyajit Sahoo
9e3650831c fix: add missing check for initial state on web 2020-10-01 11:23:28 +02:00
128 changed files with 6976 additions and 2835 deletions

View File

@@ -87,6 +87,9 @@ jobs:
- run: - run:
name: Build packages in the monorepo name: Build packages in the monorepo
command: yarn lerna run prepare command: yarn lerna run prepare
- run:
name: Verify built type definitions are correct
command: yarn typescript
- run: - run:
name: Verify paths for types name: Verify paths for types
command: node scripts/check-types-path.js command: node scripts/check-types-path.js

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

@@ -0,0 +1,42 @@
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@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = context.payload.comment
? context.payload.comment.body
: context.payload.issue.body
if (!/https?:\/\/((github\.com\/[^/]+\/[^/]+\/?[\s\n]+)|(snack\.expo\.io\/.+))/gm.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

@@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- main - main
workflow_dispatch:
jobs: jobs:
publish: publish:

View File

@@ -16,7 +16,7 @@ jobs:
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. 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: needs-repro:
@@ -76,7 +76,7 @@ jobs:
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports 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: react-native-reanimated:
@@ -91,7 +91,7 @@ jobs:
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports 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: react-native-gesture-handler:
@@ -106,7 +106,7 @@ jobs:
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports 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: react-native-safe-area-context:
@@ -121,5 +121,5 @@ jobs:
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports 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

@@ -4,7 +4,7 @@
"expo": { "expo": {
"name": "React Navigation", "name": "React Navigation",
"owner": "react-navigation", "owner": "react-navigation",
"slug": "NavigationPlayground", "slug": "react-navigation-example",
"description": "Demonstrates the functionality and various capabilities of React Navigation.", "description": "Demonstrates the functionality and various capabilities of React Navigation.",
"privacy": "public", "privacy": "public",
"version": "1.0.0", "version": "1.0.0",

View File

@@ -68,14 +68,9 @@ module.exports = {
enhanceMiddleware: (middleware) => { enhanceMiddleware: (middleware) => {
return (req, res, next) => { return (req, res, next) => {
// When an asset is imported outside the project root, it has wrong path on Android // 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 // So we fix the path to correct one
const assets = '/packages/stack/src/views/assets'; if (/\/packages\/.+\.png\?.+$/.test(req.url)) {
req.url = `/assets/../${req.url}`;
if (req.url.startsWith(assets)) {
req.url = req.url.replace(
assets,
'/assets/../packages/stack/src/views/assets'
);
} }
return middleware(req, res, next); return middleware(req, res, next);

View File

@@ -14,13 +14,14 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.0.0",
"@react-native-async-storage/async-storage": "^1.13.1",
"@react-native-community/masked-view": "0.1.10", "@react-native-community/masked-view": "0.1.10",
"color": "^3.1.2", "color": "^3.1.3",
"expo": "^39.0.0", "expo": "^39.0.0",
"expo-asset": "~8.2.0", "expo-asset": "~8.2.0",
"expo-blur": "~8.2.0", "expo-blur": "~8.2.0",
"expo-linking": "^1.0.4", "expo-linking": "^1.0.4",
"expo-updates": "~0.3.3", "expo-updates": "~0.3.5",
"koa": "^2.13.0", "koa": "^2.13.0",
"react": "~16.13.1", "react": "~16.13.1",
"react-dom": "~16.13.1", "react-dom": "~16.13.1",
@@ -31,30 +32,30 @@
"react-native-reanimated": "~1.13.0", "react-native-reanimated": "~1.13.0",
"react-native-safe-area-context": "3.1.4", "react-native-safe-area-context": "3.1.4",
"react-native-screens": "~2.10.1", "react-native-screens": "~2.10.1",
"react-native-tab-view": "^2.15.1", "react-native-tab-view": "^2.15.2",
"react-native-vector-icons": "^7.0.0", "react-native-vector-icons": "^7.0.0",
"react-native-web": "^0.13.13" "react-native-web": "^0.13.16"
}, },
"devDependencies": { "devDependencies": {
"@babel/node": "^7.10.1", "@babel/node": "^7.12.1",
"@expo/webpack-config": "^0.12.21", "@expo/webpack-config": "^0.12.40",
"@types/cheerio": "^0.22.21", "@types/cheerio": "^0.22.22",
"@types/jest-dev-server": "^4.2.0", "@types/jest-dev-server": "^4.2.0",
"@types/koa": "^2.11.4", "@types/koa": "^2.11.6",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/react": "~16.9.35", "@types/react": "~16.9.53",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-native": "~0.63.2", "@types/react-native": "~0.63.30",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-module-resolver": "^4.0.0", "babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "^8.3.0", "babel-preset-expo": "^8.3.0",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"expo-cli": "^3.27.7", "expo-cli": "^3.28.2",
"jest": "^26.4.2", "jest": "^26.6.1",
"jest-dev-server": "^4.4.0", "jest-dev-server": "^4.4.0",
"mock-require-assets": "^0.0.1", "mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"nodemon": "^2.0.4", "nodemon": "^2.0.6",
"playwright": "^0.14.0", "playwright": "^0.14.0",
"serve": "^11.3.0", "serve": "^11.3.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"

View File

@@ -1,3 +0,0 @@
import { AsyncStorage } from 'react-native';
export default AsyncStorage;

View File

@@ -1,14 +0,0 @@
export default {
getItem(key: string) {
return Promise.resolve(localStorage.getItem(key));
},
setItem(key: string, value: string) {
return Promise.resolve(localStorage.setItem(key, value));
},
removeItem(key: string) {
return Promise.resolve(localStorage.removeItem(key));
},
clear() {
return Promise.resolve(localStorage.clear());
},
};

View File

@@ -31,9 +31,11 @@ const AuthContext = React.createContext<{
}); });
const SplashScreen = () => { const SplashScreen = () => {
const { colors } = useTheme();
return ( return (
<View style={styles.content}> <View style={styles.content}>
<ActivityIndicator /> <ActivityIndicator color={colors.primary} />
</View> </View>
); );
}; };

View File

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

View File

@@ -25,18 +25,9 @@ const LinkButton = ({
to, to,
...rest ...rest
}: React.ComponentProps<typeof Button> & { to: string }) => { }: React.ComponentProps<typeof Button> & { to: string }) => {
const { onPress, ...props } = useLinkProps({ to }); const props = useLinkProps({ to });
return ( return <Button {...props} {...rest} />;
<Button
{...props}
{...rest}
{...Platform.select({
web: { onClick: onPress } as any,
default: { onPress },
})}
/>
);
}; };
const ArticleScreen = ({ const ArticleScreen = ({

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { import {
ScrollView, ScrollView,
YellowBox,
Platform, Platform,
StatusBar, StatusBar,
I18nManager, I18nManager,
Dimensions, Dimensions,
ScaledSize, ScaledSize,
Linking, Linking,
LogBox,
} from 'react-native'; } from 'react-native';
import { enableScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
@@ -15,7 +15,6 @@ import {
Provider as PaperProvider, Provider as PaperProvider,
DefaultTheme as PaperLightTheme, DefaultTheme as PaperLightTheme,
DarkTheme as PaperDarkTheme, DarkTheme as PaperDarkTheme,
Appbar,
List, List,
Divider, Divider,
Text, Text,
@@ -28,19 +27,16 @@ import {
PathConfigMap, PathConfigMap,
NavigationContainerRef, NavigationContainerRef,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { import { createDrawerNavigator } from '@react-navigation/drawer';
createDrawerNavigator,
DrawerScreenProps,
} from '@react-navigation/drawer';
import { import {
createStackNavigator, createStackNavigator,
StackScreenProps, StackScreenProps,
HeaderStyleInterpolators, HeaderStyleInterpolators,
} from '@react-navigation/stack'; } from '@react-navigation/stack';
import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { useReduxDevToolsExtension } from '@react-navigation/devtools';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { restartApp } from './Restart'; import { restartApp } from './Restart';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes'; import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem'; import SettingsItem from './Shared/SettingsItem';
import SimpleStack from './Screens/SimpleStack'; import SimpleStack from './Screens/SimpleStack';
@@ -58,13 +54,14 @@ import PreventRemove from './Screens/PreventRemove';
import CompatAPI from './Screens/CompatAPI'; import CompatAPI from './Screens/CompatAPI';
import LinkComponent from './Screens/LinkComponent'; import LinkComponent from './Screens/LinkComponent';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']); if (Platform.OS !== 'web') {
LogBox.ignoreLogs(['Require cycle:']);
}
enableScreens(); enableScreens();
type RootDrawerParamList = { type RootDrawerParamList = {
Root: undefined; Examples: undefined;
Another: undefined;
}; };
const SCREENS = { const SCREENS = {
@@ -229,50 +226,49 @@ export default function App() {
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`) // The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
prefixes: LinkingPrefixes, prefixes: LinkingPrefixes,
config: { config: {
screens: { initialRouteName: 'Home',
Root: { screens: Object.keys(SCREENS).reduce<PathConfigMap>(
path: '', (acc, name) => {
initialRouteName: 'Home', // Convert screen names such as SimpleStack to kebab case (simple-stack)
screens: Object.keys(SCREENS).reduce<PathConfigMap>( const path = name
(acc, name) => { .replace(/([A-Z]+)/g, '-$1')
// Convert screen names such as SimpleStack to kebab case (simple-stack) .replace(/^-/, '')
const path = name .toLowerCase();
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
acc[name] = { acc[name] = {
path, path,
screens: { screens: {
Article: { Article: {
path: 'article/:author?', path: 'article/:author?',
parse: { parse: {
author: (author) => author: (author) =>
author.charAt(0).toUpperCase() + author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '), author.slice(1).replace(/-/g, ' '),
},
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
Dialog: 'dialog',
}, },
}; stringify: {
author: (author: string) =>
return acc; 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>} fallback={<Text>Loading</Text>}
@@ -281,35 +277,29 @@ export default function App() {
`${options?.title ?? route?.name} - React Navigation Example`, `${options?.title ?? route?.name} - React Navigation Example`,
}} }}
> >
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}> <Stack.Navigator
<Drawer.Screen screenOptions={{
name="Root" headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
}}
>
<Stack.Screen
name="Home"
options={{ options={{
title: 'Examples', headerShown: false,
drawerIcon: ({ size, color }) => (
<MaterialIcons size={size} color={color} name="folder" />
),
}} }}
> >
{({ navigation }: DrawerScreenProps<RootDrawerParamList>) => ( {() => (
<Stack.Navigator <Drawer.Navigator
screenOptions={{ drawerType={isLargeScreen ? 'permanent' : undefined}
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit, screenOptions={{ headerShown: true }}
}}
> >
<Stack.Screen <Drawer.Screen
name="Home" name="Examples"
options={{ options={{
title: 'Examples', title: 'Examples',
headerLeft: isLargeScreen drawerIcon: ({ size, color }) => (
? undefined <MaterialIcons size={size} color={color} name="folder" />
: () => ( ),
<Appbar.Action
color={theme.colors.text}
icon="menu"
onPress={() => navigation.toggleDrawer()}
/>
),
}} }}
> >
{({ navigation }: StackScreenProps<RootStackParamList>) => ( {({ navigation }: StackScreenProps<RootStackParamList>) => (
@@ -350,26 +340,24 @@ export default function App() {
)} )}
</ScrollView> </ScrollView>
)} )}
</Stack.Screen> </Drawer.Screen>
<Stack.Screen </Drawer.Navigator>
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> </Stack.Screen>
</Drawer.Navigator> <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> </NavigationContainer>
</PaperProvider> </PaperProvider>
); );

View File

@@ -26,14 +26,14 @@
}, },
"devDependencies": { "devDependencies": {
"@commitlint/config-conventional": "^11.0.0", "@commitlint/config-conventional": "^11.0.0",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.15",
"babel-jest": "^26.3.0", "babel-jest": "^26.6.1",
"codecov": "^3.7.2", "codecov": "^3.8.0",
"commitlint": "^11.0.0", "commitlint": "^11.0.0",
"eslint": "^7.9.0", "eslint": "^7.12.0",
"eslint-config-satya164": "^3.1.8", "eslint-config-satya164": "^3.1.8",
"husky": "^4.3.0", "husky": "^4.3.0",
"jest": "^26.4.2", "jest": "^26.6.1",
"lerna": "^3.22.1", "lerna": "^3.22.1",
"metro-react-native-babel-preset": "^0.63.0", "metro-react-native-babel-preset": "^0.63.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",

View File

@@ -3,6 +3,118 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.11.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
## [5.10.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.2...@react-navigation/bottom-tabs@5.10.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.10.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.1...@react-navigation/bottom-tabs@5.10.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.10.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.0...@react-navigation/bottom-tabs@5.10.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.10.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.2...@react-navigation/bottom-tabs@5.10.0) (2020-10-24)
### Features
* add optional screens per navigator ([#8805](https://github.com/react-navigation/react-navigation/issues/8805)) ([7196889](https://github.com/react-navigation/react-navigation/commit/7196889bf1218eb6a736d9475e33a909c2248c3b))
* add sceneContainerStyle prop to bottom-tabs navigator ([#8947](https://github.com/react-navigation/react-navigation/issues/8947)) ([f01bb48](https://github.com/react-navigation/react-navigation/commit/f01bb4834b01e13ab9a6b220328349f77ca49428))
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.9.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.1...@react-navigation/bottom-tabs@5.9.2) (2020-10-07)
### Bug Fixes
* use route keys instead of index for lazy load ([c49dab3](https://github.com/react-navigation/react-navigation/commit/c49dab31b2c63a1735f0ed0a1936ecf7bbcd8b13))
## [5.9.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.0...@react-navigation/bottom-tabs@5.9.1) (2020-09-28) ## [5.9.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.0...@react-navigation/bottom-tabs@5.9.1) (2020-09-28)
**Note:** Version bump only for package @react-navigation/bottom-tabs **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/bottom-tabs", "name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines", "description": "Bottom tab navigator following iOS design guidelines",
"version": "5.9.1", "version": "5.11.2",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -36,16 +36,16 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2", "color": "^3.1.3",
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.5", "@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.20", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",

View File

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

View File

@@ -6,6 +6,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import BottomTabView from '../views/BottomTabView'; import BottomTabView from '../views/BottomTabView';
import type { import type {
@@ -23,11 +25,13 @@ function BottomTabNavigator({
backBehavior, backBehavior,
children, children,
screenOptions, screenOptions,
sceneContainerStyle,
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
TabActionHelpers<ParamListBase>,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap BottomTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
@@ -43,12 +47,13 @@ function BottomTabNavigator({
state={state} state={state}
navigation={navigation} navigation={navigation}
descriptors={descriptors} descriptors={descriptors}
sceneContainerStyle={sceneContainerStyle}
/> />
); );
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState, TabNavigationState<ParamListBase>,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap, BottomTabNavigationEventMap,
typeof BottomTabNavigator typeof BottomTabNavigator

View File

@@ -33,7 +33,8 @@ export type LabelPosition = 'beside-icon' | 'below-icon';
export type BottomTabNavigationHelpers = NavigationHelpers< export type BottomTabNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
BottomTabNavigationEventMap BottomTabNavigationEventMap
>; > &
TabActionHelpers<ParamListBase>;
export type BottomTabNavigationProp< export type BottomTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -41,7 +42,7 @@ export type BottomTabNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
TabNavigationState, TabNavigationState<ParamList>,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap BottomTabNavigationEventMap
> & > &
@@ -108,6 +109,12 @@ export type BottomTabNavigationOptions = {
*/ */
tabBarBadge?: number | string; 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. * 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. * It's recommended to set this if you don't have a label for the tab.
@@ -148,7 +155,7 @@ export type BottomTabNavigationOptions = {
export type BottomTabDescriptor = Descriptor< export type BottomTabDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
TabNavigationState, TabNavigationState<ParamListBase>,
BottomTabNavigationOptions BottomTabNavigationOptions
>; >;
@@ -170,6 +177,16 @@ export type BottomTabNavigationConfig<T = BottomTabBarOptions> = {
* Options for the tab bar which will be passed as props to the tab bar component. * Options for the tab bar which will be passed as props to the tab bar component.
*/ */
tabBarOptions?: T; tabBarOptions?: T;
/**
* 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` on Android.
*/
detachInactiveScreens?: boolean;
/**
* Style object for the component wrapping the screen content.
*/
sceneContainerStyle?: StyleProp<ViewStyle>;
}; };
export type BottomTabBarOptions = { export type BottomTabBarOptions = {
@@ -240,7 +257,7 @@ export type BottomTabBarOptions = {
}; };
export type BottomTabBarProps<T = BottomTabBarOptions> = T & { export type BottomTabBarProps<T = BottomTabBarOptions> = T & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
descriptors: BottomTabDescriptorMap; descriptors: BottomTabDescriptorMap;
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>; navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
}; };

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, StyleSheet,
Platform, Platform,
LayoutChangeEvent, LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native'; } from 'react-native';
import { import {
NavigationContext, NavigationContext,
NavigationRouteContext, NavigationRouteContext,
TabNavigationState,
ParamListBase,
CommonActions, CommonActions,
useTheme, useTheme,
useLinkBuilder, useLinkBuilder,
} from '@react-navigation/native'; } 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 BottomTabItem from './BottomTabItem';
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
import useWindowDimensions from '../utils/useWindowDimensions'; import useWindowDimensions from '../utils/useWindowDimensions';
import useIsKeyboardShown from '../utils/useIsKeyboardShown'; import useIsKeyboardShown from '../utils/useIsKeyboardShown';
import type { BottomTabBarProps } from '../types'; import type { BottomTabBarProps, LabelPosition } from '../types';
type Props = BottomTabBarProps & { type Props = BottomTabBarProps & {
activeTintColor?: string; activeTintColor?: string;
@@ -31,13 +36,93 @@ const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
const useNativeDriver = Platform.OS !== 'web'; 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({ export default function BottomTabBar({
state, state,
navigation, navigation,
descriptors, descriptors,
activeBackgroundColor, activeBackgroundColor,
activeTintColor, activeTintColor,
adaptive = true, adaptive,
allowFontScaling, allowFontScaling,
inactiveBackgroundColor, inactiveBackgroundColor,
inactiveTintColor, inactiveTintColor,
@@ -60,6 +145,8 @@ export default function BottomTabBar({
const dimensions = useWindowDimensions(); const dimensions = useWindowDimensions();
const isKeyboardShown = useIsKeyboardShown(); const isKeyboardShown = useIsKeyboardShown();
const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
const shouldShowTabBar = const shouldShowTabBar =
focusedOptions.tabBarVisible !== false && focusedOptions.tabBarVisible !== false &&
!(keyboardHidesTabBar && isKeyboardShown); !(keyboardHidesTabBar && isKeyboardShown);
@@ -120,11 +207,19 @@ export default function BottomTabBar({
width: dimensions.width, width: dimensions.width,
}); });
const isLandscape = () => dimensions.width > dimensions.height;
const handleLayout = (e: LayoutChangeEvent) => { const handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout; 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) => { setLayout((layout) => {
if (height === layout.height && width === layout.width) { if (height === layout.height && width === layout.width) {
return layout; return layout;
@@ -138,34 +233,6 @@ export default function BottomTabBar({
}; };
const { routes } = state; 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(); const defaultInsets = useSafeArea();
@@ -176,22 +243,26 @@ export default function BottomTabBar({
left: safeAreaInsets?.left ?? defaultInsets.left, left: safeAreaInsets?.left ?? defaultInsets.left,
}; };
const paddingBottom = Math.max( const paddingBottom = getPaddingBottom(insets);
insets.bottom - Platform.select({ ios: 4, default: 0 }), const tabBarHeight = getTabBarHeight({
0 state,
); insets,
dimensions,
layout,
adaptive,
labelPosition,
tabStyle,
style,
});
const getDefaultTabBarHeight = () => { const hasHorizontalLabels = shouldUseHorizontalLabels({
if ( state,
Platform.OS === 'ios' && dimensions,
!Platform.isPad && layout,
isLandscape() && adaptive,
shouldUseHorizontalLabels() labelPosition,
) { tabStyle,
return COMPACT_TABBAR_HEIGHT; });
}
return DEFAULT_TABBAR_HEIGHT;
};
return ( return (
<Animated.View <Animated.View
@@ -218,7 +289,7 @@ export default function BottomTabBar({
position: isTabBarHidden ? 'absolute' : (null as any), position: isTabBarHidden ? 'absolute' : (null as any),
}, },
{ {
height: getDefaultTabBarHeight() + paddingBottom, height: tabBarHeight,
paddingBottom, paddingBottom,
paddingHorizontal: Math.max(insets.left, insets.right), paddingHorizontal: Math.max(insets.left, insets.right),
}, },
@@ -276,7 +347,7 @@ export default function BottomTabBar({
<BottomTabItem <BottomTabItem
route={route} route={route}
focused={focused} focused={focused}
horizontal={shouldUseHorizontalLabels()} horizontal={hasHorizontalLabels}
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
@@ -290,6 +361,7 @@ export default function BottomTabBar({
button={options.tabBarButton} button={options.tabBarButton}
icon={options.tabBarIcon} icon={options.tabBarIcon}
badge={options.tabBarBadge} badge={options.tabBarBadge}
badgeStyle={options.tabBarBadgeStyle}
label={label} label={label}
showLabel={showLabel} showLabel={showLabel}
labelStyle={labelStyle} labelStyle={labelStyle}

View File

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

View File

@@ -1,16 +1,27 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet } from 'react-native'; import {
View,
StyleSheet,
Dimensions,
StyleProp,
ViewStyle,
} from 'react-native';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
ParamListBase,
TabNavigationState, TabNavigationState,
useTheme, useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer } from 'react-native-screens';
import SafeAreaProviderCompat from './SafeAreaProviderCompat'; import SafeAreaProviderCompat, {
initialSafeAreaInsets,
} from './SafeAreaProviderCompat';
import ResourceSavingScene from './ResourceSavingScene'; 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 { import type {
BottomTabNavigationConfig, BottomTabNavigationConfig,
BottomTabDescriptorMap, BottomTabDescriptorMap,
@@ -19,21 +30,24 @@ import type {
} from '../types'; } from '../types';
type Props = BottomTabNavigationConfig & { type Props = BottomTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: BottomTabNavigationHelpers; navigation: BottomTabNavigationHelpers;
descriptors: BottomTabDescriptorMap; descriptors: BottomTabDescriptorMap;
}; };
type State = { type State = {
loaded: number[]; loaded: string[];
tabBarHeight: number;
}; };
function SceneContent({ function SceneContent({
isFocused, isFocused,
children, children,
style,
}: { }: {
isFocused: boolean; isFocused: boolean;
children: React.ReactNode; children: React.ReactNode;
style?: StyleProp<ViewStyle>;
}) { }) {
const { colors } = useTheme(); const { colors } = useTheme();
@@ -41,7 +55,7 @@ function SceneContent({
<View <View
accessibilityElementsHidden={!isFocused} accessibilityElementsHidden={!isFocused}
importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'} importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'}
style={[styles.content, { backgroundColor: colors.background }]} style={[styles.content, { backgroundColor: colors.background }, style]}
> >
{children} {children}
</View> </View>
@@ -54,19 +68,38 @@ export default class BottomTabView extends React.Component<Props, State> {
}; };
static getDerivedStateFromProps(nextProps: Props, prevState: State) { static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const { index } = nextProps.state; const focusedRouteKey = nextProps.state.routes[nextProps.state.index].key;
return { return {
// Set the current tab to be loaded if it was not loaded before // Set the current tab to be loaded if it was not loaded before
loaded: prevState.loaded.includes(index) loaded: prevState.loaded.includes(focusedRouteKey)
? prevState.loaded ? prevState.loaded
: [...prevState.loaded, index], : [...prevState.loaded, focusedRouteKey],
}; };
} }
state = { constructor(props: Props) {
loaded: [this.props.state.index], 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 = () => { private renderTabBar = () => {
const { const {
@@ -84,16 +117,37 @@ 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() { render() {
const { state, descriptors, navigation, lazy } = this.props; const {
state,
descriptors,
navigation,
lazy,
detachInactiveScreens = true,
sceneContainerStyle,
} = this.props;
const { routes } = state; const { routes } = state;
const { loaded } = this.state; const { loaded, tabBarHeight } = this.state;
return ( return (
<NavigationHelpersContext.Provider value={navigation}> <NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat> <SafeAreaProviderCompat>
<View style={styles.container}> <View style={styles.container}>
<ScreenContainer style={styles.pages}> <ScreenContainer
// @ts-ignore
enabled={detachInactiveScreens}
style={styles.pages}
>
{routes.map((route, index) => { {routes.map((route, index) => {
const descriptor = descriptors[route.key]; const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options; const { unmountOnBlur } = descriptor.options;
@@ -103,7 +157,7 @@ export default class BottomTabView extends React.Component<Props, State> {
return null; return null;
} }
if (lazy && !loaded.includes(index) && !isFocused) { if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a screen if we've never navigated to it // Don't render a screen if we've never navigated to it
return null; return null;
} }
@@ -113,15 +167,25 @@ export default class BottomTabView extends React.Component<Props, State> {
key={route.key} key={route.key}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
isVisible={isFocused} isVisible={isFocused}
enabled={detachInactiveScreens}
> >
<SceneContent isFocused={isFocused}> <SceneContent
{descriptor.render()} isFocused={isFocused}
style={sceneContainerStyle}
>
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
{descriptor.render()}
</BottomTabBarHeightContext.Provider>
</SceneContent> </SceneContent>
</ResourceSavingScene> </ResourceSavingScene>
); );
})} })}
</ScreenContainer> </ScreenContainer>
{this.renderTabBar()} <BottomTabBarHeightCallbackContext.Provider
value={this.handleTabBarHeightChange}
>
{this.renderTabBar()}
</BottomTabBarHeightCallbackContext.Provider>
</View> </View>
</SafeAreaProviderCompat> </SafeAreaProviderCompat>
</NavigationHelpersContext.Provider> </NavigationHelpersContext.Provider>

View File

@@ -1,10 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native'; import { Platform, StyleSheet, View } from 'react-native';
import { Screen, screensEnabled } from 'react-native-screens'; import {
Screen,
screensEnabled,
// @ts-ignore
shouldUseActivityState,
} from 'react-native-screens';
type Props = { type Props = {
isVisible: boolean; isVisible: boolean;
children: React.ReactNode; children: React.ReactNode;
enabled: boolean;
style?: any; style?: any;
}; };
@@ -16,8 +22,17 @@ export default class ResourceSavingScene extends React.Component<Props> {
if (screensEnabled?.() && Platform.OS !== 'web') { if (screensEnabled?.() && Platform.OS !== 'web') {
const { isVisible, ...rest } = this.props; const { isVisible, ...rest } = this.props;
// @ts-expect-error: stackPresentation is incorrectly marked as required if (shouldUseActivityState) {
return <Screen active={isVisible ? 1 : 0} {...rest} />; 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} />
);
}
} }
const { isVisible, children, style, ...rest } = this.props; const { isVisible, children, style, ...rest } = this.props;

View File

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

View File

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

View File

@@ -3,6 +3,106 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.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
## [5.3.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.2...@react-navigation/compat@5.3.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.1...@react-navigation/compat@5.3.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.0...@react-navigation/compat@5.3.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/compat
# [5.3.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.8...@react-navigation/compat@5.3.0) (2020-10-24)
### Features
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.2.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.7...@react-navigation/compat@5.2.8) (2020-10-07)
**Note:** Version bump only for package @react-navigation/compat
## [5.2.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.6...@react-navigation/compat@5.2.7) (2020-09-28) ## [5.2.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.6...@react-navigation/compat@5.2.7) (2020-09-28)
**Note:** Version bump only for package @react-navigation/compat **Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/compat", "name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format", "description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.2.7", "version": "5.3.10",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -32,8 +32,8 @@
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.5", "@react-navigation/native": "^5.8.10",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"react": "~16.13.1", "react": "~16.13.1",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },

View File

@@ -19,7 +19,11 @@ type EventName =
export default function createCompatNavigationProp< export default function createCompatNavigationProp<
NavigationPropType extends NavigationProp<ParamListBase>, NavigationPropType extends NavigationProp<ParamListBase>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase : ParamListBase
@@ -143,7 +147,6 @@ export default function createCompatNavigationProp<
} }
}, },
state: { state: {
// @ts-expect-error: these properties may actually exist
key: state.key, key: state.key,
// @ts-expect-error // @ts-expect-error
routeName: state.name, routeName: state.name,
@@ -198,7 +201,6 @@ export default function createCompatNavigationProp<
const { routes } = navigation.dangerouslyGetState(); const { routes } = navigation.dangerouslyGetState();
// @ts-expect-error
return routes[0].key === state.key; return routes[0].key === state.key;
}, },
dangerouslyGetParent() { dangerouslyGetParent() {

View File

@@ -32,7 +32,11 @@ export default function createCompatNavigatorFactory<
const createCompatNavigator = < const createCompatNavigator = <
NavigationPropType extends NavigationProp<any, any, any, any, any>, NavigationPropType extends NavigationProp<any, any, any, any, any>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase, : ParamListBase,

View File

@@ -5,6 +5,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import createCompatNavigatorFactory from './createCompatNavigatorFactory'; import createCompatNavigatorFactory from './createCompatNavigatorFactory';
@@ -12,17 +14,21 @@ type Props = DefaultNavigatorOptions<{}> & TabRouterOptions;
function SwitchNavigator(props: Props) { function SwitchNavigator(props: Props) {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
{}, {},
{} {},
TabActionHelpers<ParamListBase>
>(TabRouter, props); >(TabRouter, props);
return descriptors[state.routes[state.index].key].render(); return descriptors[state.routes[state.index].key].render();
} }
export default createCompatNavigatorFactory( export default createCompatNavigatorFactory(
createNavigatorFactory<TabNavigationState, {}, {}, typeof SwitchNavigator>( createNavigatorFactory<
SwitchNavigator TabNavigationState<ParamListBase>,
) {},
{},
typeof SwitchNavigator
>(SwitchNavigator)
); );

View File

@@ -8,7 +8,11 @@ import type * as helpers from './helpers';
export type CompatNavigationProp< export type CompatNavigationProp<
NavigationPropType extends NavigationProp<ParamListBase>, NavigationPropType extends NavigationProp<ParamListBase>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase, : ParamListBase,
@@ -67,7 +71,11 @@ export type CompatScreenType<
export type CompatRouteConfig< export type CompatRouteConfig<
NavigationPropType extends NavigationProp<ParamListBase>, NavigationPropType extends NavigationProp<ParamListBase>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase : ParamListBase

View File

@@ -3,6 +3,151 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.14.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/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)
### Bug Fixes
* fix nested navigation not working the first time ([ebc7f9e](https://github.com/react-navigation/react-navigation/commit/ebc7f9ea75bbf6e3b6303027cfa023d7c97342ff))
## [5.13.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.2...@react-navigation/core@5.13.3) (2020-11-03)
### Bug Fixes
* handle navigating to same screen again for nested screens ([0945689](https://github.com/react-navigation/react-navigation/commit/0945689b70d71a4b5d766c61d57009761c460bf6))
## [5.13.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.1...@react-navigation/core@5.13.2) (2020-10-30)
### Bug Fixes
* fix params from for the root screen when creating action ([e8515f9](https://github.com/react-navigation/react-navigation/commit/e8515f9cd94a912c107a407dea3d953c4172393f)), closes [#9006](https://github.com/react-navigation/react-navigation/issues/9006)
* trim routes if an index is specified in state ([fb7ac96](https://github.com/react-navigation/react-navigation/commit/fb7ac960c8e1ffca200ecb12696ce5531a139e50))
## [5.13.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.0...@react-navigation/core@5.13.1) (2020-10-28)
### Bug Fixes
* improve types for route prop in screenOptions ([d26bcc0](https://github.com/react-navigation/react-navigation/commit/d26bcc057ef31f8950f909adf83e263171a42d74))
# [5.13.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.5...@react-navigation/core@5.13.0) (2020-10-24)
### Bug Fixes
* fix imports from query-string. closes [#8971](https://github.com/react-navigation/react-navigation/issues/8971) ([#8976](https://github.com/react-navigation/react-navigation/issues/8976)) ([261a33a](https://github.com/react-navigation/react-navigation/commit/261a33a0d03150c87b06f01aeace4926b1c03eb6))
### Features
* add an unhandled action listener ([#8895](https://github.com/react-navigation/react-navigation/issues/8895)) ([80ff5a9](https://github.com/react-navigation/react-navigation/commit/80ff5a9c543a44fa2fd7ba7fda0598f1b0d52a64))
* allow deep linking to reset state ([#8973](https://github.com/react-navigation/react-navigation/issues/8973)) ([7f3b27a](https://github.com/react-navigation/react-navigation/commit/7f3b27a9ec8edd9604ac19774baa1f60963ccdc9)), closes [#8952](https://github.com/react-navigation/react-navigation/issues/8952)
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.12.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.4...@react-navigation/core@5.12.5) (2020-10-07)
**Note:** Version bump only for package @react-navigation/core
## [5.12.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.3...@react-navigation/core@5.12.4) (2020-09-22) ## [5.12.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.3...@react-navigation/core@5.12.4) (2020-09-22)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/core", "name": "@react-navigation/core",
"description": "Core utilities for building navigators", "description": "Core utilities for building navigators",
"version": "5.12.4", "version": "5.14.4",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -35,19 +35,17 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.4.12", "@react-navigation/routers": "^5.6.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.12", "nanoid": "^3.1.15",
"query-string": "^6.13.1", "query-string": "^6.13.6",
"react-is": "^16.13.0", "react-is": "^16.13.0"
"use-subscription": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"@types/react-is": "^16.7.1", "@types/react-is": "^16.7.1",
"@types/use-subscription": "^1.0.0",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-test-renderer": "~16.13.1", "react-test-renderer": "~16.13.1",

View File

@@ -8,9 +8,11 @@ import {
NavigationAction, NavigationAction,
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import EnsureSingleNavigator from './EnsureSingleNavigator'; import EnsureSingleNavigator from './EnsureSingleNavigator';
import UnhandledActionContext from './UnhandledActionContext';
import NavigationBuilderContext from './NavigationBuilderContext'; import NavigationBuilderContext from './NavigationBuilderContext';
import NavigationStateContext from './NavigationStateContext'; import NavigationStateContext from './NavigationStateContext';
import UnhandledActionContext from './UnhandledActionContext'; import NavigationRouteContext from './NavigationRouteContext';
import NavigationContext from './NavigationContext';
import { ScheduleUpdateContext } from './useScheduleUpdate'; import { ScheduleUpdateContext } from './useScheduleUpdate';
import useChildListeners from './useChildListeners'; import useChildListeners from './useChildListeners';
import useKeyedChildListeners from './useKeyedChildListeners'; import useKeyedChildListeners from './useKeyedChildListeners';
@@ -94,6 +96,7 @@ const BaseNavigationContainer = React.forwardRef(
{ {
initialState, initialState,
onStateChange, onStateChange,
onUnhandledAction,
independent, independent,
children, children,
}: NavigationContainerProps, }: NavigationContainerProps,
@@ -159,9 +162,20 @@ const BaseNavigationContainer = React.forwardRef(
const resetRoot = React.useCallback( const resetRoot = React.useCallback(
(state?: PartialState<NavigationState> | NavigationState) => { (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(() => { const getRootState = React.useCallback(() => {
@@ -342,57 +356,75 @@ const BaseNavigationContainer = React.forwardRef(
isFirstMountRef.current = false; isFirstMountRef.current = false;
}, [getRootState, emitter, state]); }, [getRootState, emitter, state]);
const onUnhandledAction = React.useCallback((action: NavigationAction) => { const defaultOnUnhandledAction = React.useCallback(
if (process.env.NODE_ENV === 'production') { (action: NavigationAction) => {
return; if (process.env.NODE_ENV === 'production') {
} return;
}
const payload: Record<string, any> | undefined = action.payload; const payload: Record<string, any> | undefined = action.payload;
let message = `The action '${action.type}'${ let message = `The action '${action.type}'${
payload ? ` with payload ${JSON.stringify(action.payload)}` : '' payload ? ` with payload ${JSON.stringify(action.payload)}` : ''
} was not handled by any navigator.`; } was not handled by any navigator.`;
switch (action.type) { switch (action.type) {
case 'NAVIGATE': case 'NAVIGATE':
case 'PUSH': case 'PUSH':
case 'REPLACE': case 'REPLACE':
case 'JUMP_TO': case 'JUMP_TO':
if (payload?.name) { if (payload?.name) {
message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`; message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`;
} else { } else {
message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`; message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`;
} }
break; break;
case 'GO_BACK': case 'GO_BACK':
case 'POP': case 'POP':
case 'POP_TO_TOP': case 'POP_TO_TOP':
message += `\n\nIs there any screen to go back to?`; message += `\n\nIs there any screen to go back to?`;
break; break;
case 'OPEN_DRAWER': case 'OPEN_DRAWER':
case 'CLOSE_DRAWER': case 'CLOSE_DRAWER':
case 'TOGGLE_DRAWER': case 'TOGGLE_DRAWER':
message += `\n\nIs your screen inside a Drawer navigator?`; message += `\n\nIs your screen inside a Drawer navigator?`;
break; break;
} }
message += `\n\nThis is a development-only warning and won't be shown in production.`; message += `\n\nThis is a development-only warning and won't be shown in production.`;
console.error(message); console.error(message);
}, []); },
[]
);
return ( let element = (
<ScheduleUpdateContext.Provider value={scheduleContext}> <ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationBuilderContext.Provider value={builderContext}> <NavigationBuilderContext.Provider value={builderContext}>
<NavigationStateContext.Provider value={context}> <NavigationStateContext.Provider value={context}>
<UnhandledActionContext.Provider value={onUnhandledAction}> <UnhandledActionContext.Provider
value={onUnhandledAction ?? defaultOnUnhandledAction}
>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator> <EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</UnhandledActionContext.Provider> </UnhandledActionContext.Provider>
</NavigationStateContext.Provider> </NavigationStateContext.Provider>
</NavigationBuilderContext.Provider> </NavigationBuilderContext.Provider>
</ScheduleUpdateContext.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>; screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>; navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
route: Route<string> & { route: Route<string>;
state?: NavigationState | PartialState<NavigationState>; routeState: NavigationState | PartialState<NavigationState> | undefined;
};
getState: () => State; getState: () => State;
setState: (state: State) => void; setState: (state: State) => void;
options: object; options: object;
@@ -38,6 +37,7 @@ export default function SceneView<
screen, screen,
route, route,
navigation, navigation,
routeState,
getState, getState,
setState, setState,
options, options,
@@ -86,7 +86,7 @@ export default function SceneView<
const context = React.useMemo( const context = React.useMemo(
() => ({ () => ({
state: route.state, state: routeState,
getState: getCurrentState, getState: getCurrentState,
setState: setCurrentState, setState: setCurrentState,
getKey, getKey,
@@ -95,7 +95,7 @@ export default function SceneView<
addOptionsGetter, addOptionsGetter,
}), }),
[ [
route.state, routeState,
getCurrentState, getCurrentState,
setCurrentState, setCurrentState,
getKey, getKey,

View File

@@ -721,3 +721,103 @@ it("throws if the ref hasn't finished initializing", () => {
render(element); render(element);
}); });
it('invokes the unhandled action listener with the unhandled action', () => {
const ref = React.createRef<NavigationContainerRef>();
const fn = jest.fn();
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const TestScreen = () => <></>;
render(
<BaseNavigationContainer ref={ref} onUnhandledAction={fn}>
<TestNavigator>
<Screen name="foo" component={TestScreen} />
<Screen name="bar" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
act(() => ref.current!.navigate('bar'));
act(() => ref.current!.navigate('baz'));
expect(fn).toHaveBeenCalledWith({
payload: {
name: 'baz',
},
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

@@ -132,6 +132,17 @@ export default function MockRouter(options: DefaultRouterOptions) {
}; };
} }
case 'GO_BACK': {
if (state.index === 0) {
return null;
}
return {
...state,
index: state.index - 1,
};
}
default: default:
return BaseRouter.getStateForAction(state, action); return BaseRouter.getStateForAction(state, action);
} }

View File

@@ -43,11 +43,58 @@ it('gets navigate action from state', () => {
}, },
type: 'NAVIGATE', type: 'NAVIGATE',
}); });
});
expect( it('gets navigate action from state for top-level screen', () => {
getActionFromState({ const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: { answer: 42 },
},
type: 'NAVIGATE',
});
});
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: [ routes: [
{ {
key: 'test',
name: 'foo', name: 'foo',
state: { state: {
routes: [ routes: [
@@ -55,11 +102,7 @@ it('gets navigate action from state', () => {
name: 'bar', name: 'bar',
state: { state: {
routes: [ routes: [
{ { key: 'test', name: 'qux', params: { author: 'jane' } },
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
], ],
}, },
}, },
@@ -67,8 +110,459 @@ it('gets navigate action from state', () => {
}, },
}, },
], ],
}) },
).toEqual({ type: 'RESET',
});
});
it('gets reset action from state for top-level screen with 2 screens', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
},
type: 'RESET',
});
});
it('gets reset action from state for top-level screen with more than 2 screens with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
{ name: 'baz' },
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
{ name: 'baz' },
],
},
type: 'RESET',
});
});
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,
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
{ name: 'baz' },
],
};
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 with 2 screens', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens with lower index', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
index: 0,
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
screen: 'qux',
initial: true,
params: {
author: 'jane',
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with more than 2 screens', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
params: { answer: 42 },
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
params: {
answer: 42,
params: {
author: 'jane',
},
screen: 'qux',
initial: true,
},
screen: 'bar',
initial: true,
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state for top-level screen with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
],
};
const config = {
screens: {
initialRouteName: 'bar',
foo: {
path: 'some-path/:answer',
parse: {
answer: Number,
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: { answer: 42 },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens including initial route and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: { payload: {
name: 'foo', name: 'foo',
params: { params: {
@@ -84,7 +578,332 @@ it('gets navigate action from state', () => {
}); });
}); });
it('gets reset action from state', () => { it('gets navigate action from state with 2 screens without initial route and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'quz',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens including route with key on initial route and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
key: 'test',
name: 'qux',
params: { author: 'jane' },
},
{ 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: [
{
key: 'test',
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
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: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
],
},
},
],
};
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',
},
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with more than 2 screens with lower index', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
index: 1,
routes: [
{ name: 'quu' },
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'quu',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
screen: 'qux',
initial: false,
params: {
author: 'jane',
},
},
},
},
type: 'NAVIGATE',
});
});
it("doesn't return action if no routes are provided'", () => {
expect(getActionFromState({ routes: [] })).toBe(undefined);
});
it('gets undefined action from state', () => {
const state = { const state = {
routes: [ 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', () => { it('throws if wildcard is specified with legacy config', () => {
const path = '/bar/42/baz/test'; const path = '/bar/42/baz/test';
const config = { const config = {

View File

@@ -735,6 +735,20 @@ it('navigates to nested child in a navigator', () => {
expect(element).toMatchInlineSnapshot( expect(element).toMatchInlineSnapshot(
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"` `"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
); );
act(() => navigation.current?.navigate('bar', { screen: 'bar-b' }));
act(() => navigation.current?.goBack());
expect(element).toMatchInlineSnapshot(
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
);
act(() => navigation.current?.navigate('bar', { screen: 'bar-b' }));
expect(element).toMatchInlineSnapshot(
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42,\\"whoa\\":\\"test\\"}]"`
);
}); });
it('navigates to nested child in a navigator with initial: false', () => { it('navigates to nested child in a navigator with initial: false', () => {
@@ -1093,6 +1107,194 @@ it('navigates to nested child in a navigator with initial: false', () => {
}); });
}); });
it('resets state of a nested child in a navigator', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestComponent = ({ route }: any): any =>
`[${route.name}, ${JSON.stringify(route.params)}]`;
const onStateChange = jest.fn();
const navigation = React.createRef<NavigationContainerRef>();
const first = render(
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen name="bar-a" component={TestComponent} />
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 0,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '1',
routeNames: ['foo-a', 'foo-b'],
routes: [
{
key: 'foo-a',
name: 'foo-a',
},
{
key: 'foo-b',
name: 'foo-b',
},
],
stale: false,
type: 'test',
},
},
{ key: 'bar', name: 'bar' },
],
stale: false,
type: 'test',
});
act(() =>
navigation.current?.navigate('bar', {
state: {
routes: [{ name: 'bar-a' }, { name: 'bar-b' }],
},
})
);
expect(first).toMatchInlineSnapshot(`"[bar-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 1,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{
key: 'bar',
name: 'bar',
params: {
state: {
routes: [{ name: 'bar-a' }, { name: 'bar-b' }],
},
},
state: {
index: 0,
key: '4',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a-2',
name: 'bar-a',
},
{
key: 'bar-b-3',
name: 'bar-b',
params: { some: 'stuff' },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
act(() =>
navigation.current?.navigate('bar', {
state: {
index: 2,
routes: [
{ key: '37', name: 'bar-b' },
{ name: 'bar-b' },
{ name: 'bar-a', params: { test: 18 } },
],
},
})
);
expect(first).toMatchInlineSnapshot(`"[bar-a, {\\"test\\":18}]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 1,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{
key: 'bar',
name: 'bar',
params: {
state: {
index: 2,
routes: [
{ key: '37', name: 'bar-b' },
{ name: 'bar-b' },
{ name: 'bar-a', params: { test: 18 } },
],
},
},
state: {
index: 2,
key: '7',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: '37',
name: 'bar-b',
params: { some: 'stuff' },
},
{
key: 'bar-b-5',
name: 'bar-b',
params: { some: 'stuff' },
},
{
key: 'bar-a-6',
name: 'bar-a',
params: { test: 18 },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
});
it('gives access to internal state', () => { it('gives access to internal state', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -1260,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', () => { it('throws when a React Element is not the direct children', () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props); useNavigationBuilder(MockRouter, props);

View File

@@ -22,6 +22,7 @@ it('sets options with options prop as an object', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -67,6 +68,7 @@ it('sets options with options prop as a fuction', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -113,6 +115,7 @@ it('sets options with screenOptions prop as an object', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -173,6 +176,7 @@ it('sets options with screenOptions prop as a fuction', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -245,6 +249,7 @@ it('sets initial options with setOptions', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ {
title?: string; title?: string;
color?: string; color?: string;
@@ -302,6 +307,7 @@ it('updates options with setOptions', () => {
NavigationState, NavigationState,
any, any,
any, any,
any,
any any
>(MockRouter, props); >(MockRouter, props);
const { render, options } = descriptors[state.routes[state.index].key]; const { render, options } = descriptors[state.routes[state.index].key];
@@ -378,6 +384,7 @@ it("returns correct value for canGoBack when it's not overridden", () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -441,6 +448,7 @@ it(`returns false for canGoBack when current router doesn't handle GO_BACK`, ()
NavigationState, NavigationState,
any, any,
any, any,
any,
any any
>(TestRouter, props); >(TestRouter, props);
@@ -491,6 +499,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(ParentRouter, props); >(ParentRouter, props);
@@ -501,6 +510,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -558,6 +568,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(OverrodeRouter, props); >(OverrodeRouter, props);
@@ -568,6 +579,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);

View File

@@ -1178,3 +1178,149 @@ it("prevents removing by multiple screens with 'beforeRemove' event", () => {
type: 'stack', 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,56 +1,144 @@
import type { PartialState, NavigationState } from '@react-navigation/routers'; import type {
Route,
PartialRoute,
ParamListBase,
NavigationState,
PartialState,
CommonActions,
} from '@react-navigation/routers';
import type { PathConfig, PathConfigMap, NavigatorScreenParams } from './types';
type NavigateParams = { type ConfigItem = {
screen?: string; initialRouteName?: string;
params?: NavigateParams; screens?: Record<string, ConfigItem>;
initial?: boolean;
}; };
type NavigateAction = { type Options = { initialRouteName?: string; screens: PathConfigMap };
type NavigateAction<State extends NavigationState> = {
type: 'NAVIGATE'; type: 'NAVIGATE';
payload: { name: string; params: NavigateParams }; payload: {
name: string;
params?: NavigatorScreenParams<State>;
};
}; };
export default function getActionFromState( export default function getActionFromState(
state: PartialState<NavigationState> state: PartialState<NavigationState>,
): NavigateAction | undefined { options?: Options
if (state.routes.length === 0) { ): NavigateAction<NavigationState> | CommonActions.Action | undefined {
// Create a normalized configs object which will be easier to use
const normalizedConfig = options ? createNormalizedConfigItem(options) : {};
const routes =
state.index != null ? state.routes.slice(0, state.index + 1) : state.routes;
if (routes.length === 0) {
return undefined; return undefined;
} }
// Try to construct payload for a `NAVIGATE` action from the state if (
// This lets us preserve the navigation state and not lose it !(
let route = state.routes[state.routes.length - 1]; (routes.length === 1 && routes[0].key === undefined) ||
(routes.length === 2 &&
routes[0].key === undefined &&
routes[0].name === normalizedConfig?.initialRouteName &&
routes[1].key === undefined)
)
) {
return {
type: 'RESET',
payload: state,
};
}
let payload: { name: string; params: NavigateParams } = { const route = state.routes[state.index ?? state.routes.length - 1];
name: route.name,
params: { ...route.params },
};
let current = route.state; let current: PartialState<NavigationState> | undefined = route?.state;
let params = payload.params; let config: ConfigItem | undefined = normalizedConfig?.screens?.[route?.name];
let params = { ...route.params } as NavigatorScreenParams<
ParamListBase,
NavigationState
>;
let payload = route ? { name: route.name, params } : undefined;
while (current) { while (current) {
if (current.routes.length === 0) { if (current.routes.length === 0) {
return undefined; return undefined;
} }
route = current.routes[current.routes.length - 1]; const routes =
params.initial = current.routes.length === 1; current.index != null
params.screen = route.name; ? current.routes.slice(0, current.index + 1)
: current.routes;
const route: Route<string> | PartialRoute<Route<string>> =
routes[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;
} else if (
routes.length === 2 &&
routes[0].key === undefined &&
routes[0].name === config?.initialRouteName &&
routes[1].key === undefined
) {
params.initial = false;
params.screen = route.name;
} else {
params.state = current;
break;
}
if (route.state) { if (route.state) {
params.params = { ...route.params }; params.params = { ...route.params };
params = params.params; params = params.params as NavigatorScreenParams<
ParamListBase,
NavigationState
>;
} else { } else {
params.params = route.params; params.params = route.params;
} }
current = route.state; current = route.state;
config = config?.screens?.[route.name];
} }
if (!payload) {
return;
}
// Try to construct payload for a `NAVIGATE` action from the state
// This lets us preserve the navigation state and not lose it
return { return {
type: 'NAVIGATE', type: 'NAVIGATE',
payload, payload,
}; };
} }
const createNormalizedConfigItem = (config: PathConfig | string) =>
typeof config === 'object' && config != null
? {
initialRouteName: config.initialRouteName,
screens:
config.screens != null
? createNormalizedConfigs(config.screens)
: undefined,
}
: {};
const createNormalizedConfigs = (options: PathConfigMap) =>
Object.entries(options).reduce<Record<string, ConfigItem>>((acc, [k, v]) => {
acc[k] = createNormalizedConfigItem(v);
return acc;
}, {});

View File

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

View File

@@ -1,4 +1,4 @@
import queryString from 'query-string'; import * as queryString from 'query-string';
import type { import type {
NavigationState, NavigationState,
PartialState, PartialState,
@@ -35,7 +35,7 @@ const getActiveRoute = (state: State): { name: string; params?: object } => {
/** /**
* Utility to serialize a navigation state object to a path string. * Utility to serialize a navigation state object to a path string.
* *
* Example: * @example
* ```js * ```js
* getPathFromState( * getPathFromState(
* { * {
@@ -239,6 +239,10 @@ export default function getPathFromState(
// Object.fromEntries is not available in older iOS versions // Object.fromEntries is not available in older iOS versions
const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) => const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
entries.reduce((acc, [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; acc[k] = v;
return acc; return acc;
}, {} as Record<K, V>); }, {} as Record<K, V>);

View File

@@ -1,5 +1,5 @@
import escape from 'escape-string-regexp'; import escape from 'escape-string-regexp';
import queryString from 'query-string'; import * as queryString from 'query-string';
import type { import type {
NavigationState, NavigationState,
PartialState, PartialState,
@@ -33,11 +33,16 @@ type ResultState = PartialState<NavigationState> & {
state?: ResultState; 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. * 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. * This is useful for deep linking when we need to handle the incoming URL.
* *
* Example: * @example
* ```js * ```js
* getStateFromPath( * getStateFromPath(
* '/chat/jane/42', * '/chat/jane/42',
@@ -119,6 +124,12 @@ export default function getStateFromPath(
// - the most exhaustive ones are always at the beginning // - the most exhaustive ones are always at the beginning
// - patterns with wildcard are always at the end // - 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 // If one of the patterns starts with the other, it's more exhaustive
// So move it up // So move it up
if (a.pattern.startsWith(b.pattern)) { if (a.pattern.startsWith(b.pattern)) {
@@ -155,6 +166,35 @@ export default function getStateFromPath(
return bWildcardIndex - aWildcardIndex; 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 === '/') { if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works // 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 // 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 (legacy === false) {
// If we're not in legacy mode,, we match the whole path against the regex instead of segments // 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 // 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, remaining,
configs.map((c) => ({ configs.map((c) => ({
...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 // This will always be empty if full path matched
current = createNestedStateObject(routes, initialRoutes);
remaining = remainingPath; remaining = remainingPath;
current = createNestedStateObject(
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
result = current; result = current;
} }
} else { } else {
// In legacy mode, we divide the path into segments and match piece by piece // 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 // This preserves the legacy behaviour, but we should remove it in next major
while (remaining) { while (remaining) {
let { routeNames, allParams, remainingPath } = matchAgainstConfigs( let { routes, remainingPath } = matchAgainstConfigs(remaining, configs);
remaining,
configs
);
remaining = remainingPath; remaining = remainingPath;
// If we hadn't matched any segments earlier, use the path as route name // If we hadn't matched any segments earlier, use the path as route name
if (routeNames === undefined) { if (routes === undefined) {
const segments = remaining.split('/'); const segments = remaining.split('/');
routeNames = [decodeURIComponent(segments[0])]; routes = [{ name: decodeURIComponent(segments[0]) }];
segments.shift(); segments.shift();
remaining = segments.join('/'); remaining = segments.join('/');
} }
const state = createNestedStateObject( const state = createNestedStateObject(routes, initialRoutes);
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
if (current) { if (current) {
// The state should be nested inside the deepest route we parsed before // The state should be nested inside the deepest route we parsed before
@@ -274,8 +305,7 @@ const joinPaths = (...paths: string[]): string =>
.join('/'); .join('/');
const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => { const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
let routeNames: string[] | undefined; let routes: ParsedRoute[] | undefined;
let allParams: Record<string, any> | undefined;
let remainingPath = remaining; let remainingPath = remaining;
// Go through all configs, and see if the next path segment matches our regex // 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 our regex matches, we need to extract params from the path
if (match) { 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 routes = config.routeNames.map((name) => {
.split('/') const config = configs.find((c) => c.screen === name);
.filter((p) => p.startsWith(':')); const params = config?.path
?.split('/')
.filter((p) => p.startsWith(':'))
.reduce<Record<string, any>>((acc, p) => {
const value = matchedParams[p];
if (paramPatterns.length) { if (value) {
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => { const key = p.replace(/^:/, '').replace(/\?$/, '');
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result 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], ''); remainingPath = remainingPath.replace(match[1], '');
@@ -310,7 +359,7 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
} }
} }
return { routeNames, allParams, remainingPath }; return { routes, remainingPath };
}; };
const createNormalizedConfigs = ( const createNormalizedConfigs = (
@@ -473,57 +522,48 @@ const findInitialRoute = (
// it is the end of state and if there is initialRoute for this level // it is the end of state and if there is initialRoute for this level
const createStateObject = ( const createStateObject = (
initialRoute: string | undefined, initialRoute: string | undefined,
routeName: string, route: ParsedRoute,
params: Record<string, any> | undefined,
isEmpty: boolean isEmpty: boolean
): InitialState => { ): InitialState => {
if (isEmpty) { if (isEmpty) {
if (initialRoute) { if (initialRoute) {
return { return {
index: 1, index: 1,
routes: [{ name: initialRoute }, { name: routeName as string, params }], routes: [{ name: initialRoute }, route],
}; };
} else { } else {
return { return {
routes: [{ name: routeName as string, params }], routes: [route],
}; };
} }
} else { } else {
if (initialRoute) { if (initialRoute) {
return { return {
index: 1, index: 1,
routes: [ routes: [{ name: initialRoute }, { ...route, state: { routes: [] } }],
{ name: initialRoute },
{ name: routeName as string, params, state: { routes: [] } },
],
}; };
} else { } else {
return { return {
routes: [{ name: routeName as string, params, state: { routes: [] } }], routes: [{ ...route, state: { routes: [] } }],
}; };
} }
} }
}; };
const createNestedStateObject = ( const createNestedStateObject = (
routes: { name: string; params?: object }[], routes: ParsedRoute[],
initialRoutes: InitialRouteConfig[] initialRoutes: InitialRouteConfig[]
) => { ) => {
let state: InitialState; let state: InitialState;
let route = routes.shift() as { name: string; params?: object }; let route = routes.shift() as ParsedRoute;
let initialRoute = findInitialRoute(route.name, initialRoutes); let initialRoute = findInitialRoute(route.name, initialRoutes);
state = createStateObject( state = createStateObject(initialRoute, route, routes.length === 0);
initialRoute,
route.name,
route.params,
routes.length === 0
);
if (routes.length > 0) { if (routes.length > 0) {
let nestedState = state; let nestedState = state;
while ((route = routes.shift() as { name: string; params?: object })) { while ((route = routes.shift() as ParsedRoute)) {
initialRoute = findInitialRoute(route.name, initialRoutes); initialRoute = findInitialRoute(route.name, initialRoutes);
const nestedStateIndex = const nestedStateIndex =
@@ -531,8 +571,7 @@ const createNestedStateObject = (
nestedState.routes[nestedStateIndex].state = createStateObject( nestedState.routes[nestedStateIndex].state = createStateObject(
initialRoute, initialRoute,
route.name, route,
route.params,
routes.length === 0 routes.length === 0
); );
@@ -546,46 +585,6 @@ const createNestedStateObject = (
return state; 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) => { const findFocusedRoute = (state: InitialState) => {
let current: InitialState | undefined = state; let current: InitialState | undefined = state;

View File

@@ -10,8 +10,9 @@ import type {
} from '@react-navigation/routers'; } from '@react-navigation/routers';
export type DefaultNavigatorOptions< export type DefaultNavigatorOptions<
ScreenOptions extends {} ScreenOptions extends {},
> = DefaultRouterOptions & { ParamList extends ParamListBase = ParamListBase
> = DefaultRouterOptions<Extract<keyof ParamList, string>> & {
/** /**
* Children React Elements to extract the route configuration from. * Children React Elements to extract the route configuration from.
* Only `Screen` components are supported as children. * Only `Screen` components are supported as children.
@@ -23,7 +24,7 @@ export type DefaultNavigatorOptions<
screenOptions?: screenOptions?:
| ScreenOptions | ScreenOptions
| ((props: { | ((props: {
route: RouteProp<ParamListBase, string>; route: RouteProp<ParamList, keyof ParamList>;
navigation: any; navigation: any;
}) => ScreenOptions); }) => ScreenOptions);
}; };
@@ -237,6 +238,10 @@ export type NavigationContainerProps = {
* Callback which is called with the latest navigation state when it changes. * Callback which is called with the latest navigation state when it changes.
*/ */
onStateChange?: (state: NavigationState | undefined) => void; onStateChange?: (state: NavigationState | undefined) => void;
/**
* Callback which is called when an action is not handled.
*/
onUnhandledAction?: (action: NavigationAction) => void;
/** /**
* Whether this navigation container should be independent of parent containers. * Whether this navigation container should be independent of parent containers.
* If this is not set to `true`, this container cannot be nested inside another container. * If this is not set to `true`, this container cannot be nested inside another container.
@@ -252,7 +257,7 @@ export type NavigationContainerProps = {
export type NavigationProp< export type NavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string, RouteName extends keyof ParamList = string,
State extends NavigationState = NavigationState, State extends NavigationState = NavigationState<ParamList>,
ScreenOptions extends {} = {}, ScreenOptions extends {} = {},
EventMap extends EventMapBase = {} EventMap extends EventMapBase = {}
> = NavigationHelpersCommon<ParamList, State> & { > = NavigationHelpersCommon<ParamList, State> & {
@@ -277,20 +282,7 @@ export type NavigationProp<
export type RouteProp< export type RouteProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList RouteName extends keyof ParamList
> = Omit<Route<Extract<RouteName, string>>, 'params'> & > = Route<Extract<RouteName, string>, ParamList[RouteName]>;
(undefined extends ParamList[RouteName]
? Readonly<{
/**
* Params for this route
*/
params?: Readonly<ParamList[RouteName]>;
}>
: Readonly<{
/**
* Params for this route
*/
params: Readonly<ParamList[RouteName]>;
}>);
export type CompositeNavigationProp< export type CompositeNavigationProp<
A extends NavigationProp<ParamListBase, string, any, any>, A extends NavigationProp<ParamListBase, string, any, any>,
@@ -498,14 +490,11 @@ export type TypedNavigator<
* Navigator component which manages the child screens. * Navigator component which manages the child screens.
*/ */
Navigator: React.ComponentType< Navigator: React.ComponentType<
Omit<React.ComponentProps<Navigator>, keyof DefaultNavigatorOptions<any>> & Omit<
Omit<DefaultNavigatorOptions<ScreenOptions>, 'initialRouteName'> & { React.ComponentProps<Navigator>,
/** keyof DefaultNavigatorOptions<any, any>
* Name of the route to focus by on initial render. > &
* If not specified, usually the first route is used. DefaultNavigatorOptions<ScreenOptions, ParamList>
*/
initialRouteName?: keyof ParamList;
}
>; >;
/** /**
* Component used for specifying route configuration. * Component used for specifying route configuration.
@@ -515,6 +504,32 @@ export type TypedNavigator<
) => null; ) => null;
}; };
export type NavigatorScreenParams<
ParamList,
State extends NavigationState = NavigationState
> =
| {
screen?: never;
params?: never;
initial?: never;
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 = { export type PathConfig = {
path?: string; path?: string;
exact?: boolean; exact?: boolean;

View File

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

View File

@@ -1,32 +1,42 @@
import * as React from 'react'; import * as React from 'react';
import { useSubscription } from 'use-subscription'; import { useState } from 'react';
import useNavigation from './useNavigation'; import useNavigation from './useNavigation';
/** /**
* Hook to get the current focus state of the screen. Returns a `true` if screen is focused, otherwise `false`. * 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. * 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 { export default function useIsFocused(): boolean {
const navigation = useNavigation(); const navigation = useNavigation();
// eslint-disable-next-line react-hooks/exhaustive-deps const [isFocused, setIsFocused] = useState(navigation.isFocused);
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
const subscribe = React.useCallback(
(callback: () => void) => {
const unsubscribeFocus = navigation.addListener('focus', callback);
const unsubscribeBlur = navigation.addListener('blur', callback); const valueToReturn = navigation.isFocused();
return () => { if (isFocused !== valueToReturn) {
unsubscribeFocus(); // If the value has changed since the last render, we need to update it.
unsubscribeBlur(); // 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.
[navigation] // This is the same logic as in https://github.com/facebook/react/tree/master/packages/use-subscription
); setIsFocused(valueToReturn);
}
return useSubscription({ React.useEffect(() => {
getCurrentValue, const unsubscribeFocus = navigation.addListener('focus', () =>
subscribe, setIsFocused(true)
}); );
const unsubscribeBlur = navigation.addListener('blur', () =>
setIsFocused(false)
);
return () => {
unsubscribeFocus();
unsubscribeBlur();
};
}, [navigation]);
React.useDebugValue(valueToReturn);
return valueToReturn;
} }

View File

@@ -23,30 +23,27 @@ import useFocusEvents from './useFocusEvents';
import useOnRouteFocus from './useOnRouteFocus'; import useOnRouteFocus from './useOnRouteFocus';
import useChildListeners from './useChildListeners'; import useChildListeners from './useChildListeners';
import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter'; import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter';
import useKeyedChildListeners from './useKeyedChildListeners';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import isArrayEqual from './isArrayEqual';
import { import {
DefaultNavigatorOptions, DefaultNavigatorOptions,
RouteConfig, RouteConfig,
PrivateValueStore, PrivateValueStore,
EventMapBase, EventMapBase,
EventMapCore, EventMapCore,
NavigatorScreenParams,
} from './types'; } from './types';
import useKeyedChildListeners from './useKeyedChildListeners';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import isArrayEqual from './isArrayEqual';
// This is to make TypeScript compiler happy // This is to make TypeScript compiler happy
// eslint-disable-next-line babel/no-unused-expressions // eslint-disable-next-line babel/no-unused-expressions
PrivateValueStore; PrivateValueStore;
type NavigatorRoute = { type NavigatorRoute<State extends NavigationState> = {
key: string; key: string;
params?: { params?: NavigatorScreenParams<ParamListBase, State>;
screen?: string;
params?: object;
initial?: boolean;
};
}; };
/** /**
@@ -93,10 +90,17 @@ const getRouteConfigsFromChildren = <
} }
throw new Error( throw new Error(
`A navigator can only contain 'Screen' components as its direct children (found '${ `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 React.isValidElement(child)
child.type?.name ? child.type.name : String(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'.`
); );
}, []); }, []);
@@ -182,6 +186,7 @@ const getRouteConfigsFromChildren = <
export default function useNavigationBuilder< export default function useNavigationBuilder<
State extends NavigationState, State extends NavigationState,
RouterOptions extends DefaultRouterOptions, RouterOptions extends DefaultRouterOptions,
ActionHelpers extends Record<string, () => void>,
ScreenOptions extends {}, ScreenOptions extends {},
EventMap extends Record<string, any> EventMap extends Record<string, any>
>( >(
@@ -191,20 +196,15 @@ export default function useNavigationBuilder<
const navigatorKey = useRegisterNavigator(); const navigatorKey = useRegisterNavigator();
const route = React.useContext(NavigationRouteContext) as const route = React.useContext(NavigationRouteContext) as
| NavigatorRoute | NavigatorRoute<State>
| undefined; | undefined;
const previousNestedParamsRef = React.useRef(route?.params);
React.useEffect(() => {
previousNestedParamsRef.current = route?.params;
}, [route]);
const { children, ...rest } = options; const { children, ...rest } = options;
const { current: router } = React.useRef<Router<State, any>>( const { current: router } = React.useRef<Router<State, any>>(
createRouter({ createRouter({
...((rest as unknown) as RouterOptions), ...((rest as unknown) as RouterOptions),
...(route?.params && ...(route?.params &&
route.params.state == null &&
route.params.initial !== false && route.params.initial !== false &&
typeof route.params.screen === 'string' typeof route.params.screen === 'string'
? { initialRouteName: route.params.screen } ? { initialRouteName: route.params.screen }
@@ -239,7 +239,9 @@ export default function useNavigationBuilder<
(acc, curr) => { (acc, curr) => {
const { initialParams } = screens[curr]; const { initialParams } = screens[curr];
const initialParamsFromParams = const initialParamsFromParams =
route?.params?.initial !== false && route?.params?.screen === curr route?.params?.state == null &&
route?.params?.initial !== false &&
route?.params?.screen === curr
? route.params.params ? route.params.params
: undefined; : undefined;
@@ -287,7 +289,10 @@ export default function useNavigationBuilder<
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset) // We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
// Otherwise assume that the state was provided as initial state // Otherwise assume that the state was provided as initial state
// So we need to rehydrate it to make it usable // So we need to rehydrate it to make it usable
if (currentState === undefined || !isStateValid(currentState)) { if (
(currentState === undefined || !isStateValid(currentState)) &&
route?.params?.state == null
) {
return [ return [
router.getInitialState({ router.getInitialState({
routeNames, routeNames,
@@ -297,10 +302,13 @@ export default function useNavigationBuilder<
]; ];
} else { } else {
return [ return [
router.getRehydratedState(currentState as PartialState<State>, { router.getRehydratedState(
routeNames, route?.params?.state ?? (currentState as PartialState<State>),
routeParamList, {
}), routeNames,
routeParamList,
}
),
false, false,
]; ];
} }
@@ -331,21 +339,41 @@ export default function useNavigationBuilder<
}); });
} }
if ( const previousNestedParamsRef = React.useRef(route?.params);
typeof route?.params?.screen === 'string' &&
(route.params !== previousNestedParamsRef.current || React.useEffect(() => {
(route.params.initial === false && isFirstStateInitialization)) previousNestedParamsRef.current = route?.params;
) { }, [route?.params]);
// If the route was updated with new name and/or params, we should navigate there
if (route?.params) {
const previousParams = previousNestedParamsRef.current;
let action: CommonActions.Action | undefined;
if (
typeof route.params.state === 'object' &&
route.params.state != null &&
route.params.state !== previousParams?.state
) {
// If the route was updated with new state, we should reset to it
action = CommonActions.reset(route.params.state);
} else if (
typeof route.params.screen === 'string' &&
((route.params.initial === false && isFirstStateInitialization) ||
route.params !== previousParams)
) {
// FIXME: Since params are merged, `route.params.params` might contain params from an older route
// If the route was updated with new screen name and/or params, we should navigate there
action = CommonActions.navigate(route.params.screen, route.params.params);
}
// The update should be limited to current navigator only, so we call the router manually // The update should be limited to current navigator only, so we call the router manually
const updatedState = router.getStateForAction( const updatedState = action
nextState, ? router.getStateForAction(nextState, action, {
CommonActions.navigate(route.params.screen, route.params.params), routeNames,
{ routeParamList,
routeNames, })
routeParamList, : null;
}
);
nextState = nextState =
updatedState !== null updatedState !== null
@@ -484,7 +512,12 @@ export default function useNavigationBuilder<
setState, setState,
}); });
const navigation = useNavigationHelpers<State, NavigationAction, EventMap>({ const navigation = useNavigationHelpers<
State,
ActionHelpers,
NavigationAction,
EventMap
>({
onAction, onAction,
getState, getState,
emitter, emitter,

View File

@@ -31,6 +31,7 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
*/ */
export default function useNavigationHelpers< export default function useNavigationHelpers<
State extends NavigationState, State extends NavigationState,
ActionHelpers extends Record<string, () => void>,
Action extends NavigationAction, Action extends NavigationAction,
EventMap extends Record<string, any> EventMap extends Record<string, any>
>({ onAction, getState, emitter, router }: Options<State, Action>) { >({ onAction, getState, emitter, router }: Options<State, Action>) {
@@ -85,7 +86,8 @@ export default function useNavigationHelpers<
dangerouslyGetParent: () => parentNavigationHelpers as any, dangerouslyGetParent: () => parentNavigationHelpers as any,
dangerouslyGetState: getState, dangerouslyGetState: getState,
} as NavigationHelpers<ParamListBase, EventMap> & } as NavigationHelpers<ParamListBase, EventMap> &
(NavigationProp<ParamListBase, string, any, any, any> | undefined); (NavigationProp<ParamListBase, string, any, any, any> | undefined) &
ActionHelpers;
}, [ }, [
emitter.emit, emitter.emit,
getState, getState,

View File

@@ -90,18 +90,11 @@ export default function useOnAction({
onDispatchAction(action, state === result); onDispatchAction(action, state === result);
if (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( const isPrevented = shouldPreventRemove(
emitter, emitter,
beforeRemoveListeners, beforeRemoveListeners,
removedRoutes, state.routes,
result.routes,
action action
); );

View File

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

View File

@@ -3,6 +3,102 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.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
## [5.1.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.10...@react-navigation/devtools@5.1.11) (2020-11-03)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.9...@react-navigation/devtools@5.1.10) (2020-10-30)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.8...@react-navigation/devtools@5.1.9) (2020-10-28)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.7...@react-navigation/devtools@5.1.8) (2020-10-24)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.6...@react-navigation/devtools@5.1.7) (2020-10-07)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.5...@react-navigation/devtools@5.1.6) (2020-09-22) ## [5.1.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.5...@react-navigation/devtools@5.1.6) (2020-09-22)
**Note:** Version bump only for package @react-navigation/devtools **Note:** Version bump only for package @react-navigation/devtools

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/devtools", "name": "@react-navigation/devtools",
"description": "Developer tools for React Navigation", "description": "Developer tools for React Navigation",
"version": "5.1.6", "version": "5.1.18",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -36,14 +36,14 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.12.4", "@react-navigation/core": "^5.14.4",
"deep-equal": "^2.0.3" "deep-equal": "^2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/deep-equal": "^1.0.1", "@types/deep-equal": "^1.0.1",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"typescript": "^4.0.3" "typescript": "^4.0.3"

View File

@@ -3,6 +3,141 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.11.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
## [5.10.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.2...@react-navigation/drawer@5.10.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.1...@react-navigation/drawer@5.10.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.0...@react-navigation/drawer@5.10.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/drawer
# [5.10.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.3...@react-navigation/drawer@5.10.0) (2020-10-24)
### Features
* add optional screens per navigator ([#8805](https://github.com/react-navigation/react-navigation/issues/8805)) ([7196889](https://github.com/react-navigation/react-navigation/commit/7196889bf1218eb6a736d9475e33a909c2248c3b))
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.9.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.2...@react-navigation/drawer@5.9.3) (2020-10-07)
### Bug Fixes
* use route keys instead of index for lazy load ([c49dab3](https://github.com/react-navigation/react-navigation/commit/c49dab31b2c63a1735f0ed0a1936ecf7bbcd8b13))
## [5.9.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.1...@react-navigation/drawer@5.9.2) (2020-09-28) ## [5.9.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.1...@react-navigation/drawer@5.9.2) (2020-09-28)
**Note:** Version bump only for package @react-navigation/drawer **Note:** Version bump only for package @react-navigation/drawer

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/drawer", "name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess", "description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.9.2", "version": "5.11.4",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -41,15 +41,15 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2", "color": "^3.1.3",
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.5", "@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.20", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",

View File

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

View File

@@ -6,6 +6,8 @@ import {
DrawerNavigationState, DrawerNavigationState,
DrawerRouterOptions, DrawerRouterOptions,
DrawerRouter, DrawerRouter,
DrawerActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import DrawerView from '../views/DrawerView'; import DrawerView from '../views/DrawerView';
@@ -28,8 +30,9 @@ function DrawerNavigator({
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerRouterOptions, DrawerRouterOptions,
DrawerActionHelpers<ParamListBase>,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap DrawerNavigationEventMap
>(DrawerRouter, { >(DrawerRouter, {
@@ -51,7 +54,7 @@ function DrawerNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap, DrawerNavigationEventMap,
typeof DrawerNavigator typeof DrawerNavigator

View File

@@ -18,6 +18,8 @@ export type Scene = {
color?: string; color?: string;
}; };
export type Layout = { width: number; height: number };
export type DrawerNavigationConfig<T = DrawerContentOptions> = { export type DrawerNavigationConfig<T = DrawerContentOptions> = {
/** /**
* Position of the drawer on the screen. Defaults to `left`. * Position of the drawer on the screen. Defaults to `left`.
@@ -86,14 +88,103 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
* You can pass a custom background color for a drawer or a custom width here. * You can pass a custom background color for a drawer or a custom width here.
*/ */
drawerStyle?: StyleProp<ViewStyle>; drawerStyle?: StyleProp<ViewStyle>;
/**
* 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`.
*/
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 text for the screen.
*/ */
title?: string; 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 * Title string of a screen displayed in the drawer
* or a function that given { focused: boolean, color: string } returns a React.Node * or a function that given { focused: boolean, color: string } returns a React.Node
@@ -136,7 +227,7 @@ export type DrawerNavigationOptions = {
}; };
export type DrawerContentComponentProps<T = DrawerContentOptions> = T & { export type DrawerContentComponentProps<T = DrawerContentOptions> = T & {
state: DrawerNavigationState; state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
/** /**
@@ -181,6 +272,20 @@ export type DrawerContentOptions = {
style?: StyleProp<ViewStyle>; 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 = { export type DrawerNavigationEventMap = {
/** /**
* Event which fires when the drawer opens. * Event which fires when the drawer opens.
@@ -195,7 +300,8 @@ export type DrawerNavigationEventMap = {
export type DrawerNavigationHelpers = NavigationHelpers< export type DrawerNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
DrawerNavigationEventMap DrawerNavigationEventMap
>; > &
DrawerActionHelpers<ParamListBase>;
export type DrawerNavigationProp< export type DrawerNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -203,7 +309,7 @@ export type DrawerNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
DrawerNavigationState, DrawerNavigationState<ParamList>,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap DrawerNavigationEventMap
> & > &
@@ -220,7 +326,7 @@ export type DrawerScreenProps<
export type DrawerDescriptor = Descriptor< export type DrawerDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerNavigationOptions DrawerNavigationOptions
>; >;

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

View File

@@ -3,6 +3,7 @@ import {
CommonActions, CommonActions,
DrawerActions, DrawerActions,
DrawerNavigationState, DrawerNavigationState,
ParamListBase,
useLinkBuilder, useLinkBuilder,
} from '@react-navigation/native'; } from '@react-navigation/native';
import DrawerItem from './DrawerItem'; import DrawerItem from './DrawerItem';
@@ -13,7 +14,7 @@ import type {
} from '../types'; } from '../types';
type Props = Omit<DrawerContentOptions, 'contentContainerStyle' | 'style'> & { type Props = Omit<DrawerContentOptions, 'contentContainerStyle' | 'style'> & {
state: DrawerNavigationState; state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
}; };

View File

@@ -10,28 +10,34 @@ import {
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer } from 'react-native-screens';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
NavigationContext,
NavigationRouteContext,
DrawerNavigationState, DrawerNavigationState,
DrawerActions, DrawerActions,
useTheme, useTheme,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { GestureHandlerRootView } from './GestureHandler'; import { GestureHandlerRootView } from './GestureHandler';
import SafeAreaProviderCompat from './SafeAreaProviderCompat'; import SafeAreaProviderCompat from './SafeAreaProviderCompat';
import ResourceSavingScene from './ResourceSavingScene'; import ResourceSavingScene from './ResourceSavingScene';
import Header from './Header';
import DrawerContent from './DrawerContent'; import DrawerContent from './DrawerContent';
import Drawer from './Drawer'; import Drawer from './Drawer';
import DrawerOpenContext from '../utils/DrawerOpenContext'; import DrawerOpenContext from '../utils/DrawerOpenContext';
import DrawerPositionContext from '../utils/DrawerPositionContext'; import DrawerPositionContext from '../utils/DrawerPositionContext';
import useWindowDimensions from '../utils/useWindowDimensions'; import useWindowDimensions from '../utils/useWindowDimensions';
import getIsDrawerOpenFromState from '../utils/getIsDrawerOpenFromState';
import type { import type {
DrawerDescriptorMap, DrawerDescriptorMap,
DrawerNavigationConfig, DrawerNavigationConfig,
DrawerNavigationHelpers, DrawerNavigationHelpers,
DrawerContentComponentProps, DrawerContentComponentProps,
DrawerHeaderProps,
} from '../types'; } from '../types';
type Props = DrawerNavigationConfig & { type Props = DrawerNavigationConfig & {
state: DrawerNavigationState; state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
}; };
@@ -82,13 +88,14 @@ export default function DrawerView({
gestureHandlerProps, gestureHandlerProps,
minSwipeDistance, minSwipeDistance,
sceneContainerStyle, sceneContainerStyle,
detachInactiveScreens = true,
}: Props) { }: Props) {
const [loaded, setLoaded] = React.useState([state.index]); const [loaded, setLoaded] = React.useState([state.routes[state.index].key]);
const dimensions = useWindowDimensions(); const dimensions = useWindowDimensions();
const { colors } = useTheme(); const { colors } = useTheme();
const isDrawerOpen = state.history.some((it) => it.type === 'drawer'); const isDrawerOpen = getIsDrawerOpenFromState(state);
const handleDrawerOpen = React.useCallback(() => { const handleDrawerOpen = React.useCallback(() => {
navigation.dispatch({ navigation.dispatch({
@@ -129,8 +136,10 @@ export default function DrawerView({
return () => subscription?.remove(); return () => subscription?.remove();
}, [handleDrawerClose, isDrawerOpen, navigation, state.key]); }, [handleDrawerClose, isDrawerOpen, navigation, state.key]);
if (!loaded.includes(state.index)) { const focusedRouteKey = state.routes[state.index].key;
setLoaded([...loaded, state.index]);
if (!loaded.includes(focusedRouteKey)) {
setLoaded([...loaded, focusedRouteKey]);
} }
const renderNavigationView = ({ progress }: any) => { const renderNavigationView = ({ progress }: any) => {
@@ -149,7 +158,8 @@ export default function DrawerView({
const renderContent = () => { const renderContent = () => {
return ( return (
<ScreenContainer style={styles.content}> // @ts-ignore
<ScreenContainer enabled={detachInactiveScreens} style={styles.content}>
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
const descriptor = descriptors[route.key]; const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options; const { unmountOnBlur } = descriptor.options;
@@ -159,17 +169,33 @@ export default function DrawerView({
return null; return null;
} }
if (lazy && !loaded.includes(index) && !isFocused) { if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a screen if we've never navigated to it // Don't render a screen if we've never navigated to it
return null; return null;
} }
const {
header = (props: DrawerHeaderProps) => <Header {...props} />,
headerShown = false,
} = descriptor.options;
return ( return (
<ResourceSavingScene <ResourceSavingScene
key={route.key} key={route.key}
style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]} style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]}
isVisible={isFocused} isVisible={isFocused}
enabled={detachInactiveScreens}
> >
{headerShown ? (
<NavigationContext.Provider value={descriptor.navigation}>
<NavigationRouteContext.Provider value={route}>
{header({
layout: dimensions,
scene: { route, descriptor },
})}
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
) : null}
{descriptor.render()} {descriptor.render()}
</ResourceSavingScene> </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

@@ -1,10 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native'; import { Platform, StyleSheet, View } from 'react-native';
import { Screen, screensEnabled } from 'react-native-screens'; import {
Screen,
screensEnabled,
// @ts-ignore
shouldUseActivityState,
} from 'react-native-screens';
type Props = { type Props = {
isVisible: boolean; isVisible: boolean;
children: React.ReactNode; children: React.ReactNode;
enabled: boolean;
style?: any; style?: any;
}; };
@@ -16,8 +22,17 @@ export default class ResourceSavingScene extends React.Component<Props> {
if (screensEnabled?.() && Platform.OS !== 'web') { if (screensEnabled?.() && Platform.OS !== 'web') {
const { isVisible, ...rest } = this.props; const { isVisible, ...rest } = this.props;
// @ts-expect-error: stackPresentation is incorrectly marked as required if (shouldUseActivityState) {
return <Screen active={isVisible ? 1 : 0} {...rest} />; 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} />
);
}
} }
const { isVisible, children, style, ...rest } = this.props; const { isVisible, children, style, ...rest } = this.props;

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,107 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.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
## [5.3.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.2...@react-navigation/material-bottom-tabs@5.3.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.1...@react-navigation/material-bottom-tabs@5.3.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.0...@react-navigation/material-bottom-tabs@5.3.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.3.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.19...@react-navigation/material-bottom-tabs@5.3.0) (2020-10-24)
### Features
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* make react-native-vector-icons optional ([#8936](https://github.com/react-navigation/react-navigation/issues/8936)) ([90ebfc4](https://github.com/react-navigation/react-navigation/commit/90ebfc40b387b209031e6275aaa0be95192f7d04)), closes [/github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14](https://github.com//github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx/issues/L14) [#8821](https://github.com/react-navigation/react-navigation/issues/8821)
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.2.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.18...@react-navigation/material-bottom-tabs@5.2.19) (2020-10-07)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.18](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.17...@react-navigation/material-bottom-tabs@5.2.18) (2020-09-28) ## [5.2.18](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.17...@react-navigation/material-bottom-tabs@5.2.18) (2020-09-28)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-bottom-tabs", "name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper", "description": "Integration for bottom navigation component from react-native-paper",
"version": "5.2.18", "version": "5.3.10",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -42,10 +42,10 @@
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.5", "@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.20", "@types/react-native": "^0.63.30",
"@types/react-native-vector-icons": "^6.4.6", "@types/react-native-vector-icons": "^6.4.6",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",

View File

@@ -6,6 +6,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import MaterialBottomTabView from '../views/MaterialBottomTabView'; import MaterialBottomTabView from '../views/MaterialBottomTabView';
@@ -27,8 +29,9 @@ function MaterialBottomTabNavigator({
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
TabActionHelpers<ParamListBase>,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
@@ -49,7 +52,7 @@ function MaterialBottomTabNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap, MaterialBottomTabNavigationEventMap,
typeof MaterialBottomTabNavigator typeof MaterialBottomTabNavigator

View File

@@ -19,7 +19,8 @@ export type MaterialBottomTabNavigationEventMap = {
export type MaterialBottomTabNavigationHelpers = NavigationHelpers< export type MaterialBottomTabNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
>; > &
TabActionHelpers<ParamListBase>;
export type MaterialBottomTabNavigationProp< export type MaterialBottomTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -27,7 +28,7 @@ export type MaterialBottomTabNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
TabNavigationState, TabNavigationState<ParamList>,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
> & > &
@@ -84,7 +85,7 @@ export type MaterialBottomTabNavigationOptions = {
export type MaterialBottomTabDescriptor = Descriptor< export type MaterialBottomTabDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialBottomTabNavigationOptions MaterialBottomTabNavigationOptions
>; >;

View File

@@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper'; import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
Route, Route,
@@ -10,6 +9,7 @@ import {
useTheme, useTheme,
useLinkBuilder, useLinkBuilder,
Link, Link,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import type { import type {
@@ -19,13 +19,55 @@ import type {
} from '../types'; } from '../types';
type Props = MaterialBottomTabNavigationConfig & { type Props = MaterialBottomTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: MaterialBottomTabNavigationHelpers; navigation: MaterialBottomTabNavigationHelpers;
descriptors: MaterialBottomTabDescriptorMap; descriptors: MaterialBottomTabDescriptorMap;
}; };
type Scene = { route: { key: string } }; 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;
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;
// 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;
}
return null;
};
}
}
function MaterialBottomTabViewInner({ function MaterialBottomTabViewInner({
state, state,
navigation, navigation,

View File

@@ -3,6 +3,106 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.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
## [5.3.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.2...@react-navigation/material-top-tabs@5.3.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.1...@react-navigation/material-top-tabs@5.3.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.0...@react-navigation/material-top-tabs@5.3.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.3.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.19...@react-navigation/material-top-tabs@5.3.0) (2020-10-24)
### Features
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.2.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.18...@react-navigation/material-top-tabs@5.2.19) (2020-10-07)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.18](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.17...@react-navigation/material-top-tabs@5.2.18) (2020-09-28) ## [5.2.18](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.17...@react-navigation/material-top-tabs@5.2.18) (2020-09-28)
**Note:** Version bump only for package @react-navigation/material-top-tabs **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-top-tabs", "name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view", "description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.2.18", "version": "5.3.10",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -41,20 +41,20 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2" "color": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.5", "@react-navigation/native": "^5.8.10",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.20", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",
"react-native-gesture-handler": "~1.7.0", "react-native-gesture-handler": "~1.7.0",
"react-native-reanimated": "~1.13.0", "react-native-reanimated": "~1.13.0",
"react-native-tab-view": "^2.15.1", "react-native-tab-view": "^2.15.2",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -6,6 +6,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import MaterialTopTabView from '../views/MaterialTopTabView'; import MaterialTopTabView from '../views/MaterialTopTabView';
import type { import type {
@@ -26,8 +28,9 @@ function MaterialTopTabNavigator({
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
TabActionHelpers<ParamListBase>,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
@@ -48,7 +51,7 @@ function MaterialTopTabNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap, MaterialTopTabNavigationEventMap,
typeof MaterialTopTabNavigator typeof MaterialTopTabNavigator

View File

@@ -37,7 +37,8 @@ export type MaterialTopTabNavigationEventMap = {
export type MaterialTopTabNavigationHelpers = NavigationHelpers< export type MaterialTopTabNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
>; > &
TabActionHelpers<ParamListBase>;
export type MaterialTopTabNavigationProp< export type MaterialTopTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -45,7 +46,7 @@ export type MaterialTopTabNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
TabNavigationState, TabNavigationState<ParamList>,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
> & > &
@@ -94,7 +95,7 @@ export type MaterialTopTabNavigationOptions = {
export type MaterialTopTabDescriptor = Descriptor< export type MaterialTopTabDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialTopTabNavigationOptions MaterialTopTabNavigationOptions
>; >;
@@ -192,7 +193,7 @@ export type MaterialTopTabBarOptions = Partial<
export type MaterialTopTabBarProps = MaterialTopTabBarOptions & export type MaterialTopTabBarProps = MaterialTopTabBarOptions &
SceneRendererProps & { SceneRendererProps & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: NavigationHelpers< navigation: NavigationHelpers<
ParamListBase, ParamListBase,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap

View File

@@ -4,6 +4,7 @@ import {
NavigationHelpersContext, NavigationHelpersContext,
TabNavigationState, TabNavigationState,
TabActions, TabActions,
ParamListBase,
useTheme, useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
@@ -16,7 +17,7 @@ import type {
} from '../types'; } from '../types';
type Props = MaterialTopTabNavigationConfig & { type Props = MaterialTopTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: MaterialTopTabNavigationHelpers; navigation: MaterialTopTabNavigationHelpers;
descriptors: MaterialTopTabDescriptorMap; descriptors: MaterialTopTabDescriptorMap;
tabBarPosition?: 'top' | 'bottom'; tabBarPosition?: 'top' | 'bottom';

View File

@@ -3,6 +3,116 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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
## [5.8.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.2...@react-navigation/native@5.8.3) (2020-11-03)
### Bug Fixes
* make sure that invalid linking config doesn't work if app is open ([52451d1](https://github.com/react-navigation/react-navigation/commit/52451d11094b8551e3c6950b3e005d68225c7da9))
## [5.8.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.1...@react-navigation/native@5.8.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/native
## [5.8.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.0...@react-navigation/native@5.8.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/native
# [5.8.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.6...@react-navigation/native@5.8.0) (2020-10-24)
### Features
* add `getInitialURL` and `subscribe` options to linking config ([748e92f](https://github.com/react-navigation/react-navigation/commit/748e92f120b9ff73c6b1e14515f60c76701081db))
* allow deep linking to reset state ([#8973](https://github.com/react-navigation/react-navigation/issues/8973)) ([7f3b27a](https://github.com/react-navigation/react-navigation/commit/7f3b27a9ec8edd9604ac19774baa1f60963ccdc9)), closes [#8952](https://github.com/react-navigation/react-navigation/issues/8952)
* support wildcard string prefixes ([#8942](https://github.com/react-navigation/react-navigation/issues/8942)) ([23ab350](https://github.com/react-navigation/react-navigation/commit/23ab3504921b7e741a48d66c6a953905206df4b7)), closes [#8941](https://github.com/react-navigation/react-navigation/issues/8941)
## [5.7.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.5...@react-navigation/native@5.7.6) (2020-10-07)
### Bug Fixes
* add missing check for initial state on web ([9e36508](https://github.com/react-navigation/react-navigation/commit/9e3650831c22b47130d2b388390f7eb7910fe91d))
## [5.7.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.4...@react-navigation/native@5.7.5) (2020-09-28) ## [5.7.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.4...@react-navigation/native@5.7.5) (2020-09-28)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/native", "name": "@react-navigation/native",
"description": "React Native integration for React Navigation", "description": "React Native integration for React Navigation",
"version": "5.7.5", "version": "5.8.10",
"keywords": [ "keywords": [
"react-native", "react-native",
"react-navigation", "react-navigation",
@@ -37,15 +37,16 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.12.4", "@react-navigation/core": "^5.14.4",
"nanoid": "^3.1.12" "escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.15"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.49", "@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-native": "^0.63.20", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",

View File

@@ -27,12 +27,23 @@ export type LinkingOptions = {
* The prefixes are stripped from the URL before parsing them. * The prefixes are stripped from the URL before parsing them.
* Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`) * Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`)
* Only applicable on Android and iOS. * Only applicable on Android and iOS.
*
* @example
* ```js
* {
* prefixes: [
* "myapp://", // App-specific scheme
* "https://example.com", // Prefix for universal links
* "https://*.example.com" // Prefix which matches any subdomain
* ]
* }
* ```
*/ */
prefixes: string[]; prefixes: string[];
/** /**
* Config to fine-tune how to parse the path. * Config to fine-tune how to parse the path.
* *
* Example: * @example
* ```js * ```js
* { * {
* Chat: { * Chat: {
@@ -43,13 +54,47 @@ export type LinkingOptions = {
* ``` * ```
*/ */
config?: { initialRouteName?: string; screens: PathConfigMap }; config?: { initialRouteName?: string; screens: PathConfigMap };
/**
* Custom function to get the initial URL used for linking.
* Uses `Linking.getInitialURL()` by default.
* Not supported on Web.
*
* @example
* ```js
* {
* getInitialURL () => Linking.getInitialURL(),
* }
* ```
*/
getInitialURL?: () => Promise<string | null | undefined>;
/**
* Custom function to get subscribe to URL updates.
* Uses `Linking.addEventListener('url', callback)` by default.
* Not supported on Web.
*
* @example
* ```js
* {
* subscribe: (listener) => {
* const onReceiveURL = ({ url }) => listener(url);
*
* Linking.addEventListener('url', onReceiveURL);
*
* return () => Linking.removeEventListener('url', onReceiveURL);
* }
* }
* ```
*/
subscribe?: (
listener: (url: string) => void
) => undefined | void | (() => void);
/** /**
* Custom function to parse the URL to a valid navigation state (advanced). * Custom function to parse the URL to a valid navigation state (advanced).
* Only applicable on Web.
*/ */
getStateFromPath?: typeof getStateFromPathDefault; getStateFromPath?: typeof getStateFromPathDefault;
/** /**
* Custom function to convert the state object to a valid URL (advanced). * Custom function to convert the state object to a valid URL (advanced).
* Only applicable on Web.
*/ */
getPathFromState?: typeof getPathFromStateDefault; getPathFromState?: typeof getPathFromStateDefault;
}; };

View File

@@ -22,7 +22,7 @@ export default function useLinkProps({ to, action }: Props) {
const linkTo = useLinkTo(); const linkTo = useLinkTo();
const onPress = ( const onPress = (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent e?: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => { ) => {
let shouldHandle = false; let shouldHandle = false;

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