Compare commits

..

175 Commits

Author SHA1 Message Date
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
Satyajit Sahoo
babb5027f9 chore: publish
- @react-navigation/stack@5.2.8
2020-03-27 15:01:32 +01:00
Satyajit Sahoo
78d7a66b2b chore: remove detox dep coz we don't use it 2020-03-27 14:54:54 +01:00
osdnk
a248c453ba chore: publish
- @react-navigation/stack@5.2.7
2020-03-26 17:07:40 +01:00
Wojciech Stanisz
e097df880a fix: add pointerEvents=box-none to overlay View (#7871) 2020-03-26 13:38:30 +01:00
Satyajit Sahoo
856449b200 chore: publish
- @react-navigation/bottom-tabs@5.2.4
 - @react-navigation/compat@5.1.6
 - @react-navigation/core@5.3.1
 - @react-navigation/drawer@5.3.4
 - @react-navigation/material-bottom-tabs@5.1.6
 - @react-navigation/material-top-tabs@5.1.6
 - @react-navigation/native@5.1.3
 - @react-navigation/stack@5.2.6
2020-03-23 17:07:43 +01:00
Steven Bell
d94e43c3c8 fix: add info about android launchMode in useLinking error 2020-03-23 16:39:18 +01:00
Satyajit Sahoo
3096de6286 fix: only call listeners for focused screen for global events 2020-03-23 13:43:43 +01:00
Satyajit Sahoo
1c001424b5 fix: don't emit events for screens that don't exist anymore 2020-03-23 13:03:33 +01:00
Satyajit Sahoo
0f2368965c chore: publish
- @react-navigation/stack@5.2.5
2020-03-23 11:42:01 +01:00
Satyajit Sahoo
61f16d3f25 fix: fix swipe gestures requiring a lot of velocity to dismiss 2020-03-23 11:40:37 +01:00
Satyajit Sahoo
853740bfaf chore: publish
- @react-navigation/bottom-tabs@5.2.3
 - @react-navigation/compat@5.1.5
 - @react-navigation/core@5.3.0
 - @react-navigation/drawer@5.3.3
 - @react-navigation/material-bottom-tabs@5.1.5
 - @react-navigation/material-top-tabs@5.1.5
 - @react-navigation/native@5.1.2
 - @react-navigation/routers@5.2.0
 - @react-navigation/stack@5.2.4
2020-03-23 00:00:55 +01:00
Satyajit Sahoo
179b6312fe chore: update prettier 2020-03-22 23:58:06 +01:00
Satyajit Sahoo
043924ca48 fix: fix swipe not dismissing card in RTL
closes #7841
2020-03-22 23:55:16 +01:00
Satyajit Sahoo
813a5903b5 feat: add keys to routes missing keys during reset 2020-03-22 23:38:40 +01:00
Satyajit Sahoo
3709e652f4 feat: support function in listeners prop 2020-03-22 23:33:25 +01:00
Satyajit Sahoo
5b15c7164f fix: return correct value for isFocused after changing screens
fixes #7843
2020-03-22 23:31:04 +01:00
Satyajit Sahoo
e030932497 chore: publish
- @react-navigation/stack@5.2.3
2020-03-19 21:56:36 +01:00
Tien Pham
adbfedcd58 fix: use the correct velocity value in closing animation (#7836)
In this commit f24d3a3461 we modified the `velocity` in inverted gesture, but since we also use this value in the closing animation, the change in that commit also introduced a new bug:
![Mar-20-2020 03-40-05](https://user-images.githubusercontent.com/57227217/77113229-006f0500-6a5d-11ea-97b5-571e8301cd87.gif)

This PR fixes the issue by keeping the original velocity value.
2020-03-19 21:55:03 +01:00
Satyajit Sahoo
bc9b044fb3 chore: publish
- @react-navigation/bottom-tabs@5.2.2
 - @react-navigation/compat@5.1.4
 - @react-navigation/core@5.2.3
 - @react-navigation/drawer@5.3.2
 - @react-navigation/material-bottom-tabs@5.1.4
 - @react-navigation/material-top-tabs@5.1.4
 - @react-navigation/native@5.1.1
 - @react-navigation/stack@5.2.2
2020-03-19 19:48:37 +01:00
Alexey Vlasenko
f24d3a3461 fix: fix closing stack using inverted gesture. (#7824)
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-03-19 19:32:29 +01:00
Satyajit Sahoo
3df65e2819 fix: initialize height and width to zero if undefined
closes #6789
2020-03-19 19:03:23 +01:00
Satyajit Sahoo
5c4afc5cb4 fix: close drawer on pressing Esc on web
closes #6745
2020-03-19 18:51:16 +01:00
Satyajit Sahoo
d5bb357053 chore: temporarily disables devtools until we add a public API
closes #7726
2020-03-19 18:39:04 +01:00
Satyajit Sahoo
b1fe73097f fix: only dismiss previously focused input on page change. closes #6918 2020-03-19 18:30:54 +01:00
Satyajit Sahoo
49f6fed6d3 fix: fix blank page if stack was inside display: none before 2020-03-19 18:11:55 +01:00
Satyajit Sahoo
b1a65fc73e fix: don't use react-native-screens on web
seems `react-native-screens` doesn't handle active screens properly and shows a blank page instead on web when a number is specified in the `active` prop.

closes #7485
2020-03-19 17:28:35 +01:00
Noemi Rozpara
3ea8eec432 fix: fix permanent sidebar position (#7830) 2020-03-19 11:44:13 +01:00
Satyajit Sahoo
00e0f05190 chore: publish
- @react-navigation/drawer@5.3.1
2020-03-17 20:13:03 +01:00
Satyajit Sahoo
193c344ba5 refactor: fix useIsDrawerOpen hook 2020-03-17 19:22:12 +01:00
Satyajit Sahoo
358d9e9feb chore: publish
- @react-navigation/bottom-tabs@5.2.1
 - @react-navigation/compat@5.1.3
 - @react-navigation/drawer@5.3.0
 - @react-navigation/material-bottom-tabs@5.1.3
 - @react-navigation/material-top-tabs@5.1.3
 - @react-navigation/native@5.1.0
 - @react-navigation/stack@5.2.1
2020-03-17 14:37:21 +01:00
Satyajit Sahoo
6a5d0a035a feat: add permanent drawer type (#7818)
Co-authored-by: NoemiRozpara <nrozpara@gmail.com>
2020-03-17 14:11:00 +01:00
Satyajit Sahoo
b75744abd5 chore: publish
- @react-navigation/bottom-tabs@5.2.0
 - @react-navigation/compat@5.1.2
 - @react-navigation/core@5.2.2
 - @react-navigation/drawer@5.2.0
 - @react-navigation/material-bottom-tabs@5.1.2
 - @react-navigation/material-top-tabs@5.1.2
 - @react-navigation/native@5.0.10
 - @react-navigation/routers@5.1.1
 - @react-navigation/stack@5.2.0
2020-03-16 14:29:25 +01:00
Satyajit Sahoo
6dbda1a0c2 chore: upgrade depenendecies 2020-03-16 14:28:10 +01:00
Michał Osadnik
70029d6c13 feat: add an option to change use a custom card overlay (#7809)
I find it sometimes useful to define overlay renderer on my own. Eg. I needed to replace the background with BlurView and with this API I find it quite easy

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-03-16 14:28:10 +01:00
Tien Pham
469d0542c7 fix: fix back gesture cancellation (#7700)
The problem here is that when we scroll back really fast, even though velocity is negative, `Math.abs(translation + velocity * gestureVelocityImpact)` will end up bigger than `distance / 2`.

I removed the `Math.abs`, I think it's not necessary. When `translation + velocity * gestureVelocityImpact` is negative, it's also < `distance / 2` and we should just close the screen.

Closes #6782
2020-03-16 12:03:16 +01:00
Vojtech Novak
0dcaea3242 fix: fix android header title font weight (#7720)
the previously used fort weight of 500 would effectively be converted to `fontWeight: bold` because of https://github.com/facebook/react-native/pull/25341

this fixes the title appearance to look as customary
2020-03-16 11:05:02 +01:00
Satyajit Sahoo
646cbfb28e refactor: move action helper types to routers 2020-03-13 12:34:37 +01:00
Satyajit Sahoo
660cac3557 fix: don't handle action if no routes are present 2020-03-11 18:17:19 +01:00
Satyajit Sahoo
e637250a7e chore: add a triage action for feature requests 2020-03-11 03:19:49 +01:00
Satyajit Sahoo
82af7bed71 feat: add safeAreaInsets to bottom tabs 2020-03-09 22:21:04 +01:00
Michał Osadnik
cb46d0bca4 feat: make useIsDrawerOpen workable inside drawer content (#7746) 2020-03-06 15:46:13 +01:00
Mike Rogers
b3665a325d Correcting spelling 'Supress' > 'Suppress' (#7731) 2020-03-06 07:24:43 -03:00
Satyajit Sahoo
0cc7a12b9c chore: remove stale action coz it's not keeping issues open after reply 2020-03-04 13:53:42 +01:00
Satyajit Sahoo
90e417248d chore: fix typo in expo preview url 2020-03-03 18:38:20 +01:00
Satyajit Sahoo
e071a978e6 chore: publish
- @react-navigation/bottom-tabs@5.1.1
 - @react-navigation/compat@5.1.1
 - @react-navigation/core@5.2.1
 - @react-navigation/drawer@5.1.1
 - @react-navigation/material-bottom-tabs@5.1.1
 - @react-navigation/material-top-tabs@5.1.1
 - @react-navigation/native@5.0.9
 - @react-navigation/routers@5.1.0
 - @react-navigation/stack@5.1.1
2020-03-03 11:58:45 +01:00
Satyajit Sahoo
296c836064 fix: ignore back button press if screen isn't focused. closes #7673 2020-03-03 11:34:38 +01:00
Satyajit Sahoo
09f6808d7d feat: make reset bubble up 2020-03-01 02:45:08 +01:00
Satyajit Sahoo
5bb0f405ce fix: fix links for documentation 2020-02-28 17:12:18 +01:00
Satyajit Sahoo
2dfa4f3629 fix: move updating state to useEffect 2020-02-28 17:01:58 +01:00
Satyajit Sahoo
cf41288760 chore: run clean before release 2020-02-26 15:03:57 +01:00
Satyajit Sahoo
3677818f63 chore: publish
- @react-navigation/bottom-tabs@5.1.0
 - @react-navigation/compat@5.1.0
 - @react-navigation/core@5.2.0
 - @react-navigation/drawer@5.1.0
 - @react-navigation/material-bottom-tabs@5.1.0
 - @react-navigation/material-top-tabs@5.1.0
 - @react-navigation/native@5.0.8
 - @react-navigation/routers@5.0.3
 - @react-navigation/stack@5.1.0
2020-02-26 13:57:42 +01:00
Satyajit Sahoo
162410843c feat: add ability add listeners with listeners prop
This adds ability to listen to events from the component where the navigator is defined, even if the screen is not rendered.

```js
<Tabs.Screen
  name="Chat"
  component={Chat}
  options={{ title: 'Chat' }}
  listeners={{
    tabPress: e => console.log('Tab press', e.target),
  }}
/>
```

Closes #6756
2020-02-26 13:02:22 +01:00
Satyajit Sahoo
028c2887c6 refactor: tweak error messages more 2020-02-25 20:58:14 +01:00
Satyajit Sahoo
7a44cda136 refactor: tweak error messages 2020-02-25 17:58:09 +01:00
Satyajit Sahoo
a046db536f chore: publish
- @react-navigation/stack@5.0.9
2020-02-24 14:45:00 +01:00
Satyajit Sahoo
d115787b1c chore: mark yarn script as binary 2020-02-24 14:44:29 +01:00
Michał Osadnik
80a337024a fix: enhance border radius in modals on new iPhones (#6945)
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-02-24 14:44:20 +01:00
Satyajit Sahoo
c19da31240 refactor: enable screens only for last screen
This will avoid issues such as https://github.com/react-navigation/react-navigation/issues/6909
2020-02-24 11:37:25 +01:00
Satyajit Sahoo
85e9376302 chore: publish
- @react-navigation/stack@5.0.8
2020-02-21 20:09:06 +01:00
Satyajit Sahoo
a67b49477e fix: fix transparent header on Android 2020-02-21 20:07:38 +01:00
192 changed files with 13236 additions and 5368 deletions

View File

@@ -1,58 +1,96 @@
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
- v2-dependencies-{{ checksum "yarn.lock" }}
- v2-dependencies-
- run:
name: Install project dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v1-dependencies-{{ checksum "yarn.lock" }}
paths: node_modules
key: v2-dependencies-{{ 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 +100,9 @@ workflows:
- unit-tests:
requires:
- install-dependencies
- integration-tests:
requires:
- install-dependencies
- build-packages:
requires:
- install-dependencies

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
yarn-*.js binary

View File

@@ -44,7 +44,7 @@ jobs:
- name: Get expo link
id: expo
run: echo "::set-output name=path::@react-navigation/react-react-navigationample?release-channel=pr-${{ github.event.number }}"
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
- name: Comment on PR
uses: unsplash/comment-on-pr@master

View File

@@ -1,20 +0,0 @@
name: "Close stale issues and pull requests"
on:
schedule:
- cron: "0 0 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Hello 👋, this issue has been open for more than 3 months with no activity on it. If the issue is still present in the latest version, please leave a comment within 7 days to keep it open, otherwise it will be closed automatically. If you found a solution on workaround for the issue, please comment here for others to find. If this issue is critical for you, please consider sending a pull request to fix the issue.'
stale-pr-message: 'Hello 👋, this pull request has been open for more than 3 months with no activity on it. If you think this is still necessary with the latest version, please comment and ping a maintainer to get this reviewed, otherwise it will be closed automatically in 7 days.'
days-before-stale: 90
days-before-close: 7
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-label: 'keep open'
exempt-pr-label: 'keep open'

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
@@ -36,3 +36,14 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. This helps us prioritize fixing bugs in the library. Seems you have a usage question. Please ask the question on [StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation) instead using the `react-navigation` label. You can also chat with other community members on [Reactiflux Discord server](https://www.reactiflux.com/) in the `#react-navigation` channel."
feature-request:
runs-on: ubuntu-latest
if: github.event.label.name == 'feature-request'
steps:
- uses: actions/checkout@master
- uses: actions/github@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: comment "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. Seems you have a feature request. Please post the feature request on [Canny](https://react-navigation.canny.io/feature-requests). This lets other users upvote your feature request and helps us prioritize the most requested features."

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

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
.gradle
.project
.settings
.history
local.properties

View File

@@ -7,7 +7,7 @@
"slug": "react-navigation-example",
"description": "Demo app to showcase various functionality of React Navigation",
"privacy": "public",
"sdkVersion": "36.0.0",
"sdkVersion": "37.0.0",
"platforms": [
"ios",
"android",

View File

@@ -1,4 +1,4 @@
module.exports = function(api) {
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],

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

@@ -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,8 @@
import { setup } from 'jest-dev-server';
export default async function () {
await setup({
command: 'yarn serve -l 3579 web-build',
port: 3579,
});
}

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 jest/no-jasmine-globals, 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();
});

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

@@ -15,8 +15,8 @@ const modules = ['@expo/vector-icons']
// List all packages under `packages/`
.readdirSync(packages)
// Ignore hidden files such as .DS_Store
.filter(p => !p.startsWith('.'))
.map(p => {
.filter((p) => !p.startsWith('.'))
.map((p) => {
const pak = JSON.parse(
fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8')
);
@@ -50,9 +50,9 @@ module.exports = {
blacklistRE: blacklist(
fs
.readdirSync(packages)
.map(p => path.join(packages, p))
.map((p) => path.join(packages, p))
.map(
it => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
(it) => new RegExp(`^${escape(path.join(it, 'node_modules'))}\\/.*$`)
)
),
@@ -65,7 +65,7 @@ module.exports = {
},
server: {
enhanceMiddleware: middleware => {
enhanceMiddleware: (middleware) => {
return (req, res, next) => {
// When an asset is imported outside the project root, it has wrong path on Android
// This happens for the back button in stack, so we fix the path to correct one

View File

@@ -8,35 +8,40 @@
"web": "expo start:web",
"native": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios"
"ios": "react-native run-ios",
"test": "jest"
},
"dependencies": {
"@expo/vector-icons": "^10.0.0",
"@react-native-community/masked-view": "0.1.6",
"@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.0.0",
"expo-blur": "^8.0.0",
"expo": "^37.0.8",
"expo-asset": "~8.1.3",
"expo-blur": "~8.1.0",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "~0.61.5",
"react-native-gesture-handler": "^1.5.6",
"react-native-paper": "^3.6.0",
"react-native-reanimated": "^1.7.0",
"react-native-restart": "^0.0.13",
"react-native-safe-area-context": "^0.7.2",
"react-native-screens": "^2.0.0-beta.2",
"react-native-tab-view": "2.13.0",
"react-native-unimodules": "^0.7.0",
"react-native-gesture-handler": "^1.6.0",
"react-native-paper": "^3.10.1",
"react-native-reanimated": "^1.8.0",
"react-native-restart": "^0.0.15",
"react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.7.0",
"react-native-tab-view": "2.14.0",
"react-native-unimodules": "~0.9.1",
"react-native-web": "^0.11.7"
},
"devDependencies": {
"@expo/webpack-config": "^0.10.12",
"@types/react": "^16.9.19",
"@types/react-native": "^0.60.30",
"babel-preset-expo": "^8.0.0",
"expo-cli": "^3.11.9",
"typescript": "^3.7.5"
"@expo/webpack-config": "^0.11.19",
"@types/jest-dev-server": "^4.2.0",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",
"babel-preset-expo": "^8.1.0",
"expo-cli": "^3.20.1",
"jest": "^26.0.1",
"jest-dev-server": "^4.4.0",
"playwright": "^0.14.0",
"serve": "^11.3.0",
"typescript": "^3.8.3"
}
}

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

@@ -1,4 +1,5 @@
import * as React from 'react';
import { Platform } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import TouchableBounce from '../Shared/TouchableBounce';
@@ -28,7 +29,10 @@ export default function BottomTabsScreen() {
return (
<BottomTabs.Navigator
screenOptions={{
tabBarButton: props => <TouchableBounce {...props} />,
tabBarButton:
Platform.OS === 'web'
? undefined
: (props) => <TouchableBounce {...props} />,
}}
>
<BottomTabs.Screen
@@ -38,7 +42,7 @@ export default function BottomTabsScreen() {
tabBarIcon: getTabBarIcon('file-document-box'),
}}
>
{props => <SimpleStackScreen {...props} headerMode="none" />}
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
</BottomTabs.Screen>
<BottomTabs.Screen
name="Chat"

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

@@ -15,7 +15,7 @@ export default function BottomTabsScreen() {
return (
<BottomTabs.Navigator>
{tabs.map(i => (
{tabs.map((i) => (
<BottomTabs.Screen
key={i}
name={`tab-${i}`}
@@ -29,12 +29,14 @@ export default function BottomTabsScreen() {
{() => (
<View style={styles.container}>
<Title>Tab {i}</Title>
<Button onPress={() => setTabs(tabs => [...tabs, tabs.length])}>
<Button onPress={() => setTabs((tabs) => [...tabs, tabs.length])}>
Add a tab
</Button>
<Button
onPress={() =>
setTabs(tabs => (tabs.length > 1 ? tabs.slice(0, -1) : tabs))
setTabs((tabs) =>
tabs.length > 1 ? tabs.slice(0, -1) : tabs
)
}
>
Remove a tab

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,127 @@
import * as React from 'react';
import { Dimensions, ScaledSize } from 'react-native';
import { Appbar } from 'react-native-paper';
import { ParamListBase } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import {
createDrawerNavigator,
DrawerNavigationProp,
DrawerContent,
} 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 isLargeScreen = useIsLargeScreen();
return (
<Appbar.Header>
{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 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) => (
<>
<Appbar.Header>
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
<Appbar.Content title="Pages" />
</Appbar.Header>
<DrawerContent {...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

@@ -28,7 +28,7 @@ export default function MaterialBottomTabsScreen() {
tabBarColor: '#C9E7F8',
}}
>
{props => <SimpleStackScreen {...props} headerMode="none" />}
{(props) => <SimpleStackScreen {...props} headerMode="none" />}
</MaterialBottomTabs.Screen>
<MaterialBottomTabs.Screen
name="Chat"

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>
);
};
@@ -107,9 +112,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

@@ -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,7 +104,7 @@ const AlbumsScreen = ({
Pop by 2
</Button>
</View>
<Albums scrollEnabled={false} />
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
@@ -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

@@ -15,11 +15,13 @@ 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 +34,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 +47,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 +80,7 @@ const AlbumsScreen = ({
Go back
</Button>
</View>
<Albums scrollEnabled={false} />
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
@@ -126,10 +131,10 @@ 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: () => (

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

@@ -68,10 +68,7 @@ export default function Chat(props: Partial<ScrollViewProps>) {
styles.input,
{ backgroundColor: colors.card, color: colors.text },
]}
placeholderTextColor={Color(colors.text)
.alpha(0.5)
.rgb()
.string()}
placeholderTextColor={Color(colors.text).alpha(0.5).rgb().string()}
placeholder="Write a message"
underlineColorAndroid="transparent"
/>

View File

@@ -51,10 +51,7 @@ export default function NewsFeed(props: Props) {
<Card style={styles.card}>
<TextInput
placeholder="What's on your mind?"
placeholderTextColor={Color(colors.text)
.alpha(0.5)
.rgb()
.string()}
placeholderTextColor={Color(colors.text).alpha(0.5).rgb().string()}
style={styles.input}
/>
</Card>

View File

@@ -1,13 +1,19 @@
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 {
Provider as PaperProvider,
@@ -16,12 +22,10 @@ import {
Appbar,
List,
Divider,
Text,
} from 'react-native-paper';
import { Asset } from 'expo-asset';
import {
InitialState,
useLinking,
NavigationContainerRef,
NavigationContainer,
DefaultTheme,
DarkTheme,
@@ -37,7 +41,9 @@ import {
HeaderStyleInterpolators,
} from '@react-navigation/stack';
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';
@@ -48,11 +54,16 @@ import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import DynamicTabs from './Screens/DynamicTabs';
import AuthFlow from './Screens/AuthFlow';
import CompatAPI from './Screens/CompatAPI';
import SettingsItem from './Shared/SettingsItem';
import { Updates } from 'expo';
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;
@@ -91,6 +102,10 @@ const SCREENS = {
title: 'Dynamic Tabs',
component: DynamicTabs,
},
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
},
AuthFlow: {
title: 'Auth Flow',
component: AuthFlow,
@@ -99,6 +114,10 @@ const SCREENS = {
title: 'Compat Layer',
component: CompatAPI,
},
LinkComponent: {
title: '<Link />',
component: LinkComponent,
},
};
const Drawer = createDrawerNavigator<RootDrawerParamList>();
@@ -110,36 +129,6 @@ 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);
@@ -150,17 +139,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 {
@@ -176,7 +166,7 @@ export default function App() {
};
restoreState();
}, [getInitialState]);
}, []);
const paperTheme = React.useMemo(() => {
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
@@ -192,27 +182,84 @@ export default function App() {
};
}, [theme.colors, theme.dark]);
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);
}, []);
if (!isReady) {
return null;
}
const isLargeScreen = dimensions.width >= 1024;
return (
<PaperProvider theme={paperTheme}>
{Platform.OS === 'ios' && (
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
)}
<NavigationContainer
ref={containerRef}
initialState={initialState}
onStateChange={state =>
onStateChange={(state) =>
AsyncStorage.setItem(
NAVIGATION_PERSISTENCE_KEY,
JSON.stringify(state)
)
}
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<{ [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: '' }
),
},
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',
},
}}
fallback={<Text>Loading</Text>}
>
<Drawer.Navigator>
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen
name="Root"
options={{
@@ -236,13 +283,15 @@ export default function App() {
name="Home"
options={{
title: 'Examples',
headerLeft: () => (
<Appbar.Action
color={theme.colors.text}
icon="menu"
onPress={() => navigation.toggleDrawer()}
/>
),
headerLeft: isLargeScreen
? undefined
: () => (
<Appbar.Action
color={theme.colors.text}
icon="menu"
onPress={() => navigation.toggleDrawer()}
/>
),
}}
>
{({
@@ -276,14 +325,15 @@ export default function App() {
theme.dark ? 'light' : 'dark'
);
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme));
}}
/>
<Divider />
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
name => (
(name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
/>
@@ -293,7 +343,7 @@ export default function App() {
)}
</Stack.Screen>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
name => (
(name) => (
<Stack.Screen
key={name}
name={name}

1
example/web/_redirects Normal file
View File

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

View File

@@ -7,7 +7,7 @@ const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const node_modules = path.resolve(__dirname, '..', 'node_modules');
const packages = path.resolve(__dirname, '..', 'packages');
module.exports = async function(env, argv) {
module.exports = async function (env, argv) {
const config = await createExpoWebpackConfigAsync(env, argv);
config.context = path.resolve(__dirname, '..');
@@ -20,23 +20,25 @@ module.exports = async function(env, argv) {
});
config.resolve.plugins = config.resolve.plugins.filter(
p => !(p instanceof ModuleScopePlugin)
(p) => !(p instanceof ModuleScopePlugin)
);
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;
};

View File

@@ -1,7 +1,7 @@
const error = console.error;
console.error = (...args) =>
// Supress error messages regarding error boundary in tests
// Suppress error messages regarding error boundary in tests
/(Consider adding an error boundary to your tree to customize error handling behavior|React will try to recreate this component tree from scratch using the error boundary you provided|Error boundaries should implement getDerivedStateFromError)/m.test(
args[0]
)

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,37 +13,38 @@
"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",
"example": "yarn --cwd example"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-flow": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@babel/preset-typescript": "^7.8.3",
"@babel/runtime": "^7.8.4",
"@babel/plugin-proposal-optional-chaining": "^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.6",
"@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^25.1.2",
"@types/jest": "^25.2.1",
"babel-jest": "^26.0.1",
"codecov": "^3.6.5",
"commitlint": "^8.3.5",
"core-js": "^3.6.4",
"detox": "^15.1.4",
"core-js": "^3.6.5",
"eslint": "^6.8.0",
"eslint-config-satya164": "^3.1.5",
"husky": "^4.2.1",
"jest": "^25.1.0",
"eslint-config-satya164": "^3.1.7",
"husky": "^4.2.5",
"jest": "^26.0.1",
"lerna": "^3.20.2",
"prettier": "^1.19.1",
"typescript": "^3.7.5"
"prettier": "^2.0.5",
"typescript": "^3.8.3"
},
"resolutions": {
"react": "~16.9.0",

View File

@@ -3,6 +3,205 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.2...@react-navigation/bottom-tabs@5.2.3) (2020-03-22)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.1...@react-navigation/bottom-tabs@5.2.2) (2020-03-19)
### Bug Fixes
* don't use react-native-screens on web ([b1a65fc](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b1a65fc73e8603ae2c06ef101a74df31e80bb9b2)), closes [#7485](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/7485)
* initialize height and width to zero if undefined ([3df65e2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/3df65e28197db3bb8371059146546d57661c5ba3)), closes [#6789](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6789)
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.0...@react-navigation/bottom-tabs@5.2.1) (2020-03-17)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.1...@react-navigation/bottom-tabs@5.2.0) (2020-03-16)
### Features
* add safeAreaInsets to bottom tabs ([82af7be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/82af7bed7135e42e24693b48cf7f1c6f9f5a6981))
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.1.0...@react-navigation/bottom-tabs@5.1.1) (2020-03-03)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.7...@react-navigation/bottom-tabs@5.1.0) (2020-02-26)
### Features
* add ability add listeners with listeners prop ([1624108](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6756)
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.6...@react-navigation/bottom-tabs@5.0.7) (2020-02-21)
**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.0.7",
"version": "5.4.5",
"keywords": [
"react-native-component",
"react-component",
@@ -15,6 +15,7 @@
"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": [
@@ -34,22 +35,24 @@
"react-native-iphone-x-helper": "^1.2.1"
},
"devDependencies": {
"@react-native-community/bob": "^0.9.3",
"@react-navigation/native": "^5.0.7",
"@react-native-community/bob": "^0.13.1",
"@react-navigation/native": "^5.4.0",
"@types/color": "^3.0.1",
"@types/react": "^16.9.19",
"@types/react-native": "^0.60.30",
"@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.2",
"typescript": "^3.7.5"
"react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.7.0",
"typescript": "^3.8.3"
},
"peerDependencies": {
"@react-navigation/native": "^5.0.5",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 0.6.0"
"react-native-safe-area-context": ">= 0.6.0",
"react-native-screens": ">= 2.0.0-alpha.0 || >= 2.0.0-beta.0 || >= 2.0.0"
},
"@react-native-community/bob": {
"source": "src",

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

@@ -48,6 +48,8 @@ function BottomTabNavigator({
}
export default createNavigatorFactory<
TabNavigationState,
BottomTabNavigationOptions,
BottomTabNavigationEventMap,
typeof BottomTabNavigator
>(BottomTabNavigator);

View File

@@ -4,6 +4,7 @@ import {
StyleProp,
TextStyle,
ViewStyle,
GestureResponderEvent,
} from 'react-native';
import {
NavigationHelpers,
@@ -11,6 +12,8 @@ import {
ParamListBase,
Descriptor,
TabNavigationState,
TabActionHelpers,
RouteProp,
} from '@react-navigation/native';
export type BottomTabNavigationEventMap = {
@@ -40,18 +43,15 @@ export type BottomTabNavigationProp<
TabNavigationState,
BottomTabNavigationOptions,
BottomTabNavigationEventMap
> & {
/**
* Jump to an existing tab.
*
* @param name Name of the route for the tab.
* @param [params] Params object for the route.
*/
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends undefined | any
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
> &
TabActionHelpers<ParamList>;
export type BottomTabScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: BottomTabNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type BottomTabNavigationOptions = {
@@ -148,7 +148,7 @@ export type BottomTabBarOptions = {
*/
inactiveTintColor?: string;
/**
* Background olor for the active tab.
* Background color for the active tab.
*/
activeBackgroundColor?: string;
/**
@@ -184,6 +184,16 @@ export type BottomTabBarOptions = {
* Whether the label position should adapt to the orientation.
*/
adaptive?: boolean;
/**
* Safe area insets for the tab bar. This is used to avoid elements like the navigation bar on Android and bottom safe area on iOS.
* By default, the device's safe area insets are automatically detected. You can override the behavior with this option.
*/
safeAreaInsets?: {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
/**
* Style object for the tab bar container.
*/
@@ -196,6 +206,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,8 +14,9 @@ import {
NavigationRouteContext,
CommonActions,
useTheme,
useLinkBuilder,
} from '@react-navigation/native';
import { SafeAreaConsumer } from 'react-native-safe-area-context';
import { useSafeArea } from 'react-native-safe-area-context';
import BottomTabItem from './BottomTabItem';
import { BottomTabBarProps } from '../types';
@@ -43,14 +44,21 @@ export default function BottomTabBar({
keyboardHidesTabBar = false,
labelPosition,
labelStyle,
safeAreaInsets,
showIcon,
showLabel,
style,
tabStyle,
}: Props) {
const { colors } = useTheme();
const buildLink = useLinkBuilder();
const [dimensions, setDimensions] = React.useState(() => {
const { height = 0, width = 0 } = Dimensions.get('window');
return { height, width };
});
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
const [layout, setLayout] = React.useState({
height: 0,
width: dimensions.width,
@@ -115,7 +123,7 @@ export default function BottomTabBar({
const handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout;
setLayout(layout => {
setLayout((layout) => {
if (height === layout.height && width === layout.width) {
return layout;
} else {
@@ -158,116 +166,123 @@ export default function BottomTabBar({
}
};
const defaultInsets = useSafeArea();
const insets = {
top: safeAreaInsets?.top ?? defaultInsets.top,
right: safeAreaInsets?.right ?? defaultInsets.right,
bottom: safeAreaInsets?.bottom ?? defaultInsets.bottom,
left: safeAreaInsets?.left ?? defaultInsets.left,
};
return (
<SafeAreaConsumer>
{insets => (
<Animated.View
style={[
styles.tabBar,
{
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,
{
height: DEFAULT_TABBAR_HEIGHT + (insets ? insets.bottom : 0),
paddingBottom: insets ? insets.bottom : 0,
},
style,
]}
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
>
<View style={styles.content} onLayout={handleLayout}>
{routes.map((route, index) => {
const focused = index === state.index;
const { options } = descriptors[route.key];
<Animated.View
style={[
styles.tabBar,
{
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,
{
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
paddingBottom: insets.bottom,
paddingHorizontal: Math.max(insets.left, insets.right),
},
style,
]}
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
>
<View style={styles.content} onLayout={handleLayout}>
{routes.map((route, index) => {
const focused = index === state.index;
const { options } = descriptors[route.key];
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!focused && !event.defaultPrevented) {
navigation.dispatch({
...CommonActions.navigate(route.name),
target: state.key,
});
}
};
if (!focused && !event.defaultPrevented) {
navigation.dispatch({
...CommonActions.navigate(route.name),
target: state.key,
});
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const accessibilityLabel =
options.tabBarAccessibilityLabel !== undefined
? options.tabBarAccessibilityLabel
: typeof label === 'string'
? `${label}, tab, ${index + 1} of ${routes.length}`
: undefined;
const accessibilityLabel =
options.tabBarAccessibilityLabel !== undefined
? options.tabBarAccessibilityLabel
: typeof label === 'string'
? `${label}, tab, ${index + 1} of ${routes.length}`
: undefined;
return (
<NavigationContext.Provider
key={route.key}
value={descriptors[route.key].navigation}
>
<NavigationRouteContext.Provider value={route}>
<BottomTabItem
route={route}
focused={focused}
horizontal={shouldUseHorizontalLabels()}
onPress={onPress}
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
testID={options.tabBarTestID}
allowFontScaling={allowFontScaling}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
activeBackgroundColor={activeBackgroundColor}
inactiveBackgroundColor={inactiveBackgroundColor}
button={options.tabBarButton}
icon={options.tabBarIcon}
label={label}
showIcon={showIcon}
showLabel={showLabel}
labelStyle={labelStyle}
style={tabStyle}
/>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
);
})}
</View>
</Animated.View>
)}
</SafeAreaConsumer>
return (
<NavigationContext.Provider
key={route.key}
value={descriptors[route.key].navigation}
>
<NavigationRouteContext.Provider value={route}>
<BottomTabItem
route={route}
focused={focused}
horizontal={shouldUseHorizontalLabels()}
onPress={onPress}
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
to={buildLink(route.name, route.params)}
testID={options.tabBarTestID}
allowFontScaling={allowFontScaling}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
activeBackgroundColor={activeBackgroundColor}
inactiveBackgroundColor={inactiveBackgroundColor}
button={options.tabBarButton}
icon={options.tabBarIcon}
label={label}
showIcon={showIcon}
showLabel={showLabel}
labelStyle={labelStyle}
style={tabStyle}
/>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
);
})}
</View>
</Animated.View>
);
}

View File

@@ -4,11 +4,13 @@ import {
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,
@@ -133,9 +179,7 @@ export default function BottomTabBarItem({
const inactiveTintColor =
customInactiveTintColor === undefined
? Color(colors.text)
.mix(Color(colors.card), 0.5)
.hex()
? Color(colors.text).mix(Color(colors.card), 0.5).hex()
: customInactiveTintColor;
const renderLabel = ({ focused }: { focused: boolean }) => {
@@ -198,6 +242,7 @@ export default function BottomTabBarItem({
: inactiveBackgroundColor;
return button({
to,
onPress,
onLongPress,
testID,
@@ -250,4 +295,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';
@@ -91,44 +95,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

@@ -1,6 +1,5 @@
import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
// eslint-disable-next-line import/no-unresolved
import { Screen, screensEnabled } from 'react-native-screens';
@@ -10,12 +9,14 @@ type Props = {
style?: any;
};
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container
export default class ResourceSavingScene extends React.Component<Props> {
render() {
if (screensEnabled?.()) {
// react-native-screens is buggy on web
if (screensEnabled?.() && Platform.OS !== 'web') {
const { isVisible, ...rest } = this.props;
// @ts-ignore
return <Screen active={isVisible ? 1 : 0} {...rest} />;
}
@@ -24,7 +25,13 @@ export default class ResourceSavingScene extends React.Component<Props> {
return (
<View
style={[styles.container, style, { opacity: isVisible ? 1 : 0 }]}
style={[
styles.container,
Platform.OS === 'web'
? { display: isVisible ? 'flex' : 'none' }
: null,
style,
]}
collapsable={false}
removeClippedSubviews={
// On iOS, set removeClippedSubviews to true only when not focused

View File

@@ -30,7 +30,7 @@ type Props = {
export default function SafeAreaProviderCompat({ children }: Props) {
return (
<SafeAreaConsumer>
{insets => {
{(insets) => {
if (insets) {
// If we already have insets, don't wrap the stack in another safe area provider
// This avoids an issue with updates at the cost of potentially incorrect values

View File

@@ -3,6 +3,195 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.4...@react-navigation/compat@5.1.5) (2020-03-22)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.3...@react-navigation/compat@5.1.4) (2020-03-19)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.2...@react-navigation/compat@5.1.3) (2020-03-17)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.1...@react-navigation/compat@5.1.2) (2020-03-16)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.0...@react-navigation/compat@5.1.1) (2020-03-03)
**Note:** Version bump only for package @react-navigation/compat
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.7...@react-navigation/compat@5.1.0) (2020-02-26)
### Features
* add ability add listeners with listeners prop ([1624108](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/6756)
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.6...@react-navigation/compat@5.0.7) (2020-02-21)
**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.0.7",
"version": "5.1.21",
"license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
"bugs": {
@@ -10,6 +10,7 @@
"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": [
@@ -25,11 +26,11 @@
"clean": "del lib"
},
"devDependencies": {
"@react-native-community/bob": "^0.9.3",
"@react-navigation/native": "^5.0.7",
"@types/react": "^16.9.19",
"@react-native-community/bob": "^0.13.1",
"@react-navigation/native": "^5.4.0",
"@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

@@ -6,6 +6,8 @@ import {
TypedNavigator,
NavigationProp,
RouteProp,
EventMapBase,
NavigationRouteContext,
} from '@react-navigation/native';
import CompatScreen from './CompatScreen';
import ScreenPropsContext from './ScreenPropsContext';
@@ -15,7 +17,9 @@ import { CompatScreenType, CompatRouteConfig } from './types';
export default function createCompatNavigatorFactory<
CreateNavigator extends () => TypedNavigator<
ParamListBase,
NavigationState,
{},
EventMapBase,
React.ComponentType<any>
>
>(createNavigator: CreateNavigator) {
@@ -64,9 +68,12 @@ 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 => {
routeNames.map((name) => {
let getScreenComponent: () => CompatScreenType<NavigationPropType>;
let initialParams;
@@ -132,7 +139,7 @@ export default function createCompatNavigatorFactory<
<Pair.Screen
key={name}
name={name}
initialParams={initialParams}
initialParams={{ ...parentRouteParams, ...initialParams }}
options={screenOptions}
>
{({ navigation, route }) => (
@@ -145,7 +152,7 @@ export default function createCompatNavigatorFactory<
</Pair.Screen>
);
}),
[screenProps]
[parentRouteParams, screenProps]
);
return (
@@ -160,7 +167,7 @@ export default function createCompatNavigatorFactory<
);
}
Navigator.navigationOtions = parentNavigationOptions;
Navigator.navigationOptions = parentNavigationOptions;
return Navigator;
};

View File

@@ -22,5 +22,7 @@ function SwitchNavigator(props: Props) {
}
export default createCompatNavigatorFactory(
createNavigatorFactory<{}, typeof SwitchNavigator>(SwitchNavigator)
createNavigatorFactory<TabNavigationState, {}, {}, typeof SwitchNavigator>(
SwitchNavigator
)
);

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

@@ -16,7 +16,7 @@ export default function useCompatNavigation<
const route = useRoute();
const isFirstRouteInParent = useNavigationState(
state => state.routes[0].key === route.key
(state) => state.routes[0].key === route.key
);
const context = React.useRef<Record<string, any>>({});

View File

@@ -26,8 +26,9 @@ export default function withNavigation<
return <Comp ref={onRef} navigation={navigation} {...rest} />;
};
WrappedComponent.displayName = `withNavigation(${Comp.displayName ||
Comp.name})`;
WrappedComponent.displayName = `withNavigation(${
Comp.displayName || Comp.name
})`;
return WrappedComponent;
}

View File

@@ -23,8 +23,9 @@ export default function withNavigationFocus<
return <Comp ref={onRef} isFocused={isFocused} {...rest} />;
};
WrappedComponent.displayName = `withNavigationFocus(${Comp.displayName ||
Comp.name})`;
WrappedComponent.displayName = `withNavigationFocus(${
Comp.displayName || Comp.name
})`;
return WrappedComponent;
}

View File

@@ -3,6 +3,215 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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)
### Bug Fixes
* don't emit events for screens that don't exist anymore ([1c00142](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/1c001424b595b40f9db9343096c833f75353b099))
* only call listeners for focused screen for global events ([3096de6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3096de62868a7ed9ed65e529c8ddfa001b9be486))
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.3...@react-navigation/core@5.3.0) (2020-03-22)
### Bug Fixes
* return correct value for isFocused after changing screens ([5b15c71](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/5b15c7164f5503f2f0d51006a3f23bd0c58fd9b7)), closes [#7843](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/7843)
### Features
* support function in listeners prop ([3709e65](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3709e652f41a16c2c2b05d5dbbe1da2017ba2c3f))
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.2...@react-navigation/core@5.2.3) (2020-03-19)
**Note:** Version bump only for package @react-navigation/core
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.1...@react-navigation/core@5.2.2) (2020-03-16)
**Note:** Version bump only for package @react-navigation/core
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.2.0...@react-navigation/core@5.2.1) (2020-03-03)
### Bug Fixes
* fix links for documentation ([5bb0f40](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/5bb0f405ceb5755d39a0b5b1f2e4ecee0da051bc))
* move updating state to useEffect ([2dfa4f3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2dfa4f36293a2acb718814f6b2fa79d7c7ddf09c))
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.6...@react-navigation/core@5.2.0) (2020-02-26)
### Features
* add ability add listeners with listeners prop ([1624108](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/6756)
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.5...@react-navigation/core@5.1.6) (2020-02-21)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/core",
"description": "Core utilities for building navigators",
"version": "5.1.6",
"version": "5.7.0",
"keywords": [
"react",
"react-native",
@@ -15,6 +15,7 @@
"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": [
@@ -29,24 +30,23 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/routers": "^5.0.2",
"escape-string-regexp": "^2.0.0",
"query-string": "^6.10.1",
"react-is": "^16.12.0",
"shortid": "^2.2.15",
"use-subscription": "^1.3.0"
"@react-navigation/routers": "^5.4.4",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.5",
"query-string": "^6.12.1",
"react-is": "^16.13.0",
"use-subscription": "^1.4.0"
},
"devDependencies": {
"@react-native-community/bob": "^0.9.3",
"@types/react": "^16.9.19",
"@react-native-community/bob": "^0.13.1",
"@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.12.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,22 +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 useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState';
import isSerializable from './isSerializable';
import { NavigationContainerRef, NavigationContainerProps } from './types';
import useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState';
type State = NavigationState | PartialState<NavigationState> | undefined;
const DEVTOOLS_CONFIG_KEY =
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
const MISSING_CONTEXT_ERROR =
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/en/getting-started.html for setup instructions.";
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
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/en/navigating-without-navigation-prop.html#handling-initialization for more details.";
"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;
@@ -73,7 +77,7 @@ const getPartialState = (
return {
...partialState,
stale: true,
routes: state.routes.map(route => {
routes: state.routes.map((route) => {
if (route.state === undefined) {
return route as Route<string> & {
state?: PartialState<NavigationState>;
@@ -102,7 +106,7 @@ const BaseNavigationContainer = React.forwardRef(
independent,
children,
}: NavigationContainerProps,
ref: React.Ref<NavigationContainerRef>
ref?: React.Ref<NavigationContainerRef>
) {
const parent = React.useContext(NavigationStateContext);
@@ -112,7 +116,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,6 +146,9 @@ const BaseNavigationContainer = React.forwardRef(
);
const { trackState, trackAction } = useDevTools({
enabled:
// @ts-ignore
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
name: '@react-navigation',
reset,
state,
@@ -155,7 +168,7 @@ const BaseNavigationContainer = React.forwardRef(
throw new Error(NOT_INITIALIZED_ERROR);
}
listeners[0](navigation => navigation.dispatch(action));
listeners[0]((navigation) => navigation.dispatch(action));
};
const canGoBack = () => {
@@ -163,7 +176,7 @@ const BaseNavigationContainer = React.forwardRef(
return false;
}
const { result, handled } = listeners[0](navigation =>
const { result, handled } = listeners[0]((navigation) =>
navigation.canGoBack()
);
@@ -206,6 +219,8 @@ const BaseNavigationContainer = React.forwardRef(
dispatch,
canGoBack,
getRootState,
dangerouslyGetState: () => state,
dangerouslyGetParent: () => undefined,
}));
const builderContext = React.useMemo(
@@ -217,6 +232,11 @@ const BaseNavigationContainer = React.forwardRef(
[addFocusedListener, trackAction, addStateGetter]
);
const scheduleContext = React.useMemo(
() => ({ scheduleUpdate, flushUpdates }),
[scheduleUpdate, flushUpdates]
);
const context = React.useMemo(
() => ({
state,
@@ -238,7 +258,7 @@ const BaseNavigationContainer = React.forwardRef(
hasWarnedForSerialization = true;
console.warn(
"We found non-serializable values in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/en/troubleshooting.html#i-get-the-warning-we-found-non-serializable-values-in-the-navigation-state for more details."
"Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details."
);
}
}
@@ -262,11 +282,13 @@ const BaseNavigationContainer = React.forwardRef(
}, [onStateChange, 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

@@ -4,7 +4,7 @@ type Props = {
children: React.ReactNode;
};
const MULTIPLE_NAVIGATOR_ERROR = `Another navigator is already registered for this container. You likely have multiple navigators under a single "NavigationContainer" or "Screen". Make sure each navigator is under a separate "Screen" container. See https://reactnavigation.org/docs/en/nesting-navigators.html for a guide on nesting.`;
const MULTIPLE_NAVIGATOR_ERROR = `Another navigator is already registered for this container. You likely have multiple navigators under a single "NavigationContainer" or "Screen". Make sure each navigator is under a separate "Screen" container. See https://reactnavigation.org/docs/nesting-navigators for a guide on nesting.`;
export const SingleNavigatorContext = React.createContext<
| {

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

@@ -10,10 +10,14 @@ import NavigationContext from './NavigationContext';
import NavigationRouteContext from './NavigationRouteContext';
import StaticContainer from './StaticContainer';
import EnsureSingleNavigator from './EnsureSingleNavigator';
import { NavigationProp, RouteConfig } from './types';
import { NavigationProp, RouteConfig, EventMapBase } from './types';
type Props<State extends NavigationState, ScreenOptions extends object> = {
screen: RouteConfig<ParamListBase, string, ScreenOptions>;
type Props<
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
> = {
screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
route: Route<string> & {
state?: NavigationState | PartialState<NavigationState>;
@@ -28,14 +32,15 @@ type Props<State extends NavigationState, ScreenOptions extends object> = {
*/
export default function SceneView<
State extends NavigationState,
ScreenOptions extends object
ScreenOptions extends object,
EventMap extends EventMapBase
>({
screen,
route,
navigation,
getState,
setState,
}: Props<State, ScreenOptions>) {
}: Props<State, ScreenOptions, EventMap>) {
const navigatorKeyRef = React.useRef<string | undefined>();
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
@@ -46,7 +51,7 @@ export default function SceneView<
const getCurrentState = React.useCallback(() => {
const state = getState();
const currentRoute = state.routes.find(r => r.key === route.key);
const currentRoute = state.routes.find((r) => r.key === route.key);
return currentRoute ? currentRoute.state : undefined;
}, [getState, route.key]);
@@ -57,7 +62,7 @@ export default function SceneView<
setState({
...state,
routes: state.routes.map(r =>
routes: state.routes.map((r) =>
r.key === route.key ? { ...r, state: child } : r
),
});

View File

@@ -1,5 +1,5 @@
import { ParamListBase } from '@react-navigation/routers';
import { RouteConfig } from './types';
import { ParamListBase, NavigationState } from '@react-navigation/routers';
import { RouteConfig, EventMapBase } from './types';
/**
* Empty component used for specifying route configuration.
@@ -7,8 +7,10 @@ import { RouteConfig } from './types';
export default function Screen<
ParamList extends ParamListBase,
RouteName extends keyof ParamList,
ScreenOptions extends object
>(_: RouteConfig<ParamList, RouteName, ScreenOptions>) {
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
>(_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>) {
/* istanbul ignore next */
return null;
}

View File

@@ -28,7 +28,7 @@ it('throws when getState is accessed without a container', () => {
const element = <Test />;
expect(() => render(element).update(element)).toThrowError(
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
);
});
@@ -47,7 +47,7 @@ it('throws when setState is accessed without a container', () => {
const element = <Test />;
expect(() => render(element).update(element)).toThrowError(
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
);
});
@@ -122,7 +122,7 @@ it('handle dispatching with ref', () => {
return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
@@ -220,7 +220,7 @@ it('handle resetting state with ref', () => {
return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
@@ -371,7 +371,7 @@ it('emits state events when the state changes', () => {
return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};

View File

@@ -27,7 +27,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
key: String(MockRouterKey.current++),
index,
routeNames,
routes: routeNames.map(name => ({
routes: routeNames.map((name) => ({
name,
key: name,
params: routeParamList[name],
@@ -43,9 +43,9 @@ export default function MockRouter(options: DefaultRouterOptions) {
}
const routes = state.routes
.filter(route => routeNames.includes(route.name))
.filter((route) => routeNames.includes(route.name))
.map(
route =>
(route) =>
({
...route,
key: route.key || `${route.name}-${MockRouterKey.current++}`,
@@ -73,7 +73,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
},
getStateForRouteNamesChange(state, { routeNames }) {
const routes = state.routes.filter(route =>
const routes = state.routes.filter((route) =>
routeNames.includes(route.name)
);
@@ -86,7 +86,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
},
getStateForRouteFocus(state, key) {
const index = state.routes.findIndex(r => r.key === key);
const index = state.routes.findIndex((r) => r.key === key);
if (index === -1 || index === state.index) {
return state;
@@ -105,7 +105,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
case 'NAVIGATE': {
const index = state.routes.findIndex(
route => route.name === action.payload.name
(route) => route.name === action.payload.name
);
if (index === -1) {

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,
},
},
},

View File

@@ -42,7 +42,8 @@ it('converts state to path string with config', () => {
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
id: (id: string) => Number(id.replace(/^x/, '')),
valid: Boolean,
},
@@ -116,7 +117,8 @@ it("doesn't add query param for empty params", () => {
});
it('handles state with config with nested screens', () => {
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
const path =
'/foo/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
@@ -128,7 +130,77 @@ it('handles state with config with nested screens', () => {
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) => author.toLowerCase(),
id: (id: number) => `x${id}`,
unknown: (_: unknown) => 'x',
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: '10',
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles state with config with nested screens and exact', () => {
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
@@ -181,7 +253,7 @@ it('handles state with config with nested screens', () => {
});
it('handles state with config with nested screens and unused configs', () => {
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
const path = '/foo/foe/baz/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
@@ -192,12 +264,74 @@ it('handles state with config with nested screens and unused configs', () => {
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) => author.replace(/^\w/, c => c.toLowerCase()),
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
unknown: (_: unknown) => 'x',
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles state with config with nested screens and unused configs with exact', () => {
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
unknown: (_: unknown) => 'x',
},
},
@@ -248,18 +382,91 @@ it('handles nested object with stringify in it', () => {
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
stringify: {
author: (author: string) =>
author.replace(/^\w/, c => c.toLowerCase()),
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, c => c.toUpperCase()),
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object with stringify in it with exact', () => {
const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
@@ -309,7 +516,7 @@ it('handles nested object with stringify in it', () => {
});
it('handles nested object for second route depth', () => {
const path = '/baz';
const path = '/foo/bar/baz';
const config = {
Foo: {
path: 'foo',
@@ -347,7 +554,95 @@ it('handles nested object for second route depth', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object for second route depth and and path and stringify in roots', () => {
it('handles nested object for second route depth with exact', () => {
const path = '/baz';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: 'foe',
Bar: {
path: 'bar',
screens: {
Baz: {
path: 'baz',
exact: true,
},
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object for second route depth and path and stringify in roots', () => {
const path = '/foo/dathomir/bar/42/baz';
const config = {
Foo: {
path: 'foo/:planet',
stringify: {
id: (id: number) => `planet=${id}`,
},
screens: {
Foe: 'foe',
Bar: {
path: 'bar/:id',
parse: {
id: Number,
},
screens: {
Baz: 'baz',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
params: { planet: 'dathomir' },
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz', params: { id: 42 } }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object for second route depth and path and stringify in roots with exact', () => {
const path = '/baz';
const config = {
Foo: {
@@ -366,7 +661,10 @@ it('handles nested object for second route depth and and path and stringify in r
id: Number,
},
screens: {
Baz: 'baz',
Baz: {
path: 'baz',
exact: true,
},
},
},
},
@@ -422,8 +720,51 @@ it('ignores empty string paths', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('keeps query params if path is empty', () => {
const path = '/?foo=42';
const config = {
Foo: {
screens: {
Foe: 'foe',
Bar: {
screens: {
Qux: {
path: '',
parse: { foo: Number },
},
Baz: 'baz',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Qux', params: { foo: 42 } }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toEqual(
path
);
});
it('cuts nested configs too', () => {
const path = '/baz';
const path = '/foo/baz';
const config = {
Foo: {
path: 'foo',
@@ -431,7 +772,48 @@ it('cuts nested configs too', () => {
Bar: '',
},
},
Baz: { path: 'baz' },
Baz: {
path: 'baz',
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('cuts nested configs too with exact', () => {
const path = '/baz';
const config = {
Foo: {
path: 'foo',
screens: {
Bar: {
path: '',
exact: true,
},
},
},
Baz: {
path: 'baz',
},
};
const state = {
@@ -457,7 +839,7 @@ it('cuts nested configs too', () => {
});
it('handles empty path at the end', () => {
const path = '/bar';
const path = '/foo/bar';
const config = {
Foo: {
path: 'foo',
@@ -491,6 +873,8 @@ it('handles empty path at the end', () => {
});
it('returns "/" for empty path', () => {
const path = '/';
const config = {
Foo: {
path: '',
@@ -515,5 +899,369 @@ it('returns "/" for empty path', () => {
],
};
expect(getPathFromState(state, config)).toBe('/');
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('parses no path specified', () => {
const path = '/Foo/bar';
const config = {
Foo: {
screens: {
Foe: {},
},
},
Bar: 'bar',
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [{ name: 'Bar' }],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('parses no path specified in nested config', () => {
const path = '/Foo/Foe/bar';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {},
},
},
Bar: 'bar',
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [{ name: 'Bar' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('strips undefined query params', () => {
const path = '/bar/sweet/apple/foo/bis/jane?count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('strips undefined query params with exact', () => {
const path = '/bar/sweet/apple/foo/bis/jane?count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles stripping all query params', () => {
const path = '/bar/sweet/apple/foo/bis/jane';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles stripping all query params with exact', () => {
const path = '/bar/sweet/apple/foo/bis/jane';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('replaces undefined query params', () => {
const path = '/bar/undefined/apple';
const config = {
Bar: 'bar/:type/:fruit',
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple' },
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});

File diff suppressed because it is too large Load Diff

View File

@@ -372,13 +372,15 @@ it("doesn't update state if action wasn't handled", () => {
expect(onStateChange).toBeCalledTimes(0);
expect(spy.mock.calls[0][0]).toMatch(
"The action 'INVALID' with payload 'undefined' was not handled by any navigator."
"The action 'INVALID' was not handled by any navigator."
);
spy.mockRestore();
});
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);
@@ -1085,7 +1449,7 @@ it('throws descriptive error for invalid screen component', () => {
);
expect(() => render(element).update(element)).toThrowError(
"Got an invalid value for 'component' prop for the screen 'foo'. It must be a a valid React Component."
"Got an invalid value for 'component' prop for the screen 'foo'. It must be a valid React Component."
);
});

View File

@@ -119,7 +119,7 @@ it('sets options with screenOptions prop as an object', () => {
return (
<>
{state.routes.map(route => {
{state.routes.map((route) => {
const { render, options } = descriptors[route.key];
return (
@@ -179,7 +179,7 @@ it('sets options with screenOptions prop as a fuction', () => {
return (
<>
{state.routes.map(route => {
{state.routes.map((route) => {
const { render, options } = descriptors[route.key];
return (
@@ -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';
@@ -273,7 +275,7 @@ it('sets initial options with setOptions', () => {
<BaseNavigationContainer>
<TestNavigator>
<Screen name="foo" options={{ color: 'blue' }}>
{props => <TestScreen {...props} />}
{(props) => <TestScreen {...props} />}
</Screen>
<Screen name="bar" component={jest.fn()} />
</TestNavigator>
@@ -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',
@@ -338,7 +340,7 @@ it('updates options with setOptions', () => {
<BaseNavigationContainer>
<TestNavigator>
<Screen name="foo" options={{ color: 'blue' }}>
{props => <TestScreen {...props} />}
{(props) => <TestScreen {...props} />}
</Screen>
<Screen name="bar" component={jest.fn()} />
</TestNavigator>

View File

@@ -15,7 +15,7 @@ it('fires focus and blur events in root navigator', () => {
React.useImperativeHandle(ref, () => navigation, [navigation]);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
});
const firstFocusCallback = jest.fn();
@@ -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(
@@ -106,7 +169,7 @@ it('fires focus and blur events in nested navigator', () => {
React.useImperativeHandle(ref, () => navigation, [navigation]);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
});
const firstFocusCallback = jest.fn();
@@ -362,7 +425,7 @@ it('fires blur event when a route is removed with a delay', async () => {
expect(blurCallback).toBeCalledTimes(1);
});
it('fires custom events', () => {
it('fires custom events added with addListener', () => {
const eventName = 'someSuperCoolEvent';
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
@@ -376,7 +439,7 @@ it('fires custom events', () => {
state,
]);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
});
const firstCallback = jest.fn();
@@ -409,10 +472,13 @@ it('fires custom events', () => {
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(0);
const target =
ref.current.state.routes[ref.current.state.routes.length - 1].key;
act(() => {
ref.current.navigation.emit({
type: eventName,
target: ref.current.state.routes[ref.current.state.routes.length - 1].key,
target,
data: 42,
});
});
@@ -422,6 +488,7 @@ it('fires custom events', () => {
expect(thirdCallback).toBeCalledTimes(1);
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
@@ -429,11 +496,280 @@ it('fires custom events', () => {
ref.current.navigation.emit({ type: eventName });
});
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
expect(secondCallback.mock.calls[0][0].target).toBe(undefined);
expect(thirdCallback.mock.calls[1][0].target).toBe(undefined);
expect(firstCallback).toBeCalledTimes(1);
expect(secondCallback).toBeCalledTimes(1);
expect(thirdCallback).toBeCalledTimes(2);
});
it("doesn't call same listener multiple times with addListener", () => {
const eventName = 'someSuperCoolEvent';
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation, descriptors } = useNavigationBuilder(
MockRouter,
props
);
React.useImperativeHandle(ref, () => ({ navigation, state }), [
navigation,
state,
]);
return state.routes.map((route) => descriptors[route.key].render());
});
const callback = jest.fn();
const Test = ({ navigation }: any) => {
React.useEffect(() => navigation.addListener(eventName, callback), [
navigation,
]);
return null;
};
const ref = React.createRef<any>();
const element = (
<BaseNavigationContainer>
<TestNavigator ref={ref}>
<Screen name="first" component={Test} />
<Screen name="second" component={Test} />
<Screen name="third" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
expect(callback).toBeCalledTimes(0);
act(() => {
ref.current.navigation.emit({ type: eventName });
});
expect(callback).toBeCalledTimes(1);
});
it('fires custom events added with listeners prop', () => {
const eventName = 'someSuperCoolEvent';
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation } = useNavigationBuilder(MockRouter, props);
React.useImperativeHandle(ref, () => ({ navigation, state }), [
navigation,
state,
]);
return null;
});
const firstCallback = jest.fn();
const secondCallback = jest.fn();
const thirdCallback = jest.fn();
const ref = React.createRef<any>();
const element = (
<BaseNavigationContainer>
<TestNavigator ref={ref}>
<Screen
name="first"
listeners={{ someSuperCoolEvent: firstCallback }}
component={jest.fn()}
/>
<Screen
name="second"
listeners={{ someSuperCoolEvent: secondCallback }}
component={jest.fn()}
/>
<Screen
name="third"
listeners={{ someSuperCoolEvent: thirdCallback }}
component={jest.fn()}
/>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
expect(firstCallback).toBeCalledTimes(0);
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(0);
const target =
ref.current.state.routes[ref.current.state.routes.length - 1].key;
act(() => {
ref.current.navigation.emit({
type: eventName,
target,
data: 42,
});
});
expect(firstCallback).toBeCalledTimes(0);
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(1);
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
act(() => {
ref.current.navigation.emit({ type: eventName });
});
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
expect(firstCallback).toBeCalledTimes(1);
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(1);
});
it("doesn't call same listener multiple times with listeners", () => {
const eventName = 'someSuperCoolEvent';
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation } = useNavigationBuilder(MockRouter, props);
React.useImperativeHandle(ref, () => ({ navigation, state }), [
navigation,
state,
]);
return null;
});
const callback = jest.fn();
const ref = React.createRef<any>();
const element = (
<BaseNavigationContainer>
<TestNavigator ref={ref}>
<Screen
name="first"
listeners={{ someSuperCoolEvent: callback }}
component={jest.fn()}
/>
<Screen
name="second"
listeners={{ someSuperCoolEvent: callback }}
component={jest.fn()}
/>
<Screen
name="third"
listeners={{ someSuperCoolEvent: callback }}
component={jest.fn()}
/>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
expect(callback).toBeCalledTimes(0);
act(() => {
ref.current.navigation.emit({ type: eventName });
});
expect(callback).toBeCalledTimes(1);
});
it('fires listeners when callback is provided for listeners prop', () => {
const eventName = 'someSuperCoolEvent';
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation } = useNavigationBuilder(MockRouter, props);
React.useImperativeHandle(ref, () => ({ navigation, state }), [
navigation,
state,
]);
return null;
});
const firstCallback = jest.fn();
const secondCallback = jest.fn();
const thirdCallback = jest.fn();
const ref = React.createRef<any>();
const element = (
<BaseNavigationContainer>
<TestNavigator ref={ref}>
<Screen
name="first"
listeners={({ route, navigation }) => ({
someSuperCoolEvent: (e) => firstCallback(e, route, navigation),
})}
component={jest.fn()}
/>
<Screen
name="second"
listeners={({ route, navigation }) => ({
someSuperCoolEvent: (e) => secondCallback(e, route, navigation),
})}
component={jest.fn()}
/>
<Screen
name="third"
listeners={({ route, navigation }) => ({
someSuperCoolEvent: (e) => thirdCallback(e, route, navigation),
})}
component={jest.fn()}
/>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
expect(firstCallback).toBeCalledTimes(0);
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(0);
const target =
ref.current.state.routes[ref.current.state.routes.length - 1].key;
act(() => {
ref.current.navigation.emit({
type: eventName,
target,
data: 42,
});
});
expect(firstCallback).toBeCalledTimes(0);
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(1);
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
expect(thirdCallback.mock.calls[0][0].target).toBe(target);
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
act(() => {
ref.current.navigation.emit({ type: eventName });
});
expect(firstCallback.mock.calls[0][0].target).toBe(undefined);
expect(firstCallback).toBeCalledTimes(1);
expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(1);
});
it('has option to prevent default', () => {
expect.assertions(5);
@@ -450,7 +786,7 @@ it('has option to prevent default', () => {
state,
]);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
});
const callback = (e: any) => {

View File

@@ -10,7 +10,7 @@ it('runs focus effect on focus change', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const focusEffect = jest.fn();
@@ -107,7 +107,7 @@ it('runs focus effect when initial state is given', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const focusEffect = jest.fn();

View File

@@ -10,7 +10,7 @@ it('renders correct focus state', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const Test = () => {

View File

@@ -12,7 +12,7 @@ it('gets navigation prop from context', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const Test = () => {
@@ -38,7 +38,7 @@ it("gets navigation's parent from context", () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const Test = () => {
@@ -70,7 +70,7 @@ it("gets navigation's parent's parent from context", () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const Test = () => {
@@ -112,7 +112,7 @@ it('throws if called outside a navigation context', () => {
const Test = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
expect(() => useNavigation()).toThrow(
"We couldn't find a navigation object. Is your component inside a screen in a navigator?"
"Couldn't find a navigation object. Is your component inside a screen in a navigator?"
);
return null;

View File

@@ -1,7 +1,10 @@
import * as React from 'react';
import { render } from 'react-native-testing-library';
import { render, act } from 'react-native-testing-library';
import useEventEmitter from '../useEventEmitter';
import useNavigationCache from '../useNavigationCache';
import useNavigationBuilder from '../useNavigationBuilder';
import BaseNavigationContainer from '../BaseNavigationContainer';
import Screen from '../Screen';
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
beforeEach(() => (MockRouterKey.current = 0));
@@ -40,7 +43,7 @@ it('preserves reference for navigation objects', () => {
});
if (previous.current) {
Object.keys(navigations).forEach(key => {
Object.keys(navigations).forEach((key) => {
expect(navigations[key]).toBe(previous.current[key]);
});
}
@@ -56,3 +59,136 @@ it('preserves reference for navigation objects', () => {
root.update(<Test />);
});
it('returns correct value for isFocused', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map((route) => descriptors[route.key].render());
};
let navigation: any;
const Test = (props: any) => {
navigation = props.navigation;
return null;
};
render(
<BaseNavigationContainer>
<TestNavigator>
<Screen name="first">{() => null}</Screen>
<Screen name="second" component={Test} />
<Screen name="third">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(navigation.isFocused()).toBe(false);
act(() => navigation.navigate('second'));
expect(navigation.isFocused()).toBe(true);
act(() => navigation.navigate('third'));
expect(navigation.isFocused()).toBe(false);
act(() => navigation.navigate('second'));
expect(navigation.isFocused()).toBe(true);
});
it('returns correct value for isFocused after changing screens', () => {
const TestRouter = (
options: Parameters<typeof MockRouter>[0]
): ReturnType<typeof MockRouter> => {
const router = MockRouter(options);
return {
...router,
getStateForRouteNamesChange(state, { routeNames }) {
const routes = routeNames.map(
(name) =>
state.routes.find((r) => r.name === name) || {
name,
key: name,
}
);
return {
...state,
routeNames,
routes,
index: routes.length - 1,
};
},
};
};
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
return state.routes.map((route) => descriptors[route.key].render());
};
let navigation: any;
const Test = (props: any) => {
navigation = props.navigation;
return null;
};
const root = render(
<BaseNavigationContainer>
<TestNavigator>
<Screen name="first">{() => null}</Screen>
<Screen name="second" component={Test} />
<Screen name="third">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(navigation.isFocused()).toBe(false);
root.update(
<BaseNavigationContainer>
<TestNavigator>
<Screen name="first">{() => null}</Screen>
<Screen name="third">{() => null}</Screen>
<Screen name="second" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
expect(navigation.isFocused()).toBe(true);
root.update(
<BaseNavigationContainer>
<TestNavigator>
<Screen name="first">{() => null}</Screen>
<Screen name="third">{() => null}</Screen>
<Screen name="fourth">{() => null}</Screen>
<Screen name="second" component={Test} />
</TestNavigator>
</BaseNavigationContainer>
);
expect(navigation.isFocused()).toBe(true);
root.update(
<BaseNavigationContainer>
<TestNavigator>
<Screen name="first">{() => null}</Screen>
<Screen name="third">{() => null}</Screen>
<Screen name="second" component={Test} />
<Screen name="fourth">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(navigation.isFocused()).toBe(false);
});

View File

@@ -11,13 +11,13 @@ it('gets the current navigation state', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const callback = jest.fn();
const Test = () => {
const state = useNavigationState(state => state);
const state = useNavigationState((state) => state);
callback(state);
@@ -62,13 +62,13 @@ it('gets the current navigation state with selector', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const callback = jest.fn();
const Test = () => {
const index = useNavigationState(state => state.index);
const index = useNavigationState((state) => state.index);
callback(index);
@@ -112,7 +112,7 @@ it('gets the correct value if selector changes', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const callback = jest.fn();
@@ -144,12 +144,12 @@ it('gets the correct value if selector changes', () => {
);
};
const root = render(<App selector={state => state.index} />);
const root = render(<App selector={(state) => state.index} />);
expect(callback).toBeCalledTimes(1);
expect(callback.mock.calls[0][0]).toBe(0);
root.update(<App selector={state => state.routes[state.index].name} />);
root.update(<App selector={(state) => state.routes[state.index].name} />);
expect(callback).toBeCalledTimes(2);
expect(callback.mock.calls[1][0]).toBe('first');

View File

@@ -137,7 +137,7 @@ it("lets children handle the action if parent didn't", () => {
return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
@@ -270,7 +270,7 @@ it("action doesn't bubble if target is specified", () => {
return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
@@ -317,7 +317,7 @@ it('logs error if no navigator handled the action', () => {
return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
@@ -374,7 +374,7 @@ it('logs error if no navigator handled the action', () => {
render(element).update(element);
expect(spy.mock.calls[0][0]).toMatch(
"The action 'UNKNOWN' with payload 'undefined' was not handled by any navigator."
"The action 'UNKNOWN' was not handled by any navigator."
);
spy.mockRestore();

View File

@@ -13,7 +13,7 @@ it('gets route prop from context', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return state.routes.map(route => descriptors[route.key].render());
return state.routes.map((route) => descriptors[route.key].render());
};
const Test = () => {

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { ParamListBase } from '@react-navigation/routers';
import { ParamListBase, NavigationState } from '@react-navigation/routers';
import Screen from './Screen';
import { TypedNavigator } from './types';
import { TypedNavigator, EventMapBase } from './types';
/**
* Higher order component to create a `Navigator` and `Screen` pair.
@@ -11,17 +11,21 @@ import { TypedNavigator } from './types';
* @returns Factory method to create a `Navigator` and `Screen` pair.
*/
export default function createNavigatorFactory<
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase,
NavigatorComponent extends React.ComponentType<any>
>(Navigator: NavigatorComponent) {
return function<ParamList extends ParamListBase>(): TypedNavigator<
return function <ParamList extends ParamListBase>(): TypedNavigator<
ParamList,
State,
ScreenOptions,
EventMap,
typeof Navigator
> {
if (arguments[0] !== undefined) {
throw new Error(
"Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5? See https://reactnavigation.org/docs/en/upgrading-from-4.x.html for migration guide."
"Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5? See https://reactnavigation.org/docs/upgrading-from-4.x for migration guide."
);
}

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,18 @@ 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>;
};
/**
@@ -48,120 +47,192 @@ 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 currentParams: Record<string, any> = { ...route.params };
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;
currentParams = fromEntries(
Object.entries(route.params).map(([key, value]) => [
key,
stringify?.[key] ? stringify[key](value) : String(value),
])
);
if (pattern) {
Object.assign(allParams, currentParams);
}
}
// 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 = p.replace(/^:/, '').replace(/\?$/, '');
if (currentOptions[route.name] !== undefined) {
path += pattern
.split('/')
.map(p => {
const name = p.replace(/^:/, '');
// If the path has a pattern for a param, put the param in the path
if (p.startsWith(':')) {
const value = allParams[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
// Remove the used value from the params object since we'll use the rest for query string
if (currentParams) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete params[name];
return encodeURIComponent(value);
delete currentParams[name];
}
return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
if (value === undefined && p.endsWith('?')) {
// Optional params without value assigned in route.params should be ignored
return '';
}
return encodeURIComponent(value);
}
return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
}
if (route.state) {
path += '/';
} else if (currentParams) {
for (let param in currentParams) {
if (currentParams[param] === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete currentParams[param];
}
}
if (route.state) {
path += '/';
} else if (params) {
const query = queryString.stringify(params);
const query = queryString.stringify(currentParams);
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 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,34 +48,73 @@ 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 configs so the most exhaustive is always first to be chosen
b.pattern.split('/').length - a.pattern.split('/').length
);
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
while (remaining) {
let routeNames: string[] | undefined;
let params: Record<string, any> | undefined;
let allParams: Record<string, any> | undefined;
// 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 (!config.regex) {
continue;
}
const match = remaining.match(config.regex);
// If our regex matches, we need to extract params from the path
if (match) {
@@ -91,24 +122,19 @@ export default function getStateFromPath(
const paramPatterns = config.pattern
.split('/')
.filter(p => p.startsWith(':'));
.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
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[key] =
config.parse && config.parse[key]
? config.parse[key](value)
: value;
acc[p] = value;
return acc;
}, {});
}
// Remove the matched segment from the remaining path
remaining = remaining.replace(match[0], '');
remaining = remaining.replace(match[1], '');
break;
}
@@ -123,34 +149,46 @@ export default function getStateFromPath(
remaining = segments.join('/');
}
let state: InitialState;
let routeName = routeNames.shift() as string;
let initialRoute = findInitialRoute(routeName, initialRoutes);
const state = createNestedStateObject(
routeNames.map((name) => {
const config = configs.find((c) => c.screen === name);
state = createNestedState(
initialRoute,
routeName,
routeNames.length === 0,
params
);
let params: object | undefined;
if (routeNames.length > 0) {
let nestedState = state;
if (allParams && config?.path) {
const pattern = config.path;
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 (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 };
}),
initialRoutes
);
if (current) {
// The state should be nested inside the deepest route we parsed before
@@ -172,74 +210,78 @@ 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 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 +290,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 `${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 +354,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 +381,87 @@ 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 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,6 +3,7 @@ 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';

View File

@@ -48,6 +48,7 @@ export type EventArg<
* Type of the event (e.g. `focus`, `blur`)
*/
readonly type: EventName;
readonly target?: string;
} & (CanPreventDefault extends true
? {
/**
@@ -167,18 +168,6 @@ type NavigationHelpersCommon<
| { name: RouteName; key?: string; params: ParamList[RouteName] }
): void;
/**
* Replace the current route with a new one.
*
* @param name Route name of the new route.
* @param [params] Params object for the new route.
*/
replace<RouteName extends keyof ParamList>(
...args: ParamList[RouteName] extends undefined
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
/**
* Reset the navigation state to the provided state.
*
@@ -204,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<
@@ -265,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>;
@@ -357,10 +346,22 @@ export type Descriptor<
>;
};
export type ScreenListeners<
State extends NavigationState,
EventMap extends EventMapBase
> = Partial<
{
[EventName in keyof (EventMap &
EventMapCore<State>)]: EventListenerCallback<EventMap, EventName>;
}
>;
export type RouteConfig<
ParamList extends ParamListBase,
RouteName extends keyof ParamList,
ScreenOptions extends object
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
> = {
/**
* Route name of this screen.
@@ -377,6 +378,16 @@ export type RouteConfig<
navigation: any;
}) => ScreenOptions);
/**
* Event listeners for this screen.
*/
listeners?:
| ScreenListeners<State, EventMap>
| ((props: {
route: RouteProp<ParamList, RouteName>;
navigation: any;
}) => ScreenListeners<State, EventMap>);
/**
* Initial params object for the route.
*/
@@ -399,28 +410,25 @@ 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;
};
export type TypedNavigator<
ParamList extends ParamListBase,
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase,
Navigator extends React.ComponentType<any>
> = {
/**
@@ -451,6 +459,19 @@ export type TypedNavigator<
* Component used for specifying route configuration.
*/
Screen: <RouteName extends keyof ParamList>(
_: RouteConfig<ParamList, RouteName, ScreenOptions>
_: 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

@@ -13,11 +13,24 @@ import NavigationBuilderContext, {
} from './NavigationBuilderContext';
import { NavigationEventEmitter } from './useEventEmitter';
import useNavigationCache from './useNavigationCache';
import { Descriptor, NavigationHelpers, RouteConfig, RouteProp } from './types';
import {
Descriptor,
NavigationHelpers,
RouteConfig,
RouteProp,
EventMapBase,
} from './types';
type Options<State extends NavigationState, ScreenOptions extends object> = {
type Options<
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
> = {
state: State;
screens: Record<string, RouteConfig<ParamListBase, string, ScreenOptions>>;
screens: Record<
string,
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
>;
navigation: NavigationHelpers<ParamListBase>;
screenOptions?:
| ScreenOptions
@@ -49,7 +62,8 @@ type Options<State extends NavigationState, ScreenOptions extends object> = {
*/
export default function useDescriptors<
State extends NavigationState,
ScreenOptions extends object
ScreenOptions extends object,
EventMap extends EventMapBase
>({
state,
screens,
@@ -64,7 +78,7 @@ export default function useDescriptors<
onRouteFocus,
router,
emitter,
}: Options<State, ScreenOptions>) {
}: Options<State, ScreenOptions, EventMap>) {
const [options, setOptions] = React.useState<Record<string, object>>({});
const { trackAction } = React.useContext(NavigationBuilderContext);
@@ -133,6 +147,7 @@ export default function useDescriptors<
: screen.options({
// @ts-ignore
route,
// @ts-ignore
navigation,
})),
// The options set via `navigation.setOptions`

View File

@@ -8,6 +8,7 @@ import {
type State = NavigationState | PartialState<NavigationState> | undefined;
type Options = {
enabled: boolean;
name: string;
reset: (state: NavigationState) => void;
state: State;
@@ -35,10 +36,11 @@ declare global {
}
}
export default function useDevTools({ name, reset, state }: Options) {
export default function useDevTools({ name, reset, state, enabled }: Options) {
const devToolsRef = React.useRef<DevTools>();
if (
enabled &&
process.env.NODE_ENV !== 'production' &&
global.__REDUX_DEVTOOLS_EXTENSION__ &&
devToolsRef.current === undefined
@@ -56,7 +58,7 @@ export default function useDevTools({ name, reset, state }: Options) {
React.useEffect(
() =>
devTools?.subscribe(message => {
devTools?.subscribe((message) => {
if (message.type === 'DISPATCH' && message.state) {
reset(JSON.parse(message.state));
}

View File

@@ -5,12 +5,20 @@ export type NavigationEventEmitter = EventEmitter<Record<string, any>> & {
create: (target: string) => EventConsumer<Record<string, any>>;
};
type Listeners = ((data: any) => void)[];
type Listeners = ((e: any) => void)[];
/**
* Hook to manage the event system used by the navigator to notify screens of various events.
*/
export default function useEventEmitter(): NavigationEventEmitter {
export default function useEventEmitter(
listen?: (e: any) => void
): NavigationEventEmitter {
const listenRef = React.useRef(listen);
React.useEffect(() => {
listenRef.current = listen;
});
const listeners = React.useRef<Record<string, Record<string, Listeners>>>({});
const create = React.useCallback((target: string) => {
@@ -60,7 +68,9 @@ export default function useEventEmitter(): NavigationEventEmitter {
const callbacks =
target !== undefined
? items[target] && items[target].slice()
: ([] as Listeners).concat(...Object.keys(items).map(t => items[t]));
: ([] as Listeners)
.concat(...Object.keys(items).map((t) => items[t]))
.filter((cb, i, self) => self.lastIndexOf(cb) === i);
const event: EventArg<any, any, any> = {
get type() {
@@ -68,8 +78,18 @@ export default function useEventEmitter(): NavigationEventEmitter {
},
};
if (target !== undefined) {
Object.defineProperty(event, 'target', {
enumerable: true,
get() {
return target;
},
});
}
if (data !== undefined) {
Object.defineProperty(event, 'data', {
enumerable: true,
get() {
return data;
},
@@ -81,11 +101,13 @@ export default function useEventEmitter(): NavigationEventEmitter {
Object.defineProperties(event, {
defaultPrevented: {
enumerable: true,
get() {
return defaultPrevented;
},
},
preventDefault: {
enumerable: true,
value() {
defaultPrevented = true;
},
@@ -93,7 +115,9 @@ export default function useEventEmitter(): NavigationEventEmitter {
});
}
callbacks?.forEach(cb => cb(event));
listenRef.current?.(event);
callbacks?.forEach((cb) => cb(event));
return event as any;
},

View File

@@ -46,7 +46,7 @@ export default function useFocusEffect(effect: EffectCallback) {
' fetchData();\n' +
' }, [someId])\n' +
'};\n\n' +
'See usage guide: https://reactnavigation.org/docs/en/use-focus-effect.html';
'See usage guide: https://reactnavigation.org/docs/use-focus-effect';
} else {
message += ` You returned: '${JSON.stringify(destroy)}'`;
}

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,6 +9,7 @@ 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) => {

View File

@@ -15,7 +15,7 @@ export default function useNavigation<
if (navigation === undefined) {
throw new Error(
"We couldn't find a navigation object. Is your component inside a screen in a navigator?"
"Couldn't find a navigation object. Is your component inside a screen in a navigator?"
);
}

View File

@@ -9,6 +9,7 @@ import {
RouterFactory,
PartialState,
NavigationAction,
Route,
} from '@react-navigation/routers';
import { NavigationStateContext } from './BaseNavigationContainer';
import NavigationRouteContext from './NavigationRouteContext';
@@ -27,9 +28,11 @@ import {
DefaultNavigatorOptions,
RouteConfig,
PrivateValueStore,
EventMapBase,
} from './types';
import useStateGetters from './useStateGetters';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
// This is to make TypeScript compiler happy
// eslint-disable-next-line babel/no-unused-expressions
@@ -40,6 +43,7 @@ type NavigatorRoute = {
params?: {
screen?: string;
params?: object;
initial?: boolean;
};
};
@@ -55,18 +59,28 @@ const isArrayEqual = (a: any[], b: any[]) =>
*
* @param children React Elements to extract the config from.
*/
const getRouteConfigsFromChildren = <ScreenOptions extends object>(
const getRouteConfigsFromChildren = <
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
>(
children: React.ReactNode
) => {
const configs = React.Children.toArray(children).reduce<
RouteConfig<ParamListBase, string, ScreenOptions>[]
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>[]
>((acc, child) => {
if (React.isValidElement(child)) {
if (child.type === Screen) {
// We can only extract the config from `Screen` elements
// If something else was rendered, it's probably a bug
acc.push(
child.props as RouteConfig<ParamListBase, string, ScreenOptions>
child.props as RouteConfig<
ParamListBase,
string,
State,
ScreenOptions,
EventMap
>
);
return acc;
}
@@ -75,7 +89,9 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
// When we encounter a fragment, we need to dive into its children to extract the configs
// This is handy to conditionally define a group of screens
acc.push(
...getRouteConfigsFromChildren<ScreenOptions>(child.props.children)
...getRouteConfigsFromChildren<State, ScreenOptions, EventMap>(
child.props.children
)
);
return acc;
}
@@ -90,7 +106,7 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
}, []);
if (process.env.NODE_ENV !== 'production') {
configs.forEach(config => {
configs.forEach((config) => {
const { name, children, component } = config as any;
if (typeof name !== 'string' || !name) {
@@ -116,7 +132,7 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
if (component !== undefined && !isValidElementType(component)) {
throw new Error(
`Got an invalid value for 'component' prop for the screen '${name}'. It must be a a valid React Component.`
`Got an invalid value for 'component' prop for the screen '${name}'. It must be a valid React Component.`
);
}
@@ -161,25 +177,35 @@ 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),
})
);
const routeConfigs = getRouteConfigsFromChildren<ScreenOptions>(children);
const routeConfigs = getRouteConfigsFromChildren<
State,
ScreenOptions,
EventMap
>(children);
const screens = routeConfigs.reduce<
Record<string, RouteConfig<ParamListBase, string, ScreenOptions>>
Record<
string,
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
>
>((acc, config) => {
if (config.name in acc) {
throw new Error(
@@ -191,12 +217,12 @@ export default function useNavigationBuilder<
return acc;
}, {});
const routeNames = routeConfigs.map(config => config.name);
const routeNames = routeConfigs.map((config) => config.name);
const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
(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;
@@ -220,12 +246,12 @@ export default function useNavigationBuilder<
}
const isStateValid = React.useCallback(
state => state.type === undefined || state.type === router.type,
(state) => state.type === undefined || state.type === router.type,
[router.type]
);
const isStateInitialized = React.useCallback(
state =>
(state) =>
state !== undefined && state.stale === false && isStateValid(state),
[isStateValid]
);
@@ -243,6 +269,8 @@ export default function useNavigationBuilder<
>();
const initializedStateRef = React.useRef<State>();
let isFirstStateInitialization = false;
if (
initializedStateRef.current === undefined ||
currentState !== previousStateRef.current
@@ -251,16 +279,21 @@ export default function useNavigationBuilder<
// 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,
});
if (currentState === undefined || !isStateValid(currentState)) {
isFirstStateInitialization = true;
initializedStateRef.current = router.getInitialState({
routeNames,
routeParamList,
});
} else {
initializedStateRef.current = router.getRehydratedState(
currentState as PartialState<State>,
{
routeNames,
routeParamList,
}
);
}
}
React.useEffect(() => {
@@ -286,16 +319,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,
@@ -309,15 +340,17 @@ export default function useNavigationBuilder<
routeNames,
routeParamList,
})
: state;
: nextState;
}
if (state !== nextState) {
// If the state needs to be updated, we'll schedule an update with React
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
setState(nextState);
}
const shouldUpdate = state !== nextState;
useScheduleUpdate(() => {
if (shouldUpdate) {
// If the state needs to be updated, we'll schedule an update
setState(nextState);
}
});
// 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
@@ -329,9 +362,14 @@ 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
}, []);
@@ -344,7 +382,50 @@ export default function useNavigationBuilder<
: (initializedStateRef.current as State);
}, [getCurrentState, isStateInitialized]);
const emitter = useEventEmitter();
const emitter = useEventEmitter((e) => {
let routeNames = [];
let route: Route<string> | undefined;
if (e.target) {
route = state.routes.find((route) => route.key === e.target);
if (route?.name) {
routeNames.push(route.name);
}
} else {
route = state.routes[state.index];
routeNames.push(
...Object.keys(screens).filter((name) => route?.name === name)
);
}
if (route == null) {
return;
}
const navigation = descriptors[route.key].navigation;
const listeners = ([] as (((e: any) => void) | undefined)[])
.concat(
...routeNames.map((name) => {
const { listeners } = screens[name];
const map =
typeof listeners === 'function'
? listeners({ route: route as any, navigation })
: listeners;
return map
? Object.keys(map)
.filter((type) => type === e.type)
.map((type) => map?.[type])
: undefined;
})
)
.filter((cb, i, self) => cb && self.lastIndexOf(cb) === i);
listeners.forEach((listener) => listener?.(e));
});
useFocusEvents({ state, emitter });
@@ -400,7 +481,7 @@ export default function useNavigationBuilder<
getStateForRoute,
});
const descriptors = useDescriptors<State, ScreenOptions>({
const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
state,
screens,
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 = {
@@ -63,7 +60,7 @@ export default function useNavigationCache<
};
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(
(acc, route, index) => {
(acc, route) => {
const previous = cache.current[route.key];
if (previous) {
@@ -99,18 +96,16 @@ export default function useNavigationCache<
...rest,
...helpers,
...emitter.create(route.key),
dangerouslyGetParent: () => parentNavigation as any,
dangerouslyGetState: getState,
dispatch,
setOptions: (options: object) =>
setOptions(o => ({
setOptions((o) => ({
...o,
[route.key]: { ...o[route.key], ...options },
})),
isFocused: () => {
const state = getState();
if (index !== state.index) {
if (state.routes[state.index].key !== route.key) {
return false;
}

View File

@@ -36,18 +36,45 @@ export default function useNavigationHelpers<
const parentNavigationHelpers = React.useContext(NavigationContext);
return React.useMemo(() => {
const dispatch = (action: Action | ((state: State) => Action)) => {
const payload =
typeof action === 'function' ? action(getState()) : action;
const dispatch = (op: Action | ((state: State) => Action)) => {
const action = typeof op === 'function' ? op(getState()) : op;
const handled = onAction(payload);
const handled = onAction(action);
if (!handled && process.env.NODE_ENV !== 'production') {
console.error(
`The action '${payload.type}' with payload '${JSON.stringify(
payload.payload
)}' was not handled by any navigator. If you are trying to navigate to a screen, check if the screen exists in your navigator. If you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/en/nesting-navigators.html#navigating-to-a-screen-in-a-nested-navigator.`
);
const payload: Record<string, any> | undefined = action.payload;
let message = `The action '${action.type}'${
payload ? ` with payload ${JSON.stringify(action.payload)}` : ''
} was not handled by any navigator.`;
switch (action.type) {
case 'NAVIGATE':
case 'PUSH':
case 'REPLACE':
case 'JUMP_TO':
if (payload?.name) {
message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`;
} else {
message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`;
}
break;
case 'GO_BACK':
case 'POP':
case 'POP_TO_TOP':
message += `\n\nIs there any screen to go back to?`;
break;
case 'OPEN_DRAWER':
case 'CLOSE_DRAWER':
case 'TOGGLE_DRAWER':
message += `\n\nIs your screen inside a Drawer navigator?`;
break;
}
message += `\n\nThis is a development-only warning and won't be shown in production.`;
console.error(message);
}
};
@@ -85,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

@@ -26,7 +26,7 @@ export default function useNavigationState<T>(selector: Selector<T>): T {
});
React.useEffect(() => {
const unsubscribe = navigation.addListener('state', e => {
const unsubscribe = navigation.addListener('state', (e) => {
setResult(selectorRef.current(e.data.state));
});

View File

@@ -18,7 +18,7 @@ export default function useOnGetState({
const state = getState();
return {
...state,
routes: state.routes.map(route => ({
routes: state.routes.map((route) => ({
...route,
state: getStateForRoute(route.key),
})),

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