Compare commits

...

164 Commits

Author SHA1 Message Date
Satyajit Sahoo
220af93db5 chore: publish
- @react-navigation/stack@5.5.0
2020-06-08 10:56:32 +02:00
Satyajit Sahoo
1f27e4b1f6 fix: ignore onOpen from route that wasn't closing
closes #8257
2020-06-08 10:48:04 +02:00
Satyajit Sahoo
9c06a92d09 fix: fix blank screen with animationEnabled: false & headerShown: false
closes #8391
2020-06-08 10:17:02 +02:00
Satyajit Sahoo
e0e0f79793 feat: automatically hide header in nested stacks 2020-06-08 08:14:34 +02:00
Jeroen Verfallie
c7e4bf94e6 fix: pass gestureRef to PanGestureHandlerNative (#8394)
In the current implementation the ref is unused, resulting in a constant `current: {null}` on the context.
2020-06-08 08:05:59 +02:00
Satyajit Sahoo
7024d4bb81 docs: fix comment about headerBacktitleVisible 2020-06-08 01:47:09 +02:00
Satyajit Sahoo
21f61d6eeb chore: publish
- @react-navigation/bottom-tabs@5.5.2
 - @react-navigation/compat@5.1.26
 - @react-navigation/core@5.10.0
 - @react-navigation/drawer@5.8.2
 - @react-navigation/material-bottom-tabs@5.2.10
 - @react-navigation/material-top-tabs@5.2.10
 - @react-navigation/native@5.5.1
 - @react-navigation/stack@5.4.2
2020-06-06 02:15:26 +02:00
Jean Regisser
8774ca97e1 fix: catch missing params when they are required in navigate (#8389)
There is a problem with the enforcement of required params in `navigation.navigate(...)` in TypeScript as described in https://github.com/react-navigation/react-navigation/issues/7936

@Miyou found a fix 🥳. All credits go to him.

I needed this so went ahead and submitted a PR and added a test to avoid it from breaking in the future.

However note that until this project switches to TypeScript 3.9 with support for [`@ts-expect-error`](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9-beta/#ts-expect-error-comments), it can still break silently.

Before the change, this is how the test looks:
<img width="861" alt="Screenshot 2020-06-05 at 22 51 00" src="https://user-images.githubusercontent.com/57791/83923057-c53dc180-a781-11ea-8c35-36406a23a717.png">
As you can see it doesn't catch the missing params which are required on line 28.

After the change, all expected errors are raised:
<img width="812" alt="Screenshot 2020-06-05 at 22 51 59" src="https://user-images.githubusercontent.com/57791/83923413-80665a80-a782-11ea-8ff2-f5af3f4e1f32.png">

Let me know what you think.
2020-06-06 00:57:28 +02:00
Satyajit Sahoo
e653d55479 refactor: minor tweaks 2020-06-06 00:38:38 +02:00
Ashoat Tevosyan
78afbffe97 fix: relatively position float Header if !headerTransparent (#8285)
## Motivation

Right now `headerMode: float` renders an absolutely-positioned header. To offset the content appropriately, it then measures the height of the header and compensates with a margin. This approach unfortunately doesn't work well for animations.

Before             |  After
:-------------------------:|:-------------------------:
<img src="http://ashoat.com/jerky_absolute.gif" width="300" />  |  <img src="http://ashoat.com/smooth_relative.gif" width="300" />

## Approach

When rendering the header absolutely we want to render it above (after, in sibling order) the content. But when rendering it relatively we want to render it first (before, in sibling order).

The margin compensation code is no longer necessary so I removed it.

## Test plan

I used the `StackHeaderCustomization` example to make sure transitions between `headerTransparent` and `!headerTransparent` looked good. I added a custom (taller) header to test if height transitions looked good, and toggled `headerShown` to make sure that transitioned well too.

Would be open to any other suggestions of things to test!
2020-06-06 00:12:00 +02:00
Diego Mello
762cc44578 fix: typo on drawerPosition default props (#8357)
Fix a minor typo from `drawerPostion` to `drawerPosition` :)

Fix https://github.com/react-navigation/react-navigation/issues/8358
2020-06-05 23:48:10 +02:00
Satyajit Sahoo
c3bd349d77 fix: make sure the wildcard pattern catches nested unmatched routes 2020-06-05 23:03:37 +02:00
Satyajit Sahoo
5dcaf903f3 refactor: rework history stack integration (#8367)
The PR reworks history integration to better integrate with browser's history stack and supports nested navigation more reliably:

- On each navigation, save the navigation in memory and use it to reset the state when user presses back/forward
- Improve heuristic to determine if we should do a push, replace or back

This closes #8230, closes #8284 and closes #8344
2020-06-05 23:02:35 +02:00
Satyajit Sahoo
2d66ef93ec fix: only use the query params for focused route in path 2020-06-05 20:01:52 +02:00
Satyajit Sahoo
4fe72e3ce7 feat: add wildcard patterns for paths
Currently, if we don't have matching routes for a path, we'll reuse the path name for the route name. This doesn't produce an error, and renders the initial route in the navigator. However, the user doesn't have a way of handling this with the default configuration.

This PR adds support for a wildcard pattern ('*'). The wildcard pattern will be matched after all other patterns were matched and will always match unmatched screens. This allows the user to implement a 404 screen.

Example:

```js
{
  Home: '',
  Profile: 'user/:id',
  404: '*',
}
```

This config will return the `404` route for paths which didn't match `Home` or `Profile`, e.g. - `/test`

Closes #8019

Co-authored-by: Evan Bacon <baconbrix@gmail.com>
2020-06-05 17:13:00 +02:00
Satyajit Sahoo
ab1f79c096 fix: prevent state change being emitted unnecessarily 2020-06-01 21:32:08 +02:00
Satyajit Sahoo
9305bfa939 chore: try to fix yarn caching on gh actions 2020-05-27 20:12:33 +02:00
Satyajit Sahoo
0c3c450f5f chore: tweak SSR output 2020-05-27 19:55:37 +02:00
Satyajit Sahoo
7ac4c13d44 chore: publish
- @react-navigation/bottom-tabs@5.5.1
 - @react-navigation/compat@5.1.25
 - @react-navigation/core@5.9.0
 - @react-navigation/drawer@5.8.1
 - @react-navigation/material-bottom-tabs@5.2.9
 - @react-navigation/material-top-tabs@5.2.9
 - @react-navigation/native@5.5.0
 - @react-navigation/stack@5.4.1
2020-05-27 18:32:30 +02:00
Raviraj
a0b9f94120 refactor: remove unnecessary check for type of bottom tab bar label 2020-05-27 18:12:21 +02:00
Satyajit Sahoo
717dffdb81 chore: improve caching of yarn on gh actions 2020-05-27 13:31:50 +02:00
Satyajit Sahoo
9016ba00e3 chore: improve caching of yarn on circleci 2020-05-27 13:19:41 +02:00
Satyajit Sahoo
9d822b95a6 fix: fix type of style for various options 2020-05-26 17:33:50 +02:00
Satyajit Sahoo
52d5cb4179 chore: add an example for SSR (#8298)
<img width="740" alt="Screen Shot 2020-05-20 at 16 31 30" src="https://user-images.githubusercontent.com/1174278/82458770-673d8880-9ab7-11ea-81d3-8ac0c1e52705.png">
2020-05-26 16:07:47 +02:00
Satyajit Sahoo
af1722d1e9 fix: export types from /native 2020-05-26 14:11:48 +02:00
Satyajit Sahoo
0b1a718756 feat: add ref to get current options in ServerContainer (#8333)
User can pass a `ref` to the container to get current options, like they can with `NavigationContainer`:

```js
const ref = React.createRef();

const html = renderToString(
  <ServerContainer ref={ref}>
    <App />
  </ServerContainer>
);

ref.current.getCurrentOptions(); // Options for screen
```
2020-05-26 13:55:06 +02:00
Michał Osadnik
9ab29558d0 refactor: remove useless callback arg in useSubscription (#8332) 2020-05-26 13:21:51 +02:00
Bright Lee
00c23f2c9e fix: allow HeaderBackground's subViews to be touchable (#8317) 2020-05-25 15:50:24 +02:00
Satyajit Sahoo
68e750d5a6 feat: add a ServerContainer component for SSR (#8297)
When doing SSR, the app needs to be aware of request URL to render correct navigation state.
The `ServerContainer` component lets us pass the `location` object to use for SSR.
The shape of the `location` object matches the `location` object in the browser.

Usage:

```js
ReactDOM.renderToString(
  <ServerContainer location={{ pathname: req.path, search: req.search }}>
    <App />
  </ServerContainer>
);
```

Updated example: https://github.com/react-navigation/react-navigation/pull/8298
2020-05-24 14:28:16 +02:00
Satyajit Sahoo
ced2a24aa6 chore: publish
- @react-navigation/bottom-tabs@5.5.0
 - @react-navigation/compat@5.1.24
 - @react-navigation/core@5.8.2
 - @react-navigation/drawer@5.8.0
 - @react-navigation/material-bottom-tabs@5.2.8
 - @react-navigation/material-top-tabs@5.2.8
 - @react-navigation/native@5.4.3
 - @react-navigation/routers@5.4.7
 - @react-navigation/stack@5.4.0
2020-05-23 18:36:57 +02:00
Satyajit Sahoo
ebf1345b39 refactor: simplify bottom tab bar 2020-05-23 18:34:12 +02:00
Satyajit Sahoo
df3544d9b4 Revert "fix: allow HeaderBackground's subViews to be touchable" (#8316) 2020-05-23 18:22:35 +02:00
Ashoat Tevosyan
c1e46f8e33 feat: animate changes to tabBarVisible in BottomTabBar (#8286)
## Motivation

Some designs call for custom keyboard inputs, or other bottom-aligned views meant overlap over the keyboard. Right now the best option on Android for this case is to set `tabBarVisible`. However changes to `tabBarVisible` doesn't get animated currently, which makes the custom-keyboard-open experience a bit more jarring than the native-keyboard-open one.

## Approach

I basically cribbed the `Animated.Value` we were using for `keyboardHidesTabBar` and made it depend on both. Note that the offset height depends on which of the two uses cases we're dealing with, which is explained in the code.

## Test plan

I played around with the `BottomTabs` example, setting certain screens to `tabBarVisible: true` and making sure it animated.
2020-05-23 18:16:30 +02:00
Bright Lee
021a9111d7 fix: allow HeaderBackground's subViews to be touchable (#8314)
When you're using the following options on `Stack`, the touch event goes to the behind of the Header and we can't really solve this problem in given `headerBackground` component level.
```
 options={{
          headerTransparent: true,
          headerBackground: 
```

### Problem
![May-23-2020 09-31-01](https://user-images.githubusercontent.com/916690/82726502-ebc11e80-9ce4-11ea-81d0-cc6a18bd70a7.gif)
2020-05-23 18:14:59 +02:00
Satyajit Sahoo
d3ace96981 chore: add linking prefix for expo to slience the warning 2020-05-23 17:37:45 +02:00
Satyajit Sahoo
edbc6b1e84 chore: tweak metro config 2020-05-23 17:33:34 +02:00
Satyajit Sahoo
c52d19bec8 chore: add example to hide and show bottom tab bar 2020-05-21 15:31:34 +02:00
Satyajit Sahoo
6dd45fcff9 fix: don't ignore previous header heights on layout update 2020-05-21 12:54:12 +02:00
Janic Duplessis
d62fbfe255 feat: update react-native-safe-area-context to 1.0.0 (#8182)
I made sure 1.0 is backwards compatible with react-navigation, which means using rn-safe-area-context@1+ with older versions of react-navigation will still work.
2020-05-21 11:25:36 +02:00
Evan Bacon
b14094619f chore: ignore __tests__ in prod builds (#8307)
The tests are being bundled and shipped in prod, this adds a bit of unneeded weight to npm installs. Now they won't be included.

```
@react-navigation/core
- before: 274 files - pkg: 211.0 kB - unpkg: 1 MB
- after: 238 files - pkg: 192.1 kB - unpkg: 827.3 kB
```
2020-05-21 11:15:12 +02:00
Ashoat Tevosyan
4c4d864af2 refactor: memoize initializedState in useNavigationBuilder (#8281) 2020-05-21 11:13:06 +02:00
Michał Osadnik
e1969f4e17 refactor: extract NavigationStateContext (#8304) 2020-05-21 10:41:34 +02:00
Satyajit Sahoo
175c07a28c chore: publish
- @react-navigation/example@5.1.0
 - @react-navigation/bottom-tabs@5.4.7
 - @react-navigation/compat@5.1.23
 - @react-navigation/core@5.8.1
 - @react-navigation/drawer@5.7.7
 - @react-navigation/material-bottom-tabs@5.2.7
 - @react-navigation/material-top-tabs@5.2.7
 - @react-navigation/native@5.4.2
 - @react-navigation/routers@5.4.6
 - @react-navigation/stack@5.3.9
2020-05-20 13:27:29 +02:00
osdnk
2980627cbf chore: publish
- @react-navigation/bottom-tabs@5.4.6
 - @react-navigation/compat@5.1.22
 - @react-navigation/core@5.8.0
 - @react-navigation/drawer@5.7.6
 - @react-navigation/material-bottom-tabs@5.2.6
 - @react-navigation/material-top-tabs@5.2.6
 - @react-navigation/native@5.4.1
 - @react-navigation/routers@5.4.5
 - @react-navigation/stack@5.3.8
2020-05-20 10:29:05 +02:00
Michał Osadnik
d024ec6d74 feat: add getCurrentOptions (#8277) 2020-05-19 20:53:08 +02:00
Satyajit Sahoo
4d1b85c751 chore: limit height of material top tabs example 2020-05-19 19:27:05 +02:00
Satyajit Sahoo
4a818fdfb3 chore: tweak master-detail example 2020-05-19 18:56:51 +02:00
Satyajit Sahoo
0194de1061 chore: upgrade bob 2020-05-19 14:25:20 +02:00
Michał Osadnik
7b25c8eb2e feat: add getCurrentRoute (#8254) 2020-05-18 01:55:09 +02:00
Satyajit Sahoo
9304a8a16c chore: publish
- @react-navigation/bottom-tabs@5.4.5
 - @react-navigation/compat@5.1.21
 - @react-navigation/core@5.7.0
 - @react-navigation/drawer@5.7.5
 - @react-navigation/material-bottom-tabs@5.2.5
 - @react-navigation/material-top-tabs@5.2.5
 - @react-navigation/native@5.4.0
 - @react-navigation/stack@5.3.7
2020-05-17 01:20:24 +02:00
Satyajit Sahoo
51b40879bd fix: center icons in material tab bar. fixes #8248 2020-05-17 01:06:29 +02:00
Satyajit Sahoo
51f4d11fdf fix: don't use Object.fromEntries 2020-05-17 00:56:19 +02:00
Satyajit Sahoo
c2aa4bb2eb test: fix integration tests 2020-05-16 21:18:42 +02:00
Satyajit Sahoo
199a892a6d chore: fix the links in the example 2020-05-16 01:58:05 +02:00
Satyajit Sahoo
60cb3c9ba7 feat: add a PathConfig type 2020-05-15 18:57:28 +02:00
Satyajit Sahoo
6ccceaea8b chore: tweak linking config in the example 2020-05-15 18:48:03 +02:00
Satyajit Sahoo
d14f38b80a fix: fix types for linking options 2020-05-15 18:37:58 +02:00
Satyajit Sahoo
c481748f00 chore: publish
- @react-navigation/stack@5.3.6
2020-05-15 17:39:47 +02:00
Satyajit Sahoo
d45dbe97dc fix: reduce header title margin. fixes #8267 2020-05-15 17:39:33 +02:00
Janic Duplessis
7623945f6e chore: fix repo url in readme (#8235) 2020-05-14 19:10:08 +02:00
Satyajit Sahoo
1dddaff45c chore: publish
- @react-navigation/bottom-tabs@5.4.4
 - @react-navigation/compat@5.1.20
 - @react-navigation/core@5.6.1
 - @react-navigation/drawer@5.7.4
 - @react-navigation/material-bottom-tabs@5.2.4
 - @react-navigation/material-top-tabs@5.2.4
 - @react-navigation/native@5.3.2
 - @react-navigation/stack@5.3.5
2020-05-14 13:22:54 +02:00
Satyajit Sahoo
21b397f0d6 fix: don't use flat since it's not supported in node 2020-05-14 13:22:14 +02:00
Satyajit Sahoo
2ff0531695 chore: publish
- @react-navigation/bottom-tabs@5.4.3
 - @react-navigation/compat@5.1.19
 - @react-navigation/core@5.6.0
 - @react-navigation/drawer@5.7.3
 - @react-navigation/material-bottom-tabs@5.2.3
 - @react-navigation/material-top-tabs@5.2.3
 - @react-navigation/native@5.3.1
 - @react-navigation/stack@5.3.4
2020-05-14 12:45:50 +02:00
Satyajit Sahoo
0149e85a95 fix: ignore state updates when we're not mounted
closes #8226
2020-05-14 12:43:44 +02:00
Satyajit Sahoo
3c47716826 fix: ignore extra slashes in the pattern 2020-05-14 04:40:44 +02:00
Satyajit Sahoo
acc9646426 feat: merge path patterns for nested screens (#8253)
Currently, when we define path patterns in the linking config, they ignore the parent screen's path when parsing, for example, say we have this config:

{
  Home: {
    path: 'home',
    screens: {
      Profile: 'u/:id',
    },
  },
}
If we parse the URL /home, we'll get this state:

{
  routes: [{ name: 'Home' }],
}
If we parse the URL /home/u/cal, we'll get this state:

{
  routes: [
    {
      name: 'Home',
      state: {
        routes: [
          {
            name: 'Home',
            state: {
              routes: [
                {
                  name: 'Profile',
                  params: { id: 'cal' },
                },
              ],
            },
          },
        ],
      },
    },
  ],
}
Note how we have 2 Home screens nested inside each other. This is not something people usually expect and it seems to trip up a lot of people. Something like following will be more intuitive:

{
  routes: [
    {
      name: 'Home',
      state: {
        routes: [
          {
            name: 'Profile',
            params: { id: 'cal' },
          },
        ],
      },
    },
  ],
}
Essentially, we're treating patterns as relative to their parent rather than matching the exact segments. This PR changes the behavior of parsing links to treat configs like so. I hope it'll be easier to understand for people.

There is also a new option, exact, which brings back the olde behavior of matching the exact pattern:

{
  Home: {
    path: 'home',
    screens: {
      Profile: {
        path: 'u/:id',
        exact: true,
      },
    },
  },
}
Which will be useful if home is not in the URL, e.g. /u/cal.

This change only affects situations where both parent and child screen configuration have a pattern defined. If the parent didn't have a pattern defined, nothing changes from previous behavior:

{
  Home: {
    screens: {
      Profile: {
        path: 'u/:id',
      },
    },
  },
}
2020-05-14 00:39:32 +02:00
Satyajit Sahoo
6dce0780ed chore: publish
- @react-navigation/stack@5.3.3
2020-05-11 17:31:35 +02:00
Hein Rutjes
dd7cff2016 fix: fix ios transitionspec settle time (#8028)
# Why

When using the stack navigator on iOS, it takes a (too) long time before the  `didFocus` and stack `onTransitionEnd` lifecycle events are triggered. The visual animation is typically completed well within 500 msec, but it takes around 1000 msec before the previously mentioned events are emitted. This causes problems with for instance `react-navigation-shared-element`, which relies on these events to fire in a timely manner(https://github.com/IjzerenHein/react-navigation-shared-element/issues/24)

# How

This PR updates the resting threshold so that the underlying spring settles faster. No visual differences or differences in smoothness were witnessed during testing.

## Before

Time to settle `didFocus`: **941**
Time to settle `stack.onTransitionEnd`: **924**

```
15:59:55.743 [ListViewStack]startTransition, closing: false, nestingDepth: 1
15:59:55.744 [ListViewStack]willFocus, scene: "DetailScreen", depth: 1, closing: false
15:59:55.745 Transition start: "ListScreen" -> "DetailScreen"
15:59:56.667 [ListViewStack]endTransition, closing: false, nestingDepth: 1
15:59:56.668 Transition end: "DetailScreen"
15:59:56.685 [ListViewStack]didFocus, scene: "DetailScreen", depth: 1
```

## After

Time to settle `didFocus`: **529**
Time to settle `stack.onTransitionEnd`: **512**

```
15:55:00.686 [ListViewStack]startTransition, closing: false, nestingDepth: 1
15:55:00.687 [ListViewStack]willFocus, scene: "DetailScreen", depth: 1, closing: false
15:55:00.687 Transition start: "ListScreen" -> "DetailScreen"
15:55:01.198 [ListViewStack]endTransition, closing: false, nestingDepth: 1
15:55:01.199 Transition end: "DetailScreen"
15:55:01.216 [ListViewStack]didFocus, scene: "DetailScreen", depth: 1
2020-05-11 17:03:12 +02:00
Satyajit Sahoo
740c6b6706 chore: publish
- @react-navigation/bottom-tabs@5.4.2
 - @react-navigation/compat@5.1.18
 - @react-navigation/drawer@5.7.2
 - @react-navigation/material-bottom-tabs@5.2.2
 - @react-navigation/material-top-tabs@5.2.2
 - @react-navigation/native@5.3.0
 - @react-navigation/stack@5.3.2
2020-05-10 08:34:40 +02:00
Satyajit Sahoo
039017bc2a feat: initialState should take priority over deep link 2020-05-10 08:28:48 +02:00
Satyajit Sahoo
b85a1c3055 chore: publish
- @react-navigation/bottom-tabs@5.4.1
 - @react-navigation/compat@5.1.17
 - @react-navigation/core@5.5.2
 - @react-navigation/drawer@5.7.1
 - @react-navigation/material-bottom-tabs@5.2.1
 - @react-navigation/material-top-tabs@5.2.1
 - @react-navigation/native@5.2.6
 - @react-navigation/routers@5.4.4
 - @react-navigation/stack@5.3.1
2020-05-08 19:16:47 +02:00
Satyajit Sahoo
18f8188dc8 chore: add source key to package.json 2020-05-08 19:14:29 +02:00
Satyajit Sahoo
47a1229837 fix: fix building typescript definitions. closes #8216 2020-05-08 19:09:13 +02:00
Satyajit Sahoo
00b11e303e chore: publish
- @react-navigation/bottom-tabs@5.4.0
 - @react-navigation/compat@5.1.16
 - @react-navigation/core@5.5.1
 - @react-navigation/drawer@5.7.0
 - @react-navigation/material-bottom-tabs@5.2.0
 - @react-navigation/material-top-tabs@5.2.0
 - @react-navigation/native@5.2.5
 - @react-navigation/routers@5.4.3
 - @react-navigation/stack@5.3.0
2020-05-08 16:34:03 +02:00
Satyajit Sahoo
f384706741 feat: use links in bottom navigation tabs 2020-05-08 16:11:24 +02:00
Satyajit Sahoo
d1a6f3e30e chore: upgrade depenendecies 2020-05-08 16:06:28 +02:00
Satyajit Sahoo
fd6636a8cd chore: update circleci config 2020-05-08 03:19:47 +02:00
Satyajit Sahoo
eb24fea8b9 chore: upgrade depenendecies 2020-05-07 21:08:55 +02:00
Linus Unnebäck
85ae378d8c fix: return a promise-like from getInitialState (#8210) 2020-05-07 20:56:55 +02:00
Satyajit Sahoo
bea14aa26f feat: add generic type aliases for screen props
closes #7971
2020-05-06 19:00:04 +02:00
Satyajit Sahoo
4d1e102f8c fix: include safe are insets in title's margins 2020-05-06 16:49:02 +02:00
Satyajit Sahoo
f07cd13561 fix: add proper margins to the header title 2020-05-06 16:14:40 +02:00
Satyajit Sahoo
f6d06768d3 fix: avoid cleaning up state when a new navigator is mounted. fixes #8195 2020-05-06 15:49:59 +02:00
Satyajit Sahoo
3381d680d7 chore: publish
- @react-navigation/bottom-tabs@5.3.4
 - @react-navigation/compat@5.1.15
 - @react-navigation/core@5.5.0
 - @react-navigation/drawer@5.6.4
 - @react-navigation/material-bottom-tabs@5.1.15
 - @react-navigation/material-top-tabs@5.1.15
 - @react-navigation/native@5.2.4
 - @react-navigation/stack@5.2.19
2020-05-05 20:07:13 +02:00
Wojciech Lewicki
fcd1cc64c1 feat: add support for optional params to linking (#8196) 2020-05-05 17:18:34 +02:00
Wojciech Lewicki
3999fc2836 feat: support params anywhere in path segement (#8184) 2020-05-04 15:07:27 +02:00
Satyajit Sahoo
9fd2635756 fix: return undefined for buildLink if linking is not enabled 2020-05-04 06:35:22 +02:00
Satyajit Sahoo
6bec620a3f chore: publish
- @react-navigation/bottom-tabs@5.3.3
 - @react-navigation/compat@5.1.14
 - @react-navigation/drawer@5.6.3
 - @react-navigation/material-bottom-tabs@5.1.14
 - @react-navigation/material-top-tabs@5.1.14
 - @react-navigation/native@5.2.3
 - @react-navigation/stack@5.2.18
2020-05-01 17:31:59 +02:00
Satyajit Sahoo
c7b8e2e966 fix: default linking enabled to true 2020-05-01 17:28:41 +02:00
Satyajit Sahoo
719e1a7b46 chore: publish
- @react-navigation/bottom-tabs@5.3.2
 - @react-navigation/compat@5.1.13
 - @react-navigation/drawer@5.6.2
 - @react-navigation/material-bottom-tabs@5.1.13
 - @react-navigation/material-top-tabs@5.1.13
 - @react-navigation/native@5.2.2
 - @react-navigation/stack@5.2.17
2020-05-01 16:51:12 +02:00
Satyajit Sahoo
10eca8b92e fix: don't throw when using 'useLinking'. fixes #8171 2020-05-01 16:49:06 +02:00
Satyajit Sahoo
b66e3436a7 chore: publish
- @react-navigation/bottom-tabs@5.3.1
 - @react-navigation/compat@5.1.12
 - @react-navigation/drawer@5.6.1
 - @react-navigation/material-bottom-tabs@5.1.12
 - @react-navigation/material-top-tabs@5.1.12
 - @react-navigation/native@5.2.1
 - @react-navigation/stack@5.2.16
2020-05-01 00:28:55 +02:00
Satyajit Sahoo
1c075ffb16 fix: render fallback only if linking is enabled. closes #8161 2020-05-01 00:27:42 +02:00
Satyajit Sahoo
1ee3038a4d chore: publish
- @react-navigation/bottom-tabs@5.3.0
 - @react-navigation/compat@5.1.11
 - @react-navigation/core@5.4.0
 - @react-navigation/drawer@5.6.0
 - @react-navigation/material-bottom-tabs@5.1.11
 - @react-navigation/material-top-tabs@5.1.11
 - @react-navigation/native@5.2.0
 - @react-navigation/routers@5.4.2
 - @react-navigation/stack@5.2.15
2020-04-30 23:01:46 +02:00
Evan Bacon
961b519fb1 chore: create _redirects for netlify deploy (#8160) 2020-04-30 23:01:21 +02:00
Satyajit Sahoo
0a19e94b23 fix: make sure the address bar hides when scrolling on web
This commit adds a check to detect if the screen content fills the available body, and if yes, then it adjusts the styles so that scrolling triggers a scroll on the body which hides the address bar in browser.

Tested on Safari in iOS and Chrome on Android.

This behaviour can be overriden by the user by specifying `cardStyle: { flex: 1 }`, which will keep both the header and the address bar always visible.
2020-04-30 21:53:17 +02:00
Evan Bacon
1e73fed6de chore: fix scrolling in web examples (#8020) 2020-04-30 13:17:55 +02:00
Satyajit Sahoo
3193a30da6 refactor: add missing methods to container navigation prop 2020-04-29 19:14:24 +02:00
Satyajit Sahoo
499c50cd43 refactor: make history type-checked 2020-04-29 19:13:14 +02:00
ainar
420f6926e1 fix: fix backBehavior with initialRoute (#8110) 2020-04-29 13:37:15 +02:00
Satyajit Sahoo
70be3f6d86 fix: fix closing drawer on web with tap on overlay 2020-04-29 13:05:30 +02:00
WoLewicki
bd35b4fc20 fix: parsing url 2020-04-29 12:52:30 +02:00
Satyajit Sahoo
c511bc0b2b refactor: stub gesture handler on web
Gesture handler doesn't work great on Web and causes issues such as disabling text selection even when not enabled. So we stub it out. It also reduces bundle size on web.
2020-04-29 12:49:46 +02:00
Satyajit Sahoo
b4834ce703 chore: replace AsyncStorage with localStorage on web 2020-04-29 02:16:11 +02:00
Satyajit Sahoo
ae5442ebe8 fix: return onPress instead of onClick for useLinkProps 2020-04-28 23:05:16 +02:00
Satyajit Sahoo
6dd52d35cf refactor: simplify resolving the thenable 2020-04-28 16:14:58 +02:00
Satyajit Sahoo
d6fa279d93 fix: add catch to thenable returned by getInitialState 2020-04-28 15:35:06 +02:00
Satyajit Sahoo
c3fa83efe0 fix: handle empty paths when parsing 2020-04-28 15:12:43 +02:00
Satyajit Sahoo
f2291d110f feat: add a useLinkProps hook 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
942d2be2c7 feat: add action prop to Link 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
b747e527a4 refactor: remove onLink prop for now 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
38020de80b refactor: simplify API for useLinkBuilder 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
67404f4999 test: configure playwright for e2e tests 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
2792f438fe feat: add useLinkBuilder hook to build links
We need to be able to create links from a navigate action to have accessible links in the built-in components such as drawer and tabs.
2020-04-27 17:45:20 +02:00
satyajit.happy
2573b5beaa feat: add Link component as useLinkTo hook for navigating to links
The `Link` component can be used to navigate to URLs. On web, it'll use an `a` tag for proper accessibility. On React Native, it'll use a `Text`.

Example:

```js
<Link to="/feed/hot">Go to 🔥</Link>
```

Sometimes we might want more complex styling and more control over the behaviour, or navigate to a URL programmatically. The `useLinkTo` hook can be used for that.

Example:

```js
function LinkButton({ to, ...rest }) {
  const linkTo = useLinkTo();

  return (
    <Button
      {...rest}
      href={to}
      onPress={(e) => {
        e.preventDefault();
        linkTo(to);
      }}
    />
  );
}
```
2020-04-27 17:45:20 +02:00
Satyajit Sahoo
2697355ab2 chore: publish
- @react-navigation/bottom-tabs@5.2.8
 - @react-navigation/compat@5.1.10
 - @react-navigation/core@5.3.5
 - @react-navigation/drawer@5.5.1
 - @react-navigation/material-bottom-tabs@5.1.10
 - @react-navigation/material-top-tabs@5.1.10
 - @react-navigation/native@5.1.7
 - @react-navigation/routers@5.4.1
 - @react-navigation/stack@5.2.14
2020-04-27 02:57:03 +02:00
Satyajit Sahoo
a695cf9c05 fix: don't add back the route being replaced 2020-04-27 02:41:46 +02:00
Satyajit Sahoo
c9c825bee6 fix: add config to enable redux devtools integration 2020-04-25 21:46:57 +02:00
Satyajit Sahoo
b172b51f17 fix: fix behaviour of openByDefault in drawer when focus changes 2020-04-23 20:00:47 +02:00
Satyajit Sahoo
9c05af50b4 test: add more tests for TabRouter and history 2020-04-23 18:11:30 +02:00
Satyajit Sahoo
24febf6ea9 fix: spread parent params to children in compat navigator
fixes #6785
2020-04-23 14:10:26 +02:00
Satyajit Sahoo
8cbb201f1a fix: fix typo in navigationOptions 2020-04-23 13:51:40 +02:00
Satyajit Sahoo
2467ce4ff7 chore: publish
- @react-navigation/stack@5.2.13
2020-04-22 17:57:16 +02:00
Satyajit Sahoo
5683bebfd6 chore: publish
- @react-navigation/stack@5.2.12
2020-04-22 16:26:11 +02:00
Satyajit Sahoo
78485cea69 fix: animate card to existing closing state on gesture end
fixes #7938
2020-04-22 15:16:39 +02:00
Satyajit Sahoo
1613915669 chore: mark screens and masked view as optional in stack
Needs e54819c4de to work.
2020-04-22 14:02:21 +02:00
Satyajit Sahoo
335a04edc1 chore: add action to check package versions 2020-04-20 14:35:07 +02:00
Satyajit Sahoo
5e0069a896 chore: publish
- @react-navigation/bottom-tabs@5.2.7
 - @react-navigation/compat@5.1.9
 - @react-navigation/core@5.3.4
 - @react-navigation/drawer@5.5.0
 - @react-navigation/material-bottom-tabs@5.1.9
 - @react-navigation/material-top-tabs@5.1.9
 - @react-navigation/native@5.1.6
 - @react-navigation/routers@5.4.0
 - @react-navigation/stack@5.2.11
2020-04-18 01:28:05 +02:00
Satyajit Sahoo
249248e741 chore: update yarn.lock 2020-04-18 01:24:16 +02:00
Evan Bacon
821343fed3 fix: webkit style error in overlay 2020-04-18 01:14:56 +02:00
Satyajit Sahoo
82edb2581b fix: hide inactive screens for stack on web (#8010) 2020-04-18 01:14:11 +02:00
Satyajit Sahoo
cb67530dc5 chore: tweak album example 2020-04-18 01:13:34 +02:00
Satyajit Sahoo
36689e24c2 feat: add openByDefault option to drawer 2020-04-18 01:13:34 +02:00
Gheorghe Pinzaru
6e51f596fa fix: ios presentation modal cuts the topOffset on the bottom (#7943)
* Add padding bottom to ios presentation modal

Because of the translateY moving the screen out to the bottom of view by 10 pt, these 10pt are hidden under the screen, or steal this size from the safe area. To avoid cutting elements, the size of the screen could be decreased by the `topOffset` using padding on the bottom. Fixes #7856

* Update packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx

Co-Authored-By: Serhii Vecherenko <SDSLeon999@gmail.com>

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
Co-authored-by: Serhii Vecherenko <SDSLeon999@gmail.com>
2020-04-18 01:13:34 +02:00
Satyajit Sahoo
402df73aa2 chore: add link to how to create minimal repro 2020-04-17 00:24:22 +02:00
Satyajit Sahoo
187aefe9c4 fix: handle initial: false for nested route after first initialization 2020-04-14 17:06:58 +02:00
Satyajit Sahoo
2613a62874 chore: add config for netlify 2020-04-12 22:11:22 +02:00
Satyajit Sahoo
6bdf6ae4ed fix: handle in-page go back when there's no history
fixes #7852
2020-04-10 17:59:40 +02:00
Satyajit Sahoo
e2bcf5168c fix: fix drawer not closing on web
fixes #6759
2020-04-10 17:59:07 +02:00
Satyajit Sahoo
dfdba8d741 fix: disable animation by default on web for stack 2020-04-10 17:02:32 +02:00
Satyajit Sahoo
a3f7a5feba fix: add initial param for actions from deep link 2020-04-10 12:05:16 +02:00
Satyajit Sahoo
004c7d7ab1 fix: add initial option for navigating to nested navigators
By default, params passed to nested navigators is used to initialize the navigator if it's not rendered already. The `initial` option would let the user control this behaviour. By specifying `initial: false`, it'll be possible to acheive the old behaviour of rendering the initial route of the stack before navigating to the new screen.

Example:

```js
navigation.navigate('Account', {
  screen: 'Settings',
  initial: false,
});
```
2020-04-10 11:51:32 +02:00
Satyajit Sahoo
49f658fbc0 chore: publish
- @react-navigation/bottom-tabs@5.2.6
 - @react-navigation/compat@5.1.8
 - @react-navigation/core@5.3.3
 - @react-navigation/drawer@5.4.1
 - @react-navigation/material-bottom-tabs@5.1.8
 - @react-navigation/material-top-tabs@5.1.8
 - @react-navigation/native@5.1.5
 - @react-navigation/routers@5.3.0
 - @react-navigation/stack@5.2.10
2020-04-08 12:17:31 +02:00
Satyajit Sahoo
cb2f157a56 fix: don't hide content from accessibility with permanent drawer
closes #7976
2020-04-08 12:17:09 +02:00
Juang, Yi-Lin
c4acdaa703 docs: fix typo (#7865) 2020-04-08 11:35:45 +02:00
Satyajit Sahoo
f1a8bceba5 fix: make color of shadow element same as card color in stack 2020-04-07 23:34:55 +02:00
Ruben Grimm
44081172d4 fix: use 1 as default in compatibility pop action 2020-04-07 23:33:38 +02:00
Satyajit Sahoo
de5d985f3b chore: upgrade depenendecies 2020-04-07 15:44:58 +02:00
Satyajit Sahoo
b71de6cc79 fix: mark type exports for all packages 2020-04-07 11:22:47 +02:00
raajnadar
303f0b78a5 fix: separate normal exports and type exports 2020-04-07 11:17:06 +02:00
Satyajit Sahoo
ce3994c82c fix: switch order of focus and blur events. closes #7963 2020-04-07 11:07:16 +02:00
Satyajit Sahoo
ba1f405129 feat: make replace bubble up 2020-04-07 00:02:54 +02:00
Satyajit Sahoo
d4fd906915 fix: workaround warning about setState in another component in render 2020-04-06 23:58:25 +02:00
Vinícius Fraga Modesto
b7fa90bf8d docs: fixes typo (#7923)
This PR fixes a typo in activeBackgroundColor's description
2020-03-31 17:56:26 +02:00
Satyajit Sahoo
9556aa9eff chore: publish
- @react-navigation/bottom-tabs@5.2.5
 - @react-navigation/compat@5.1.7
 - @react-navigation/core@5.3.2
 - @react-navigation/drawer@5.4.0
 - @react-navigation/material-bottom-tabs@5.1.7
 - @react-navigation/material-top-tabs@5.1.7
 - @react-navigation/native@5.1.4
 - @react-navigation/routers@5.2.1
 - @react-navigation/stack@5.2.9
2020-03-30 22:22:25 +02:00
Satyajit Sahoo
9a8fea8f2c fix: when comparing changed routes, only check keys 2020-03-30 22:20:16 +02:00
Satyajit Sahoo
9973db86f0 chore: use non-secure nanoid to be able to run in RN 2020-03-30 22:04:53 +02:00
max
8432e5ab25 fix: dismiss keyboard on screen change for android 2020-03-30 21:50:52 +02:00
Satyajit Sahoo
9bb5cfded3 refactor: replace shortid with nanoid. closes #7858 2020-03-30 21:42:58 +02:00
Satyajit Sahoo
4ac40b5c5d chore: update typescript and babel 2020-03-30 21:42:58 +02:00
Wojciech Lewicki
cd47915861 fix: handle no path property and undefined query params (#7911) 2020-03-30 17:11:33 +02:00
Andrius Janauskas
d649fbc669 fix: finish stack animation on CANCELLED event (#7898)
fixes #7897
2020-03-30 14:36:04 +02:00
Satyajit Sahoo
105da6ab2f fix: disable only swipe gesture on safari 2020-03-30 13:56:30 +02:00
Rajendran Nadar
ac7f972e92 feat: add swipeEnabled option to disable swipe gesture in drawer (#7834) 2020-03-30 13:51:32 +02:00
175 changed files with 13700 additions and 6648 deletions

View File

@@ -1,58 +1,97 @@
version: 2
version: 2.1
defaults: &defaults
docker:
- image: circleci/node:10
working_directory: ~/project
executors:
default:
docker:
- image: circleci/node:10
working_directory: ~/project
environment:
YARN_CACHE_FOLDER: "~/.cache/yarn"
commands:
attach_project:
steps:
- attach_workspace:
at: ~/project
jobs:
install-dependencies:
<<: *defaults
executor: default
steps:
- checkout
- attach_workspace:
at: ~/project
- attach_project
- restore_cache:
keys:
- v1-dependencies-{{ checksum "yarn.lock" }}
- v1-dependencies-
- run: yarn install --frozen-lockfile
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- run:
name: Install project dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v1-dependencies-{{ checksum "yarn.lock" }}
paths: node_modules
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths: ~/.cache/yarn
- persist_to_workspace:
root: .
paths: .
lint-and-typecheck:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: |
yarn lint
yarn typescript
executor: default
steps:
- attach_project
- run:
name: Lint files
command: yarn lint
- run:
name: Typecheck files
command: yarn typescript
unit-tests:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: |
yarn test --coverage
cat ./coverage/lcov.info | ./node_modules/.bin/codecov
- store_artifacts:
path: coverage
destination: coverage
executor: default
steps:
- attach_project
- run:
name: Run unit tests
command: yarn test --coverage
- run:
name: Upload test coverage
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
- store_artifacts:
path: coverage
destination: coverage
integration-tests:
executor: default
steps:
- attach_project
- run:
name: Install Headless Chrome dependencies
command: |
sudo apt-get install -yq \
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
- run:
name: Build example for web
command: yarn example expo build:web --no-pwa
- run:
name: Run integration tests
command: yarn example test --maxWorkers=2
build-packages:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: |
yarn lerna run prepare
node scripts/check-types-path.js
executor: default
steps:
- attach_project
- run:
name: Build packages in the monorepo
command: yarn lerna run prepare
- run:
name: Verify paths for types
command: node scripts/check-types-path.js
workflows:
version: 2
build-and-test:
jobs:
- install-dependencies
@@ -62,6 +101,9 @@ workflows:
- unit-tests:
requires:
- install-dependencies
- integration-tests:
requires:
- install-dependencies
- build-packages:
requires:
- install-dependencies

View File

@@ -23,20 +23,16 @@ jobs:
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-cache: true
- name: Get yarn cache
- name: Restore yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Check yarn cache
uses: actions/cache@v1
uses: actions/cache@master
with:
path: ${{ steps.yarn-cache.outputs.dir }}
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Publish Expo app
working-directory: ./example

View File

@@ -25,19 +25,16 @@ jobs:
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-cache: true
- name: Get yarn cache
- name: Restore yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
uses: actions/cache@master
with:
path: ${{ steps.yarn-cache.outputs.dir }}
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Publish Expo app
working-directory: ./example

View File

@@ -13,7 +13,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to reproduce the issue with minimal code (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
needs-repro:
runs-on: ubuntu-latest
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: comment "Hey! Thanks for opening the issue. Can you provide a minimal repro which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
args: comment "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
question:
runs-on: ubuntu-latest

27
.github/workflows/versions.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Check versions
on:
issues:
types: [opened]
jobs:
check-versions:
runs-on: ubuntu-latest
steps:
- uses: react-navigation/check-versions-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
packages: |
@react-navigation/bottom-tabs
@react-navigation/compat
@react-navigation/core
@react-navigation/drawer
@react-navigation/material-bottom-tabs
@react-navigation/material-top-tabs
@react-navigation/native
@react-navigation/routers
@react-navigation/stack
react-navigation-animated-switch
react-navigation-drawer
react-navigation-material-bottom-tabs
react-navigation-stack
react-navigation-tabs

View File

@@ -1,6 +1,10 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { Asset } from 'expo-asset';
import { Assets as StackAssets } from '@react-navigation/stack';
import App from './src/index';
Asset.loadAsync(StackAssets);
registerRootComponent(App);

41
example/CHANGELOG.md Normal file
View File

@@ -0,0 +1,41 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.1.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/example@5.0.0-alpha.23...@react-navigation/example@5.1.0) (2020-05-20)
### Bug Fixes
* add config to enable redux devtools integration ([c9c825b](https://github.com/react-navigation/react-navigation/commit/c9c825bee61426635a28ee149eeeff3d628171cd))
* clamp interpolated styles ([67798af](https://github.com/react-navigation/react-navigation/commit/67798af869dcbbf323629fc7e7cc9062d1e12c29))
* disable screens when mode is modal on older expo versions ([94d7b28](https://github.com/react-navigation/react-navigation/commit/94d7b28c0b2ce0d56c99b224610f305be6451626))
* dispatch pop early when screen is closed with gesture ([#336](https://github.com/react-navigation/react-navigation/issues/336)) ([3d937d1](https://github.com/react-navigation/react-navigation/commit/3d937d1e6571cd613e830d64f7b2e7426076d371)), closes [#267](https://github.com/react-navigation/react-navigation/issues/267)
* provide initial values for safe area to prevent blank screen ([#238](https://github.com/react-navigation/react-navigation/issues/238)) ([77b7570](https://github.com/react-navigation/react-navigation/commit/77b757091c0451e20bca01138629669c7da544a8))
* render fallback only if linking is enabled. closes [#8161](https://github.com/react-navigation/react-navigation/issues/8161) ([1c075ff](https://github.com/react-navigation/react-navigation/commit/1c075ffb169d233ed0515efea264a5a69b4de52e))
* return onPress instead of onClick for useLinkProps ([ae5442e](https://github.com/react-navigation/react-navigation/commit/ae5442ebe812b91fa1f12164f27d1aeed918ab0e))
* rtl in native app example ([50b366e](https://github.com/react-navigation/react-navigation/commit/50b366e7341f201d29a44f20b7771b3a832b0045))
* screens integration on Android ([#294](https://github.com/react-navigation/react-navigation/issues/294)) ([9bfb295](https://github.com/react-navigation/react-navigation/commit/9bfb29562020c61b4d5c9bee278bcb1c7bdb8b67))
* spread parent params to children in compat navigator ([24febf6](https://github.com/react-navigation/react-navigation/commit/24febf6ea99be2e5f22005fdd2a82136d647255c)), closes [#6785](https://github.com/react-navigation/react-navigation/issues/6785)
* update screens for native stack ([5411816](https://github.com/react-navigation/react-navigation/commit/54118161885738a6d20b062c7e6679f3bace8424))
* wrap navigators in gesture handler root ([41a5e1a](https://github.com/react-navigation/react-navigation/commit/41a5e1a385aa5180abc3992a4c67077c37b998b9))
### Features
* add `animationTypeForReplace` option ([#297](https://github.com/react-navigation/react-navigation/issues/297)) ([6262f72](https://github.com/react-navigation/react-navigation/commit/6262f7298bff843571fb4b1a677d3beabe29833e))
* add `screens` prop for nested configs ([#308](https://github.com/react-navigation/react-navigation/issues/308)) ([b931ae6](https://github.com/react-navigation/react-navigation/commit/b931ae62dfb2c5253c94ea5ace73e9070ec17c4a))
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
* add a useLinkProps hook ([f2291d1](https://github.com/react-navigation/react-navigation/commit/f2291d110faa2aa8e10c9133c1c0c28d54af7917))
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/commit/942d2be2c72720469475ce12ec8df23825994dbf))
* add custom theme support ([#211](https://github.com/react-navigation/react-navigation/issues/211)) ([00fc616](https://github.com/react-navigation/react-navigation/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
* add deeplinking to native example ([#309](https://github.com/react-navigation/react-navigation/issues/309)) ([e55e866](https://github.com/react-navigation/react-navigation/commit/e55e866af2f2163ee89bc527997cda13ffeb2abe))
* add headerStatusBarHeight option to stack ([b201fd2](https://github.com/react-navigation/react-navigation/commit/b201fd20716a2f03cb9373c72281f5d396a9356d))
* add Link component as useLinkTo hook for navigating to links ([2573b5b](https://github.com/react-navigation/react-navigation/commit/2573b5beaac1240434e52f3f57bb29da2f541c88))
* add openByDefault option to drawer ([36689e2](https://github.com/react-navigation/react-navigation/commit/36689e24c21b474692bb7ecd0b901c8afbbe9a20))
* add permanent drawer type ([#7818](https://github.com/react-navigation/react-navigation/issues/7818)) ([6a5d0a0](https://github.com/react-navigation/react-navigation/commit/6a5d0a035afae60d91aef78401ec8826295746fe))
* add preventDefault functionality in material bottom tabs ([3dede31](https://github.com/react-navigation/react-navigation/commit/3dede316ccab3b2403a475f60ce20b5c4e4cc068))
* emit appear and dismiss events for native stack ([f1df4a0](https://github.com/react-navigation/react-navigation/commit/f1df4a080877b3642e748a41a5ffc2da8c449a8c))
* initialState should take priority over deep link ([039017b](https://github.com/react-navigation/react-navigation/commit/039017bc2af69120d2d10e8f2c8a62919c37eb65))
* integrate with history API on web ([5a3f835](https://github.com/react-navigation/react-navigation/commit/5a3f8356b05bff7ed20893a5db6804612da3e568))

View File

@@ -7,12 +7,6 @@
"slug": "react-navigation-example",
"description": "Demo app to showcase various functionality of React Navigation",
"privacy": "public",
"sdkVersion": "36.0.0",
"platforms": [
"ios",
"android",
"web"
],
"version": "1.0.0",
"icon": "./assets/icon.png",
"splash": {
@@ -20,15 +14,16 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"sdkVersion": "37.0.0",
"platforms": ["ios", "android", "web"],
"ios": {
"supportsTablet": true
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": ["**/*"],
"scheme": "rne",
"entryPoint": "App.tsx"
}
}

View File

@@ -1,10 +0,0 @@
{
"settings": {
"import/core-modules": [
"detox",
"detox/runners/jest/adapter",
"detox/runners/jest/specReporter"
]
},
"env": { "jest": true, "jasmine": true }
}

View File

@@ -0,0 +1,44 @@
import { page } from '../config/setup-playwright';
beforeEach(async () => {
await page.click('[data-testid=LinkComponent]');
});
it('loads the article page', async () => {
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/article/gandalf'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Article by Gandalf');
});
it('goes to the album page and goes back', async () => {
await page.click('[href="/link-component/music"]');
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/music'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Albums');
await page.click('[aria-label="Article by Gandalf, back"]');
await page.waitForNavigation();
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/article/gandalf'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Article by Gandalf');
});

View File

@@ -1,9 +0,0 @@
import { by, element, expect, device } from 'detox';
beforeEach(async () => {
await device.reloadReactNative();
});
it('has dark theme toggle', async () => {
await expect(element(by.text('Dark theme'))).toBeVisible();
});

View File

@@ -0,0 +1,13 @@
import { page } from '../config/setup-playwright';
it('loads the example app', async () => {
const snapshot = await page.accessibility.snapshot();
// @ts-ignore
expect(snapshot?.children?.find((it) => it.role === 'heading')?.name).toBe(
'Examples'
);
const title = await page.$eval('[role=heading]', (el) => el.textContent);
expect(title).toBe('Examples');
});

View File

@@ -0,0 +1,13 @@
import fetch from 'node-fetch';
import cheerio from 'cheerio';
const server = 'http://localhost:3275';
it('renders the home page', async () => {
const res = await fetch(server);
const html = await res.text();
const $ = cheerio.load(html);
expect($('title').text()).toBe('Examples');
});

View File

@@ -1,6 +0,0 @@
{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}

View File

@@ -0,0 +1,24 @@
/* eslint-env jest */
import { chromium, Browser, BrowserContext, Page } from 'playwright';
let browser: Browser;
let context: BrowserContext;
let page: Page;
beforeAll(async () => {
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
});
beforeEach(async () => {
context = await browser.newContext();
page = await context.newPage();
await page.goto('http://localhost:3579');
});
export { browser, context, page };

View File

@@ -0,0 +1,16 @@
import { setup } from 'jest-dev-server';
export default async function () {
await setup([
{
command: 'yarn serve -l 3579 web-build',
launchTimeout: 50000,
port: 3579,
},
{
command: 'yarn server',
launchTimeout: 50000,
port: 3275,
},
]);
}

View File

@@ -0,0 +1,5 @@
import { teardown } from 'jest-dev-server';
export default async function () {
await teardown();
}

View File

@@ -1,28 +0,0 @@
/* eslint-disable import/no-commonjs */
const detox = require('detox');
const config = require('../../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');
// Set the default timeout
jest.setTimeout(120000);
jasmine.getEnv().addReporter(adapter);
// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
// This is strictly optional.
jasmine.getEnv().addReporter(specReporter);
beforeAll(async () => {
await detox.init(config);
}, 300000);
beforeEach(async () => {
await adapter.beforeEach();
});
afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});

View File

@@ -1,35 +1,38 @@
PODS:
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.6)
- EXAppLoaderProvider (8.0.0)
- EXBlur (8.0.0):
- EXBlur (8.1.0):
- UMCore
- EXConstants (8.0.0):
- EXConstants (9.0.0):
- UMConstantsInterface
- UMCore
- EXErrorRecovery (1.0.0):
- EXErrorRecovery (1.1.0):
- UMCore
- EXFileSystem (8.0.0):
- EXFileSystem (8.1.0):
- UMCore
- UMFileSystemInterface
- EXFont (8.0.0):
- EXFont (8.1.0):
- UMCore
- UMFontInterface
- EXKeepAwake (8.0.0):
- EXImageLoader (1.0.1):
- React-Core
- UMCore
- EXLinearGradient (8.0.0):
- UMImageLoaderInterface
- EXKeepAwake (8.1.0):
- UMCore
- EXLocation (8.0.0):
- EXLinearGradient (8.1.0):
- UMCore
- EXLocation (8.1.0):
- UMCore
- UMPermissionsInterface
- UMTaskManagerInterface
- EXPermissions (8.0.0):
- EXPermissions (8.1.0):
- UMCore
- UMPermissionsInterface
- EXSQLite (8.0.0):
- EXSQLite (8.1.0):
- UMCore
- UMFileSystemInterface
- EXWebBrowser (8.0.0):
- EXWebBrowser (8.2.1):
- UMCore
- FBLazyVector (0.61.5)
- FBReactNativeSpec (0.61.5):
@@ -50,8 +53,6 @@ PODS:
- glog
- glog (0.3.5)
- RCTRequired (0.61.5)
- RCTRestart (0.0.13):
- React
- RCTTypeSafety (0.61.5):
- FBLazyVector (= 0.61.5)
- Folly (= 2018.10.22.00)
@@ -214,7 +215,9 @@ PODS:
- React-cxxreact (= 0.61.5)
- React-jsi (= 0.61.5)
- React-jsinspector (0.61.5)
- react-native-safe-area-context (0.6.2):
- react-native-restart (0.0.15):
- React
- react-native-safe-area-context (1.0.0):
- React
- React-RCTActionSheet (0.61.5):
- React-Core/RCTActionSheetHeaders (= 0.61.5)
@@ -251,39 +254,41 @@ PODS:
- React-cxxreact (= 0.61.5)
- React-jsi (= 0.61.5)
- ReactCommon/jscallinvoker (= 0.61.5)
- RNCMaskedView (0.1.5):
- RNCMaskedView (0.1.10):
- React
- RNGestureHandler (1.5.5):
- RNGestureHandler (1.6.1):
- React
- RNReanimated (1.4.0):
- RNReanimated (1.8.0):
- React
- RNScreens (2.0.0-alpha.33):
- RNScreens (2.7.0):
- React
- UMBarCodeScannerInterface (5.0.0)
- UMCameraInterface (5.0.0)
- UMConstantsInterface (5.0.0)
- UMCore (5.0.0)
- UMFaceDetectorInterface (5.0.0)
- UMFileSystemInterface (5.0.0)
- UMFontInterface (5.0.0)
- UMImageLoaderInterface (5.0.0)
- UMPermissionsInterface (5.0.0)
- UMReactNativeAdapter (5.0.0):
- UMAppLoader (1.0.2)
- UMBarCodeScannerInterface (5.1.0)
- UMCameraInterface (5.1.0)
- UMConstantsInterface (5.1.0)
- UMCore (5.1.2)
- UMFaceDetectorInterface (5.1.0)
- UMFileSystemInterface (5.1.0)
- UMFontInterface (5.1.0)
- UMImageLoaderInterface (5.1.0)
- UMPermissionsInterface (5.1.0):
- UMCore
- UMReactNativeAdapter (5.2.0):
- React-Core
- UMCore
- UMFontInterface
- UMSensorsInterface (5.0.0)
- UMTaskManagerInterface (5.0.0)
- UMSensorsInterface (5.1.0)
- UMTaskManagerInterface (5.1.0)
- Yoga (1.14.0)
DEPENDENCIES:
- DoubleConversion (from `../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAppLoaderProvider (from `../../node_modules/expo-app-loader-provider/ios`)
- EXBlur (from `../../node_modules/expo-blur/ios`)
- EXConstants (from `../../node_modules/expo-constants/ios`)
- EXErrorRecovery (from `../../node_modules/expo-error-recovery/ios`)
- EXFileSystem (from `../../node_modules/expo-file-system/ios`)
- EXFont (from `../../node_modules/expo-font/ios`)
- EXImageLoader (from `../../node_modules/expo-image-loader/ios`)
- EXKeepAwake (from `../../node_modules/expo-keep-awake/ios`)
- EXLinearGradient (from `../../node_modules/expo-linear-gradient/ios`)
- EXLocation (from `../../node_modules/expo-location/ios`)
@@ -295,7 +300,6 @@ DEPENDENCIES:
- Folly (from `../../node_modules/react-native/third-party-podspecs/Folly.podspec`)
- glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`)
- RCTRequired (from `../../node_modules/react-native/Libraries/RCTRequired`)
- RCTRestart (from `../../node_modules/react-native-restart/ios`)
- RCTTypeSafety (from `../../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../../node_modules/react-native/`)
- React-Core (from `../../node_modules/react-native/`)
@@ -306,6 +310,7 @@ DEPENDENCIES:
- React-jsi (from `../../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../../node_modules/react-native/ReactCommon/jsinspector`)
- react-native-restart (from `../../node_modules/react-native-restart`)
- react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`)
- React-RCTActionSheet (from `../../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../../node_modules/react-native/Libraries/NativeAnimation`)
@@ -322,10 +327,11 @@ DEPENDENCIES:
- RNGestureHandler (from `../../node_modules/react-native-gesture-handler`)
- RNReanimated (from `../../node_modules/react-native-reanimated`)
- RNScreens (from `../../node_modules/react-native-screens`)
- UMAppLoader (from `../../node_modules/unimodules-app-loader/ios`)
- UMBarCodeScannerInterface (from `../../node_modules/unimodules-barcode-scanner-interface/ios`)
- UMCameraInterface (from `../../node_modules/unimodules-camera-interface/ios`)
- UMConstantsInterface (from `../../node_modules/unimodules-constants-interface/ios`)
- "UMCore (from `../../node_modules/@unimodules/core/ios`)"
- "UMCore (from `../../node_modules/react-native-unimodules/node_modules/@unimodules/core/ios`)"
- UMFaceDetectorInterface (from `../../node_modules/unimodules-face-detector-interface/ios`)
- UMFileSystemInterface (from `../../node_modules/unimodules-file-system-interface/ios`)
- UMFontInterface (from `../../node_modules/unimodules-font-interface/ios`)
@@ -343,42 +349,30 @@ SPEC REPOS:
EXTERNAL SOURCES:
DoubleConversion:
:podspec: "../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EXAppLoaderProvider:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-app-loader-provider/ios"
EXBlur:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-blur/ios"
:path: "../../node_modules/expo-blur/ios"
EXConstants:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-constants/ios"
:path: "../../node_modules/expo-constants/ios"
EXErrorRecovery:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-error-recovery/ios"
:path: "../../node_modules/expo-error-recovery/ios"
EXFileSystem:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-file-system/ios"
:path: "../../node_modules/expo-file-system/ios"
EXFont:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-font/ios"
:path: "../../node_modules/expo-font/ios"
EXImageLoader:
:path: "../../node_modules/expo-image-loader/ios"
EXKeepAwake:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-keep-awake/ios"
:path: "../../node_modules/expo-keep-awake/ios"
EXLinearGradient:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-linear-gradient/ios"
:path: "../../node_modules/expo-linear-gradient/ios"
EXLocation:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-location/ios"
:path: "../../node_modules/expo-location/ios"
EXPermissions:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-permissions/ios"
:path: "../../node_modules/expo-permissions/ios"
EXSQLite:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-sqlite/ios"
:path: "../../node_modules/expo-sqlite/ios"
EXWebBrowser:
:path: !ruby/object:Pathname
path: "../../node_modules/expo-web-browser/ios"
:path: "../../node_modules/expo-web-browser/ios"
FBLazyVector:
:path: "../../node_modules/react-native/Libraries/FBLazyVector"
FBReactNativeSpec:
@@ -389,8 +383,6 @@ EXTERNAL SOURCES:
:podspec: "../../node_modules/react-native/third-party-podspecs/glog.podspec"
RCTRequired:
:path: "../../node_modules/react-native/Libraries/RCTRequired"
RCTRestart:
:path: "../../node_modules/react-native-restart/ios"
RCTTypeSafety:
:path: "../../node_modules/react-native/Libraries/TypeSafety"
React:
@@ -407,6 +399,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../../node_modules/react-native/ReactCommon/jsinspector"
react-native-restart:
:path: "../../node_modules/react-native-restart"
react-native-safe-area-context:
:path: "../../node_modules/react-native-safe-area-context"
React-RCTActionSheet:
@@ -437,66 +431,55 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-reanimated"
RNScreens:
:path: "../../node_modules/react-native-screens"
UMAppLoader:
:path: "../../node_modules/unimodules-app-loader/ios"
UMBarCodeScannerInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-barcode-scanner-interface/ios"
:path: "../../node_modules/unimodules-barcode-scanner-interface/ios"
UMCameraInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-camera-interface/ios"
:path: "../../node_modules/unimodules-camera-interface/ios"
UMConstantsInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-constants-interface/ios"
:path: "../../node_modules/unimodules-constants-interface/ios"
UMCore:
:path: !ruby/object:Pathname
path: "../../node_modules/@unimodules/core/ios"
:path: "../../node_modules/react-native-unimodules/node_modules/@unimodules/core/ios"
UMFaceDetectorInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-face-detector-interface/ios"
:path: "../../node_modules/unimodules-face-detector-interface/ios"
UMFileSystemInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-file-system-interface/ios"
:path: "../../node_modules/unimodules-file-system-interface/ios"
UMFontInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-font-interface/ios"
:path: "../../node_modules/unimodules-font-interface/ios"
UMImageLoaderInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-image-loader-interface/ios"
:path: "../../node_modules/unimodules-image-loader-interface/ios"
UMPermissionsInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-permissions-interface/ios"
:path: "../../node_modules/unimodules-permissions-interface/ios"
UMReactNativeAdapter:
:path: !ruby/object:Pathname
path: "../../node_modules/@unimodules/react-native-adapter/ios"
:path: "../../node_modules/@unimodules/react-native-adapter/ios"
UMSensorsInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-sensors-interface/ios"
:path: "../../node_modules/unimodules-sensors-interface/ios"
UMTaskManagerInterface:
:path: !ruby/object:Pathname
path: "../../node_modules/unimodules-task-manager-interface/ios"
:path: "../../node_modules/unimodules-task-manager-interface/ios"
Yoga:
:path: "../../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
EXAppLoaderProvider: ebdb6bc2632c1ccadbe49f5e4104d8d690969c49
EXBlur: d1604f66f89a9414f5ee65dfb23874437c1bb147
EXConstants: 4051b16c17ef3defa03c541d42811dc92b249146
EXErrorRecovery: d36db99ec6a3808f313f01b0890eb443796dd1c2
EXFileSystem: 6e0d9bb6cc4ea404dbb8f583c1a8a2dcdf4b83b6
EXFont: 6187b5ab46ee578d5f8e7f2ea092752e78772235
EXKeepAwake: 66e9f80b6d129633725a0e42f8d285c229876811
EXLinearGradient: 75f302f9d6484267a3f6d3252df2e7a5f00e716a
EXLocation: 3c75d012ca92eed94d4338778d79c49d1252393a
EXPermissions: 9bc08859a675d291e89be9a0870155c27c16ac35
EXSQLite: 220226a354912b100dfe913f5fe6f31762c8927e
EXWebBrowser: db32607359fb7b55b7b7b91df32dd3d8355bb3b7
EXBlur: aa14d84bff6e9c2232fbcaf54ad809eee1cc41dc
EXConstants: 5304709b1bea70a4828f48ba4c7fc3ec3b2d9b17
EXErrorRecovery: 8f4c21ab2f51bf75defe4536f841a37de59b0661
EXFileSystem: cf4232ba7c62dc49b78c2d36005f97b6fddf0b01
EXFont: 8326ecf966be559f7ced7c8e221a32fc4d9ed8b0
EXImageLoader: 5ad6896fa1ef2ee814b551873cbf7a7baccc694a
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
EXLinearGradient: 97d8095d1e4ad96f7893e010e564796ed8aeea42
EXLocation: bbd487fd96a18a3ad9725389bbb94c4a5f78edf3
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
EXSQLite: 877ad6c8eb169353a2f94d5ad26510ffadd46a1f
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
FBLazyVector: aaeaf388755e4f29cd74acbc9e3b8da6d807c37f
FBReactNativeSpec: 118d0d177724c2d67f08a59136eb29ef5943ec75
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1
RCTRestart: dd19aab87fc1118e05b6b5b91b959105647f56b4
RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320
React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78
React-Core: 688b451f7d616cc1134ac95295b593d1b5158a04
@@ -505,7 +488,8 @@ SPEC CHECKSUMS:
React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7
React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386
React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0
react-native-safe-area-context: 25260c5d0b9c53fd7aa88e569e2edae72af1f6a3
react-native-restart: fff228304625f55de2ebd4de43938110f4c888ed
react-native-safe-area-context: a346c75f2288147527365ce27b59ca6d38c27805
React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72
@@ -516,24 +500,25 @@ SPEC CHECKSUMS:
React-RCTText: 9ccc88273e9a3aacff5094d2175a605efa854dbe
React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad
ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd
RNCMaskedView: dd13f9f7b146a9ad82f9b7eb6c9b5548fcf6e990
RNGestureHandler: d2270608171c868581b840cfc692f2962c05cd17
RNReanimated: b2ab0b693dddd2339bd2f300e770f6302d2e960c
RNScreens: 1c7fd499b915c77c21e8e6c327890c5af9b4cf7e
UMBarCodeScannerInterface: 3802c8574ef119c150701d679ab386e2266d6a54
UMCameraInterface: 985d301f688ed392f815728f0dd906ca34b7ccb1
UMConstantsInterface: bda5f8bd3403ad99e663eb3c4da685d063c5653c
UMCore: 7ab08669a8bb2e61f557c1fe9784521cb5aa28e3
UMFaceDetectorInterface: ce14e8e597f6a52aa66e4ab956cb5bff4fa8acf8
UMFileSystemInterface: 2ed004c9620f43f0b36b33c42ce668500850d6a4
UMFontInterface: 24fbc0a02ade6c60ad3ee3e2b5d597c8dcfc3208
UMImageLoaderInterface: 3976a14c588341228881ff75970fbabf122efca4
UMPermissionsInterface: 2abf9f7f4aa7110e27beaf634a7deda2d50ff3d7
UMReactNativeAdapter: 230406e3335a8dbd4c9c0e654488a1cf3b44552f
UMSensorsInterface: d708a892ef1500bdd9fc3ff03f7836c66d1634d3
UMTaskManagerInterface: a98e37a576a5220bf43b8faf33cfdc129d2f441d
RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
UMCameraInterface: 625878bbf2ba188a8548675e1d1d2e438a653e6d
UMConstantsInterface: 64060cf86587bcd90b1dbd804cceb6d377a308c1
UMCore: eb200e882eadafcd31ead290770835fd648c0945
UMFaceDetectorInterface: d6677d6ddc9ab95a0ca857aa7f8ba76656cc770f
UMFileSystemInterface: c70ea7147198b9807080f3597f26236be49b0165
UMFontInterface: d9d3b27af698c5389ae9e20b99ef56a083f491fb
UMImageLoaderInterface: 14dd2c46c67167491effc9e91250e9510f12709e
UMPermissionsInterface: 5e83a9167c177e4a0f0a3539345983cc749efb3e
UMReactNativeAdapter: 126da3486c1a1f11945b649d557d6c2ebb9407b2
UMSensorsInterface: 48941f70175e2975af1a9386c6d6cb16d8126805
UMTaskManagerInterface: cb890c79c63885504ddc0efd7a7d01481760aca2
Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b
PODFILE CHECKSUM: c48a21ff513d3eadafa50f8797207ef2be75e234
COCOAPODS: 1.8.4
COCOAPODS: 1.9.1

6
example/jest.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
testRegex: '/__integration_tests__/.*\\.(test|spec)\\.(js|tsx?)$',
globalSetup: './e2e/config/setup-server.tsx',
globalTeardown: './e2e/config/teardown-server.tsx',
setupFilesAfterEnv: ['./e2e/config/setup-playwright.tsx'],
};

View File

@@ -8,26 +8,25 @@ const blacklist = require('metro-config/src/defaults/blacklist');
const root = path.resolve(__dirname, '..');
const packages = path.resolve(root, 'packages');
const workspaces = fs
// List all packages under `packages/`
.readdirSync(packages)
// Ignore hidden files such as .DS_Store
.filter((p) => !p.startsWith('.'))
.map((p) => path.join(packages, p));
// Get the list of dependencies for all packages in the monorepo
const modules = ['@expo/vector-icons']
.concat(
...fs
// List all packages under `packages/`
.readdirSync(packages)
// Ignore hidden files such as .DS_Store
.filter((p) => !p.startsWith('.'))
.map((p) => {
const pak = JSON.parse(
fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8')
);
...workspaces.map((it) => {
const pak = JSON.parse(
fs.readFileSync(path.join(it, 'package.json'), 'utf8')
);
// We need to collect list of deps that this package imports
// Collecting both dependencies are peerDependencies should do it
return Object.keys({
...pak.dependencies,
...pak.peerDependencies,
});
})
// We need to make sure that only one version is loaded for peerDependencies
// So we blacklist them at the root, and alias them to the versions in example's node_modules
return pak.peerDependencies ? Object.keys(pak.peerDependencies) : [];
})
)
.sort()
.filter(
@@ -45,15 +44,16 @@ module.exports = {
watchFolders: [root],
resolver: {
// We need to blacklist `node_modules` of all our packages
// This will avoid Metro throwing duplicate module errors
// We need to blacklist the peerDependencies we've collected in packages' node_modules
blacklistRE: blacklist(
fs
.readdirSync(packages)
.map((p) => path.join(packages, p))
.map(
(it) => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
[].concat(
...workspaces.map((it) =>
modules.map(
(m) =>
new RegExp(`^${escape(path.join(it, 'node_modules', m))}\\/.*$`)
)
)
)
),
// When we import a package from the monorepo, metro won't be able to find their deps

View File

@@ -1,42 +1,60 @@
{
"name": "@react-navigation/example",
"description": "Demo app to showcase various functionality of React Navigation",
"version": "5.0.0",
"version": "5.1.0",
"private": true,
"scripts": {
"start": "expo start",
"web": "expo start:web",
"native": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios"
"ios": "react-native run-ios",
"server": "nodemon -e '.js,.ts,.tsx' --exec \"babel-node -i '/node_modules[/\\](?react-native)/' -x '.web.tsx,.web.ts,.web.js,.tsx,.ts,.js' --config-file ./server/babel.config.js server\"",
"test": "jest"
},
"dependencies": {
"@expo/vector-icons": "^10.0.0",
"@react-native-community/masked-view": "0.1.7",
"@types/react-native-restart": "^0.0.0",
"@expo/vector-icons": "^10.2.0",
"@react-native-community/masked-view": "^0.1.10",
"color": "^3.1.2",
"expo": "^36.0.2",
"expo-asset": "~8.1.1",
"expo": "^37.0.8",
"expo-asset": "~8.1.3",
"expo-blur": "~8.1.0",
"koa": "^2.12.0",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "~0.61.5",
"react-native-gesture-handler": "^1.6.0",
"react-native-paper": "^3.6.0",
"react-native-reanimated": "^1.7.0",
"react-native-restart": "^0.0.14",
"react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0",
"react-native-tab-view": "2.13.0",
"react-native-unimodules": "^0.8.1",
"react-native-paper": "^3.10.1",
"react-native-reanimated": "^1.8.0",
"react-native-restart": "^0.0.15",
"react-native-safe-area-context": "^1.0.0",
"react-native-screens": "^2.7.0",
"react-native-tab-view": "2.14.0",
"react-native-unimodules": "~0.9.1",
"react-native-vector-icons": "^6.6.0",
"react-native-web": "^0.11.7"
},
"devDependencies": {
"@expo/webpack-config": "^0.11.9",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",
"@babel/node": "^7.8.7",
"@expo/webpack-config": "^0.11.19",
"@types/cheerio": "^0.22.18",
"@types/jest-dev-server": "^4.2.0",
"@types/koa": "^2.11.3",
"@types/node-fetch": "^2.5.7",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.8",
"@types/react-native": "^0.62.7",
"babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "^8.1.0",
"expo-cli": "^3.15.5",
"typescript": "^3.7.5"
"cheerio": "^1.0.0-rc.3",
"expo-cli": "^3.20.1",
"jest": "^26.0.1",
"jest-dev-server": "^4.4.0",
"mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.0",
"nodemon": "^2.0.4",
"playwright": "^0.14.0",
"serve": "^11.3.0",
"typescript": "^3.8.3"
}
}

View File

@@ -0,0 +1,40 @@
const path = require('path');
const fs = require('fs');
const packages = path.resolve(__dirname, '..', '..', 'packages');
const alias = Object.fromEntries(
fs
.readdirSync(packages)
.filter((name) => !name.startsWith('.'))
.map((name) => [
`@react-navigation/${name}`,
path.resolve(
packages,
name,
require(`../../packages/${name}/package.json`).source
),
])
);
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-flow',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-class-properties',
[
'module-resolver',
{
root: ['..'],
alias: {
'react-native': 'react-native-web',
...alias,
},
},
],
],
};

54
example/server/index.tsx Normal file
View File

@@ -0,0 +1,54 @@
import './resolve-hooks';
import Koa from 'koa';
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import { AppRegistry } from 'react-native-web';
import { ServerContainer, ServerContainerRef } from '@react-navigation/native';
import App from '../src/index';
AppRegistry.registerComponent('App', () => App);
const PORT = process.env.PORT || 3275;
const app = new Koa();
app.use(async (ctx) => {
const { element, getStyleElement } = AppRegistry.getApplication('App');
const ref = React.createRef<ServerContainerRef>();
const html = ReactDOMServer.renderToString(
<ServerContainer
ref={ref}
location={{ pathname: ctx.path, search: ctx.search }}
>
{element}
</ServerContainer>
);
const css = ReactDOMServer.renderToStaticMarkup(getStyleElement());
const document = `
<!DOCTYPE html>
<html style="height: 100%">
<meta charset="utf-8">
<meta httpEquiv="X-UA-Compatible" content="IE=edge">
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover"
>
${css}
<title>${ref.current?.getCurrentOptions()?.title}</title>
<body style="min-height: 100%">
<div id="root" style="display: flex; min-height: 100vh">
${html}
</div>
`;
ctx.body = document;
});
app.listen(PORT, () => {
console.log(`Running at http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,12 @@
import 'mock-require-assets';
import Module from 'module';
// We need to make sure that .web.xx extensions are resolved before .xx
// @ts-ignore
Module._extensions = Object.fromEntries(
// @ts-ignore
Object.entries(Module._extensions).sort((a, b) => {
return b[0].split('.').length - a[0].split('.').length;
})
);

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
export function restartApp() {
// @ts-ignore
if (global.Expo) {
Updates.reloadFromCache();
} else {
RNRestart.Restart();
}
}

1
example/src/Restart.tsx Normal file
View File

@@ -0,0 +1 @@
export function restartApp() {}

View File

@@ -1,6 +1,11 @@
import * as React from 'react';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {
createBottomTabNavigator,
BottomTabNavigationProp,
} from '@react-navigation/bottom-tabs';
import TouchableBounce from '../Shared/TouchableBounce';
import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts';
@@ -22,24 +27,56 @@ type BottomTabParams = {
Chat: undefined;
};
const scrollEnabled = Platform.select({ web: true, default: false });
const AlbumsScreen = ({
navigation,
}: {
navigation: BottomTabNavigationProp<BottomTabParams>;
}) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="outlined"
onPress={() => navigation.setOptions({ tabBarVisible: false })}
style={styles.button}
>
Hide tab bar
</Button>
<Button
mode="outlined"
onPress={() => navigation.setOptions({ tabBarVisible: true })}
style={styles.button}
>
Show tab bar
</Button>
</View>
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
const BottomTabs = createBottomTabNavigator<BottomTabParams>();
export default function BottomTabsScreen() {
return (
<BottomTabs.Navigator
screenOptions={{
tabBarButton: (props) => <TouchableBounce {...props} />,
tabBarButton:
Platform.OS === 'web'
? undefined
: (props) => <TouchableBounce {...props} />,
}}
>
<BottomTabs.Screen
name="Article"
component={SimpleStackScreen}
options={{
title: 'Article',
tabBarIcon: getTabBarIcon('file-document-box'),
}}
>
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
</BottomTabs.Screen>
/>
<BottomTabs.Screen
name="Chat"
component={Chat}
@@ -58,7 +95,7 @@ export default function BottomTabsScreen() {
/>
<BottomTabs.Screen
name="Albums"
component={Albums}
component={AlbumsScreen}
options={{
title: 'Albums',
tabBarIcon: getTabBarIcon('image-album'),
@@ -67,3 +104,13 @@ export default function BottomTabsScreen() {
</BottomTabs.Navigator>
);
}
const styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
padding: 8,
},
button: {
margin: 8,
},
});

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import {
createCompatNavigatorFactory,
@@ -11,25 +11,32 @@ import {
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type CompatStackParams = {
Article: { author: string };
Album: undefined;
Albums: undefined;
Nested: { author: string };
};
const ArticleScreen: CompatScreenType<StackNavigationProp<
CompatStackParams,
'Article'
type NestedStackParams = {
Feed: undefined;
Article: { author: string };
};
const scrollEnabled = Platform.select({ web: true, default: false });
const AlbumsScreen: CompatScreenType<StackNavigationProp<
CompatStackParams
>> = ({ navigation }) => {
return (
<React.Fragment>
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Album')}
onPress={() => navigation.push('Nested', { author: 'Babel fish' })}
style={styles.button}
>
Push album
Push nested
</Button>
<Button
mode="outlined"
@@ -39,24 +46,20 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
Go back
</Button>
</View>
<Article author={{ name: navigation.getParam('author') }} />
</React.Fragment>
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
ArticleScreen.navigationOptions = ({ navigation }) => ({
title: `Article by ${navigation.getParam('author')}`,
});
const AlbumsScreen: CompatScreenType<StackNavigationProp<
CompatStackParams
>> = ({ navigation }) => {
const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
navigation,
}) => {
return (
<React.Fragment>
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
onPress={() => navigation.push('Article')}
style={styles.button}
>
Push article
@@ -69,22 +72,69 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
Go back
</Button>
</View>
<Albums />
</React.Fragment>
<NewsFeed scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
const CompatStack = createCompatNavigatorFactory(createStackNavigator)<
const ArticleScreen: CompatScreenType<StackNavigationProp<
NestedStackParams,
'Article'
>> = ({ navigation }) => {
navigation.dangerouslyGetParent();
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Albums')}
style={styles.button}
>
Push albums
</Button>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Article
author={{ name: navigation.getParam('author') }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
ArticleScreen.navigationOptions = ({ navigation }) => ({
title: `Article by ${navigation.getParam('author')}`,
});
const createCompatStackNavigator = createCompatNavigatorFactory(
createStackNavigator
);
const CompatStack = createCompatStackNavigator<
StackNavigationProp<CompatStackParams>
>(
{
Article: {
screen: ArticleScreen,
Albums: AlbumsScreen,
Nested: {
screen: createCompatStackNavigator<
StackNavigationProp<NestedStackParams>
>(
{
Feed: FeedScreen,
Article: ArticleScreen,
},
{ navigationOptions: { headerShown: false } }
),
params: {
author: 'Gandalf',
},
},
Album: AlbumsScreen,
},
{
mode: 'modal',

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { Title, Button } from 'react-native-paper';
import { Feather } from '@expo/vector-icons';
import Feather from 'react-native-vector-icons/Feather';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
type BottomTabParams = {

View File

@@ -0,0 +1,162 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import {
Link,
StackActions,
RouteProp,
ParamListBase,
useLinkProps,
} from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
type SimpleStackParams = {
Article: { author: string };
Albums: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const LinkButton = ({
to,
...rest
}: React.ComponentProps<typeof Button> & { to: string }) => {
const { onPress, ...props } = useLinkProps({ to });
return (
<Button
{...props}
{...rest}
{...Platform.select({
web: { onClick: onPress } as any,
default: { onPress },
})}
/>
);
};
const ArticleScreen = ({
navigation,
route,
}: {
navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'Article'>;
}) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Link
to="/link-component/music"
style={[styles.button, { padding: 8 }]}
>
Go to /link-component/music
</Link>
<Link
to="/link-component/music"
action={StackActions.replace('Albums')}
style={[styles.button, { padding: 8 }]}
>
Replace with /link-component/music
</Link>
<LinkButton
to="/link-component/music"
mode="contained"
style={styles.button}
>
Go to /link-component/music
</LinkButton>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
const AlbumsScreen = ({
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Link
to="/link-component/article/babel"
style={[styles.button, { padding: 8 }]}
>
Go to /link-component/article
</Link>
<LinkButton
to="/link-component/article/babel"
mode="contained"
style={styles.button}
>
Go to /link-component/article
</LinkButton>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
return (
<SimpleStack.Navigator {...rest}>
<SimpleStack.Screen
name="Article"
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params.author}`,
})}
initialParams={{ author: 'Gandalf' }}
/>
<SimpleStack.Screen
name="Albums"
component={AlbumsScreen}
options={{ title: 'Albums' }}
/>
</SimpleStack.Navigator>
);
}
const styles = StyleSheet.create({
buttons: {
padding: 8,
},
button: {
margin: 8,
},
});

View File

@@ -0,0 +1,143 @@
import * as React from 'react';
import { Dimensions, ScaledSize } from 'react-native';
import { Appbar } from 'react-native-paper';
import {
useTheme,
useNavigation,
ParamListBase,
} from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import {
createDrawerNavigator,
DrawerNavigationProp,
DrawerContent,
DrawerContentComponentProps,
DrawerContentOptions,
} from '@react-navigation/drawer';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type DrawerParams = {
Article: undefined;
NewsFeed: undefined;
Albums: undefined;
};
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
const useIsLargeScreen = () => {
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
React.useEffect(() => {
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
Dimensions.addEventListener('change', onDimensionsChange);
return () => Dimensions.removeEventListener('change', onDimensionsChange);
}, []);
return dimensions.width > 414;
};
const Header = ({
onGoBack,
title,
}: {
onGoBack: () => void;
title: string;
}) => {
const { colors } = useTheme();
const isLargeScreen = useIsLargeScreen();
return (
<Appbar.Header style={{ backgroundColor: colors.card, elevation: 1 }}>
{isLargeScreen ? null : <Appbar.BackAction onPress={onGoBack} />}
<Appbar.Content title={title} />
</Appbar.Header>
);
};
const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
return (
<>
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
<Article />
</>
);
};
const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
return (
<>
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
<NewsFeed />
</>
);
};
const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
return (
<>
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
<Albums />
</>
);
};
const CustomDrawerContent = (
props: DrawerContentComponentProps<DrawerContentOptions>
) => {
const { colors } = useTheme();
const navigation = useNavigation();
return (
<>
<Appbar.Header style={{ backgroundColor: colors.card, elevation: 1 }}>
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
<Appbar.Content title="Pages" />
</Appbar.Header>
<DrawerContent {...props} />
</>
);
};
const Drawer = createDrawerNavigator<DrawerParams>();
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
export default function DrawerScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
gestureEnabled: false,
});
const isLargeScreen = useIsLargeScreen();
return (
<Drawer.Navigator
openByDefault
drawerType={isLargeScreen ? 'permanent' : 'back'}
drawerStyle={isLargeScreen ? null : { width: '100%' }}
overlayColor="transparent"
drawerContent={(props) => <CustomDrawerContent {...props} />}
{...rest}
>
<Drawer.Screen name="Article" component={ArticleScreen} />
<Drawer.Screen
name="NewsFeed"
component={NewsFeedScreen}
options={{ title: 'Feed' }}
/>
<Drawer.Screen
name="Albums"
component={AlbumsScreen}
options={{ title: 'Albums' }}
/>
</Drawer.Navigator>
);
}

View File

@@ -22,14 +22,13 @@ export default function MaterialBottomTabsScreen() {
<MaterialBottomTabs.Navigator barStyle={styles.tabBar}>
<MaterialBottomTabs.Screen
name="Article"
component={SimpleStackScreen}
options={{
tabBarLabel: 'Article',
tabBarIcon: 'file-document-box',
tabBarColor: '#C9E7F8',
}}
>
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
</MaterialBottomTabs.Screen>
/>
<MaterialBottomTabs.Screen
name="Chat"
component={Chat}

View File

@@ -1,4 +1,6 @@
import * as React from 'react';
import { ParamListBase } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts';
@@ -12,7 +14,15 @@ type MaterialTopTabParams = {
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
export default function MaterialTopTabsScreen() {
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function MaterialTopTabsScreen({ navigation }: Props) {
navigation.setOptions({
cardStyle: { flex: 1 },
});
return (
<MaterialTopTabs.Navigator>
<MaterialTopTabs.Screen

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native';
import {
@@ -12,11 +12,13 @@ import Albums from '../Shared/Albums';
type ModalStackParams = {
Article: { author: string };
Album: undefined;
Albums: undefined;
};
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
@@ -29,7 +31,7 @@ const ArticleScreen = ({
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Album')}
onPress={() => navigation.push('Albums')}
style={styles.button}
>
Push album
@@ -42,7 +44,10 @@ const ArticleScreen = ({
Go back
</Button>
</View>
<Article author={{ name: route.params.author }} scrollEnabled={false} />
<Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
@@ -66,7 +71,7 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
Go back
</Button>
</View>
<Albums scrollEnabled={false} />
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
@@ -86,7 +91,6 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
return (
<ModalPresentationStack.Navigator
mode="modal"
headerMode="screen"
screenOptions={({ route, navigation }) => ({
...TransitionPresets.ModalPresentationIOS,
cardOverlayEnabled: true,
@@ -107,9 +111,9 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
initialParams={{ author: 'Gandalf' }}
/>
<ModalPresentationStack.Screen
name="Album"
name="Albums"
component={AlbumsScreen}
options={{ title: 'Album' }}
options={{ title: 'Albums' }}
/>
</ModalPresentationStack.Navigator>
);

View File

@@ -0,0 +1,40 @@
import { StackNavigationProp } from '@react-navigation/stack';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-paper';
const NotFoundScreen = ({
navigation,
}: {
navigation: StackNavigationProp<{ Home: undefined }>;
}) => {
return (
<View style={styles.container}>
<Text style={styles.title}>404 Not Found</Text>
<Button
mode="contained"
onPress={() => navigation.navigate('Home')}
style={styles.button}
>
Go to home
</Button>
</View>
);
};
export default NotFoundScreen;
const styles = StyleSheet.create({
title: {
fontSize: 36,
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 8,
},
button: {
margin: 24,
},
});

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native';
import {
@@ -13,11 +13,13 @@ import NewsFeed from '../Shared/NewsFeed';
type SimpleStackParams = {
Article: { author: string };
NewsFeed: undefined;
Album: undefined;
Albums: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
@@ -43,7 +45,10 @@ const ArticleScreen = ({
Pop screen
</Button>
</View>
<Article author={{ name: route.params.author }} scrollEnabled={false} />
<Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
@@ -58,7 +63,7 @@ const NewsFeedScreen = ({
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.navigate('Album')}
onPress={() => navigation.navigate('Albums')}
style={styles.button}
>
Navigate to album
@@ -71,7 +76,7 @@ const NewsFeedScreen = ({
Go back
</Button>
</View>
<NewsFeed scrollEnabled={false} />
<NewsFeed scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
@@ -99,24 +104,24 @@ const AlbumsScreen = ({
Pop by 2
</Button>
</View>
<Albums scrollEnabled={false} />
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
export default function SimpleStackScreen({ navigation }: Props) {
navigation.setOptions({
headerShown: false,
});
return (
<SimpleStack.Navigator {...rest}>
<SimpleStack.Navigator>
<SimpleStack.Screen
name="Article"
component={ArticleScreen}
@@ -131,9 +136,9 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
options={{ title: 'Feed' }}
/>
<SimpleStack.Screen
name="Album"
name="Albums"
component={AlbumsScreen}
options={{ title: 'Album' }}
options={{ title: 'Albums' }}
/>
</SimpleStack.Navigator>
);

View File

@@ -1,25 +1,36 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
import {
Animated,
View,
StyleSheet,
ScrollView,
Alert,
Platform,
} from 'react-native';
import { Button, Appbar } from 'react-native-paper';
import { BlurView } from 'expo-blur';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { RouteProp, ParamListBase } from '@react-navigation/native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTheme, RouteProp, ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
HeaderBackground,
useHeaderHeight,
Header,
StackHeaderProps,
} from '@react-navigation/stack';
import BlurView from '../Shared/BlurView';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
type SimpleStackParams = {
Article: { author: string };
Album: undefined;
Albums: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
@@ -32,7 +43,7 @@ const ArticleScreen = ({
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Album')}
onPress={() => navigation.push('Albums')}
style={styles.button}
>
Push album
@@ -45,7 +56,10 @@ const ArticleScreen = ({
Go back
</Button>
</View>
<Article author={{ name: route.params.author }} scrollEnabled={false} />
<Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
@@ -75,7 +89,7 @@ const AlbumsScreen = ({
Go back
</Button>
</View>
<Albums scrollEnabled={false} />
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
@@ -86,11 +100,32 @@ type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
function CustomHeader(props: StackHeaderProps) {
const { current, next } = props.scene.progress;
const progress = Animated.add(current, next || 0);
const opacity = progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [0, 1, 0],
});
return (
<>
<Header {...props} />
<Animated.Text style={[styles.banner, { opacity }]}>
Why hello there, pardner!
</Animated.Text>
</>
);
}
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
const { colors, dark } = useTheme();
return (
<SimpleStack.Navigator {...rest}>
<SimpleStack.Screen
@@ -98,6 +133,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params?.author}`,
header: CustomHeader,
headerTintColor: '#fff',
headerStyle: { backgroundColor: '#ff005d' },
headerBackTitleVisible: false,
@@ -126,16 +162,22 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
initialParams={{ author: 'Gandalf' }}
/>
<SimpleStack.Screen
name="Album"
name="Albums"
component={AlbumsScreen}
options={{
title: 'Album',
title: 'Albums',
headerBackTitle: 'Back',
headerTransparent: true,
headerBackground: () => (
<HeaderBackground style={{ backgroundColor: 'transparent' }}>
<HeaderBackground
style={{
backgroundColor: 'transparent',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}}
>
<BlurView
tint="light"
tint={dark ? 'dark' : 'light'}
intensity={75}
style={StyleSheet.absoluteFill}
/>
@@ -155,4 +197,10 @@ const styles = StyleSheet.create({
button: {
margin: 8,
},
banner: {
textAlign: 'center',
color: 'tomato',
backgroundColor: 'papayawhip',
padding: 4,
},
});

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button, Paragraph } from 'react-native-paper';
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
import {
@@ -15,6 +15,8 @@ type SimpleStackParams = {
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
@@ -40,7 +42,10 @@ const ArticleScreen = ({
Go back
</Button>
</View>
<Article author={{ name: route.params.author }} scrollEnabled={false} />
<Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};

View File

@@ -9,6 +9,7 @@ import {
ScrollViewProps,
Dimensions,
Platform,
ScaledSize,
} from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
@@ -40,15 +41,38 @@ const COVERS = [
];
export default function Albums(props: Partial<ScrollViewProps>) {
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
React.useEffect(() => {
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
Dimensions.addEventListener('change', onDimensionsChange);
return () => Dimensions.removeEventListener('change', onDimensionsChange);
}, []);
const ref = React.useRef<ScrollView>(null);
useScrollToTop(ref);
const itemSize = dimensions.width / Math.floor(dimensions.width / 150);
return (
<ScrollView ref={ref} contentContainerStyle={styles.content} {...props}>
{COVERS.map((source, i) => (
// eslint-disable-next-line react/no-array-index-key
<View key={i} style={styles.item}>
<View
// eslint-disable-next-line react/no-array-index-key
key={i}
style={[
styles.item,
Platform.OS !== 'web' && {
height: itemSize,
width: itemSize,
},
]}
>
<Image source={source} style={styles.photo} />
</View>
))}
@@ -76,10 +100,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
flexWrap: 'wrap',
},
item: {
height: Dimensions.get('window').width / 2,
width: '50%',
},
},
}),
photo: {

View File

@@ -0,0 +1,3 @@
import { BlurView } from 'expo-blur';
export default BlurView;

View File

@@ -0,0 +1,12 @@
import * as React from 'react';
import { View, ViewProps } from 'react-native';
type Props = ViewProps & {
tint: 'light' | 'dark';
intensity: number;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function BlurView({ tint, intensity, ...rest }: Props) {
return <View {...rest} />;
}

View File

@@ -1,20 +1,17 @@
import * as React from 'react';
import {
ScrollView,
AsyncStorage,
YellowBox,
Platform,
StatusBar,
I18nManager,
Dimensions,
ScaledSize,
Linking,
} from 'react-native';
// eslint-disable-next-line import/no-unresolved
import { enableScreens } from 'react-native-screens';
import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
import { Asset } from 'expo-asset';
import { MaterialIcons } from '@expo/vector-icons';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {
Provider as PaperProvider,
DefaultTheme as PaperLightTheme,
@@ -22,14 +19,14 @@ import {
Appbar,
List,
Divider,
Text,
} from 'react-native-paper';
import {
InitialState,
useLinking,
NavigationContainerRef,
NavigationContainer,
DefaultTheme,
DarkTheme,
PathConfig,
} from '@react-navigation/native';
import {
createDrawerNavigator,
@@ -37,12 +34,14 @@ import {
} from '@react-navigation/drawer';
import {
createStackNavigator,
Assets as StackAssets,
StackNavigationProp,
HeaderStyleInterpolators,
} from '@react-navigation/stack';
import { restartApp } from './Restart';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem';
import SimpleStack from './Screens/SimpleStack';
import ModalPresentationStack from './Screens/ModalPresentationStack';
import StackTransparent from './Screens/StackTransparent';
@@ -50,15 +49,20 @@ import StackHeaderCustomization from './Screens/StackHeaderCustomization';
import BottomTabs from './Screens/BottomTabs';
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import NotFound from './Screens/NotFound';
import DynamicTabs from './Screens/DynamicTabs';
import AuthFlow from './Screens/AuthFlow';
import CompatAPI from './Screens/CompatAPI';
import SettingsItem from './Shared/SettingsItem';
import MasterDetail from './Screens/MasterDetail';
import LinkComponent from './Screens/LinkComponent';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
enableScreens();
// @ts-ignore
global.REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED = true;
type RootDrawerParamList = {
Root: undefined;
Another: undefined;
@@ -66,6 +70,7 @@ type RootDrawerParamList = {
type RootStackParamList = {
Home: undefined;
NotFound: undefined;
} & {
[P in keyof typeof SCREENS]: undefined;
};
@@ -97,6 +102,10 @@ const SCREENS = {
title: 'Dynamic Tabs',
component: DynamicTabs,
},
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
},
AuthFlow: {
title: 'Auth Flow',
component: AuthFlow,
@@ -105,6 +114,10 @@ const SCREENS = {
title: 'Compat Layer',
component: CompatAPI,
},
LinkComponent: {
title: '<Link />',
component: LinkComponent,
},
};
const Drawer = createDrawerNavigator<RootDrawerParamList>();
@@ -113,42 +126,10 @@ const Stack = createStackNavigator<RootStackParamList>();
const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
Asset.loadAsync(StackAssets);
export default function App() {
const containerRef = React.useRef<NavigationContainerRef>();
// To test deep linking on, run the following in the Terminal:
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
const { getInitialState } = useLinking(containerRef, {
prefixes: LinkingPrefixes,
config: {
Root: {
path: '',
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
acc[name] = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
return acc;
},
{ Home: '' }
),
},
},
});
const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false);
const [isReady, setIsReady] = React.useState(Platform.OS === 'web');
const [initialState, setInitialState] = React.useState<
InitialState | undefined
>();
@@ -156,17 +137,18 @@ export default function App() {
React.useEffect(() => {
const restoreState = async () => {
try {
let state = await getInitialState();
const initialUrl = await Linking.getInitialURL();
if (Platform.OS !== 'web' && state === undefined) {
if (Platform.OS !== 'web' || initialUrl === null) {
const savedState = await AsyncStorage.getItem(
NAVIGATION_PERSISTENCE_KEY
);
state = savedState ? JSON.parse(savedState) : undefined;
}
if (state !== undefined) {
setInitialState(state);
const state = savedState ? JSON.parse(savedState) : undefined;
if (state !== undefined) {
setInitialState(state);
}
}
} finally {
try {
@@ -182,7 +164,7 @@ export default function App() {
};
restoreState();
}, [getInitialState]);
}, []);
const paperTheme = React.useMemo(() => {
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
@@ -214,7 +196,7 @@ export default function App() {
return null;
}
const isLargeScreen = dimensions.width > 900;
const isLargeScreen = dimensions.width >= 1024;
return (
<PaperProvider theme={paperTheme}>
@@ -222,7 +204,6 @@ export default function App() {
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
)}
<NavigationContainer
ref={containerRef}
initialState={initialState}
onStateChange={(state) =>
AsyncStorage.setItem(
@@ -231,6 +212,60 @@ export default function App() {
)
}
theme={theme}
linking={{
// To test deep linking on, run the following in the Terminal:
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
prefixes: LinkingPrefixes,
config: {
Root: {
path: '',
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<PathConfig>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
const path = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
acc[name] = {
path,
screens: {
Article: {
path: 'article/:author?',
parse: {
author: (author) =>
author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '),
},
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
Dialog: 'dialog',
},
};
return acc;
},
{
Home: '',
NotFound: '*',
}
),
},
},
}}
fallback={<Text>Loading</Text>}
>
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen
@@ -280,12 +315,7 @@ export default function App() {
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
// @ts-ignore
if (global.Expo) {
Updates.reloadFromCache();
} else {
RNRestart.Restart();
}
restartApp();
}}
/>
<Divider />
@@ -306,6 +336,7 @@ export default function App() {
(name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
/>
@@ -314,6 +345,11 @@ export default function App() {
</ScrollView>
)}
</Stack.Screen>
<Stack.Screen
name="NotFound"
component={NotFound}
options={{ title: 'Oops!' }}
/>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
(name) => (
<Stack.Screen

16
example/types/react-native-web.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
declare module 'react-native-web' {
export const AppRegistry: {
registerComponent(
name: string,
callback: () => React.ComponentType<any>
): void;
getApplication(
name: string,
options?: { initialProps: object }
): {
element: React.ReactElement;
getStyleElement(): React.ReactElement;
};
};
}

1
example/web/_redirects Normal file
View File

@@ -0,0 +1 @@
/* /index.html 200

View File

@@ -24,19 +24,21 @@ module.exports = async function (env, argv) {
);
Object.assign(config.resolve.alias, {
react: path.resolve(node_modules, 'react'),
'react': path.resolve(node_modules, 'react'),
'react-native': path.resolve(node_modules, 'react-native-web'),
'react-native-web': path.resolve(node_modules, 'react-native-web'),
'@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'),
});
fs.readdirSync(packages).forEach((name) => {
config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
packages,
name,
'src'
);
});
fs.readdirSync(packages)
.filter((name) => !name.startsWith('.'))
.forEach((name) => {
config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
packages,
name,
require(`../packages/${name}/package.json`).source
);
});
return config;
};

5
netlify.toml Normal file
View File

@@ -0,0 +1,5 @@
[build]
base = "/"
publish = "example/web-build"
command = "yarn example expo build:web"

View File

@@ -13,12 +13,12 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/satya164/react-navigation.git"
"url": "git+https://github.com/react-navigation/react-navigation.git"
},
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
"scripts": {
"lint": "eslint --ext '.js,.ts,.tsx' .",
"typescript": "tsc --noEmit",
"typescript": "tsc --noEmit --composite false",
"test": "jest",
"prerelease": "lerna run clean",
"release": "lerna publish",
@@ -27,24 +27,24 @@
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/preset-env": "^7.9.6",
"@babel/preset-flow": "^7.9.0",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@babel/runtime": "^7.9.6",
"@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^25.1.4",
"babel-jest": "^25.2.1",
"@types/jest": "^25.2.1",
"babel-jest": "^26.0.1",
"codecov": "^3.6.5",
"commitlint": "^8.3.5",
"core-js": "^3.6.4",
"core-js": "^3.6.5",
"eslint": "^6.8.0",
"eslint-config-satya164": "^3.1.6",
"husky": "^4.2.3",
"jest": "^25.1.0",
"eslint-config-satya164": "^3.1.7",
"husky": "^4.2.5",
"jest": "^26.0.1",
"lerna": "^3.20.2",
"prettier": "^2.0.2",
"typescript": "^3.7.5"
"prettier": "^2.0.5",
"typescript": "^3.8.3"
},
"resolutions": {
"react": "~16.9.0",

View File

@@ -3,6 +3,186 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.5.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.1...@react-navigation/bottom-tabs@5.5.2) (2020-06-06)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.0...@react-navigation/bottom-tabs@5.5.1) (2020-05-27)
### Bug Fixes
* fix type of style for various options ([9d822b9](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/9d822b95a6df797e2e63e481573e64ea7d0f9386))
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.7...@react-navigation/bottom-tabs@5.5.0) (2020-05-23)
### Features
* animate changes to tabBarVisible in BottomTabBar ([#8286](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/8286)) ([c1e46f8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/c1e46f8e331e0054995aa476455af204d02d4170))
* update react-native-safe-area-context to 1.0.0 ([#8182](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/8182)) ([d62fbfe](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/d62fbfe255140f16b182e8b54b276a7c96f2aec6))
## [5.4.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.6...@react-navigation/bottom-tabs@5.4.7) (2020-05-20)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.5...@react-navigation/bottom-tabs@5.4.6) (2020-05-20)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.5](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.4...@react-navigation/bottom-tabs@5.4.5) (2020-05-16)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.3...@react-navigation/bottom-tabs@5.4.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.2...@react-navigation/bottom-tabs@5.4.3) (2020-05-14)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.1...@react-navigation/bottom-tabs@5.4.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.0...@react-navigation/bottom-tabs@5.4.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.4...@react-navigation/bottom-tabs@5.4.0) (2020-05-08)
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/7971)
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.3...@react-navigation/bottom-tabs@5.3.4) (2020-05-05)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.2...@react-navigation/bottom-tabs@5.3.3) (2020-05-01)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.1...@react-navigation/bottom-tabs@5.3.2) (2020-05-01)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.0...@react-navigation/bottom-tabs@5.3.1) (2020-04-30)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.8...@react-navigation/bottom-tabs@5.3.0) (2020-04-30)
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/942d2be2c72720469475ce12ec8df23825994dbf))
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.2.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.6...@react-navigation/bottom-tabs@5.2.7) (2020-04-17)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.5...@react-navigation/bottom-tabs@5.2.6) (2020-04-08)
### Bug Fixes
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
## [5.2.5](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.4...@react-navigation/bottom-tabs@5.2.5) (2020-03-30)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.2.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.3...@react-navigation/bottom-tabs@5.2.4) (2020-03-23)
**Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines",
"version": "5.2.4",
"version": "5.5.2",
"keywords": [
"react-native-component",
"react-component",
@@ -15,11 +15,13 @@
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs",
"main": "lib/commonjs/index.js",
"react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts",
"files": [
"src",
"lib"
"lib",
"!**/__tests__"
],
"sideEffects": false,
"publishConfig": {
@@ -34,17 +36,17 @@
"react-native-iphone-x-helper": "^1.2.1"
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.3",
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.5.1",
"@types/color": "^3.0.1",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.0",
"react": "~16.9.0",
"react-native": "~0.61.5",
"react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0",
"typescript": "^3.7.5"
"react-native-safe-area-context": "^1.0.0",
"react-native-screens": "^2.7.0",
"typescript": "^3.8.3"
},
"peerDependencies": {
"@react-navigation/native": "^5.0.5",

View File

@@ -12,9 +12,10 @@ export { default as BottomTabBar } from './views/BottomTabBar';
/**
* Types
*/
export {
export type {
BottomTabNavigationOptions,
BottomTabNavigationProp,
BottomTabScreenProps,
BottomTabBarProps,
BottomTabBarOptions,
} from './types';

View File

@@ -1,9 +1,11 @@
import * as React from 'react';
import {
Animated,
TouchableWithoutFeedbackProps,
StyleProp,
TextStyle,
ViewStyle,
GestureResponderEvent,
} from 'react-native';
import {
NavigationHelpers,
@@ -12,6 +14,7 @@ import {
Descriptor,
TabNavigationState,
TabActionHelpers,
RouteProp,
} from '@react-navigation/native';
export type BottomTabNavigationEventMap = {
@@ -44,6 +47,14 @@ export type BottomTabNavigationProp<
> &
TabActionHelpers<ParamList>;
export type BottomTabScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: BottomTabNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type BottomTabNavigationOptions = {
/**
* Title text for the screen.
@@ -138,7 +149,7 @@ export type BottomTabBarOptions = {
*/
inactiveTintColor?: string;
/**
* Background olor for the active tab.
* Background color for the active tab.
*/
activeBackgroundColor?: string;
/**
@@ -187,7 +198,7 @@ export type BottomTabBarOptions = {
/**
* Style object for the tab bar container.
*/
style?: StyleProp<ViewStyle>;
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};
export type BottomTabBarProps = BottomTabBarOptions & {
@@ -196,6 +207,13 @@ export type BottomTabBarProps = BottomTabBarOptions & {
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
};
export type BottomTabBarButtonProps = TouchableWithoutFeedbackProps & {
export type BottomTabBarButtonProps = Omit<
TouchableWithoutFeedbackProps,
'onPress'
> & {
to?: string;
children: React.ReactNode;
onPress?: (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => void;
};

View File

@@ -14,6 +14,7 @@ import {
NavigationRouteContext,
CommonActions,
useTheme,
useLinkBuilder,
} from '@react-navigation/native';
import { useSafeArea } from 'react-native-safe-area-context';
@@ -50,6 +51,45 @@ export default function BottomTabBar({
tabStyle,
}: Props) {
const { colors } = useTheme();
const buildLink = useLinkBuilder();
const focusedRoute = state.routes[state.index];
const focusedDescriptor = descriptors[focusedRoute.key];
const focusedOptions = focusedDescriptor.options;
const [isKeyboardShown, setIsKeyboardShown] = React.useState(false);
const shouldShowTabBar =
focusedOptions.tabBarVisible !== false &&
!(keyboardHidesTabBar && isKeyboardShown);
const [isTabBarHidden, setIsTabBarHidden] = React.useState(!shouldShowTabBar);
const [visible] = React.useState(
() => new Animated.Value(shouldShowTabBar ? 1 : 0)
);
React.useEffect(() => {
if (shouldShowTabBar) {
Animated.timing(visible, {
toValue: 1,
duration: 250,
useNativeDriver,
}).start(({ finished }) => {
if (finished) {
setIsTabBarHidden(false);
}
});
} else {
setIsTabBarHidden(true);
Animated.timing(visible, {
toValue: 0,
duration: 200,
useNativeDriver,
}).start();
}
}, [shouldShowTabBar, visible]);
const [dimensions, setDimensions] = React.useState(() => {
const { height = 0, width = 0 } = Dimensions.get('window');
@@ -57,46 +97,16 @@ export default function BottomTabBar({
return { height, width };
});
const [layout, setLayout] = React.useState({
height: 0,
width: dimensions.width,
});
const [keyboardShown, setKeyboardShown] = React.useState(false);
const [visible] = React.useState(() => new Animated.Value(1));
const { routes } = state;
React.useEffect(() => {
if (keyboardShown) {
Animated.timing(visible, {
toValue: 0,
duration: 200,
useNativeDriver,
}).start();
}
}, [keyboardShown, visible]);
React.useEffect(() => {
const handleOrientationChange = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
const handleKeyboardShow = () => setKeyboardShown(true);
const handleKeyboardHide = () =>
Animated.timing(visible, {
toValue: 1,
duration: 250,
useNativeDriver,
}).start(({ finished }) => {
if (finished) {
setKeyboardShown(false);
}
});
Dimensions.addEventListener('change', handleOrientationChange);
const handleKeyboardShow = () => setIsKeyboardShown(true);
const handleKeyboardHide = () => setIsKeyboardShown(false);
if (Platform.OS === 'ios') {
Keyboard.addListener('keyboardWillShow', handleKeyboardShow);
Keyboard.addListener('keyboardWillHide', handleKeyboardHide);
@@ -116,7 +126,12 @@ export default function BottomTabBar({
Keyboard.removeListener('keyboardDidHide', handleKeyboardHide);
}
};
}, [visible]);
}, []);
const [layout, setLayout] = React.useState({
height: 0,
width: dimensions.width,
});
const handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout;
@@ -133,6 +148,7 @@ export default function BottomTabBar({
});
};
const { routes } = state;
const shouldUseHorizontalLabels = () => {
if (labelPosition) {
return labelPosition === 'beside-icon';
@@ -181,22 +197,19 @@ export default function BottomTabBar({
backgroundColor: colors.card,
borderTopColor: colors.border,
},
keyboardHidesTabBar
? {
// When the keyboard is shown, slide down the tab bar
transform: [
{
translateY: visible.interpolate({
inputRange: [0, 1],
outputRange: [layout.height, 0],
}),
},
],
// Absolutely position the tab bar so that the content is below it
// This is needed to avoid gap at bottom when the tab bar is hidden
position: keyboardShown ? 'absolute' : null,
}
: null,
{
transform: [
{
translateY: visible.interpolate({
inputRange: [0, 1],
outputRange: [layout.height + insets.bottom, 0],
}),
},
],
// Absolutely position the tab bar so that the content is below it
// This is needed to avoid gap at bottom when the tab bar is hidden
position: isTabBarHidden ? 'absolute' : null,
},
{
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
paddingBottom: insets.bottom,
@@ -204,7 +217,7 @@ export default function BottomTabBar({
},
style,
]}
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
pointerEvents={isTabBarHidden ? 'none' : 'auto'}
>
<View style={styles.content} onLayout={handleLayout}>
{routes.map((route, index) => {
@@ -260,6 +273,7 @@ export default function BottomTabBar({
onPress={onPress}
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
to={buildLink(route.name, route.params)}
testID={options.tabBarTestID}
allowFontScaling={allowFontScaling}
activeTintColor={activeTintColor}

View File

@@ -1,14 +1,16 @@
import React from 'react';
import {
View,
Text,
TouchableWithoutFeedback,
Animated,
StyleSheet,
Platform,
StyleProp,
ViewStyle,
TextStyle,
GestureResponderEvent,
} from 'react-native';
import { Route, useTheme } from '@react-navigation/native';
import { Link, Route, useTheme } from '@react-navigation/native';
import Color from 'color';
import TabBarIcon from './TabBarIcon';
@@ -37,6 +39,10 @@ type Props = {
size: number;
color: string;
}) => React.ReactNode;
/**
* URL to use for the link to the tab.
*/
to?: string;
/**
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
*/
@@ -50,13 +56,16 @@ type Props = {
*/
testID?: string;
/**
* Function to execute on press.
* Function to execute on press in React Native.
* On the web, this will use onClick.
*/
onPress: () => void;
onPress: (
e: React.MouseEvent<HTMLElement, MouseEvent> | GestureResponderEvent
) => void;
/**
* Function to execute on long press.
*/
onLongPress: () => void;
onLongPress: (e: GestureResponderEvent) => void;
/**
* Whether the label should be aligned with the icon horizontally.
*/
@@ -104,11 +113,48 @@ export default function BottomTabBarItem({
route,
label,
icon,
button = ({ children, style, ...rest }: BottomTabBarButtonProps) => (
<TouchableWithoutFeedback {...rest}>
<View style={style}>{children}</View>
</TouchableWithoutFeedback>
),
to,
button = ({
children,
style,
onPress,
to,
accessibilityRole,
...rest
}: BottomTabBarButtonProps) => {
if (Platform.OS === 'web' && to) {
// React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
// We need to use `onClick` to be able to prevent default browser handling of links.
return (
<Link
{...rest}
to={to}
style={[styles.button, style]}
onPress={(e: any) => {
if (
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
(e.button == null || e.button === 0) // ignore everything but left clicks
) {
e.preventDefault();
onPress?.(e);
}
}}
>
{children}
</Link>
);
} else {
return (
<TouchableWithoutFeedback
{...rest}
accessibilityRole={accessibilityRole}
onPress={onPress}
>
<View style={style}>{children}</View>
</TouchableWithoutFeedback>
);
}
},
accessibilityLabel,
testID,
onPress,
@@ -145,7 +191,7 @@ export default function BottomTabBarItem({
if (typeof label === 'string') {
return (
<Animated.Text
<Text
numberOfLines={1}
style={[
styles.label,
@@ -156,14 +202,10 @@ export default function BottomTabBarItem({
allowFontScaling={allowFontScaling}
>
{label}
</Animated.Text>
</Text>
);
}
if (typeof label === 'string') {
return label;
}
return label({ focused, color });
};
@@ -196,6 +238,7 @@ export default function BottomTabBarItem({
: inactiveBackgroundColor;
return button({
to,
onPress,
onLongPress,
testID,
@@ -248,4 +291,7 @@ const styles = StyleSheet.create({
fontSize: 12,
marginLeft: 20,
},
button: {
display: 'flex',
},
});

View File

@@ -1,7 +1,11 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { TabNavigationState, useTheme } from '@react-navigation/native';
import {
NavigationHelpersContext,
TabNavigationState,
useTheme,
} from '@react-navigation/native';
// eslint-disable-next-line import/no-unresolved
import { ScreenContainer } from 'react-native-screens';
@@ -71,17 +75,8 @@ export default class BottomTabView extends React.Component<Props, State> {
tabBarOptions,
state,
navigation,
descriptors,
} = this.props;
const { descriptors } = this.props;
const route = state.routes[state.index];
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarVisible === false) {
return null;
}
return tabBar({
...tabBarOptions,
state: state,
@@ -91,44 +86,46 @@ export default class BottomTabView extends React.Component<Props, State> {
};
render() {
const { state, descriptors, lazy } = this.props;
const { state, descriptors, navigation, lazy } = this.props;
const { routes } = state;
const { loaded } = this.state;
return (
<SafeAreaProviderCompat>
<View style={styles.container}>
<ScreenContainer style={styles.pages}>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
<NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat>
<View style={styles.container}>
<ScreenContainer style={styles.pages}>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
if (unmountOnBlur && !isFocused) {
return null;
}
if (unmountOnBlur && !isFocused) {
return null;
}
if (lazy && !loaded.includes(index) && !isFocused) {
// Don't render a screen if we've never navigated to it
return null;
}
if (lazy && !loaded.includes(index) && !isFocused) {
// Don't render a screen if we've never navigated to it
return null;
}
return (
<ResourceSavingScene
key={route.key}
style={StyleSheet.absoluteFill}
isVisible={isFocused}
>
<SceneContent isFocused={isFocused}>
{descriptor.render()}
</SceneContent>
</ResourceSavingScene>
);
})}
</ScreenContainer>
{this.renderTabBar()}
</View>
</SafeAreaProviderCompat>
return (
<ResourceSavingScene
key={route.key}
style={StyleSheet.absoluteFill}
isVisible={isFocused}
>
<SceneContent isFocused={isFocused}>
{descriptor.render()}
</SceneContent>
</ResourceSavingScene>
);
})}
</ScreenContainer>
{this.renderTabBar()}
</View>
</SafeAreaProviderCompat>
</NavigationHelpersContext.Provider>
);
}
}

View File

@@ -3,6 +3,176 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.26](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.25...@react-navigation/compat@5.1.26) (2020-06-06)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.25](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.24...@react-navigation/compat@5.1.25) (2020-05-27)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.24](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.23...@react-navigation/compat@5.1.24) (2020-05-23)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.23](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.22...@react-navigation/compat@5.1.23) (2020-05-20)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.22](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.21...@react-navigation/compat@5.1.22) (2020-05-20)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.21](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.20...@react-navigation/compat@5.1.21) (2020-05-16)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.20](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.19...@react-navigation/compat@5.1.20) (2020-05-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.19](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.18...@react-navigation/compat@5.1.19) (2020-05-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.18](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.17...@react-navigation/compat@5.1.18) (2020-05-10)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.17](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.16...@react-navigation/compat@5.1.17) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
## [5.1.16](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.15...@react-navigation/compat@5.1.16) (2020-05-08)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.15](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.14...@react-navigation/compat@5.1.15) (2020-05-05)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.13...@react-navigation/compat@5.1.14) (2020-05-01)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.12...@react-navigation/compat@5.1.13) (2020-05-01)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.11...@react-navigation/compat@5.1.12) (2020-04-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.10...@react-navigation/compat@5.1.11) (2020-04-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27)
### Bug Fixes
* fix typo in navigationOptions ([8cbb201](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/8cbb201f1a7fb90e45a078df6bc42ce4771cc6a6))
* spread parent params to children in compat navigator ([24febf6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/24febf6ea99be2e5f22005fdd2a82136d647255c)), closes [#6785](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/6785)
## [5.1.9](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.8...@react-navigation/compat@5.1.9) (2020-04-17)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.8](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.7...@react-navigation/compat@5.1.8) (2020-04-08)
### Bug Fixes
* use 1 as default in compatibility pop action ([4408117](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/44081172d440c713ad3543a2d5e1e18ebc8f72a4))
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.6...@react-navigation/compat@5.1.7) (2020-03-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.5...@react-navigation/compat@5.1.6) (2020-03-23)
**Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.1.6",
"version": "5.1.26",
"license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
"bugs": {
@@ -10,11 +10,13 @@
"homepage": "https://reactnavigation.org/docs/compatibility.html",
"main": "lib/commonjs/index.js",
"react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts",
"files": [
"src",
"lib"
"lib",
"!**/__tests__"
],
"sideEffects": false,
"publishConfig": {
@@ -25,11 +27,11 @@
"clean": "del lib"
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.3",
"@types/react": "^16.9.23",
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.5.1",
"@types/react": "^16.9.34",
"react": "~16.9.0",
"typescript": "^3.7.5"
"typescript": "^3.8.3"
},
"peerDependencies": {
"@react-navigation/native": "^5.0.5",

View File

@@ -7,6 +7,7 @@ import {
NavigationProp,
RouteProp,
EventMapBase,
NavigationRouteContext,
} from '@react-navigation/native';
import CompatScreen from './CompatScreen';
import ScreenPropsContext from './ScreenPropsContext';
@@ -67,6 +68,9 @@ export default function createCompatNavigatorFactory<
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
function Navigator({ screenProps }: { screenProps?: unknown }) {
const parentRouteParams = React.useContext(NavigationRouteContext)
?.params;
const screens = React.useMemo(
() =>
routeNames.map((name) => {
@@ -135,7 +139,7 @@ export default function createCompatNavigatorFactory<
<Pair.Screen
key={name}
name={name}
initialParams={initialParams}
initialParams={{ ...parentRouteParams, ...initialParams }}
options={screenOptions}
>
{({ navigation, route }) => (
@@ -148,7 +152,7 @@ export default function createCompatNavigatorFactory<
</Pair.Screen>
);
}),
[screenProps]
[parentRouteParams, screenProps]
);
return (
@@ -163,7 +167,7 @@ export default function createCompatNavigatorFactory<
);
}
Navigator.navigationOtions = parentNavigationOptions;
Navigator.navigationOptions = parentNavigationOptions;
return Navigator;
};

View File

@@ -57,7 +57,7 @@ export function push(routeName: string, params?: object, action?: never) {
});
}
export function pop(n: number) {
export function pop(n: number = 1) {
return StackActions.pop(typeof n === 'number' ? { n } : n);
}

View File

@@ -3,6 +3,206 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.10.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.9.0...@react-navigation/core@5.10.0) (2020-06-06)
### Bug Fixes
* catch missing params when they are required in navigate ([#8389](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8389)) ([8774ca9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/8774ca97e1da91e97677ecd816c85f66af296b93))
* make sure the wildcard pattern catches nested unmatched routes ([c3bd349](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3bd349d77688011c9c55027edd66c6f39de2ade))
* only use the query params for focused route in path ([2d66ef9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2d66ef93ec9923a452415c482c40e7c6b769917c))
* prevent state change being emitted unnecessarily ([ab1f79c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/ab1f79c096e94475a4da1acf1c850d04fb1bc4cf))
### Features
* add wildcard patterns for paths ([4fe72e3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/4fe72e3ce7bae9120d04e490401f3bad58ebdf5c)), closes [#8019](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8019)
# [5.9.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.2...@react-navigation/core@5.9.0) (2020-05-27)
### Features
* add ref to get current options in `ServerContainer` ([#8333](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8333)) ([0b1a718](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/0b1a718756e208d84b20e45ca56004332308ad54))
## [5.8.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.1...@react-navigation/core@5.8.2) (2020-05-23)
**Note:** Version bump only for package @react-navigation/core
## [5.8.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.0...@react-navigation/core@5.8.1) (2020-05-20)
**Note:** Version bump only for package @react-navigation/core
# [5.8.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.7.0...@react-navigation/core@5.8.0) (2020-05-20)
### Features
* add getCurrentOptions ([#8277](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8277)) ([d024ec6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/d024ec6d74dffe481ce6fde732c729e20c1668f4))
* add getCurrentRoute ([#8254](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8254)) ([7b25c8e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/7b25c8eb2e6f96128fd86b92615346ce55bedeca))
# [5.7.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.6.1...@react-navigation/core@5.7.0) (2020-05-16)
### Bug Fixes
* don't use Object.fromEntries ([51f4d11](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/51f4d11fdf4bd2bb06f8cd4094f051816590e62c))
### Features
* add a PathConfig type ([60cb3c9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/60cb3c9ba76d7ef166c9fe8b55f23728975b5b6e))
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.6.0...@react-navigation/core@5.6.1) (2020-05-14)
### Bug Fixes
* don't use flat since it's not supported in node ([21b397f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/21b397f0d6b96ec4875d3172f47533130bb08009))
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.2...@react-navigation/core@5.6.0) (2020-05-14)
### Bug Fixes
* ignore extra slashes in the pattern ([3c47716](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3c47716826d0dfa69dfa6112141c116723372ea1))
* ignore state updates when we're not mounted ([0149e85](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/0149e85a95b90c6a9d487fa753ddbf5d01c03e3d)), closes [#8226](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8226)
### Features
* merge path patterns for nested screens ([#8253](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8253)) ([acc9646](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/acc9646426fee53558d686dfbe5fd0e35361d8c0))
## [5.5.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.1...@react-navigation/core@5.5.2) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.0...@react-navigation/core@5.5.1) (2020-05-08)
### Bug Fixes
* avoid cleaning up state when a new navigator is mounted. fixes [#8195](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8195) ([f6d0676](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/f6d06768d3c36d1f5beaffcb660f3c259209f2e7))
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.4.0...@react-navigation/core@5.5.0) (2020-05-05)
### Features
* add support for optional params to linking ([#8196](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8196)) ([fcd1cc6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/fcd1cc64c151e4941f3f544a54b5048d853821f6))
* support params anywhere in path segement ([#8184](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8184)) ([3999fc2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3999fc28365c3a06a17d963c7be7fb7e897f99e0))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.5...@react-navigation/core@5.4.0) (2020-04-30)
### Bug Fixes
* handle empty paths when parsing ([c3fa83e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3fa83efe0d73db76365f8be3d6a8ca1d1289b71))
* parsing url ([bd35b4f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/bd35b4fc202c3868fb75c3675b62de67557089e1))
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27)
### Bug Fixes
* add config to enable redux devtools integration ([c9c825b](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c9c825bee61426635a28ee149eeeff3d628171cd))
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.3...@react-navigation/core@5.3.4) (2020-04-17)
### Bug Fixes
* add initial option for navigating to nested navigators ([004c7d7](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/004c7d7ab1f80faf04b2a1836ec6b79a5419e45f))
* add initial param for actions from deep link ([a3f7a5f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/a3f7a5feba2e6aa2158aeaea6cde73ae1603173e))
* handle initial: false for nested route after first initialization ([187aefe](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/187aefe9c400b499f920c212bf856414e25c5aaf))
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.2...@react-navigation/core@5.3.3) (2020-04-08)
### Bug Fixes
* switch order of focus and blur events. closes [#7963](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7963) ([ce3994c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/ce3994c82c28669d5742017eb7627e9adf996933))
* workaround warning about setState in another component in render ([d4fd906](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/d4fd906915cc20d6fb21508384c05a540d8644d8))
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.1...@react-navigation/core@5.3.2) (2020-03-30)
### Bug Fixes
* handle no path property and undefined query params ([#7911](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7911)) ([cd47915](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/cd47915861a56cd7eaa9ac79f5139cde56ca95a7))
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.0...@react-navigation/core@5.3.1) (2020-03-23)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/core",
"description": "Core utilities for building navigators",
"version": "5.3.1",
"version": "5.10.0",
"keywords": [
"react",
"react-native",
@@ -15,11 +15,13 @@
"homepage": "https://reactnavigation.org",
"main": "lib/commonjs/index.js",
"react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts",
"files": [
"src",
"lib"
"lib",
"!**/__tests__"
],
"publishConfig": {
"access": "public"
@@ -29,24 +31,23 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/routers": "^5.2.0",
"escape-string-regexp": "^2.0.0",
"query-string": "^6.11.1",
"@react-navigation/routers": "^5.4.7",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.5",
"query-string": "^6.12.1",
"react-is": "^16.13.0",
"shortid": "^2.2.15",
"use-subscription": "^1.4.0"
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@types/react": "^16.9.23",
"@react-native-community/bob": "^0.14.3",
"@types/react": "^16.9.34",
"@types/react-is": "^16.7.1",
"@types/shortid": "^0.0.29",
"@types/use-subscription": "^1.0.0",
"del-cli": "^3.0.0",
"react": "~16.9.0",
"react-native-testing-library": "^1.12.0",
"react-test-renderer": "~16.9.0",
"typescript": "^3.7.5"
"react-native-testing-library": "^1.13.2",
"react-test-renderer": "~16.13.1",
"typescript": "^3.8.3"
},
"peerDependencies": {
"react": "*"

View File

@@ -9,49 +9,26 @@ import {
} from '@react-navigation/routers';
import EnsureSingleNavigator from './EnsureSingleNavigator';
import NavigationBuilderContext from './NavigationBuilderContext';
import { ScheduleUpdateContext } from './useScheduleUpdate';
import useFocusedListeners from './useFocusedListeners';
import useDevTools from './useDevTools';
import useStateGetters from './useStateGetters';
import useOptionsGetters from './useOptionsGetters';
import useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState';
import isSerializable from './isSerializable';
import { NavigationContainerRef, NavigationContainerProps } from './types';
import useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState';
import NavigationStateContext from './NavigationStateContext';
type State = NavigationState | PartialState<NavigationState> | undefined;
const MISSING_CONTEXT_ERROR =
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
const DEVTOOLS_CONFIG_KEY =
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
const NOT_INITIALIZED_ERROR =
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
export const NavigationStateContext = React.createContext<{
isDefault?: true;
state?: NavigationState | PartialState<NavigationState>;
getKey: () => string | undefined;
setKey: (key: string) => void;
getState: () => NavigationState | PartialState<NavigationState> | undefined;
setState: (
state: NavigationState | PartialState<NavigationState> | undefined
) => void;
}>({
isDefault: true,
get getKey(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get setKey(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get getState(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get setState(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
});
let hasWarnedForSerialization = false;
/**
@@ -102,7 +79,7 @@ const BaseNavigationContainer = React.forwardRef(
independent,
children,
}: NavigationContainerProps,
ref: React.Ref<NavigationContainerRef>
ref?: React.Ref<NavigationContainerRef>
) {
const parent = React.useContext(NavigationStateContext);
@@ -112,7 +89,13 @@ const BaseNavigationContainer = React.forwardRef(
);
}
const [state, getState, setState] = useSyncState<State>(() =>
const [
state,
getState,
setState,
scheduleUpdate,
flushUpdates,
] = useSyncState<State>(() =>
getPartialState(initialState == null ? undefined : initialState)
);
@@ -136,7 +119,9 @@ const BaseNavigationContainer = React.forwardRef(
);
const { trackState, trackAction } = useDevTools({
enabled: false,
enabled:
// @ts-ignore
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
name: '@react-navigation',
reset,
state,
@@ -187,8 +172,21 @@ const BaseNavigationContainer = React.forwardRef(
return getStateForRoute('root');
}, [getStateForRoute]);
const getCurrentRoute = React.useCallback(() => {
let state = getRootState();
if (state === undefined) {
return undefined;
}
while (state.routes[state.index].state !== undefined) {
state = state.routes[state.index].state as NavigationState;
}
return state.routes[state.index];
}, [getRootState]);
const emitter = useEventEmitter();
const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({});
React.useImperativeHandle(ref, () => ({
...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce<
any
@@ -207,6 +205,10 @@ const BaseNavigationContainer = React.forwardRef(
dispatch,
canGoBack,
getRootState,
dangerouslyGetState: () => state,
dangerouslyGetParent: () => undefined,
getCurrentRoute,
getCurrentOptions,
}));
const builderContext = React.useMemo(
@@ -218,6 +220,11 @@ const BaseNavigationContainer = React.forwardRef(
[addFocusedListener, trackAction, addStateGetter]
);
const scheduleContext = React.useMemo(
() => ({ scheduleUpdate, flushUpdates }),
[scheduleUpdate, flushUpdates]
);
const context = React.useMemo(
() => ({
state,
@@ -225,10 +232,17 @@ const BaseNavigationContainer = React.forwardRef(
setState,
getKey,
setKey,
addOptionsGetter,
}),
[getKey, getState, setKey, setState, state]
[getKey, getState, setKey, setState, state, addOptionsGetter]
);
const onStateChangeRef = React.useRef(onStateChange);
React.useEffect(() => {
onStateChangeRef.current = onStateChange;
});
React.useEffect(() => {
if (process.env.NODE_ENV !== 'production') {
if (
@@ -255,19 +269,21 @@ const BaseNavigationContainer = React.forwardRef(
trackState(getRootState);
}
if (!isFirstMountRef.current && onStateChange) {
onStateChange(getRootState());
if (!isFirstMountRef.current && onStateChangeRef.current) {
onStateChangeRef.current(getRootState());
}
isFirstMountRef.current = false;
}, [onStateChange, trackState, getRootState, emitter, state]);
}, [trackState, getRootState, emitter, state]);
return (
<NavigationBuilderContext.Provider value={builderContext}>
<NavigationStateContext.Provider value={context}>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</NavigationStateContext.Provider>
</NavigationBuilderContext.Provider>
<ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationBuilderContext.Provider value={builderContext}>
<NavigationStateContext.Provider value={context}>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</NavigationStateContext.Provider>
</NavigationBuilderContext.Provider>
</ScheduleUpdateContext.Provider>
);
}
);

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
/**
* Context which holds the values for the current navigation tree.
* Intended for use in SSR. This is not safe to use on the client.
*/
const CurrentRenderContext = React.createContext<
{ options?: object } | undefined
>(undefined);
export default CurrentRenderContext;

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import { ParamListBase } from '@react-navigation/routers';
import { NavigationHelpers } from './types';
/**
* Context which holds the navigation helpers of the parent navigator.
* Navigators should use this context in their view component.
*/
const NavigationHelpersContext = React.createContext<
NavigationHelpers<ParamListBase> | undefined
>(undefined);
export default NavigationHelpersContext;

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import { NavigationState, PartialState } from '@react-navigation/routers';
const MISSING_CONTEXT_ERROR =
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
export default React.createContext<{
isDefault?: true;
state?: NavigationState | PartialState<NavigationState>;
getKey: () => string | undefined;
setKey: (key: string) => void;
getState: () => NavigationState | PartialState<NavigationState> | undefined;
setState: (
state: NavigationState | PartialState<NavigationState> | undefined
) => void;
addOptionsGetter?: (
key: string,
getter: () => object | undefined | null
) => void;
}>({
isDefault: true,
get getKey(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get setKey(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get getState(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get setState(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
});

View File

@@ -5,12 +5,13 @@ import {
NavigationState,
PartialState,
} from '@react-navigation/routers';
import { NavigationStateContext } from './BaseNavigationContainer';
import NavigationStateContext from './NavigationStateContext';
import NavigationContext from './NavigationContext';
import NavigationRouteContext from './NavigationRouteContext';
import StaticContainer from './StaticContainer';
import EnsureSingleNavigator from './EnsureSingleNavigator';
import { NavigationProp, RouteConfig, EventMapBase } from './types';
import useOptionsGetters from './useOptionsGetters';
type Props<
State extends NavigationState,
@@ -24,6 +25,7 @@ type Props<
};
getState: () => State;
setState: (state: State) => void;
options: object;
};
/**
@@ -40,11 +42,24 @@ export default function SceneView<
navigation,
getState,
setState,
options,
}: Props<State, ScreenOptions, EventMap>) {
const navigatorKeyRef = React.useRef<string | undefined>();
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
const optionsRef = React.useRef<object | undefined>(options);
React.useEffect(() => {
optionsRef.current = options;
}, [options]);
const getOptions = React.useCallback(() => optionsRef.current, []);
const { addOptionsGetter } = useOptionsGetters({
key: route.key,
getOptions,
});
const setKey = React.useCallback((key: string) => {
navigatorKeyRef.current = key;
}, []);
@@ -77,8 +92,16 @@ export default function SceneView<
setState: setCurrentState,
getKey,
setKey,
addOptionsGetter,
}),
[getCurrentState, getKey, route.state, setCurrentState, setKey]
[
getCurrentState,
getKey,
route.state,
setCurrentState,
setKey,
addOptionsGetter,
]
);
return (

View File

@@ -5,9 +5,8 @@ import {
NavigationState,
Router,
} from '@react-navigation/routers';
import BaseNavigationContainer, {
NavigationStateContext,
} from '../BaseNavigationContainer';
import BaseNavigationContainer from '../BaseNavigationContainer';
import NavigationStateContext from '../NavigationStateContext';
import MockRouter, { MockActions } from './__fixtures__/MockRouter';
import useNavigationBuilder from '../useNavigationBuilder';
import Screen from '../Screen';

View File

@@ -35,8 +35,10 @@ it('gets navigate action from state', () => {
author: 'jane',
},
screen: 'qux',
initial: true,
},
screen: 'bar',
initial: true,
},
},
type: 'NAVIGATE',
@@ -70,9 +72,11 @@ it('gets navigate action from state', () => {
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
screen: 'quz',
initial: false,
},
},
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -379,6 +379,8 @@ it("doesn't update state if action wasn't handled", () => {
});
it('cleans up state when the navigator unmounts', () => {
jest.useFakeTimers();
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -426,6 +428,8 @@ it('cleans up state when the navigator unmounts', () => {
<BaseNavigationContainer onStateChange={onStateChange} children={null} />
);
act(() => jest.runAllTimers());
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).lastCalledWith(undefined);
});
@@ -626,7 +630,7 @@ it('updates route params with setParams applied to parent', () => {
});
});
it('handles change in route names', () => {
it('handles change in route names', async () => {
const TestNavigator = (props: any): any => {
useNavigationBuilder(MockRouter, props);
return null;
@@ -635,7 +639,7 @@ it('handles change in route names', () => {
const onStateChange = jest.fn();
const root = render(
<BaseNavigationContainer onStateChange={onStateChange}>
<BaseNavigationContainer>
<TestNavigator initialRouteName="bar">
<Screen name="foo" component={jest.fn()} />
<Screen name="bar" component={jest.fn()} />
@@ -737,6 +741,366 @@ it('navigates to nested child in a navigator', () => {
);
});
it('navigates to nested child in a navigator with initial: false', () => {
const TestRouter: typeof MockRouter = (options) => {
const router = MockRouter(options);
return {
...router,
getStateForAction(state, action, options) {
switch (action.type) {
case 'NAVIGATE': {
if (!options.routeNames.includes(action.payload.name as any)) {
return null;
}
const routes = [
...state.routes,
{
key: String(MockRouterKey.current++),
name: action.payload.name,
params: action.payload.params,
},
];
return {
...state,
index: routes.length - 1,
routes,
};
}
default:
return router.getStateForAction(state, action, options);
}
},
} as typeof router;
};
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestComponent = ({ route }: any): any =>
`[${route.name}, ${JSON.stringify(route.params)}]`;
const onStateChange = jest.fn();
const navigation = React.createRef<NavigationContainerRef>();
const first = render(
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 0,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '1',
routeNames: ['foo-a', 'foo-b'],
routes: [
{
key: 'foo-a',
name: 'foo-a',
},
{
key: 'foo-b',
name: 'foo-b',
},
],
stale: false,
type: 'test',
},
},
{ key: 'bar', name: 'bar' },
],
stale: false,
type: 'test',
});
act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
screen: 'bar-b',
params: { test: 42 },
})
);
expect(first).toMatchInlineSnapshot(
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
);
expect(navigation.current?.getRootState()).toEqual({
index: 2,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar' },
{
key: '2',
name: 'bar',
params: { params: { test: 42 }, screen: 'bar-b' },
state: {
index: 1,
key: '3',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a',
name: 'bar-a',
params: { lol: 'why' },
},
{
key: 'bar-b',
name: 'bar-b',
params: { some: 'stuff', test: 42 },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
const second = render(
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(second).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 0,
key: '4',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '5',
routeNames: ['foo-a', 'foo-b'],
routes: [
{ key: 'foo-a', name: 'foo-a' },
{ key: 'foo-b', name: 'foo-b' },
],
stale: false,
type: 'test',
},
},
{ key: 'bar', name: 'bar' },
],
stale: false,
type: 'test',
});
act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
screen: 'bar-b',
params: { test: 42 },
initial: false,
})
);
expect(second).toMatchInlineSnapshot(`"[bar-b, {\\"test\\":42}]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 2,
key: '4',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar' },
{
key: '6',
name: 'bar',
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
state: {
index: 2,
key: '7',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a',
name: 'bar-a',
params: { lol: 'why' },
},
{
key: 'bar-b',
name: 'bar-b',
params: { some: 'stuff' },
},
{ key: '8', name: 'bar-b', params: { test: 42 } },
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
const third = render(
<BaseNavigationContainer
ref={navigation}
initialState={{
index: 1,
routes: [
{ name: 'foo' },
{
name: 'bar',
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
state: {
index: 1,
key: '7',
routes: [
{
name: 'bar-a',
params: { lol: 'why' },
},
{
name: 'bar-b',
params: { some: 'stuff' },
},
],
type: 'test',
},
},
],
type: 'test',
}}
>
<TestNavigator>
<Screen name="foo" component={TestComponent} />
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(third).toMatchInlineSnapshot(`"[bar-b, {\\"some\\":\\"stuff\\"}]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 1,
key: '11',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo-9', name: 'foo' },
{
key: 'bar-10',
name: 'bar',
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
state: {
index: 1,
key: '14',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a-12',
name: 'bar-a',
params: { lol: 'why' },
},
{
key: 'bar-b-13',
name: 'bar-b',
params: { some: 'stuff' },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
});
it('gives access to internal state', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -1126,3 +1490,128 @@ it("doesn't throw if children is null", () => {
expect(() => render(element).update(element)).not.toThrowError();
});
it('returns currently focused route with getCurrentRoute', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => null;
const navigation = React.createRef<NavigationContainerRef>();
const container = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="bar" options={{ a: 'b' }}>
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestScreen}
options={{ sample: 'data' }}
/>
</TestNavigator>
)}
</Screen>
<Screen name="xux" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
render(container).update(container);
expect(navigation.current?.getCurrentRoute()).toEqual({
key: 'bar-a',
name: 'bar-a',
});
});
it("returns currently focused route's options with getCurrentOptions", () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => null;
const navigation = React.createRef<NavigationContainerRef>();
const container = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="bar" options={{ a: 'b' }}>
{() => (
<TestNavigator
initialRouteName="bar-a"
screenOptions={() => ({ sample2: 'data' })}
>
<Screen
name="bar-a"
component={TestScreen}
options={{ sample: 'data' }}
/>
</TestNavigator>
)}
</Screen>
<Screen name="xux" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
render(container).update(container);
expect(navigation.current?.getCurrentOptions()).toEqual({
sample: 'data',
sample2: 'data',
});
});
it('does not throw if while getting current options with no options defined', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => null;
const navigation = React.createRef<NavigationContainerRef>();
const container = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="bar" options={{ a: 'b' }}>
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-b"
component={TestScreen}
options={{ wrongKey: true }}
/>
<Screen name="bar-a" component={TestScreen} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(container).update(container);
expect(navigation.current?.getCurrentOptions()).toEqual({});
});
it('does not throw if while getting current options with empty container', () => {
const navigation = React.createRef<NavigationContainerRef>();
// @ts-ignore
const container = <BaseNavigationContainer ref={navigation} />;
render(container).update(container);
expect(navigation.current?.getCurrentOptions()).toEqual(undefined);
});

View File

@@ -262,8 +262,10 @@ it('sets initial options with setOptions', () => {
};
const TestScreen = ({ navigation }: any): any => {
navigation.setOptions({
title: 'Hello world',
React.useEffect(() => {
navigation.setOptions({
title: 'Hello world',
});
});
return 'Test screen';
@@ -315,12 +317,12 @@ it('updates options with setOptions', () => {
};
const TestScreen = ({ navigation }: any): any => {
navigation.setOptions({
title: 'Hello world',
description: 'Something here',
});
React.useEffect(() => {
navigation.setOptions({
title: 'Hello world',
description: 'Something here',
});
const timer = setTimeout(() =>
navigation.setOptions({
title: 'Hello again',

View File

@@ -97,6 +97,69 @@ it('fires focus and blur events in root navigator', () => {
expect(fourthBlurCallback).toBeCalledTimes(0);
});
it('fires focus event after blur', () => {
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation, descriptors } = useNavigationBuilder(
MockRouter,
props
);
React.useImperativeHandle(ref, () => navigation, [navigation]);
return state.routes.map((route) => descriptors[route.key].render());
});
const callback = jest.fn();
const Test = ({ route, navigation }: any) => {
React.useEffect(
() =>
navigation.addListener('focus', () => callback(route.name, 'focus')),
[navigation, route.name]
);
React.useEffect(
() => navigation.addListener('blur', () => callback(route.name, 'blur')),
[navigation, route.name]
);
return null;
};
const navigation = React.createRef<any>();
const element = (
<BaseNavigationContainer>
<TestNavigator ref={navigation}>
<Screen name="first" component={Test} />
<Screen name="second" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
expect(callback.mock.calls).toEqual([['first', 'focus']]);
act(() => navigation.current.navigate('second'));
expect(callback.mock.calls).toEqual([
['first', 'focus'],
['first', 'blur'],
['second', 'focus'],
]);
act(() => navigation.current.navigate('first'));
expect(callback.mock.calls).toEqual([
['first', 'focus'],
['first', 'blur'],
['second', 'focus'],
['second', 'blur'],
['first', 'focus'],
]);
});
it('fires focus and blur events in nested navigator', () => {
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation, descriptors } = useNavigationBuilder(

View File

@@ -3,6 +3,7 @@ import { PartialState, NavigationState } from '@react-navigation/routers';
type NavigateParams = {
screen?: string;
params?: NavigateParams;
initial?: boolean;
};
type NavigateAction = {
@@ -35,6 +36,7 @@ export default function getActionFromState(
}
route = current.routes[current.routes.length - 1];
params.initial = current.routes.length === 1;
params.screen = route.name;
if (route.state) {

View File

@@ -4,19 +4,31 @@ import {
PartialState,
Route,
} from '@react-navigation/routers';
import { PathConfig } from './types';
type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
type StringifyConfig = Record<string, (value: any) => string>;
type Options = {
[routeName: string]:
| string
| {
path?: string;
stringify?: StringifyConfig;
screens?: Options;
};
type OptionsItem = PathConfig[string];
type ConfigItem = {
pattern?: string;
stringify?: StringifyConfig;
screens?: Record<string, ConfigItem>;
};
const getActiveRoute = (state: State): { name: string; params?: object } => {
const route =
typeof state.index === 'number'
? state.routes[state.index]
: state.routes[state.routes.length - 1];
if (route.state) {
return getActiveRoute(route.state);
}
return route;
};
/**
@@ -48,120 +60,221 @@ type Options = {
*/
export default function getPathFromState(
state?: State,
options: Options = {}
options: PathConfig = {}
): string {
if (state === undefined) {
throw Error('NavigationState not passed');
}
let path = '/';
// Create a normalized configs array which will be easier to use
const configs = createNormalizedConfigs(options);
let path = '/';
let current: State | undefined = state;
const allParams: Record<string, any> = {};
while (current) {
let index = typeof current.index === 'number' ? current.index : 0;
let route = current.routes[index] as Route<string> & {
state?: State;
};
let currentOptions = options;
let pattern = route.name;
while (route.name in currentOptions) {
if (typeof currentOptions[route.name] === 'string') {
pattern = currentOptions[route.name] as string;
break;
} else if (typeof currentOptions[route.name] === 'object') {
// if there is no `screens` property, we return pattern
if (
!(currentOptions[route.name] as {
screens: Options;
}).screens
) {
pattern = (currentOptions[route.name] as { path: string }).path;
break;
let pattern: string | undefined;
let focusedParams: Record<string, any> | undefined;
let focusedRoute = getActiveRoute(state);
let currentOptions = configs;
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
let nestedRouteNames = [];
let hasNext = true;
while (route.name in currentOptions && hasNext) {
pattern = currentOptions[route.name].pattern;
nestedRouteNames.push(route.name);
if (route.params) {
const stringify = currentOptions[route.name]?.stringify;
const currentParams = fromEntries(
Object.entries(route.params).map(([key, value]) => [
key,
stringify?.[key] ? stringify[key](value) : String(value),
])
);
if (pattern) {
Object.assign(allParams, currentParams);
}
if (focusedRoute === route) {
// If this is the focused route, keep the params for later use
// We save it here since it's been stringified already
focusedParams = { ...currentParams };
pattern
?.split('/')
.filter((p) => p.startsWith(':'))
// eslint-disable-next-line no-loop-func
.forEach((p) => {
const name = getParamName(p);
// Remove the params present in the pattern since we'll only use the rest for query string
if (focusedParams) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete focusedParams[name];
}
});
}
}
// If there is no `screens` property or no nested state, we return pattern
if (!currentOptions[route.name].screens || route.state === undefined) {
hasNext = false;
} else {
index =
typeof route.state.index === 'number'
? route.state.index
: route.state.routes.length - 1;
const nextRoute = route.state.routes[index];
const nestedConfig = currentOptions[route.name].screens;
// if there is config for next route name, we go deeper
if (nestedConfig && nextRoute.name in nestedConfig) {
route = nextRoute as Route<string> & { state?: State };
currentOptions = nestedConfig;
} else {
// if it is the end of state, we return pattern
if (route.state === undefined) {
pattern = (currentOptions[route.name] as { path: string }).path;
break;
} else {
index =
typeof route.state.index === 'number' ? route.state.index : 0;
const nextRoute = route.state.routes[index];
const deeperConfig = (currentOptions[route.name] as {
screens: Options;
}).screens;
// if there is config for next route name, we go deeper
if (nextRoute.name in deeperConfig) {
route = nextRoute as Route<string> & { state?: State };
currentOptions = deeperConfig;
} else {
// if not, there is no sense in going deeper in config
pattern = (currentOptions[route.name] as { path: string }).path;
break;
}
}
// If not, there is no sense in going deeper in config
hasNext = false;
}
}
}
// we don't add empty path strings to path
if (pattern !== '') {
const config =
currentOptions[route.name] !== undefined
? (currentOptions[route.name] as { stringify?: StringifyConfig })
.stringify
: undefined;
if (pattern === undefined) {
pattern = nestedRouteNames.join('/');
}
const params = route.params
? // Stringify all of the param values before we use them
Object.entries(route.params).reduce<{
[key: string]: string;
}>((acc, [key, value]) => {
acc[key] = config?.[key] ? config[key](value) : String(value);
return acc;
}, {})
: undefined;
if (currentOptions[route.name] !== undefined) {
path += pattern
.split('/')
.map((p) => {
const name = getParamName(p);
if (currentOptions[route.name] !== undefined) {
path += pattern
.split('/')
.map((p) => {
const name = p.replace(/^:/, '');
// We don't know what to show for wildcard patterns
// Showing the route name seems ok, though whatever we show here will be incorrect
// Since the page doesn't actually exist
if (p === '*') {
return route.name;
}
// If the path has a pattern for a param, put the param in the path
if (params && name in params && p.startsWith(':')) {
const value = params[name];
// Remove the used value from the params object since we'll use the rest for query string
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete params[name];
return encodeURIComponent(value);
// If the path has a pattern for a param, put the param in the path
if (p.startsWith(':')) {
const value = allParams[name];
if (value === undefined && p.endsWith('?')) {
// Optional params without value assigned in route.params should be ignored
return '';
}
return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
return encodeURIComponent(value);
}
return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
}
if (!focusedParams) {
focusedParams = focusedRoute.params;
}
if (route.state) {
path += '/';
} else if (focusedParams) {
for (let param in focusedParams) {
if (focusedParams[param] === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete focusedParams[param];
}
}
if (route.state) {
path += '/';
} else if (params) {
const query = queryString.stringify(params);
const query = queryString.stringify(focusedParams);
if (query) {
path += `?${query}`;
}
if (query) {
path += `?${query}`;
}
}
current = route.state;
}
path =
path !== '/' && path.slice(path.length - 1) === '/'
? path.slice(0, -1)
: path;
// Remove multiple as well as trailing slashes
path = path.replace(/\/+/g, '/');
path = path.length > 1 ? path.replace(/\/$/, '') : path;
return path;
}
// Object.fromEntries is not available in older iOS versions
const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
entries.reduce((acc, [k, v]) => {
acc[k] = v;
return acc;
}, {} as Record<K, V>);
const getParamName = (pattern: string) =>
pattern.replace(/^:/, '').replace(/\?$/, '');
const joinPaths = (...paths: string[]): string =>
([] as string[])
.concat(...paths.map((p) => p.split('/')))
.filter(Boolean)
.join('/');
const createConfigItem = (
config: OptionsItem | string,
parentPattern?: string
): ConfigItem => {
if (typeof config === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
return { pattern };
}
// If an object is specified as the value (e.g. Foo: { ... }),
// It can have `path` property and `screens` prop which has nested configs
const pattern =
config.exact !== true && parentPattern && config.path
? joinPaths(parentPattern, config.path)
: config.path;
const screens = config.screens
? createNormalizedConfigs(config.screens, pattern)
: undefined;
return {
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
pattern: pattern?.split('/').filter(Boolean).join('/'),
stringify: config.stringify,
screens,
};
};
const createNormalizedConfigs = (
options: PathConfig,
pattern?: string
): Record<string, ConfigItem> =>
fromEntries(
Object.entries(options).map(([name, c]) => {
const result = createConfigItem(c, pattern);
return [name, result];
})
);

View File

@@ -5,25 +5,17 @@ import {
PartialState,
InitialState,
} from '@react-navigation/routers';
import { PathConfig } from './types';
type ParseConfig = Record<string, (value: string) => any>;
type Options = {
[routeName: string]:
| string
| {
path?: string;
parse?: ParseConfig;
screens?: Options;
initialRouteName?: string;
};
};
type RouteConfig = {
match: RegExp;
screen: string;
regex?: RegExp;
path: string;
pattern: string;
routeNames: string[];
parse: ParseConfig | undefined;
parse?: ParseConfig;
};
type InitialRouteConfig = {
@@ -56,63 +48,128 @@ type ResultState = PartialState<NavigationState> & {
*/
export default function getStateFromPath(
path: string,
options: Options = {}
options: PathConfig = {}
): ResultState | undefined {
if (path === '') {
let initialRoutes: InitialRouteConfig[] = [];
// Create a normalized configs array which will be easier to use
const configs = ([] as RouteConfig[])
.concat(
...Object.keys(options).map((key) =>
createNormalizedConfigs(key, options, [], initialRoutes)
)
)
.sort((a, b) => {
// Sort config so that:
// - the most exhaustive ones are always at the beginning
// - patterns with wildcard are always at the end
// If one of the patterns starts with the other, it's more exhaustive
// So move it up
if (a.pattern.startsWith(b.pattern)) {
return 1;
}
if (b.pattern.startsWith(a.pattern)) {
return 1;
}
const aParts = a.pattern.split('/');
const bParts = b.pattern.split('/');
const aWildcardIndex = aParts.indexOf('*');
const bWildcardIndex = bParts.indexOf('*');
// If only one of the patterns has a wildcard, move it down in the list
if (aWildcardIndex === -1 && bWildcardIndex !== -1) {
return -1;
}
if (aWildcardIndex !== -1 && bWildcardIndex === -1) {
return 1;
}
if (aWildcardIndex === bWildcardIndex) {
// If `b` has more `/`, it's more exhaustive
// So we move it up in the list
return bParts.length - aParts.length;
}
// If the wildcard appears later in the pattern (has higher index), it's more specific
// So we move it up in the list
return bWildcardIndex - aWildcardIndex;
});
let remaining = path
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*$/, ''); // Remove query params which we will handle later
// Make sure there is a trailing slash
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works
// When handling empty path, we should only look at the root level config
const match = configs.find(
(config) =>
config.path === '' &&
config.routeNames.every(
// Make sure that none of the parent configs have a non-empty path defined
(name) => !configs.find((c) => c.screen === name)?.path
)
);
if (match) {
return createNestedStateObject(
match.routeNames.map((name, i, self) => {
if (i === self.length - 1) {
return { name, params: parseQueryParams(path, match.parse) };
}
return { name };
}),
initialRoutes
);
}
return undefined;
}
let initialRoutes: InitialRouteConfig[] = [];
// Create a normalized configs array which will be easier to use
const configs = ([] as RouteConfig[]).concat(
...Object.keys(options).map((key) =>
createNormalizedConfigs(key, options, [], initialRoutes)
)
);
let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined;
let remaining = path
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*/, ''); // Remove query params which we will handle later
// We try to match the paths in 2 passes
// In first pass, we match the whole path against the regex instead of segments
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
const { routeNames, allParams, remainingPath } = matchAgainstConfigs(
remaining,
configs.map((c) => ({
...c,
// Add `$` to the regex to make sure it matches till end of the path and not just beginning
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
}))
);
if (routeNames !== undefined) {
// This will always be empty if full path matched
remaining = remainingPath;
current = createNestedStateObject(
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
result = current;
}
// In second pass, we divide the path into segments and match piece by piece
// This preserves the old behaviour, but we should remove it in next major
while (remaining) {
let routeNames: string[] | undefined;
let params: Record<string, any> | undefined;
let { routeNames, allParams, remainingPath } = matchAgainstConfigs(
remaining,
configs
);
// Go through all configs, and see if the next path segment matches our regex
for (const config of configs) {
const match = remaining.match(config.match);
// If our regex matches, we need to extract params from the path
if (match) {
routeNames = [...config.routeNames];
const paramPatterns = config.pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const key = p.replace(/^:/, '');
const value = match[i + 1]; // The param segments start from index 1 in the regex match result
acc[key] =
config.parse && config.parse[key]
? config.parse[key](value)
: value;
return acc;
}, {});
}
// Remove the matched segment from the remaining path
remaining = remaining.replace(match[0], '');
break;
}
}
remaining = remainingPath;
// If we hadn't matched any segments earlier, use the path as route name
if (routeNames === undefined) {
@@ -123,35 +180,11 @@ export default function getStateFromPath(
remaining = segments.join('/');
}
let state: InitialState;
let routeName = routeNames.shift() as string;
let initialRoute = findInitialRoute(routeName, initialRoutes);
state = createNestedState(
initialRoute,
routeName,
routeNames.length === 0,
params
const state = createNestedStateObject(
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
if (routeNames.length > 0) {
let nestedState = state;
while ((routeName = routeNames.shift() as string)) {
initialRoute = findInitialRoute(routeName, initialRoutes);
nestedState.routes[nestedState.index || 0].state = createNestedState(
initialRoute,
routeName,
routeNames.length === 0,
params
);
if (routeNames.length > 0) {
nestedState = nestedState.routes[nestedState.index || 0]
.state as InitialState;
}
}
}
if (current) {
// The state should be nested inside the deepest route we parsed before
while (current?.routes[current.index || 0].state) {
@@ -172,74 +205,118 @@ export default function getStateFromPath(
return undefined;
}
const query = path.split('?')[1];
if (query) {
while (current?.routes[current.index || 0].state) {
// The query params apply to the deepest route
current = current.routes[current.index || 0].state;
}
const route = (current as PartialState<NavigationState>).routes[
current?.index || 0
];
const params = queryString.parse(query);
const parseFunction = findParseConfigForRoute(route.name, configs);
if (parseFunction) {
Object.keys(params).forEach((name) => {
if (parseFunction[name] && typeof params[name] === 'string') {
params[name] = parseFunction[name](params[name] as string);
}
});
}
const route = findFocusedRoute(current);
const params = parseQueryParams(
path,
findParseConfigForRoute(route.name, configs)
);
if (params) {
route.params = { ...route.params, ...params };
}
return result;
}
function createNormalizedConfigs(
key: string,
routeConfig: Options,
const joinPaths = (...paths: string[]): string =>
([] as string[])
.concat(...paths.map((p) => p.split('/')))
.filter(Boolean)
.join('/');
const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
let routeNames: string[] | undefined;
let allParams: Record<string, any> | undefined;
let remainingPath = remaining;
// Go through all configs, and see if the next path segment matches our regex
for (const config of configs) {
if (!config.regex) {
continue;
}
const match = remainingPath.match(config.regex);
// If our regex matches, we need to extract params from the path
if (match) {
routeNames = [...config.routeNames];
const paramPatterns = config.pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
acc[p] = value;
return acc;
}, {});
}
remainingPath = remainingPath.replace(match[1], '');
break;
}
}
return { routeNames, allParams, remainingPath };
};
const createNormalizedConfigs = (
screen: string,
routeConfig: PathConfig,
routeNames: string[] = [],
initials: InitialRouteConfig[]
): RouteConfig[] {
initials: InitialRouteConfig[],
parentPattern?: string
): RouteConfig[] => {
const configs: RouteConfig[] = [];
routeNames.push(key);
routeNames.push(screen);
const value = routeConfig[key];
const config = routeConfig[screen];
if (typeof value === 'string') {
if (typeof config === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
if (value !== '') {
configs.push(createConfigItem(routeNames, value));
}
} else if (typeof value === 'object') {
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
configs.push(createConfigItem(screen, routeNames, pattern, config));
} else if (typeof config === 'object') {
let pattern: string | undefined;
// if an object is specified as the value (e.g. Foo: { ... }),
// it can have `path` property and
// it could have `screens` prop which has nested configs
if (value.path && value.path !== '') {
configs.push(createConfigItem(routeNames, value.path, value.parse));
if (typeof config.path === 'string') {
pattern =
config.exact !== true && parentPattern
? joinPaths(parentPattern, config.path)
: config.path;
configs.push(
createConfigItem(screen, routeNames, pattern, config.path, config.parse)
);
}
if (value.screens) {
if (config.screens) {
// property `initialRouteName` without `screens` has no purpose
if (value.initialRouteName) {
if (config.initialRouteName) {
initials.push({
initialRouteName: value.initialRouteName,
connectedRoutes: Object.keys(value.screens),
initialRouteName: config.initialRouteName,
connectedRoutes: Object.keys(config.screens),
});
}
Object.keys(value.screens).forEach((nestedConfig) => {
Object.keys(config.screens).forEach((nestedConfig) => {
const result = createNormalizedConfigs(
nestedConfig,
value.screens as Options,
config.screens as PathConfig,
routeNames,
initials
initials,
pattern
);
configs.push(...result);
});
}
@@ -248,43 +325,62 @@ function createNormalizedConfigs(
routeNames.pop();
return configs;
}
};
function createConfigItem(
const createConfigItem = (
screen: string,
routeNames: string[],
pattern: string,
path: string,
parse?: ParseConfig
): RouteConfig {
const match = new RegExp(
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
);
): RouteConfig => {
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
pattern = pattern.split('/').filter(Boolean).join('/');
const regex = pattern
? new RegExp(
`^(${pattern
.split('/')
.map((it) => {
if (it.startsWith(':')) {
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
}
return `${it === '*' ? '.*' : escape(it)}\\/`;
})
.join('')})`
)
: undefined;
return {
match,
screen,
regex,
pattern,
path,
// The routeNames array is mutated, so copy it to keep the current state
routeNames: [...routeNames],
parse,
};
}
};
function findParseConfigForRoute(
const findParseConfigForRoute = (
routeName: string,
flatConfig: RouteConfig[]
): ParseConfig | undefined {
): ParseConfig | undefined => {
for (const config of flatConfig) {
if (routeName === config.routeNames[config.routeNames.length - 1]) {
return config.parse;
}
}
return undefined;
}
// tries to find an initial route connected with the one passed
function findInitialRoute(
return undefined;
};
// Try to find an initial route connected with the one passed
const findInitialRoute = (
routeName: string,
initialRoutes: InitialRouteConfig[]
): string | undefined {
): string | undefined => {
for (const config of initialRoutes) {
if (config.connectedRoutes.includes(routeName)) {
return config.initialRouteName === routeName
@@ -293,28 +389,25 @@ function findInitialRoute(
}
}
return undefined;
}
};
// returns nested state object with values depending on whether
// returns state object with values depending on whether
// it is the end of state and if there is initialRoute for this level
function createNestedState(
const createStateObject = (
initialRoute: string | undefined,
routeName: string,
isEmpty: boolean,
params?: Record<string, any> | undefined
): InitialState {
params: Record<string, any> | undefined,
isEmpty: boolean
): InitialState => {
if (isEmpty) {
if (initialRoute) {
return {
index: 1,
routes: [
{ name: initialRoute },
{ name: routeName as string, ...(params && { params }) },
],
routes: [{ name: initialRoute }, { name: routeName as string, params }],
};
} else {
return {
routes: [{ name: routeName as string, ...(params && { params }) }],
routes: [{ name: routeName as string, params }],
};
}
} else {
@@ -323,11 +416,130 @@ function createNestedState(
index: 1,
routes: [
{ name: initialRoute },
{ name: routeName as string, state: { routes: [] } },
{ name: routeName as string, params, state: { routes: [] } },
],
};
} else {
return { routes: [{ name: routeName as string, state: { routes: [] } }] };
return {
routes: [{ name: routeName as string, params, state: { routes: [] } }],
};
}
}
}
};
const createNestedStateObject = (
routes: { name: string; params?: object }[],
initialRoutes: InitialRouteConfig[]
) => {
let state: InitialState;
let route = routes.shift() as { name: string; params?: object };
let initialRoute = findInitialRoute(route.name, initialRoutes);
state = createStateObject(
initialRoute,
route.name,
route.params,
routes.length === 0
);
if (routes.length > 0) {
let nestedState = state;
while ((route = routes.shift() as { name: string; params?: object })) {
initialRoute = findInitialRoute(route.name, initialRoutes);
const nestedStateIndex =
nestedState.index || nestedState.routes.length - 1;
nestedState.routes[nestedStateIndex].state = createStateObject(
initialRoute,
route.name,
route.params,
routes.length === 0
);
if (routes.length > 0) {
nestedState = nestedState.routes[nestedStateIndex]
.state as InitialState;
}
}
}
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 && config.parse[key]
? config.parse[key](value)
: value;
}
return acc;
}, {});
}
}
}
if (params && Object.keys(params).length) {
return { name, params };
}
return { name };
});
const findFocusedRoute = (state: InitialState) => {
let current: InitialState | undefined = state;
while (current?.routes[current.index || 0].state) {
// The query params apply to the deepest route
current = current.routes[current.index || 0].state;
}
const route = (current as PartialState<NavigationState>).routes[
current?.index || 0
];
return route;
};
const parseQueryParams = (
path: string,
parseConfig?: Record<string, (value: string) => any>
) => {
const query = path.split('?')[1];
const params = queryString.parse(query);
if (parseConfig) {
Object.keys(params).forEach((name) => {
if (parseConfig[name] && typeof params[name] === 'string') {
params[name] = parseConfig[name](params[name] as string);
}
});
}
return Object.keys(params).length ? params : undefined;
};

View File

@@ -3,9 +3,12 @@ export * from '@react-navigation/routers';
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
export { default as createNavigatorFactory } from './createNavigatorFactory';
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
export { default as NavigationContext } from './NavigationContext';
export { default as NavigationRouteContext } from './NavigationRouteContext';
export { default as CurrentRenderContext } from './CurrentRenderContext';
export { default as useNavigationBuilder } from './useNavigationBuilder';
export { default as useNavigation } from './useNavigation';
export { default as useRoute } from './useRoute';

View File

@@ -0,0 +1,7 @@
/**
* Compare two arrays with primitive values as the content.
* We need to make sure that both values and order match.
*/
export default function isArrayEqual(a: any[], b: any[]) {
return a.length === b.length && a.every((it, index) => it === b[index]);
}

View File

@@ -152,7 +152,7 @@ type NavigationHelpersCommon<
* @param [params] Params object for the route.
*/
navigate<RouteName extends keyof ParamList>(
...args: ParamList[RouteName] extends undefined | any
...args: undefined extends ParamList[RouteName]
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
@@ -193,6 +193,20 @@ type NavigationHelpersCommon<
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
*/
canGoBack(): boolean;
/**
* Returns the parent navigator, if any. Reason why the function is called
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
* of parent and other hard-to-follow patterns.
*/
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
/**
* Returns the navigator's state. Reason why the function is called
* dangerouslyGetState is to discourage developers to use internal navigation's state.
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
*/
dangerouslyGetState(): State;
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
export type NavigationHelpers<
@@ -254,20 +268,6 @@ export type NavigationProp<
* @param options Options object for the route.
*/
setOptions(options: Partial<ScreenOptions>): void;
/**
* Returns the parent navigator, if any. Reason why the function is called
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
* of parent and other hard-to-follow patterns.
*/
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
/**
* Returns the navigator's state. Reason why the function is called
* dangerouslyGetState is to discourage developers to use internal navigation's state.
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
*/
dangerouslyGetState(): State;
} & EventConsumer<EventMap & EventMapCore<State>> &
PrivateValueStore<ParamList, RouteName, EventMap>;
@@ -410,24 +410,27 @@ export type RouteConfig<
}
);
export type NavigationContainerRef =
| (NavigationHelpers<ParamListBase> &
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
/**
* Reset the navigation state of the root navigator to the provided state.
*
* @param state Navigation state object.
*/
resetRoot(
state?: PartialState<NavigationState> | NavigationState
): void;
/**
* Get the rehydrated navigation state of the navigation tree.
*/
getRootState(): NavigationState;
})
| undefined
| null;
export type NavigationContainerRef = NavigationHelpers<ParamListBase> &
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
/**
* Reset the navigation state of the root navigator to the provided state.
*
* @param state Navigation state object.
*/
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
/**
* Get the rehydrated navigation state of the navigation tree.
*/
getRootState(): NavigationState;
/**
* Get the currently focused navigation route.
*/
getCurrentRoute(): Route<string> | undefined;
/**
* Get the currently focused route's options.
*/
getCurrentOptions(): object | undefined;
};
export type TypedNavigator<
ParamList extends ParamListBase,
@@ -467,3 +470,16 @@ export type TypedNavigator<
_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>
) => null;
};
export type PathConfig = {
[routeName: string]:
| string
| {
path?: string;
exact?: boolean;
parse?: Record<string, (value: string) => any>;
stringify?: Record<string, (value: any) => string>;
screens?: PathConfig;
initialRouteName?: string;
};
};

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import { NavigationState, ParamListBase } from '@react-navigation/routers';
import CurrentRenderContext from './CurrentRenderContext';
import { Descriptor, NavigationHelpers } from './types';
type Options = {
state: NavigationState;
navigation: NavigationHelpers<ParamListBase>;
descriptors: {
[key: string]: Descriptor<ParamListBase, string, NavigationState, object>;
};
};
/**
* Write the current options, so that server renderer can get current values
* Mutating values like this is not safe in async mode, but it doesn't apply to SSR
*/
export default function useCurrentRender({
state,
navigation,
descriptors,
}: Options) {
const current = React.useContext(CurrentRenderContext);
if (current && navigation.isFocused()) {
current.options = descriptors[state.routes[state.index].key].options;
}
}

View File

@@ -117,6 +117,28 @@ export default function useDescriptors<
const screen = screens[route.name];
const navigation = navigations[route.key];
const routeOptions = {
// The default `screenOptions` passed to the navigator
...(typeof screenOptions === 'object' || screenOptions == null
? screenOptions
: screenOptions({
// @ts-ignore
route,
navigation,
})),
// The `options` prop passed to `Screen` elements
...(typeof screen.options === 'object' || screen.options == null
? screen.options
: screen.options({
// @ts-ignore
route,
// @ts-ignore
navigation,
})),
// The options set via `navigation.setOptions`
...options[route.key],
};
acc[route.key] = {
navigation,
render() {
@@ -128,31 +150,12 @@ export default function useDescriptors<
screen={screen}
getState={getState}
setState={setState}
options={routeOptions}
/>
</NavigationBuilderContext.Provider>
);
},
options: {
// The default `screenOptions` passed to the navigator
...(typeof screenOptions === 'object' || screenOptions == null
? screenOptions
: screenOptions({
// @ts-ignore
route,
navigation,
})),
// The `options` prop passed to `Screen` elements
...(typeof screen.options === 'object' || screen.options == null
? screen.options
: screen.options({
// @ts-ignore
route,
// @ts-ignore
navigation,
})),
// The options set via `navigation.setOptions`
...options[route.key],
},
options: routeOptions,
};
return acc;

View File

@@ -48,7 +48,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
emitter.emit({ type: 'focus', target: currentFocusedKey });
}
// We should only dispatch events when the focused key changed and navigator is focused
// We should only emit events when the focused key changed and navigator is focused
// When navigator is not focused, screens inside shouldn't receive focused status either
if (
lastFocusedKey === currentFocusedKey ||
@@ -62,7 +62,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
return;
}
emitter.emit({ type: 'focus', target: currentFocusedKey });
emitter.emit({ type: 'blur', target: lastFocusedKey });
emitter.emit({ type: 'focus', target: currentFocusedKey });
}, [currentFocusedKey, emitter, navigation]);
}

View File

@@ -9,16 +9,13 @@ import useNavigation from './useNavigation';
*/
export default function useIsFocused(): boolean {
const navigation = useNavigation();
// eslint-disable-next-line react-hooks/exhaustive-deps
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
const subscribe = React.useCallback(
(callback: (value: boolean) => void) => {
const unsubscribeFocus = navigation.addListener('focus', () =>
callback(true)
);
(callback: () => void) => {
const unsubscribeFocus = navigation.addListener('focus', callback);
const unsubscribeBlur = navigation.addListener('blur', () =>
callback(false)
);
const unsubscribeBlur = navigation.addListener('blur', callback);
return () => {
unsubscribeFocus();

View File

@@ -11,7 +11,7 @@ import {
NavigationAction,
Route,
} from '@react-navigation/routers';
import { NavigationStateContext } from './BaseNavigationContainer';
import NavigationStateContext from './NavigationStateContext';
import NavigationRouteContext from './NavigationRouteContext';
import Screen from './Screen';
import useEventEmitter from './useEventEmitter';
@@ -32,6 +32,9 @@ import {
} from './types';
import useStateGetters from './useStateGetters';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import isArrayEqual from './isArrayEqual';
// This is to make TypeScript compiler happy
// eslint-disable-next-line babel/no-unused-expressions
@@ -42,16 +45,10 @@ type NavigatorRoute = {
params?: {
screen?: string;
params?: object;
initial?: boolean;
};
};
/**
* Compare two arrays with primitive values as the content.
* We need to make sure that both values and order match.
*/
const isArrayEqual = (a: any[], b: any[]) =>
a.length === b.length && a.every((it, index) => it === b[index]);
/**
* Extract route config object from React children elements.
*
@@ -175,17 +172,19 @@ export default function useNavigationBuilder<
| NavigatorRoute
| undefined;
const previousRouteRef = React.useRef(route);
const previousNestedParamsRef = React.useRef(route?.params);
React.useEffect(() => {
previousRouteRef.current = route;
previousNestedParamsRef.current = route?.params;
}, [route]);
const { children, ...rest } = options;
const { current: router } = React.useRef<Router<State, any>>(
createRouter({
...((rest as unknown) as RouterOptions),
...(route?.params && typeof route.params.screen === 'string'
...(route?.params &&
route.params.initial !== false &&
typeof route.params.screen === 'string'
? { initialRouteName: route.params.screen }
: null),
})
@@ -218,7 +217,7 @@ export default function useNavigationBuilder<
(acc, curr) => {
const { initialParams } = screens[curr];
const initialParamsFromParams =
route?.params && route.params.screen === curr
route?.params?.initial !== false && route?.params?.screen === curr
? route.params.params
: undefined;
@@ -260,34 +259,36 @@ export default function useNavigationBuilder<
getKey,
} = React.useContext(NavigationStateContext);
const previousStateRef = React.useRef<
NavigationState | PartialState<NavigationState> | undefined
>();
const initializedStateRef = React.useRef<State>();
if (
initializedStateRef.current === undefined ||
currentState !== previousStateRef.current
) {
const [initializedState, isFirstStateInitialization] = React.useMemo(() => {
// If the current state isn't initialized on first render, we initialize it
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
// Otherwise assume that the state was provided as initial state
// So we need to rehydrate it to make it usable
initializedStateRef.current =
currentState === undefined || !isStateValid(currentState)
? router.getInitialState({
routeNames,
routeParamList,
})
: router.getRehydratedState(currentState as PartialState<State>, {
routeNames,
routeParamList,
});
}
React.useEffect(() => {
previousStateRef.current = currentState;
}, [currentState]);
if (currentState === undefined || !isStateValid(currentState)) {
return [
router.getInitialState({
routeNames,
routeParamList,
}),
true,
];
} else {
return [
router.getRehydratedState(currentState as PartialState<State>, {
routeNames,
routeParamList,
}),
false,
];
}
// We explicitly don't include routeNames/routeParamList in the dep list
// below. We want to avoid forcing a new state to be calculated in cases
// where routeConfigs change without affecting routeNames/routeParamList.
// Instead, we handle changes to these in the nextState code below. Note
// that some changes to routeConfigs are explicitly ignored, such as changes
// to initialParams
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState, router, isStateValid]);
let state =
// If the state isn't initialized, or stale, use the state we initialized instead
@@ -295,7 +296,7 @@ export default function useNavigationBuilder<
// So it'll be `undefined` or stale untill the first navigation event happens
isStateInitialized(currentState)
? (currentState as State)
: (initializedStateRef.current as State);
: (initializedState as State);
let nextState: State = state;
@@ -308,16 +309,14 @@ export default function useNavigationBuilder<
}
if (
previousRouteRef.current &&
route &&
route.params &&
typeof route.params.screen === 'string' &&
route.params !== previousRouteRef.current.params
typeof route?.params?.screen === 'string' &&
(route.params !== previousNestedParamsRef.current ||
(route.params.initial === false && isFirstStateInitialization))
) {
// If the route was updated with new name and/or params, we should navigate there
// The update should be limited to current navigator only, so we call the router manually
const updatedState = router.getStateForAction(
state,
nextState,
CommonActions.navigate(route.params.screen, route.params.params),
{
routeNames,
@@ -331,17 +330,17 @@ export default function useNavigationBuilder<
routeNames,
routeParamList,
})
: state;
: nextState;
}
const shouldUpdate = state !== nextState;
React.useEffect(() => {
useScheduleUpdate(() => {
if (shouldUpdate) {
// If the state needs to be updated, we'll schedule an update with React
// If the state needs to be updated, we'll schedule an update
setState(nextState);
}
}, [nextState, setState, shouldUpdate]);
});
// The up-to-date state will come in next render, but we don't need to wait for it
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
@@ -353,13 +352,24 @@ export default function useNavigationBuilder<
return () => {
// We need to clean up state for this navigator on unmount
if (getCurrentState() !== undefined && getKey() === navigatorKey) {
setState(undefined);
}
// We do it in a timeout because we need to detect if another navigator mounted in the meantime
// For example, if another navigator has started rendering, we should skip cleanup
// Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it
setTimeout(() => {
if (getCurrentState() !== undefined && getKey() === navigatorKey) {
setState(undefined);
}
}, 0);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// We initialize this ref here to avoid a new getState getting initialized
// whenever initializedState changes. We want getState to have access to the
// latest initializedState, but don't need it to change when that happens
const initializedStateRef = React.useRef<State>();
initializedStateRef.current = initializedState;
const getState = React.useCallback((): State => {
const currentState = getCurrentState();
@@ -483,6 +493,12 @@ export default function useNavigationBuilder<
emitter,
});
useCurrentRender({
state,
navigation,
descriptors,
});
return {
state,
navigation,

View File

@@ -7,7 +7,6 @@ import {
Router,
} from '@react-navigation/routers';
import { NavigationEventEmitter } from './useEventEmitter';
import NavigationContext from './NavigationContext';
import { NavigationHelpers, NavigationProp } from './types';
@@ -49,12 +48,10 @@ export default function useNavigationCache<
// Cache object which holds navigation objects for each screen
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
// In reality, these deps will rarely change, if ever
const parentNavigation = React.useContext(NavigationContext);
const cache = React.useMemo(
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[getState, navigation, setOptions, router, emitter, parentNavigation]
[getState, navigation, setOptions, router, emitter]
);
const actions = {
@@ -99,8 +96,6 @@ export default function useNavigationCache<
...rest,
...helpers,
...emitter.create(route.key),
dangerouslyGetParent: () => parentNavigation as any,
dangerouslyGetState: getState,
dispatch,
setOptions: (options: object) =>
setOptions((o) => ({

View File

@@ -112,6 +112,8 @@ export default function useNavigationHelpers<
false
);
},
dangerouslyGetParent: () => parentNavigationHelpers as any,
dangerouslyGetState: getState,
} as NavigationHelpers<ParamListBase, EventMap> &
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { NavigationState } from '@react-navigation/routers';
import NavigationBuilderContext from './NavigationBuilderContext';
import NavigationRouteContext from './NavigationRouteContext';
import isArrayEqual from './isArrayEqual';
export default function useOnGetState({
getStateForRoute,
@@ -16,13 +17,23 @@ export default function useOnGetState({
const getRehydratedState = React.useCallback(() => {
const state = getState();
return {
...state,
routes: state.routes.map((route) => ({
...route,
state: getStateForRoute(route.key),
})),
};
// Avoid returning new route objects if we don't need to
const routes = state.routes.map((route) => {
const childState = getStateForRoute(route.key);
if (route.state === childState) {
return route;
}
return { ...route, state: childState };
});
if (isArrayEqual(state.routes, routes)) {
return state;
}
return { ...state, routes };
}, [getState, getStateForRoute]);
React.useEffect(() => {

View File

@@ -0,0 +1,70 @@
import * as React from 'react';
import NavigationStateContext from './NavigationStateContext';
import { NavigationState } from '@react-navigation/routers';
export default function useOptionsGetters({
key,
getOptions,
getState,
}: {
key?: string;
getOptions?: () => object | undefined;
getState?: () => NavigationState;
}) {
const optionsGettersFromChild = React.useRef<
Record<string, (() => object | undefined | null) | undefined>
>({});
const { addOptionsGetter: parentAddOptionsGetter } = React.useContext(
NavigationStateContext
);
const getOptionsFromListener = React.useCallback(() => {
for (let key in optionsGettersFromChild.current) {
if (optionsGettersFromChild.current.hasOwnProperty(key)) {
const result = optionsGettersFromChild.current[key]?.();
// null means unfocused route
if (result !== null) {
return result;
}
}
}
return null;
}, []);
const getCurrentOptions = React.useCallback(() => {
if (getState) {
const state = getState();
if (state.routes[state.index].key !== key) {
// null means unfocused route
return null;
}
}
const optionsFromListener = getOptionsFromListener();
if (optionsFromListener !== null) {
return optionsFromListener;
}
return getOptions?.() ?? undefined;
}, [getState, getOptionsFromListener, getOptions, key]);
React.useEffect(() => {
return parentAddOptionsGetter?.(key!, getCurrentOptions);
}, [getCurrentOptions, parentAddOptionsGetter, key]);
const addOptionsGetter = React.useCallback(
(key: string, getter: () => object | undefined | null) => {
optionsGettersFromChild.current[key] = getter;
return () => {
optionsGettersFromChild.current[key] = undefined;
};
},
[]
);
return {
addOptionsGetter,
getCurrentOptions,
};
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import shortid from 'shortid';
import { nanoid } from 'nanoid/non-secure';
import { SingleNavigatorContext } from './EnsureSingleNavigator';
/**
@@ -7,7 +7,7 @@ import { SingleNavigatorContext } from './EnsureSingleNavigator';
* This is used to prevent multiple navigators under a single container or screen.
*/
export default function useRegisterNavigator() {
const [key] = React.useState(() => shortid());
const [key] = React.useState(() => nanoid());
const container = React.useContext(SingleNavigatorContext);
if (container === undefined) {

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
const MISSING_CONTEXT_ERROR = "Couldn't find a schedule context.";
export const ScheduleUpdateContext = React.createContext<{
scheduleUpdate: (callback: () => void) => void;
flushUpdates: () => void;
}>({
scheduleUpdate() {
throw new Error(MISSING_CONTEXT_ERROR);
},
flushUpdates() {
throw new Error(MISSING_CONTEXT_ERROR);
},
});
/**
* When screen config changes, we want to update the navigator in the same update phase.
* However, navigation state is in the root component and React won't let us update it from a child.
* This is a workaround for that, the scheduled update is stored in the ref without actually calling setState.
* It lets all subsequent updates access the latest state so it stays correct.
* Then we call setState during after the component updates.
*/
export default function useScheduleUpdate(callback: () => void) {
const { scheduleUpdate, flushUpdates } = React.useContext(
ScheduleUpdateContext
);
scheduleUpdate(callback);
React.useEffect(flushUpdates);
}

View File

@@ -2,8 +2,21 @@ import * as React from 'react';
const UNINTIALIZED_STATE = {};
/**
* This is definitely not compatible with concurrent mode, but we don't have a solution for sync state yet.
*/
export default function useSyncState<T>(initialState?: (() => T) | T) {
const stateRef = React.useRef<T>(UNINTIALIZED_STATE as any);
const isSchedulingRef = React.useRef(false);
const isMountedRef = React.useRef(true);
React.useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
if (stateRef.current === UNINTIALIZED_STATE) {
stateRef.current =
@@ -11,18 +24,49 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
typeof initialState === 'function' ? initialState() : initialState;
}
const [state, setTrackingState] = React.useState(stateRef.current);
const [trackingState, setTrackingState] = React.useState(stateRef.current);
const getState = React.useCallback(() => stateRef.current, []);
const setState = React.useCallback((state: T) => {
if (state === stateRef.current) {
if (state === stateRef.current || !isMountedRef.current) {
return;
}
stateRef.current = state;
setTrackingState(state);
if (!isSchedulingRef.current) {
setTrackingState(state);
}
}, []);
return [state, getState, setState] as const;
const scheduleUpdate = React.useCallback((callback: () => void) => {
isSchedulingRef.current = true;
try {
callback();
} finally {
isSchedulingRef.current = false;
}
}, []);
const flushUpdates = React.useCallback(() => {
if (!isMountedRef.current) {
return;
}
// Make sure that the tracking state is up-to-date.
// We call it unconditionally, but React should skip the update if state is unchanged.
setTrackingState(stateRef.current);
}, []);
// If we're rendering and the tracking state is out of date, update it immediately
// This will make sure that our updates are applied as early as possible.
if (trackingState !== stateRef.current) {
setTrackingState(stateRef.current);
}
const state = stateRef.current;
return [state, getState, setState, scheduleUpdate, flushUpdates] as const;
}

View File

@@ -3,6 +3,209 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.8.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.1...@react-navigation/drawer@5.8.2) (2020-06-06)
### Bug Fixes
* typo on drawerPosition default props ([#8357](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8357)) ([762cc44](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/762cc4457842182189eeac84aedbb88169452e1e))
## [5.8.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.0...@react-navigation/drawer@5.8.1) (2020-05-27)
**Note:** Version bump only for package @react-navigation/drawer
# [5.8.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.7...@react-navigation/drawer@5.8.0) (2020-05-23)
### Features
* update react-native-safe-area-context to 1.0.0 ([#8182](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8182)) ([d62fbfe](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/d62fbfe255140f16b182e8b54b276a7c96f2aec6))
## [5.7.7](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.6...@react-navigation/drawer@5.7.7) (2020-05-20)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.6](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.5...@react-navigation/drawer@5.7.6) (2020-05-20)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.5](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.4...@react-navigation/drawer@5.7.5) (2020-05-16)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.3...@react-navigation/drawer@5.7.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.2...@react-navigation/drawer@5.7.3) (2020-05-14)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.1...@react-navigation/drawer@5.7.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.0...@react-navigation/drawer@5.7.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.7.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.4...@react-navigation/drawer@5.7.0) (2020-05-08)
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7971)
## [5.6.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.3...@react-navigation/drawer@5.6.4) (2020-05-05)
**Note:** Version bump only for package @react-navigation/drawer
## [5.6.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.2...@react-navigation/drawer@5.6.3) (2020-05-01)
**Note:** Version bump only for package @react-navigation/drawer
## [5.6.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.1...@react-navigation/drawer@5.6.2) (2020-05-01)
**Note:** Version bump only for package @react-navigation/drawer
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.0...@react-navigation/drawer@5.6.1) (2020-04-30)
**Note:** Version bump only for package @react-navigation/drawer
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.1...@react-navigation/drawer@5.6.0) (2020-04-30)
### Bug Fixes
* fix closing drawer on web with tap on overlay ([70be3f6](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/70be3f6d863c56211e2f90bdf743bd8526338248))
* make sure the address bar hides when scrolling on web ([0a19e94](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/0a19e94b23a4d2b5f22d1d9deb0544f586f475ee))
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/942d2be2c72720469475ce12ec8df23825994dbf))
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.0...@react-navigation/drawer@5.5.1) (2020-04-27)
**Note:** Version bump only for package @react-navigation/drawer
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.1...@react-navigation/drawer@5.5.0) (2020-04-17)
### Bug Fixes
* fix drawer not closing on web ([e2bcf51](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/e2bcf5168c389833eaaeadb4b8794aaea4a66d17)), closes [#6759](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6759)
* webkit style error in overlay ([821343f](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/821343fed38577cfdc87a78f13f991d5760bf8f5))
### Features
* add openByDefault option to drawer ([36689e2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/36689e24c21b474692bb7ecd0b901c8afbbe9a20))
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.0...@react-navigation/drawer@5.4.1) (2020-04-08)
### Bug Fixes
* don't hide content from accessibility with permanent drawer ([cb2f157](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/cb2f157a561a2ce3f073eb4ccb567532c77bd869)), closes [#7976](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7976)
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.4...@react-navigation/drawer@5.4.0) (2020-03-30)
### Bug Fixes
* disable only swipe gesture on safari ([105da6a](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/105da6ab2fe69847b676c4d4117638212cda1f9a))
### Features
* add swipeEnabled option to disable swipe gesture in drawer ([#7834](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7834)) ([ac7f972](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/ac7f972e922a82cd32d943356941d100b68bd8b0))
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.3...@react-navigation/drawer@5.3.4) (2020-03-23)
**Note:** Version bump only for package @react-navigation/drawer

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.3.4",
"version": "5.8.2",
"keywords": [
"react-native-component",
"react-component",
@@ -20,11 +20,13 @@
"homepage": "https://reactnavigation.org/docs/drawer-navigator.html",
"main": "lib/commonjs/index.js",
"react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts",
"files": [
"src",
"lib"
"lib",
"!**/__tests__"
],
"sideEffects": false,
"publishConfig": {
@@ -39,18 +41,18 @@
"react-native-iphone-x-helper": "^1.2.1"
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.3",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.5.1",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.0",
"react": "~16.9.0",
"react-native": "~0.61.5",
"react-native-gesture-handler": "^1.6.0",
"react-native-reanimated": "^1.7.0",
"react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0",
"typescript": "^3.7.5"
"react-native-reanimated": "^1.8.0",
"react-native-safe-area-context": "^1.0.0",
"react-native-screens": "^2.7.0",
"typescript": "^3.8.3"
},
"peerDependencies": {
"@react-navigation/native": "^5.0.5",

View File

@@ -22,9 +22,10 @@ export { default as useIsDrawerOpen } from './utils/useIsDrawerOpen';
/**
* Types
*/
export {
export type {
DrawerNavigationOptions,
DrawerNavigationProp,
DrawerScreenProps,
DrawerContentOptions,
DrawerContentComponentProps,
} from './types';

View File

@@ -21,6 +21,7 @@ type Props = DefaultNavigatorOptions<DrawerNavigationOptions> &
function DrawerNavigator({
initialRouteName,
openByDefault,
backBehavior,
children,
screenOptions,
@@ -33,6 +34,7 @@ function DrawerNavigator({
DrawerNavigationEventMap
>(DrawerRouter, {
initialRouteName,
openByDefault,
backBehavior,
children,
screenOptions,

View File

@@ -8,8 +8,9 @@ import {
NavigationHelpers,
DrawerNavigationState,
DrawerActionHelpers,
RouteProp,
} from '@react-navigation/native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
export type Scene = {
route: Route<string>;
@@ -32,6 +33,7 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
drawerType?: 'front' | 'back' | 'slide' | 'permanent';
/**
* How far from the edge of the screen the swipe gesture should activate.
* Not supported on Web.
*/
edgeWidth?: number;
/**
@@ -58,8 +60,9 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
statusBarAnimation?: 'slide' | 'none' | 'fade';
/**
* Props to pass to the underlying pan gesture handler.
* Not supported on Web.
*/
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
gestureHandlerProps?: PanGestureHandlerProperties;
/**
* Whether the screens should render the first time they are accessed. Defaults to `true`.
* Set it to `false` if you want to render all screens on initial render.
@@ -111,9 +114,20 @@ export type DrawerNavigationOptions = {
/**
* Whether you can use gestures to open or close the drawer.
* Defaults to `true`
* Setting this to `false` disables swipe gestures as well as tap on overlay to close.
* See `swipeEnabled` to disable only the swipe gesture.
* Defaults to `true`.
* Not supported on Web.
*/
gestureEnabled?: boolean;
/**
* Whether you can use swipe gestures to open or close the drawer.
* Defaults to `true`.
* Not supported on Web.
*/
swipeEnabled?: boolean;
/**
* Whether this screen should be unmounted when navigating away from it.
* Defaults to `false`.
@@ -195,6 +209,14 @@ export type DrawerNavigationProp<
> &
DrawerActionHelpers<ParamList>;
export type DrawerScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: DrawerNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type DrawerDescriptor = Descriptor<
ParamListBase,
string,

View File

@@ -10,13 +10,14 @@ import {
StyleProp,
View,
InteractionManager,
TouchableWithoutFeedback,
} from 'react-native';
import Animated from 'react-native-reanimated';
import {
PanGestureHandler,
TapGestureHandler,
State,
} from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
GestureState,
} from './GestureHandler';
import Overlay from './Overlay';
const {
@@ -78,8 +79,8 @@ type Props = {
open: boolean;
onOpen: () => void;
onClose: () => void;
onGestureRef?: (ref: PanGestureHandler | null) => void;
gestureEnabled: boolean;
swipeEnabled: boolean;
drawerPosition: 'left' | 'right';
drawerType: 'front' | 'back' | 'slide' | 'permanent';
keyboardDismissMode: 'none' | 'on-drag';
@@ -94,32 +95,15 @@ type Props = {
renderDrawerContent: Renderer;
renderSceneContent: Renderer;
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
dimensions: { width: number; height: number };
};
/**
* Disables the pan gesture by default on Apple devices in the browser.
* https://stackoverflow.com/a/9039885
*/
function shouldEnableGesture(): boolean {
if (
Platform.OS === 'web' &&
typeof navigator !== 'undefined' &&
typeof window !== 'undefined'
) {
const isWebAppleDevice =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return !isWebAppleDevice;
}
return true;
}
export default class DrawerView extends React.PureComponent<Props> {
export default class DrawerView extends React.Component<Props> {
static defaultProps = {
drawerPostion: I18nManager.isRTL ? 'left' : 'right',
drawerPosition: I18nManager.isRTL ? 'left' : 'right',
drawerType: 'front',
gestureEnabled: shouldEnableGesture(),
gestureEnabled: true,
swipeEnabled: Platform.OS !== 'web',
swipeEdgeWidth: 32,
swipeVelocityThreshold: 500,
keyboardDismissMode: 'on-drag',
@@ -138,16 +122,11 @@ export default class DrawerView extends React.PureComponent<Props> {
open,
drawerPosition,
drawerType,
gestureEnabled,
swipeDistanceThreshold,
swipeVelocityThreshold,
hideStatusBar,
} = this.props;
if (prevProps.gestureEnabled !== gestureEnabled) {
this.isGestureEnabled.setValue(gestureEnabled ? TRUE : FALSE);
}
if (
// If we're not in the middle of a transition, sync the drawer's open state
typeof this.pendingOpenValue !== 'boolean' ||
@@ -217,30 +196,54 @@ export default class DrawerView extends React.PureComponent<Props> {
}
};
private getDrawerWidth = (): number => {
const { drawerStyle, dimensions } = this.props;
const { width } = StyleSheet.flatten(drawerStyle);
if (typeof width === 'string' && width.endsWith('%')) {
// Try to calculate width if a percentage is given
const percentage = Number(width.replace(/%$/, ''));
if (Number.isFinite(percentage)) {
return dimensions.width * (percentage / 100);
}
}
return typeof width === 'number' ? width : 0;
};
private clock = new Clock();
private interactionHandle: number | undefined;
private isDrawerTypeFront = new Value<Binary>(
this.props.drawerType === 'front' ? TRUE : FALSE
);
private isGestureEnabled = new Value(
this.props.gestureEnabled ? TRUE : FALSE
);
private isOpen = new Value<Binary>(this.props.open ? TRUE : FALSE);
private nextIsOpen = new Value<Binary | -1>(UNSET);
private isSwiping = new Value<Binary>(FALSE);
private gestureState = new Value<number>(State.UNDETERMINED);
private initialDrawerWidth = this.getDrawerWidth();
private gestureState = new Value<number>(GestureState.UNDETERMINED);
private touchX = new Value<number>(0);
private velocityX = new Value<number>(0);
private gestureX = new Value<number>(0);
private offsetX = new Value<number>(0);
private position = new Value<number>(0);
private position = new Value<number>(
this.props.open
? this.initialDrawerWidth *
(this.props.drawerPosition === 'right'
? DIRECTION_RIGHT
: DIRECTION_LEFT)
: 0
);
private containerWidth = new Value<number>(0);
private drawerWidth = new Value<number>(0);
private drawerOpacity = new Value<number>(0);
private containerWidth = new Value<number>(this.props.dimensions.width);
private drawerWidth = new Value<number>(this.initialDrawerWidth);
private drawerOpacity = new Value<number>(
this.initialDrawerWidth || this.props.drawerType === 'permanent' ? 1 : 0
);
private drawerPosition = new Value<number>(
this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT
);
@@ -418,12 +421,12 @@ export default class DrawerView extends React.PureComponent<Props> {
onChange(
this.gestureState,
cond(
eq(this.gestureState, State.ACTIVE),
eq(this.gestureState, GestureState.ACTIVE),
call([], this.handleStartInteraction)
)
),
cond(
eq(this.gestureState, State.ACTIVE),
eq(this.gestureState, GestureState.ACTIVE),
[
cond(this.isSwiping, NOOP, [
// We weren't dragging before, set it to true
@@ -511,7 +514,10 @@ export default class DrawerView extends React.PureComponent<Props> {
{
nativeEvent: {
oldState: (s: Animated.Value<number>) =>
cond(eq(s, State.ACTIVE), set(this.manuallyTriggerSpring, TRUE)),
cond(
eq(s, GestureState.ACTIVE),
set(this.manuallyTriggerSpring, TRUE)
),
},
},
]);
@@ -554,13 +560,13 @@ export default class DrawerView extends React.PureComponent<Props> {
const {
open,
gestureEnabled,
swipeEnabled,
drawerPosition,
drawerType,
swipeEdgeWidth,
sceneContainerStyle,
drawerStyle,
overlayStyle,
onGestureRef,
renderDrawerContent,
renderSceneContent,
gestureHandlerProps,
@@ -569,9 +575,15 @@ export default class DrawerView extends React.PureComponent<Props> {
const isOpen = drawerType === 'permanent' ? true : open;
const isRight = drawerPosition === 'right';
const contentTranslateX = drawerType === 'front' ? 0 : this.translateX;
const contentTranslateX =
drawerType === 'front' || drawerType === 'permanent'
? 0
: this.translateX;
const drawerTranslateX =
drawerType === 'back'
drawerType === 'permanent'
? 0
: drawerType === 'back'
? I18nManager.isRTL
? multiply(
sub(this.containerWidth, this.drawerWidth),
@@ -599,13 +611,12 @@ export default class DrawerView extends React.PureComponent<Props> {
return (
<PanGestureHandler
ref={onGestureRef}
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
onGestureEvent={this.handleGestureEvent}
onHandlerStateChange={this.handleGestureStateChange}
hitSlop={hitSlop}
enabled={drawerType !== 'permanent' && gestureEnabled}
enabled={drawerType !== 'permanent' && gestureEnabled && swipeEnabled}
{...gestureHandlerProps}
>
<Animated.View
@@ -621,31 +632,46 @@ export default class DrawerView extends React.PureComponent<Props> {
<Animated.View
style={[
styles.content,
drawerType !== 'permanent' && {
transform: [{ translateX: contentTranslateX }],
},
{ transform: [{ translateX: contentTranslateX }] },
sceneContainerStyle as any,
]}
>
<View
accessibilityElementsHidden={isOpen}
accessibilityElementsHidden={isOpen && drawerType !== 'permanent'}
importantForAccessibility={
isOpen ? 'no-hide-descendants' : 'auto'
isOpen && drawerType !== 'permanent'
? 'no-hide-descendants'
: 'auto'
}
style={styles.content}
>
{renderSceneContent({ progress })}
</View>
{// Disable overlay if sidebar is permanent
drawerType === 'permanent' ? null : (
<TapGestureHandler
enabled={gestureEnabled}
onHandlerStateChange={this.handleTapStateChange}
>
<Overlay progress={progress} style={overlayStyle} />
</TapGestureHandler>
)}
{
// Disable overlay if sidebar is permanent
drawerType === 'permanent' ? null : Platform.OS === 'web' ? (
<TouchableWithoutFeedback
onPress={
gestureEnabled ? () => this.toggleDrawer(false) : undefined
}
>
<Overlay progress={progress} style={overlayStyle as any} />
</TouchableWithoutFeedback>
) : (
<TapGestureHandler
enabled={gestureEnabled}
onHandlerStateChange={this.handleTapStateChange}
>
<Overlay progress={progress} style={overlayStyle as any} />
</TapGestureHandler>
)
}
</Animated.View>
<Animated.Code
// This is needed to make sure that container width updates with `setValue`
// Without this, it won't update when not used in styles
exec={this.containerWidth}
/>
{drawerType === 'permanent' ? null : (
<Animated.Code
exec={block([
@@ -659,11 +685,15 @@ export default class DrawerView extends React.PureComponent<Props> {
/>
)}
<Animated.View
accessibilityViewIsModal={isOpen}
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
removeClippedSubviews={Platform.OS !== 'ios'}
onLayout={this.handleDrawerLayout}
style={[
styles.container,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
},
drawerType === 'permanent'
? // Without this, the `left`/`right` values don't get reset
isRight
@@ -671,10 +701,6 @@ export default class DrawerView extends React.PureComponent<Props> {
: { left: 0 }
: [
styles.nonPermanent,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
},
isRight ? { right: offset } : { left: offset },
{ zIndex: drawerType === 'back' ? -1 : 0 },
],
@@ -705,6 +731,11 @@ const styles = StyleSheet.create({
},
main: {
flex: 1,
overflow: 'hidden',
...Platform.select({
// FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
// But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
web: null,
default: { overflow: 'hidden' },
}),
},
});

View File

@@ -6,8 +6,10 @@ import {
StyleProp,
ViewStyle,
TextStyle,
Platform,
TouchableWithoutFeedbackProps,
} from 'react-native';
import { useTheme } from '@react-navigation/native';
import { Link, useTheme } from '@react-navigation/native';
import Color from 'color';
import TouchableItem from './TouchableItem';
@@ -26,6 +28,10 @@ type Props = {
size: number;
color: string;
}) => React.ReactNode;
/**
* URL to use for the link to the tab.
*/
to?: string;
/**
* Whether to highlight the drawer item as active.
*/
@@ -60,6 +66,54 @@ type Props = {
style?: StyleProp<ViewStyle>;
};
const Touchable = ({
children,
style,
onPress,
to,
accessibilityRole,
delayPressIn,
...rest
}: TouchableWithoutFeedbackProps & {
to?: string;
children: React.ReactNode;
onPress?: () => void;
}) => {
if (Platform.OS === 'web' && to) {
// React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
// We need to use `onClick` to be able to prevent default browser handling of links.
return (
<Link
{...rest}
to={to}
style={[styles.button, style]}
onPress={(e: any) => {
if (
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
(e.button == null || e.button === 0) // ignore everything but left clicks
) {
e.preventDefault();
onPress?.(e);
}
}}
>
{children}
</Link>
);
} else {
return (
<TouchableItem
{...rest}
accessibilityRole={accessibilityRole}
delayPressIn={delayPressIn}
onPress={onPress}
>
<View style={style}>{children}</View>
</TouchableItem>
);
}
};
/**
* A component used to show an action item with an icon and a label in a navigation drawer.
*/
@@ -70,6 +124,7 @@ export default function DrawerItem(props: Props) {
icon,
label,
labelStyle,
to,
focused = false,
activeTintColor = colors.primary,
inactiveTintColor = Color(colors.text).alpha(0.68).rgb().string(),
@@ -94,7 +149,7 @@ export default function DrawerItem(props: Props) {
{...rest}
style={[styles.container, { borderRadius, backgroundColor }, style]}
>
<TouchableItem
<Touchable
delayPressIn={0}
onPress={onPress}
style={[styles.wrapper, { borderRadius }]}
@@ -102,6 +157,7 @@ export default function DrawerItem(props: Props) {
accessibilityComponentType="button"
accessibilityRole="button"
accessibilityStates={focused ? ['selected'] : []}
to={to}
>
<React.Fragment>
{iconNode}
@@ -129,7 +185,7 @@ export default function DrawerItem(props: Props) {
)}
</View>
</React.Fragment>
</TouchableItem>
</Touchable>
</View>
);
}
@@ -148,4 +204,7 @@ const styles = StyleSheet.create({
label: {
marginRight: 32,
},
button: {
display: 'flex',
},
});

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