Compare commits

...

190 Commits

Author SHA1 Message Date
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
Satyajit Sahoo
225cb298b6 chore: publish
- @react-navigation/bottom-tabs@5.0.7
 - @react-navigation/compat@5.0.7
 - @react-navigation/core@5.1.6
 - @react-navigation/drawer@5.0.7
 - @react-navigation/material-bottom-tabs@5.0.7
 - @react-navigation/material-top-tabs@5.0.7
 - @react-navigation/native@5.0.7
 - @react-navigation/routers@5.0.2
 - @react-navigation/stack@5.0.7
2020-02-21 19:18:56 +01:00
Satyajit Sahoo
c8ea4199f4 fix: tweak error message for navigate 2020-02-21 19:13:11 +01:00
Satyajit Sahoo
f16700812f fix: avoid emitting focus events twice
fixes #6749
2020-02-21 18:56:06 +01:00
Satyajit Sahoo
240ce01822 fix: make sure header is visibile to accessibility tools on iOS 2020-02-21 16:30:05 +01:00
Satyajit Sahoo
c7dd3a58b1 fix: debounce back button by default in stack header 2020-02-21 15:31:50 +01:00
Satyajit Sahoo
125bd70e49 fix: preserve screen order with numeric names
fixes #6900
2020-02-21 05:43:32 +01:00
Satyajit Sahoo
4578849ebf chore: publish
- @react-navigation/bottom-tabs@5.0.6
 - @react-navigation/compat@5.0.6
 - @react-navigation/core@5.1.5
 - @react-navigation/drawer@5.0.6
 - @react-navigation/material-bottom-tabs@5.0.6
 - @react-navigation/material-top-tabs@5.0.6
 - @react-navigation/native@5.0.6
 - @react-navigation/stack@5.0.6
2020-02-19 23:34:10 +01:00
Satyajit Sahoo
c084517d7b chore: add release script 2020-02-19 23:30:12 +01:00
Satyajit Sahoo
22c85ff6a9 chore: remove native-stack from the repo
it's now moved to https://github.com/kmagiera/react-native-screens
2020-02-19 23:18:11 +01:00
Satyajit Sahoo
bf76075e0f fix: add accessibilityLabel prop to back button
fixes #6895
2020-02-19 23:09:00 +01:00
Satyajit Sahoo
d69b0db604 fix: add NavigationEvents
See https://github.com/react-navigation/react-navigation/issues/6821#issuecomment-588268512
2020-02-19 23:04:14 +01:00
Satyajit Sahoo
cdb2fed43d chore: update .gitignore 2020-02-18 21:03:18 +01:00
Satyajit Sahoo
bb0226e26d chore: tweak error message 2020-02-18 18:59:39 +01:00
Satyajit Sahoo
1a28c299b5 fix: show descriptive error for invalid return for useFocusEffect 2020-02-15 20:02:39 +01:00
Satyajit Sahoo
e0c3298e64 fix: delay showing drawer by one frame after layout 2020-02-15 19:17:20 +01:00
Satyajit Sahoo
040f5dbb9d refactor: drop use of performTransaction 2020-02-15 19:05:50 +01:00
Satyajit Sahoo
5b7bbbdfd9 chore: publish
- @react-navigation/bottom-tabs@5.0.5
- @react-navigation/compat@5.0.5
- @react-navigation/core@5.1.4
- @react-navigation/drawer@5.0.5
- @react-navigation/material-bottom-tabs@5.0.5
- @react-navigation/material-top-tabs@5.0.5
- @react-navigation/native-stack@5.0.5
- @react-navigation/native@5.0.5
- @react-navigation/stack@5.0.5
2020-02-15 00:18:52 +01:00
Satyajit Sahoo
c5fefc6ee9 chore: tweak versions for peer dependencies 2020-02-15 00:15:01 +01:00
Satyajit Sahoo
aaf01e01e7 fix: return '/' for empty paths 2020-02-14 23:17:29 +01:00
Satyajit Sahoo
ac242fd281 refactor: discard all routes but last when getting action from state 2020-02-14 23:04:09 +01:00
Satyajit Sahoo
c5fcfbd427 fix: link to migration guide on invalid usage 2020-02-14 22:48:04 +01:00
Satyajit Sahoo
424c9469e4 chore: publish
- @react-navigation/bottom-tabs@5.0.4
 - @react-navigation/compat@5.0.4
 - @react-navigation/core@5.1.3
 - @react-navigation/drawer@5.0.4
 - @react-navigation/material-bottom-tabs@5.0.4
 - @react-navigation/material-top-tabs@5.0.4
 - @react-navigation/native-stack@5.0.4
 - @react-navigation/native@5.0.4
 - @react-navigation/stack@5.0.4
2020-02-14 18:50:01 +01:00
Satyajit Sahoo
8f40a98086 fix: hard code header height for animation
closes #6818
2020-02-14 18:44:54 +01:00
Satyajit Sahoo
f964200b0d fix: update links in error messages 2020-02-14 18:32:53 +01:00
Satyajit Sahoo
bd2f008a83 chore: build related changes 2020-02-14 18:32:53 +01:00
Chris
e37d6598ca docs: Update types.tsx (#6849)
Typo :)
2020-02-14 10:43:53 +01:00
Satyajit Sahoo
c8ac5fab61 fix: return false for canGoBack if navigator hasn't finished mounting 2020-02-12 21:28:03 +01:00
Satyajit Sahoo
b6accd03f6 fix: throw a descriptive error if navigation object hasn't initialized 2020-02-12 20:59:58 +01:00
Satyajit Sahoo
0cca1309ec chore: publish
- @react-navigation/bottom-tabs@5.0.3
 - @react-navigation/compat@5.0.3
 - @react-navigation/core@5.1.2
 - @react-navigation/drawer@5.0.3
 - @react-navigation/material-bottom-tabs@5.0.3
 - @react-navigation/material-top-tabs@5.0.3
 - @react-navigation/native-stack@5.0.3
 - @react-navigation/native@5.0.3
 - @react-navigation/stack@5.0.3
2020-02-12 16:58:48 +01:00
Satyajit Sahoo
6c9447a38c fix: check if we can go baack before dispatching pop 2020-02-12 13:17:08 +01:00
Satyajit Sahoo
030c63c89f fix: fix false positives for circular object check
fixes #6827
2020-02-12 11:42:36 +01:00
Abhinandan Ramaprasath
2bf0958502 fix: static container memo check (#6825)
Memo check compared elements of prevProps to nextProps but failed
to take new props into account. Fixed the logic and added a new
test.
2020-02-12 10:56:23 +01:00
Satyajit Sahoo
94cff2380a chore: publish
- @react-navigation/bottom-tabs@5.0.2
 - @react-navigation/compat@5.0.2
 - @react-navigation/core@5.1.1
 - @react-navigation/drawer@5.0.2
 - @react-navigation/material-bottom-tabs@5.0.2
 - @react-navigation/material-top-tabs@5.0.2
 - @react-navigation/native-stack@5.0.2
 - @react-navigation/native@5.0.2
 - @react-navigation/stack@5.0.2
2020-02-11 18:57:48 +01:00
Satyajit Sahoo
359ae1bfac fix: don't cleanup state on switching navigator
This leads to a glitch. Switching navigators should be handled by the router properly.
2020-02-11 18:56:27 +01:00
Satyajit Sahoo
031136f7c8 fix: remove unnecessary borderless from drawer item
closes #6801
2020-02-11 17:21:05 +01:00
Satyajit Sahoo
b6e7e08b9a fix: provide route context to header and bottom tabs 2020-02-11 15:42:00 +01:00
Satyajit Sahoo
6c6102b459 fix: make getInitialState async on web 2020-02-11 15:40:49 +01:00
David Govea
0c59ef7328 fix: initialize keyboard-hiding tabBar to visible=true (#6740, #6799)
Looks like this was an accidental refactor bug introduced with commit
38a38b0 (refactor from 	class component to function component)
2020-02-11 12:16:41 +01:00
Satyajit Sahoo
297eabb90e chore: fix typo 2020-02-11 00:34:04 +01:00
217 changed files with 13027 additions and 6805 deletions

View File

@@ -1,58 +1,96 @@
version: 2 version: 2.1
defaults: &defaults executors:
docker: default:
- image: circleci/node:10 docker:
working_directory: ~/project - image: circleci/node:10
working_directory: ~/project
environment:
YARN_CACHE_FOLDER: "~/.cache/yarn"
commands:
attach_project:
steps:
- attach_workspace:
at: ~/project
jobs: jobs:
install-dependencies: install-dependencies:
<<: *defaults executor: default
steps: steps:
- checkout - checkout
- attach_workspace: - attach_project
at: ~/project
- restore_cache: - restore_cache:
keys: keys:
- v1-dependencies-{{ checksum "yarn.lock" }} - v2-dependencies-{{ checksum "yarn.lock" }}
- v1-dependencies- - v2-dependencies-
- run: yarn install --frozen-lockfile - run:
name: Install project dependencies
command: yarn install --frozen-lockfile
- save_cache: - save_cache:
key: v1-dependencies-{{ checksum "yarn.lock" }} key: v2-dependencies-{{ checksum "yarn.lock" }}
paths: node_modules paths: ~/.cache/yarn
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: . paths: .
lint-and-typecheck: lint-and-typecheck:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: | name: Lint files
yarn lint command: yarn lint
yarn typescript - run:
name: Typecheck files
command: yarn typescript
unit-tests: unit-tests:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: | name: Run unit tests
yarn test --coverage command: yarn test --coverage
cat ./coverage/lcov.info | ./node_modules/.bin/codecov - run:
- store_artifacts: name: Upload test coverage
path: coverage command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
destination: coverage - 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: build-packages:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: | name: Build packages in the monorepo
yarn lerna run prepare command: yarn lerna run prepare
node scripts/check-types-path.js - run:
name: Verify paths for types
command: node scripts/check-types-path.js
workflows: workflows:
version: 2
build-and-test: build-and-test:
jobs: jobs:
- install-dependencies - install-dependencies
@@ -62,6 +100,9 @@ workflows:
- unit-tests: - unit-tests:
requires: requires:
- install-dependencies - install-dependencies
- integration-tests:
requires:
- install-dependencies
- build-packages: - build-packages:
requires: requires:
- install-dependencies - install-dependencies

View File

@@ -8,7 +8,6 @@
"@react-navigation/routers", "@react-navigation/routers",
"@react-navigation/compat", "@react-navigation/compat",
"@react-navigation/stack", "@react-navigation/stack",
"@react-navigation/native-stack",
"@react-navigation/drawer", "@react-navigation/drawer",
"@react-navigation/bottom-tabs", "@react-navigation/bottom-tabs",
"@react-navigation/material-top-tabs", "@react-navigation/material-top-tabs",

1
.gitattributes vendored Normal file
View File

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

View File

@@ -1,38 +0,0 @@
---
name: Native Stack Navigator
about: Report an issue with Native Stack Navigator (@react-navigation/native-stack)
title: ''
labels: bug, package:native-stack
assignees: ''
---
**Current Behavior**
- What code are you running and what is happening?
- Include a screenshot or video if it makes sense.
**Expected Behavior**
- What do you expect should be happening?
- Include a screenshot or video if it makes sense.
**How to reproduce**
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
- Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue.
- Before reporting an issue, make sure you are on latest version of the package.
**Your Environment**
| software | version |
| ------------------------------ | ------- |
| iOS or Android |
| @react-navigation/native |
| @react-navigation/native-stack |
| react-native-screens |
| react-native |
| expo |
| node |
| npm or yarn |

View File

@@ -44,7 +44,7 @@ jobs:
- name: Get expo link - name: Get expo link
id: expo 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 - name: Comment on PR
uses: unsplash/comment-on-pr@master 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: 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: needs-repro:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -24,7 +24,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: 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: question:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -36,3 +36,14 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: 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." 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

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
.idea .idea
.expo .expo
.gradle .gradle
.project
.settings
.history
local.properties local.properties

View File

@@ -113,7 +113,7 @@ When you're sending a pull request:
Maintainers with write access to the GitHub repo and the npm organization can publish new versions. To publish a new version, first, you need to export a `GH_TOKEN` environment variable as mentioned [here](https://github.com/lerna/lerna/tree/master/commands/version#--create-release-type). Then run: Maintainers with write access to the GitHub repo and the npm organization can publish new versions. To publish a new version, first, you need to export a `GH_TOKEN` environment variable as mentioned [here](https://github.com/lerna/lerna/tree/master/commands/version#--create-release-type). Then run:
```sh ```sh
yarn lerna publish yarn release
``` ```
This will automatically bump the version and publish the packages. It'll also publish the changelogs on GitHub for each package. This will automatically bump the version and publish the packages. It'll also publish the changelogs on GitHub for each package.

View File

@@ -13,7 +13,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> <excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View File

@@ -161,32 +161,33 @@
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:2.0.0@aar" level="project" /> <orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: io.nlopez.smartlocation:library:3.2.11@aar" level="project" /> <orderEntry type="library" name="Gradle: io.nlopez.smartlocation:library:3.2.11@aar" level="project" />
<orderEntry type="library" name="Gradle: org.webkit:android-jsc:r245459@aar" level="project" /> <orderEntry type="library" name="Gradle: org.webkit:android-jsc:r245459@aar" level="project" />
<orderEntry type="module" module-name="android-expo-permissions" /> <orderEntry type="module" module-name="expo-permissions" />
<orderEntry type="module" module-name="android-expo-constants" /> <orderEntry type="module" module-name="expo-constants" />
<orderEntry type="module" module-name="android-unimodules-image-loader-interface" /> <orderEntry type="module" module-name="unimodules-image-loader-interface" />
<orderEntry type="module" module-name="android-expo-web-browser" /> <orderEntry type="module" module-name="expo-web-browser" />
<orderEntry type="module" module-name="android-unimodules-react-native-adapter" /> <orderEntry type="module" module-name="unimodules-react-native-adapter" />
<orderEntry type="module" module-name="android-expo-file-system" /> <orderEntry type="module" module-name="expo-file-system" />
<orderEntry type="module" module-name="android-expo-location" /> <orderEntry type="module" module-name="expo-location" />
<orderEntry type="module" module-name="android-expo-error-recovery" /> <orderEntry type="module" module-name="expo-error-recovery" />
<orderEntry type="module" module-name="android-unimodules-permissions-interface" /> <orderEntry type="module" module-name="unimodules-permissions-interface" />
<orderEntry type="module" module-name="android-unimodules-core" /> <orderEntry type="module" module-name="unimodules-core" />
<orderEntry type="module" module-name="android-expo-app-loader-provider" /> <orderEntry type="module" module-name="expo-app-loader-provider" />
<orderEntry type="module" module-name="android-expo-font" /> <orderEntry type="module" module-name="expo-font" />
<orderEntry type="module" module-name="android-expo-keep-awake" /> <orderEntry type="module" module-name="expo-keep-awake" />
<orderEntry type="module" module-name="android-expo-linear-gradient" /> <orderEntry type="module" module-name="expo-linear-gradient" />
<orderEntry type="module" module-name="android-expo-sqlite" /> <orderEntry type="module" module-name="expo-sqlite" />
<orderEntry type="module" module-name="android-unimodules-barcode-scanner-interface" /> <orderEntry type="module" module-name="unimodules-barcode-scanner-interface" />
<orderEntry type="module" module-name="android-unimodules-camera-interface" /> <orderEntry type="module" module-name="unimodules-camera-interface" />
<orderEntry type="module" module-name="android-unimodules-constants-interface" /> <orderEntry type="module" module-name="unimodules-constants-interface" />
<orderEntry type="module" module-name="android-unimodules-face-detector-interface" /> <orderEntry type="module" module-name="unimodules-face-detector-interface" />
<orderEntry type="module" module-name="android-unimodules-file-system-interface" /> <orderEntry type="module" module-name="unimodules-file-system-interface" />
<orderEntry type="module" module-name="android-unimodules-font-interface" /> <orderEntry type="module" module-name="unimodules-font-interface" />
<orderEntry type="module" module-name="android-unimodules-sensors-interface" /> <orderEntry type="module" module-name="unimodules-sensors-interface" />
<orderEntry type="module" module-name="android-unimodules-task-manager-interface" /> <orderEntry type="module" module-name="unimodules-task-manager-interface" />
<orderEntry type="module" module-name="android-@react-native-community_masked-view" /> <orderEntry type="module" module-name="@react-native-community_masked-view" />
<orderEntry type="module" module-name="android-react-native-gesture-handler" /> <orderEntry type="module" module-name="react-native-gesture-handler" />
<orderEntry type="module" module-name="android-react-native-reanimated" /> <orderEntry type="module" module-name="react-native-reanimated" />
<orderEntry type="module" module-name="react-native-restart" />
<orderEntry type="module" module-name="react-native-safe-area-context" /> <orderEntry type="module" module-name="react-native-safe-area-context" />
<orderEntry type="module" module-name="react-native-screens" /> <orderEntry type="module" module-name="react-native-screens" />
</component> </component>

View File

@@ -4,10 +4,10 @@
"expo": { "expo": {
"name": "@react-navigation/example", "name": "@react-navigation/example",
"owner": "react-navigation", "owner": "react-navigation",
"slug": "react-react-navigationample", "slug": "react-navigation-example",
"description": "Demo app to showcase various functionality of React Navigation", "description": "Demo app to showcase various functionality of React Navigation",
"privacy": "public", "privacy": "public",
"sdkVersion": "36.0.0", "sdkVersion": "37.0.0",
"platforms": [ "platforms": [
"ios", "ios",
"android", "android",

View File

@@ -1,4 +1,4 @@
module.exports = function(api) { module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: ['babel-preset-expo'], 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?author=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/Album"]');
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Album'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Album');
await page.click('[aria-label="Article by Gandalf, back"]');
await page.waitForNavigation();
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Article?author=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/` // List all packages under `packages/`
.readdirSync(packages) .readdirSync(packages)
// Ignore hidden files such as .DS_Store // Ignore hidden files such as .DS_Store
.filter(p => !p.startsWith('.')) .filter((p) => !p.startsWith('.'))
.map(p => { .map((p) => {
const pak = JSON.parse( const pak = JSON.parse(
fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8') fs.readFileSync(path.join(packages, p, 'package.json'), 'utf8')
); );
@@ -50,9 +50,9 @@ module.exports = {
blacklistRE: blacklist( blacklistRE: blacklist(
fs fs
.readdirSync(packages) .readdirSync(packages)
.map(p => path.join(packages, p)) .map((p) => path.join(packages, p))
.map( .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: { server: {
enhanceMiddleware: middleware => { enhanceMiddleware: (middleware) => {
return (req, res, next) => { return (req, res, next) => {
// When an asset is imported outside the project root, it has wrong path on Android // When an asset is imported outside the project root, it has wrong path on Android
// This happens for the back button in stack, so we fix the path to correct one // 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", "web": "expo start:web",
"native": "react-native start", "native": "react-native start",
"android": "react-native run-android", "android": "react-native run-android",
"ios": "react-native run-ios" "ios": "react-native run-ios",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.2.0",
"@react-native-community/masked-view": "0.1.6", "@react-native-community/masked-view": "^0.1.10",
"@types/react-native-restart": "^0.0.0",
"color": "^3.1.2", "color": "^3.1.2",
"expo": "^36.0.2", "expo": "^37.0.8",
"expo-asset": "~8.0.0", "expo-asset": "~8.1.3",
"expo-blur": "^8.0.0", "expo-blur": "~8.1.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-dom": "~16.9.0", "react-dom": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.5.6", "react-native-gesture-handler": "^1.6.0",
"react-native-paper": "^3.6.0", "react-native-paper": "^3.10.1",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.8.0",
"react-native-restart": "^0.0.13", "react-native-restart": "^0.0.15",
"react-native-safe-area-context": "^0.7.2", "react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.0.0-beta.2", "react-native-screens": "^2.7.0",
"react-native-tab-view": "2.13.0", "react-native-tab-view": "2.14.0",
"react-native-unimodules": "^0.7.0", "react-native-unimodules": "~0.9.1",
"react-native-web": "^0.11.7" "react-native-web": "^0.11.7"
}, },
"devDependencies": { "devDependencies": {
"@expo/webpack-config": "^0.10.12", "@expo/webpack-config": "^0.11.19",
"@types/react": "^16.9.19", "@types/jest-dev-server": "^4.2.0",
"@types/react-native": "^0.60.30", "@types/react": "^16.9.34",
"babel-preset-expo": "^8.0.0", "@types/react-native": "^0.62.7",
"expo-cli": "^3.11.9", "babel-preset-expo": "^8.1.0",
"typescript": "^3.7.5" "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 * as React from 'react';
import { Platform } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import TouchableBounce from '../Shared/TouchableBounce'; import TouchableBounce from '../Shared/TouchableBounce';
@@ -28,7 +29,10 @@ export default function BottomTabsScreen() {
return ( return (
<BottomTabs.Navigator <BottomTabs.Navigator
screenOptions={{ screenOptions={{
tabBarButton: props => <TouchableBounce {...props} />, tabBarButton:
Platform.OS === 'web'
? undefined
: (props) => <TouchableBounce {...props} />,
}} }}
> >
<BottomTabs.Screen <BottomTabs.Screen
@@ -38,7 +42,7 @@ export default function BottomTabsScreen() {
tabBarIcon: getTabBarIcon('file-document-box'), tabBarIcon: getTabBarIcon('file-document-box'),
}} }}
> >
{props => <SimpleStackScreen {...props} headerMode="none" />} {(props) => <SimpleStackScreen {...props} headerMode="none" />}
</BottomTabs.Screen> </BottomTabs.Screen>
<BottomTabs.Screen <BottomTabs.Screen
name="Chat" name="Chat"

View File

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

View File

@@ -15,7 +15,7 @@ export default function BottomTabsScreen() {
return ( return (
<BottomTabs.Navigator> <BottomTabs.Navigator>
{tabs.map(i => ( {tabs.map((i) => (
<BottomTabs.Screen <BottomTabs.Screen
key={i} key={i}
name={`tab-${i}`} name={`tab-${i}`}
@@ -29,12 +29,14 @@ export default function BottomTabsScreen() {
{() => ( {() => (
<View style={styles.container}> <View style={styles.container}>
<Title>Tab {i}</Title> <Title>Tab {i}</Title>
<Button onPress={() => setTabs(tabs => [...tabs, tabs.length])}> <Button onPress={() => setTabs((tabs) => [...tabs, tabs.length])}>
Add a tab Add a tab
</Button> </Button>
<Button <Button
onPress={() => onPress={() =>
setTabs(tabs => (tabs.length > 1 ? tabs.slice(0, -1) : tabs)) setTabs((tabs) =>
tabs.length > 1 ? tabs.slice(0, -1) : tabs
)
} }
> >
Remove a tab 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 };
Album: 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/Album"
style={[styles.button, { padding: 8 }]}
>
Go to /link-component/Album
</Link>
<Link
to="/link-component/Album"
action={StackActions.replace('Album')}
style={[styles.button, { padding: 8 }]}
>
Replace with /link-component/Album
</Link>
<LinkButton
to="/link-component/Album"
mode="contained"
style={styles.button}
>
Go to /link-component/Album
</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?author=Babel"
style={[styles.button, { padding: 8 }]}
>
Go to /link-component/Article
</Link>
<LinkButton
to="/link-component/Article?author=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="Album"
component={AlbumsScreen}
options={{ title: 'Album' }}
/>
</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;
Album: 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="Album"
component={AlbumsScreen}
options={{ title: 'Album' }}
/>
</Drawer.Navigator>
);
}

View File

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

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; 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 { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native'; import { RouteProp, ParamListBase } from '@react-navigation/native';
import { import {
@@ -17,6 +17,8 @@ type ModalStackParams = {
type ModalStackNavigation = StackNavigationProp<ModalStackParams>; type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -42,7 +44,10 @@ const ArticleScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -66,7 +71,7 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
Go back Go back
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };

View File

@@ -1,233 +0,0 @@
import * as React from 'react';
import { View, Text, ScrollView, StyleSheet } from 'react-native';
import { Button } from 'react-native-paper';
// eslint-disable-next-line import/no-unresolved
import { enableScreens } from 'react-native-screens';
import {
RouteProp,
ParamListBase,
useFocusEffect,
useTheme,
} from '@react-navigation/native';
import { DrawerNavigationProp } from '@react-navigation/drawer';
import { StackNavigationProp } from '@react-navigation/stack';
import {
createNativeStackNavigator,
NativeStackNavigationProp,
} from '@react-navigation/native-stack';
import Albums from '../Shared/Albums';
type NativeStackParams = {
Article: { author: string };
Album: undefined;
};
type NativeStackNavigation = NativeStackNavigationProp<NativeStackParams>;
const Title = ({ children }: { children: React.ReactNode }) => {
const { colors } = useTheme();
return <Text style={[styles.title, { color: colors.text }]}>{children}</Text>;
};
const Paragraph = ({ children }: { children: React.ReactNode }) => {
const { colors } = useTheme();
return (
<Text style={[styles.paragraph, { color: colors.text }]}>{children}</Text>
);
};
const ArticleScreen = ({
navigation,
}: {
navigation: NativeStackNavigation;
route: RouteProp<NativeStackParams, 'Article'>;
}) => {
const { colors } = useTheme();
return (
<ScrollView
style={{ backgroundColor: colors.card }}
contentContainerStyle={styles.content}
>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Album')}
style={styles.button}
>
Push album
</Button>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Title>What is Lorem Ipsum?</Title>
<Paragraph>
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry&apos;s standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five
centuries, but also the leap into electronic typesetting, remaining
essentially unchanged. It was popularised in the 1960s with the release
of Letraset sheets containing Lorem Ipsum passages, and more recently
with desktop publishing software like Aldus PageMaker including versions
of Lorem Ipsum.
</Paragraph>
<Title>Where does it come from?</Title>
<Paragraph>
Contrary to popular belief, Lorem Ipsum is not simply random text. It
has roots in a piece of classical Latin literature from 45 BC, making it
over 2000 years old. Richard McClintock, a Latin professor at
Hampden-Sydney College in Virginia, looked up one of the more obscure
Latin words, consectetur, from a Lorem Ipsum passage, and going through
the cites of the word in classical literature, discovered the
undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33
of &quot;de Finibus Bonorum et Malorum&quot; (The Extremes of Good and
Evil) by Cicero, written in 45 BC. This book is a treatise on the theory
of ethics, very popular during the Renaissance. The first line of Lorem
Ipsum, &quot;Lorem ipsum dolor sit amet..&quot;, comes from a line in
section 1.10.32.
</Paragraph>
<Paragraph>
The standard chunk of Lorem Ipsum used since the 1500s is reproduced
below for those interested. Sections 1.10.32 and 1.10.33 from &quot;de
Finibus Bonorum et Malorum&quot; by Cicero are also reproduced in their
exact original form, accompanied by English versions from the 1914
translation by H. Rackham.
</Paragraph>
<Title>Why do we use it?</Title>
<Paragraph>
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout. The point of
using Lorem Ipsum is that it has a more-or-less normal distribution of
letters, as opposed to using &quot;Content here, content here&quot;,
making it look like readable English. Many desktop publishing packages
and web page editors now use Lorem Ipsum as their default model text,
and a search for &quot;lorem ipsum&quot; will uncover many web sites
still in their infancy. Various versions have evolved over the years,
sometimes by accident, sometimes on purpose (injected humour and the
like).
</Paragraph>
<Title>Where can I get some?</Title>
<Paragraph>
There are many variations of passages of Lorem Ipsum available, but the
majority have suffered alteration in some form, by injected humour, or
randomised words which don&apos;t look even slightly believable. If you
are going to use a passage of Lorem Ipsum, you need to be sure there
isn&apos;t anything embarrassing hidden in the middle of text. All the
Lorem Ipsum generators on the Internet tend to repeat predefined chunks
as necessary, making this the first true generator on the Internet. It
uses a dictionary of over 200 Latin words, combined with a handful of
model sentence structures, to generate Lorem Ipsum which looks
reasonable. The generated Lorem Ipsum is therefore always free from
repetition, injected humour, or non-characteristic words etc.
</Paragraph>
</ScrollView>
);
};
const AlbumsScreen = ({
navigation,
}: {
navigation: NativeStackNavigation;
}) => (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
style={styles.button}
>
Push article
</Button>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Albums scrollEnabled={false} />
</ScrollView>
);
const NativeStack = createNativeStackNavigator<NativeStackParams>();
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function NativeStackScreen({ navigation }: Props) {
navigation.setOptions({
headerShown: false,
});
useFocusEffect(
React.useCallback(() => {
const drawer = navigation.dangerouslyGetParent() as DrawerNavigationProp<
ParamListBase
>;
navigation.setOptions({ gestureEnabled: false });
drawer.setOptions({ gestureEnabled: false });
return () => {
navigation.setOptions({ gestureEnabled: true });
drawer.setOptions({ gestureEnabled: true });
};
}, [navigation])
);
return (
<NativeStack.Navigator>
<NativeStack.Screen
name="Article"
component={ArticleScreen}
options={{
title: 'Lorem Ipsum',
headerLargeTitle: true,
headerHideShadow: true,
}}
/>
<NativeStack.Screen
name="Album"
component={AlbumsScreen}
options={{ title: 'Album' }}
/>
</NativeStack.Navigator>
);
}
enableScreens(true);
const styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
padding: 8,
},
button: {
margin: 8,
},
content: {
paddingVertical: 16,
},
title: {
fontWeight: 'bold',
fontSize: 24,
marginVertical: 8,
marginHorizontal: 16,
},
paragraph: {
fontSize: 16,
lineHeight: 24,
marginVertical: 8,
marginHorizontal: 16,
},
});

View File

@@ -1,23 +0,0 @@
import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export default function NativeStack() {
return (
<View style={styles.container}>
<Text style={styles.text}>Not supported on Web :(</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#eceff1',
},
text: {
fontSize: 16,
color: '#999',
},
});

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; 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 { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native'; import { RouteProp, ParamListBase } from '@react-navigation/native';
import { import {
@@ -18,6 +18,8 @@ type SimpleStackParams = {
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -43,7 +45,10 @@ const ArticleScreen = ({
Pop screen Pop screen
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -71,7 +76,7 @@ const NewsFeedScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<NewsFeed scrollEnabled={false} /> <NewsFeed scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -99,7 +104,7 @@ const AlbumsScreen = ({
Pop by 2 Pop by 2
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };

View File

@@ -20,6 +20,8 @@ type SimpleStackParams = {
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -45,7 +47,10 @@ const ArticleScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -75,7 +80,7 @@ const AlbumsScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { import {
ScrollView, ScrollView,
AsyncStorage,
YellowBox, YellowBox,
Platform, Platform,
StatusBar, StatusBar,
I18nManager, I18nManager,
Dimensions,
ScaledSize,
} from 'react-native'; } from 'react-native';
// eslint-disable-next-line import/no-unresolved
import { enableScreens } from 'react-native-screens';
import RNRestart from 'react-native-restart'; import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
import { Asset } from 'expo-asset';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { import {
Provider as PaperProvider, Provider as PaperProvider,
@@ -16,12 +21,10 @@ import {
Appbar, Appbar,
List, List,
Divider, Divider,
Text,
} from 'react-native-paper'; } from 'react-native-paper';
import { Asset } from 'expo-asset';
import { import {
InitialState, InitialState,
useLinking,
NavigationContainerRef,
NavigationContainer, NavigationContainer,
DefaultTheme, DefaultTheme,
DarkTheme, DarkTheme,
@@ -37,9 +40,10 @@ import {
HeaderStyleInterpolators, HeaderStyleInterpolators,
} from '@react-navigation/stack'; } from '@react-navigation/stack';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes'; import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem';
import SimpleStack from './Screens/SimpleStack'; import SimpleStack from './Screens/SimpleStack';
import NativeStack from './Screens/NativeStack';
import ModalPresentationStack from './Screens/ModalPresentationStack'; import ModalPresentationStack from './Screens/ModalPresentationStack';
import StackTransparent from './Screens/StackTransparent'; import StackTransparent from './Screens/StackTransparent';
import StackHeaderCustomization from './Screens/StackHeaderCustomization'; import StackHeaderCustomization from './Screens/StackHeaderCustomization';
@@ -49,11 +53,16 @@ import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import DynamicTabs from './Screens/DynamicTabs'; import DynamicTabs from './Screens/DynamicTabs';
import AuthFlow from './Screens/AuthFlow'; import AuthFlow from './Screens/AuthFlow';
import CompatAPI from './Screens/CompatAPI'; import CompatAPI from './Screens/CompatAPI';
import SettingsItem from './Shared/SettingsItem'; import MasterDetail from './Screens/MasterDetail';
import { Updates } from 'expo'; import LinkComponent from './Screens/LinkComponent';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']); YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
enableScreens();
// @ts-ignore
global.REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED = true;
type RootDrawerParamList = { type RootDrawerParamList = {
Root: undefined; Root: undefined;
Another: undefined; Another: undefined;
@@ -67,7 +76,6 @@ type RootStackParamList = {
const SCREENS = { const SCREENS = {
SimpleStack: { title: 'Simple Stack', component: SimpleStack }, SimpleStack: { title: 'Simple Stack', component: SimpleStack },
NativeStack: { title: 'Native Stack', component: NativeStack },
ModalPresentationStack: { ModalPresentationStack: {
title: 'Modal Presentation Stack', title: 'Modal Presentation Stack',
component: ModalPresentationStack, component: ModalPresentationStack,
@@ -93,6 +101,10 @@ const SCREENS = {
title: 'Dynamic Tabs', title: 'Dynamic Tabs',
component: DynamicTabs, component: DynamicTabs,
}, },
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
},
AuthFlow: { AuthFlow: {
title: 'Auth Flow', title: 'Auth Flow',
component: AuthFlow, component: AuthFlow,
@@ -101,6 +113,10 @@ const SCREENS = {
title: 'Compat Layer', title: 'Compat Layer',
component: CompatAPI, component: CompatAPI,
}, },
LinkComponent: {
title: '<Link />',
component: LinkComponent,
},
}; };
const Drawer = createDrawerNavigator<RootDrawerParamList>(); const Drawer = createDrawerNavigator<RootDrawerParamList>();
@@ -112,35 +128,6 @@ const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
Asset.loadAsync(StackAssets); Asset.loadAsync(StackAssets);
export default function App() { 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: 'root',
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;
},
{}
),
},
},
});
const [theme, setTheme] = React.useState(DefaultTheme); const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
@@ -151,12 +138,13 @@ export default function App() {
React.useEffect(() => { React.useEffect(() => {
const restoreState = async () => { const restoreState = async () => {
try { try {
let state = await getInitialState(); let state;
if (Platform.OS !== 'web' && state === undefined) { if (Platform.OS !== 'web' && state === undefined) {
const savedState = await AsyncStorage.getItem( const savedState = await AsyncStorage.getItem(
NAVIGATION_PERSISTENCE_KEY NAVIGATION_PERSISTENCE_KEY
); );
state = savedState ? JSON.parse(savedState) : undefined; state = savedState ? JSON.parse(savedState) : undefined;
} }
@@ -177,7 +165,7 @@ export default function App() {
}; };
restoreState(); restoreState();
}, [getInitialState]); }, []);
const paperTheme = React.useMemo(() => { const paperTheme = React.useMemo(() => {
const t = theme.dark ? PaperDarkTheme : PaperLightTheme; const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
@@ -193,27 +181,68 @@ export default function App() {
}; };
}, [theme.colors, theme.dark]); }, [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) { if (!isReady) {
return null; return null;
} }
const isLargeScreen = dimensions.width >= 1024;
return ( return (
<PaperProvider theme={paperTheme}> <PaperProvider theme={paperTheme}>
{Platform.OS === 'ios' && ( {Platform.OS === 'ios' && (
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} /> <StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
)} )}
<NavigationContainer <NavigationContainer
ref={containerRef}
initialState={initialState} initialState={initialState}
onStateChange={state => onStateChange={(state) =>
AsyncStorage.setItem( AsyncStorage.setItem(
NAVIGATION_PERSISTENCE_KEY, NAVIGATION_PERSISTENCE_KEY,
JSON.stringify(state) JSON.stringify(state)
) )
} }
theme={theme} 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: '' }
),
},
},
}}
fallback={<Text>Loading</Text>}
> >
<Drawer.Navigator> <Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen <Drawer.Screen
name="Root" name="Root"
options={{ options={{
@@ -237,13 +266,15 @@ export default function App() {
name="Home" name="Home"
options={{ options={{
title: 'Examples', title: 'Examples',
headerLeft: () => ( headerLeft: isLargeScreen
<Appbar.Action ? undefined
color={theme.colors.text} : () => (
icon="menu" <Appbar.Action
onPress={() => navigation.toggleDrawer()} color={theme.colors.text}
/> icon="menu"
), onPress={() => navigation.toggleDrawer()}
/>
),
}} }}
> >
{({ {({
@@ -277,14 +308,15 @@ export default function App() {
theme.dark ? 'light' : 'dark' theme.dark ? 'light' : 'dark'
); );
setTheme(t => (t.dark ? DefaultTheme : DarkTheme)); setTheme((t) => (t.dark ? DefaultTheme : DarkTheme));
}} }}
/> />
<Divider /> <Divider />
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map( {(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
name => ( (name) => (
<List.Item <List.Item
key={name} key={name}
testID={name}
title={SCREENS[name].title} title={SCREENS[name].title}
onPress={() => navigation.navigate(name)} onPress={() => navigation.navigate(name)}
/> />
@@ -294,7 +326,7 @@ export default function App() {
)} )}
</Stack.Screen> </Stack.Screen>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map( {(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
name => ( (name) => (
<Stack.Screen <Stack.Screen
key={name} key={name}
name={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 node_modules = path.resolve(__dirname, '..', 'node_modules');
const packages = path.resolve(__dirname, '..', 'packages'); const packages = path.resolve(__dirname, '..', 'packages');
module.exports = async function(env, argv) { module.exports = async function (env, argv) {
const config = await createExpoWebpackConfigAsync(env, argv); const config = await createExpoWebpackConfigAsync(env, argv);
config.context = path.resolve(__dirname, '..'); config.context = path.resolve(__dirname, '..');
@@ -20,17 +20,17 @@ module.exports = async function(env, argv) {
}); });
config.resolve.plugins = config.resolve.plugins.filter( config.resolve.plugins = config.resolve.plugins.filter(
p => !(p instanceof ModuleScopePlugin) (p) => !(p instanceof ModuleScopePlugin)
); );
Object.assign(config.resolve.alias, { 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': path.resolve(node_modules, 'react-native-web'),
'react-native-web': 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'), '@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'),
}); });
fs.readdirSync(packages).forEach(name => { fs.readdirSync(packages).forEach((name) => {
config.resolve.alias[`@react-navigation/${name}`] = path.resolve( config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
packages, packages,
name, name,

View File

@@ -1,7 +1,7 @@
const error = console.error; const error = console.error;
console.error = (...args) => 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( /(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] 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

@@ -18,31 +18,33 @@
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)", "author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
"scripts": { "scripts": {
"lint": "eslint --ext '.js,.ts,.tsx' .", "lint": "eslint --ext '.js,.ts,.tsx' .",
"typescript": "tsc --noEmit", "typescript": "tsc --noEmit --composite false",
"test": "jest", "test": "jest",
"prerelease": "lerna run clean",
"release": "lerna publish",
"example": "yarn --cwd example" "example": "yarn --cwd example"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3", "@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/preset-env": "^7.8.4", "@babel/preset-env": "^7.9.6",
"@babel/preset-flow": "^7.8.3", "@babel/preset-flow": "^7.9.0",
"@babel/preset-react": "^7.8.3", "@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.8.3", "@babel/preset-typescript": "^7.9.0",
"@babel/runtime": "^7.8.4", "@babel/runtime": "^7.9.6",
"@commitlint/config-conventional": "^8.3.4", "@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", "codecov": "^3.6.5",
"commitlint": "^8.3.5", "commitlint": "^8.3.5",
"core-js": "^3.6.4", "core-js": "^3.6.5",
"detox": "^15.1.4",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-satya164": "^3.1.5", "eslint-config-satya164": "^3.1.7",
"husky": "^4.2.1", "husky": "^4.2.5",
"jest": "^25.1.0", "jest": "^26.0.1",
"lerna": "^3.20.2", "lerna": "^3.20.2",
"prettier": "^1.19.1", "prettier": "^2.0.5",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"resolutions": { "resolutions": {
"react": "~16.9.0", "react": "~16.9.0",

View File

@@ -3,6 +3,214 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.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
## [5.0.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.5...@react-navigation/bottom-tabs@5.0.6) (2020-02-19)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.0.5](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.4...@react-navigation/bottom-tabs@5.0.5) (2020-02-14)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.0.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.3...@react-navigation/bottom-tabs@5.0.4) (2020-02-14)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.0.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.2...@react-navigation/bottom-tabs@5.0.3) (2020-02-12)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.0.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.1...@react-navigation/bottom-tabs@5.0.2) (2020-02-11)
### Bug Fixes
* initialize keyboard-hiding tabBar to visible=true ([#6740](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6740), [#6799](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/6799)) ([0c59ef7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/0c59ef7328c63108a2a2c04e927794d73cead63a))
* provide route context to header and bottom tabs ([b6e7e08](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/b6e7e08b9a05be6c04ed21e938b9580876239116))
## [5.0.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.45...@react-navigation/bottom-tabs@5.0.1) (2020-02-10) ## [5.0.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.45...@react-navigation/bottom-tabs@5.0.1) (2020-02-10)
**Note:** Version bump only for package @react-navigation/bottom-tabs **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,6 +1,7 @@
{ {
"name": "@react-navigation/bottom-tabs", "name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines", "description": "Bottom tab navigator following iOS design guidelines",
"version": "5.4.0",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -10,7 +11,6 @@
"android", "android",
"tab" "tab"
], ],
"version": "5.0.1",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs", "repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
@@ -34,22 +34,24 @@
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.9.3", "@react-native-community/bob": "^0.13.0",
"@react-navigation/native": "^5.0.1", "@react-navigation/native": "^5.2.5",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.19", "@types/react": "^16.9.34",
"@types/react-native": "^0.60.30", "@types/react-native": "^0.62.7",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-safe-area-context": "^0.7.2", "react-native-safe-area-context": "^0.7.3",
"typescript": "^3.7.5" "react-native-screens": "^2.7.0",
"typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0", "@react-navigation/native": "^5.0.5",
"react": "*", "react": "*",
"react-native": "*", "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": { "@react-native-community/bob": {
"source": "src", "source": "src",

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import {
StyleProp, StyleProp,
TextStyle, TextStyle,
ViewStyle, ViewStyle,
GestureResponderEvent,
} from 'react-native'; } from 'react-native';
import { import {
NavigationHelpers, NavigationHelpers,
@@ -11,6 +12,8 @@ import {
ParamListBase, ParamListBase,
Descriptor, Descriptor,
TabNavigationState, TabNavigationState,
TabActionHelpers,
RouteProp,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type BottomTabNavigationEventMap = { export type BottomTabNavigationEventMap = {
@@ -40,18 +43,15 @@ export type BottomTabNavigationProp<
TabNavigationState, TabNavigationState,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap BottomTabNavigationEventMap
> & { > &
/** TabActionHelpers<ParamList>;
* Jump to an existing tab.
* export type BottomTabScreenProps<
* @param name Name of the route for the tab. ParamList extends ParamListBase,
* @param [params] Params object for the route. RouteName extends keyof ParamList = string
*/ > = {
jumpTo<RouteName extends Extract<keyof ParamList, string>>( navigation: BottomTabNavigationProp<ParamList, RouteName>;
...args: ParamList[RouteName] extends undefined | any route: RouteProp<ParamList, RouteName>;
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
}; };
export type BottomTabNavigationOptions = { export type BottomTabNavigationOptions = {
@@ -148,7 +148,7 @@ export type BottomTabBarOptions = {
*/ */
inactiveTintColor?: string; inactiveTintColor?: string;
/** /**
* Background olor for the active tab. * Background color for the active tab.
*/ */
activeBackgroundColor?: string; activeBackgroundColor?: string;
/** /**
@@ -176,14 +176,24 @@ export type BottomTabBarOptions = {
*/ */
tabStyle?: StyleProp<ViewStyle>; tabStyle?: StyleProp<ViewStyle>;
/** /**
* Whether the label is renderd below the icon or beside the icon. * Whether the label is rendered below the icon or beside the icon.
* By default, in `vertical` orinetation, label is rendered below and in `horizontal` orientation, it's renderd beside. * By default, in `vertical` orinetation, label is rendered below and in `horizontal` orientation, it's rendered beside.
*/ */
labelPosition?: LabelPosition; labelPosition?: LabelPosition;
/** /**
* Whether the label position should adapt to the orientation. * Whether the label position should adapt to the orientation.
*/ */
adaptive?: boolean; 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. * Style object for the tab bar container.
*/ */
@@ -196,6 +206,13 @@ export type BottomTabBarProps = BottomTabBarOptions & {
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>; navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
}; };
export type BottomTabBarButtonProps = TouchableWithoutFeedbackProps & { export type BottomTabBarButtonProps = Omit<
TouchableWithoutFeedbackProps,
'onPress'
> & {
to?: string;
children: React.ReactNode; children: React.ReactNode;
onPress?: (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => void;
}; };

View File

@@ -11,10 +11,12 @@ import {
} from 'react-native'; } from 'react-native';
import { import {
NavigationContext, NavigationContext,
NavigationRouteContext,
CommonActions, CommonActions,
useTheme, useTheme,
useLinkBuilder,
} from '@react-navigation/native'; } 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 BottomTabItem from './BottomTabItem';
import { BottomTabBarProps } from '../types'; import { BottomTabBarProps } from '../types';
@@ -42,21 +44,28 @@ export default function BottomTabBar({
keyboardHidesTabBar = false, keyboardHidesTabBar = false,
labelPosition, labelPosition,
labelStyle, labelStyle,
safeAreaInsets,
showIcon, showIcon,
showLabel, showLabel,
style, style,
tabStyle, tabStyle,
}: Props) { }: Props) {
const { colors } = useTheme(); 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({ const [layout, setLayout] = React.useState({
height: 0, height: 0,
width: dimensions.width, width: dimensions.width,
}); });
const [keyboardShown, setKeyboardShown] = React.useState(false); const [keyboardShown, setKeyboardShown] = React.useState(false);
const [visible] = React.useState(() => new Animated.Value(0)); const [visible] = React.useState(() => new Animated.Value(1));
const { routes } = state; const { routes } = state;
@@ -114,7 +123,7 @@ export default function BottomTabBar({
const handleLayout = (e: LayoutChangeEvent) => { const handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout; const { height, width } = e.nativeEvent.layout;
setLayout(layout => { setLayout((layout) => {
if (height === layout.height && width === layout.width) { if (height === layout.height && width === layout.width) {
return layout; return layout;
} else { } else {
@@ -157,114 +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 ( return (
<SafeAreaConsumer> <Animated.View
{insets => ( style={[
<Animated.View styles.tabBar,
style={[ {
styles.tabBar, backgroundColor: colors.card,
{ borderTopColor: colors.border,
backgroundColor: colors.card, },
borderTopColor: colors.border, keyboardHidesTabBar
}, ? {
keyboardHidesTabBar // When the keyboard is shown, slide down the tab bar
? { transform: [
// When the keyboard is shown, slide down the tab bar {
transform: [ translateY: visible.interpolate({
{ inputRange: [0, 1],
translateY: visible.interpolate({ outputRange: [layout.height, 0],
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
// Absolutely position the tab bar so that the content is below it position: keyboardShown ? 'absolute' : null,
// This is needed to avoid gap at bottom when the tab bar is hidden }
position: keyboardShown ? 'absolute' : null, : null,
} {
: null, height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
{ paddingBottom: insets.bottom,
height: DEFAULT_TABBAR_HEIGHT + (insets ? insets.bottom : 0), paddingHorizontal: Math.max(insets.left, insets.right),
paddingBottom: insets ? insets.bottom : 0, },
}, style,
style, ]}
]} pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'} >
> <View style={styles.content} onLayout={handleLayout}>
<View style={styles.content} onLayout={handleLayout}> {routes.map((route, index) => {
{routes.map((route, index) => { const focused = index === state.index;
const focused = index === state.index; const { options } = descriptors[route.key];
const { options } = descriptors[route.key];
const onPress = () => { const onPress = () => {
const event = navigation.emit({ const event = navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
}); });
if (!focused && !event.defaultPrevented) { if (!focused && !event.defaultPrevented) {
navigation.dispatch({ navigation.dispatch({
...CommonActions.navigate(route.name), ...CommonActions.navigate(route.name),
target: state.key, target: state.key,
}); });
} }
}; };
const onLongPress = () => { const onLongPress = () => {
navigation.emit({ navigation.emit({
type: 'tabLongPress', type: 'tabLongPress',
target: route.key, target: route.key,
}); });
}; };
const label = const label =
options.tabBarLabel !== undefined options.tabBarLabel !== undefined
? options.tabBarLabel ? options.tabBarLabel
: options.title !== undefined : options.title !== undefined
? options.title ? options.title
: route.name; : route.name;
const accessibilityLabel = const accessibilityLabel =
options.tabBarAccessibilityLabel !== undefined options.tabBarAccessibilityLabel !== undefined
? options.tabBarAccessibilityLabel ? options.tabBarAccessibilityLabel
: typeof label === 'string' : typeof label === 'string'
? `${label}, tab, ${index + 1} of ${routes.length}` ? `${label}, tab, ${index + 1} of ${routes.length}`
: undefined; : undefined;
return ( return (
<NavigationContext.Provider <NavigationContext.Provider
key={route.key} key={route.key}
value={descriptors[route.key].navigation} value={descriptors[route.key].navigation}
> >
<BottomTabItem <NavigationRouteContext.Provider value={route}>
route={route} <BottomTabItem
focused={focused} route={route}
horizontal={shouldUseHorizontalLabels()} focused={focused}
onPress={onPress} horizontal={shouldUseHorizontalLabels()}
onLongPress={onLongPress} onPress={onPress}
accessibilityLabel={accessibilityLabel} onLongPress={onLongPress}
testID={options.tabBarTestID} accessibilityLabel={accessibilityLabel}
allowFontScaling={allowFontScaling} to={buildLink(route.name, route.params)}
activeTintColor={activeTintColor} testID={options.tabBarTestID}
inactiveTintColor={inactiveTintColor} allowFontScaling={allowFontScaling}
activeBackgroundColor={activeBackgroundColor} activeTintColor={activeTintColor}
inactiveBackgroundColor={inactiveBackgroundColor} inactiveTintColor={inactiveTintColor}
button={options.tabBarButton} activeBackgroundColor={activeBackgroundColor}
icon={options.tabBarIcon} inactiveBackgroundColor={inactiveBackgroundColor}
label={label} button={options.tabBarButton}
showIcon={showIcon} icon={options.tabBarIcon}
showLabel={showLabel} label={label}
labelStyle={labelStyle} showIcon={showIcon}
style={tabStyle} showLabel={showLabel}
/> labelStyle={labelStyle}
</NavigationContext.Provider> style={tabStyle}
); />
})} </NavigationRouteContext.Provider>
</View> </NavigationContext.Provider>
</Animated.View> );
)} })}
</SafeAreaConsumer> </View>
</Animated.View>
); );
} }

View File

@@ -4,11 +4,13 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
Animated, Animated,
StyleSheet, StyleSheet,
Platform,
StyleProp, StyleProp,
ViewStyle, ViewStyle,
TextStyle, TextStyle,
GestureResponderEvent,
} from 'react-native'; } from 'react-native';
import { Route, useTheme } from '@react-navigation/native'; import { Link, Route, useTheme } from '@react-navigation/native';
import Color from 'color'; import Color from 'color';
import TabBarIcon from './TabBarIcon'; import TabBarIcon from './TabBarIcon';
@@ -37,6 +39,10 @@ type Props = {
size: number; size: number;
color: string; color: string;
}) => React.ReactNode; }) => React.ReactNode;
/**
* URL to use for the link to the tab.
*/
to?: string;
/** /**
* The button for the tab. Uses a `TouchableWithoutFeedback` by default. * The button for the tab. Uses a `TouchableWithoutFeedback` by default.
*/ */
@@ -50,13 +56,16 @@ type Props = {
*/ */
testID?: string; 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. * Function to execute on long press.
*/ */
onLongPress: () => void; onLongPress: (e: GestureResponderEvent) => void;
/** /**
* Whether the label should be aligned with the icon horizontally. * Whether the label should be aligned with the icon horizontally.
*/ */
@@ -104,11 +113,48 @@ export default function BottomTabBarItem({
route, route,
label, label,
icon, icon,
button = ({ children, style, ...rest }: BottomTabBarButtonProps) => ( to,
<TouchableWithoutFeedback {...rest}> button = ({
<View style={style}>{children}</View> children,
</TouchableWithoutFeedback> 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, accessibilityLabel,
testID, testID,
onPress, onPress,
@@ -133,9 +179,7 @@ export default function BottomTabBarItem({
const inactiveTintColor = const inactiveTintColor =
customInactiveTintColor === undefined customInactiveTintColor === undefined
? Color(colors.text) ? Color(colors.text).mix(Color(colors.card), 0.5).hex()
.mix(Color(colors.card), 0.5)
.hex()
: customInactiveTintColor; : customInactiveTintColor;
const renderLabel = ({ focused }: { focused: boolean }) => { const renderLabel = ({ focused }: { focused: boolean }) => {
@@ -198,6 +242,7 @@ export default function BottomTabBarItem({
: inactiveBackgroundColor; : inactiveBackgroundColor;
return button({ return button({
to,
onPress, onPress,
onLongPress, onLongPress,
testID, testID,
@@ -250,4 +295,7 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
marginLeft: 20, marginLeft: 20,
}, },
button: {
display: 'flex',
},
}); });

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,203 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.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
## [5.0.6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.5...@react-navigation/compat@5.0.6) (2020-02-19)
### Bug Fixes
* add NavigationEvents ([d69b0db](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/d69b0db60455b8789276822ba73f5349db8842d7)), closes [/github.com/react-navigation/react-navigation/issues/6821#issuecomment-588268512](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/issuecomment-588268512)
## [5.0.5](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.4...@react-navigation/compat@5.0.5) (2020-02-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.0.4](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.3...@react-navigation/compat@5.0.4) (2020-02-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.0.3](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.2...@react-navigation/compat@5.0.3) (2020-02-12)
**Note:** Version bump only for package @react-navigation/compat
## [5.0.2](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.1...@react-navigation/compat@5.0.2) (2020-02-11)
**Note:** Version bump only for package @react-navigation/compat
## [5.0.1](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.34...@react-navigation/compat@5.0.1) (2020-02-10) ## [5.0.1](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.34...@react-navigation/compat@5.0.1) (2020-02-10)
**Note:** Version bump only for package @react-navigation/compat **Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/compat", "name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format", "description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.0.1", "version": "5.1.16",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat", "repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
"bugs": { "bugs": {
@@ -25,15 +25,15 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.9.3", "@react-native-community/bob": "^0.13.0",
"@react-navigation/native": "^5.0.1", "@react-navigation/native": "^5.2.5",
"@types/react": "^16.9.19", "@types/react": "^16.9.34",
"react": "~16.9.0", "react": "~16.9.0",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0", "@react-navigation/native": "^5.0.5",
"react": "~16.9.0" "react": "*"
}, },
"@react-native-community/bob": { "@react-native-community/bob": {
"source": "src", "source": "src",

View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import { useNavigation } from '@react-navigation/native';
type Props = {
onWillFocus?: () => void;
onDidFocus?: () => void;
onWillBlur?: () => void;
onDidBlur?: () => void;
};
export default function NavigationEvents(props: Props) {
const navigation = useNavigation();
const propsRef = React.useRef(props);
React.useEffect(() => {
propsRef.current = props;
});
React.useEffect(() => {
const unsubFocus = navigation.addListener('focus', () => {
propsRef.current.onWillFocus?.();
});
const unsubBlur = navigation.addListener('blur', () => {
propsRef.current.onWillBlur?.();
});
// @ts-ignore
const unsubTransitionEnd = navigation.addListener('transitionEnd', () => {
if (navigation.isFocused()) {
propsRef.current.onDidFocus?.();
} else {
propsRef.current.onDidBlur?.();
}
});
return () => {
unsubFocus();
unsubBlur();
unsubTransitionEnd();
};
}, [navigation]);
return null;
}

View File

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

View File

@@ -22,5 +22,7 @@ function SwitchNavigator(props: Props) {
} }
export default createCompatNavigatorFactory( 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); return StackActions.pop(typeof n === 'number' ? { n } : n);
} }

View File

@@ -14,4 +14,6 @@ export { default as createSwitchNavigator } from './createSwitchNavigator';
export { default as withNavigation } from './withNavigation'; export { default as withNavigation } from './withNavigation';
export { default as withNavigationFocus } from './withNavigationFocus'; export { default as withNavigationFocus } from './withNavigationFocus';
export { default as NavigationEvents } from './NavigationEvents';
export * from './types'; export * from './types';

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,231 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.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)
### Bug Fixes
* avoid emitting focus events twice ([f167008](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/f16700812f3757713b04ca3a860209795b4a6c44)), closes [#6749](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/6749)
* preserve screen order with numeric names ([125bd70](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/125bd70e49b708d936a2eee72ba5cb92eacf26a9)), closes [#6900](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/6900)
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.4...@react-navigation/core@5.1.5) (2020-02-19)
### Bug Fixes
* show descriptive error for invalid return for useFocusEffect ([1a28c29](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/1a28c299b5e3f0805eb6e9ea3cf5e9cc90c7a280))
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.3...@react-navigation/core@5.1.4) (2020-02-14)
### Bug Fixes
* link to migration guide on invalid usage ([c5fcfbd](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c5fcfbd4277541e131acbaa7602a5d7e636afebb))
* return '/' for empty paths ([aaf01e0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/aaf01e01e7b47b375f68aebe6d0effe82878d060))
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.2...@react-navigation/core@5.1.3) (2020-02-14)
### Bug Fixes
* return false for canGoBack if navigator hasn't finished mounting ([c8ac5fa](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c8ac5fab61cf127985431075a3c59c1f3dfa42da))
* throw a descriptive error if navigation object hasn't initialized ([b6accd0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/b6accd03f69dd438e595094d8bf8599cc12e71ac))
* update links in error messages ([f964200](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/f964200b0dcbc19d5f88ad2dd1eb8e5576973497))
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.1...@react-navigation/core@5.1.2) (2020-02-12)
### Bug Fixes
* fix false positives for circular object check ([030c63c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/030c63c89fe447aa484b767831c8f8e26e90431c)), closes [#6827](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/6827)
* static container memo check ([#6825](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/6825)) ([2bf0958](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2bf09585021470f500d967e9242836840efe970f))
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.0...@react-navigation/core@5.1.1) (2020-02-11)
### Bug Fixes
* don't cleanup state on switching navigator ([359ae1b](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/359ae1bfacec5ef880b3944f465c881aedb16767))
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.43...@react-navigation/core@5.1.0) (2020-02-10) # [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.43...@react-navigation/core@5.1.0) (2020-02-10)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/core", "name": "@react-navigation/core",
"description": "Core utilities for building navigators", "description": "Core utilities for building navigators",
"version": "5.1.0", "version": "5.5.1",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -29,27 +29,26 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.1", "@react-navigation/routers": "^5.4.3",
"escape-string-regexp": "^2.0.0", "escape-string-regexp": "^4.0.0",
"query-string": "^6.10.1", "nanoid": "^3.1.5",
"react-is": "^16.12.0", "query-string": "^6.12.1",
"shortid": "^2.2.15", "react-is": "^16.13.0",
"use-subscription": "^1.3.0" "use-subscription": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.9.3", "@react-native-community/bob": "^0.13.0",
"@types/react": "^16.9.19", "@types/react": "^16.9.34",
"@types/react-is": "^16.7.1", "@types/react-is": "^16.7.1",
"@types/shortid": "^0.0.29",
"@types/use-subscription": "^1.0.0", "@types/use-subscription": "^1.0.0",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native-testing-library": "^1.12.0", "react-native-testing-library": "^1.13.2",
"react-test-renderer": "~16.12.0", "react-test-renderer": "~16.13.1",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "~16.9.0" "react": "*"
}, },
"@react-native-community/bob": { "@react-native-community/bob": {
"source": "src", "source": "src",

View File

@@ -9,40 +9,51 @@ import {
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import EnsureSingleNavigator from './EnsureSingleNavigator'; import EnsureSingleNavigator from './EnsureSingleNavigator';
import NavigationBuilderContext from './NavigationBuilderContext'; import NavigationBuilderContext from './NavigationBuilderContext';
import { ScheduleUpdateContext } from './useScheduleUpdate';
import useFocusedListeners from './useFocusedListeners'; import useFocusedListeners from './useFocusedListeners';
import useDevTools from './useDevTools'; import useDevTools from './useDevTools';
import useStateGetters from './useStateGetters'; import useStateGetters from './useStateGetters';
import useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState';
import isSerializable from './isSerializable'; import isSerializable from './isSerializable';
import { NavigationContainerRef, NavigationContainerProps } from './types'; import { NavigationContainerRef, NavigationContainerProps } from './types';
import useEventEmitter from './useEventEmitter';
type State = NavigationState | PartialState<NavigationState> | undefined; type State = NavigationState | PartialState<NavigationState> | undefined;
const DEVTOOLS_CONFIG_KEY =
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
const MISSING_CONTEXT_ERROR = 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/navigating-without-navigation-prop#handling-initialization for more details.";
export const NavigationStateContext = React.createContext<{ export const NavigationStateContext = React.createContext<{
isDefault?: true; isDefault?: true;
state?: NavigationState | PartialState<NavigationState>; state?: NavigationState | PartialState<NavigationState>;
getKey: () => string | undefined;
setKey: (key: string) => void;
getState: () => NavigationState | PartialState<NavigationState> | undefined; getState: () => NavigationState | PartialState<NavigationState> | undefined;
setState: ( setState: (
state: NavigationState | PartialState<NavigationState> | undefined state: NavigationState | PartialState<NavigationState> | undefined
) => void; ) => void;
key?: string;
performTransaction: (action: () => void) => void;
}>({ }>({
isDefault: true, isDefault: true,
get getKey(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get setKey(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get getState(): any { get getState(): any {
throw new Error(MISSING_CONTEXT_ERROR); throw new Error(MISSING_CONTEXT_ERROR);
}, },
get setState(): any { get setState(): any {
throw new Error(MISSING_CONTEXT_ERROR); throw new Error(MISSING_CONTEXT_ERROR);
}, },
get performTransaction(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
}); });
let hasWarnedForSerialization = false; let hasWarnedForSerialization = false;
@@ -66,7 +77,7 @@ const getPartialState = (
return { return {
...partialState, ...partialState,
stale: true, stale: true,
routes: state.routes.map(route => { routes: state.routes.map((route) => {
if (route.state === undefined) { if (route.state === undefined) {
return route as Route<string> & { return route as Route<string> & {
state?: PartialState<NavigationState>; state?: PartialState<NavigationState>;
@@ -95,7 +106,7 @@ const BaseNavigationContainer = React.forwardRef(
independent, independent,
children, children,
}: NavigationContainerProps, }: NavigationContainerProps,
ref: React.Ref<NavigationContainerRef> ref?: React.Ref<NavigationContainerRef>
) { ) {
const parent = React.useContext(NavigationStateContext); const parent = React.useContext(NavigationStateContext);
@@ -105,66 +116,39 @@ const BaseNavigationContainer = React.forwardRef(
); );
} }
const [state, setNavigationState] = React.useState<State>(() => const [
state,
getState,
setState,
scheduleUpdate,
flushUpdates,
] = useSyncState<State>(() =>
getPartialState(initialState == null ? undefined : initialState) getPartialState(initialState == null ? undefined : initialState)
); );
const navigationStateRef = React.useRef<State>();
const transactionStateRef = React.useRef<State | null>(null);
const isTransactionActiveRef = React.useRef<boolean>(false);
const isFirstMountRef = React.useRef<boolean>(true); const isFirstMountRef = React.useRef<boolean>(true);
const skipTrackingRef = React.useRef<boolean>(false); const skipTrackingRef = React.useRef<boolean>(false);
const performTransaction = React.useCallback((callback: () => void) => { const navigatorKeyRef = React.useRef<string | undefined>();
if (isTransactionActiveRef.current) {
throw new Error(
"Only one transaction can be active at a time. Did you accidentally nest 'performTransaction'?"
);
}
setNavigationState((navigationState: State) => { const getKey = React.useCallback(() => navigatorKeyRef.current, []);
isTransactionActiveRef.current = true;
transactionStateRef.current = navigationState;
try { const setKey = React.useCallback((key: string) => {
callback(); navigatorKeyRef.current = key;
} finally {
isTransactionActiveRef.current = false;
}
return transactionStateRef.current;
});
}, []);
const getState = React.useCallback(
() =>
transactionStateRef.current !== null
? transactionStateRef.current
: navigationStateRef.current,
[]
);
const setState = React.useCallback((navigationState: State) => {
if (transactionStateRef.current === null) {
throw new Error(
"Any 'setState' calls need to be done inside 'performTransaction'"
);
}
transactionStateRef.current = navigationState;
}, []); }, []);
const reset = React.useCallback( const reset = React.useCallback(
(state: NavigationState) => { (state: NavigationState) => {
performTransaction(() => { skipTrackingRef.current = true;
skipTrackingRef.current = true; setState(state);
setState(state);
});
}, },
[performTransaction, setState] [setState]
); );
const { trackState, trackAction } = useDevTools({ const { trackState, trackAction } = useDevTools({
enabled:
// @ts-ignore
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
name: '@react-navigation', name: '@react-navigation',
reset, reset,
state, state,
@@ -180,11 +164,19 @@ const BaseNavigationContainer = React.forwardRef(
const dispatch = ( const dispatch = (
action: NavigationAction | ((state: NavigationState) => NavigationAction) action: NavigationAction | ((state: NavigationState) => NavigationAction)
) => { ) => {
listeners[0](navigation => navigation.dispatch(action)); if (listeners[0] == null) {
throw new Error(NOT_INITIALIZED_ERROR);
}
listeners[0]((navigation) => navigation.dispatch(action));
}; };
const canGoBack = () => { const canGoBack = () => {
const { result, handled } = listeners[0](navigation => if (listeners[0] == null) {
return false;
}
const { result, handled } = listeners[0]((navigation) =>
navigation.canGoBack() navigation.canGoBack()
); );
@@ -197,12 +189,10 @@ const BaseNavigationContainer = React.forwardRef(
const resetRoot = React.useCallback( const resetRoot = React.useCallback(
(state?: PartialState<NavigationState> | NavigationState) => { (state?: PartialState<NavigationState> | NavigationState) => {
performTransaction(() => { trackAction('@@RESET_ROOT');
trackAction('@@RESET_ROOT'); setState(state);
setState(state);
});
}, },
[performTransaction, setState, trackAction] [setState, trackAction]
); );
const getRootState = React.useCallback(() => { const getRootState = React.useCallback(() => {
@@ -229,6 +219,8 @@ const BaseNavigationContainer = React.forwardRef(
dispatch, dispatch,
canGoBack, canGoBack,
getRootState, getRootState,
dangerouslyGetState: () => state,
dangerouslyGetParent: () => undefined,
})); }));
const builderContext = React.useMemo( const builderContext = React.useMemo(
@@ -240,14 +232,20 @@ const BaseNavigationContainer = React.forwardRef(
[addFocusedListener, trackAction, addStateGetter] [addFocusedListener, trackAction, addStateGetter]
); );
const scheduleContext = React.useMemo(
() => ({ scheduleUpdate, flushUpdates }),
[scheduleUpdate, flushUpdates]
);
const context = React.useMemo( const context = React.useMemo(
() => ({ () => ({
state, state,
performTransaction,
getState, getState,
setState, setState,
getKey,
setKey,
}), }),
[getState, performTransaction, setState, state] [getKey, getState, setKey, setState, state]
); );
React.useEffect(() => { React.useEffect(() => {
@@ -260,7 +258,7 @@ const BaseNavigationContainer = React.forwardRef(
hasWarnedForSerialization = true; hasWarnedForSerialization = true;
console.warn( 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/header-buttons.html#header-interaction-with-its-screen-component for docs." "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."
); );
} }
} }
@@ -276,22 +274,21 @@ const BaseNavigationContainer = React.forwardRef(
trackState(getRootState); trackState(getRootState);
} }
navigationStateRef.current = state;
transactionStateRef.current = null;
if (!isFirstMountRef.current && onStateChange) { if (!isFirstMountRef.current && onStateChange) {
onStateChange(getRootState()); onStateChange(getRootState());
} }
isFirstMountRef.current = false; isFirstMountRef.current = false;
}, [state, onStateChange, trackState, getRootState, emitter]); }, [onStateChange, trackState, getRootState, emitter, state]);
return ( return (
<NavigationBuilderContext.Provider value={builderContext}> <ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationStateContext.Provider value={context}> <NavigationBuilderContext.Provider value={builderContext}>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator> <NavigationStateContext.Provider value={context}>
</NavigationStateContext.Provider> <EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</NavigationBuilderContext.Provider> </NavigationStateContext.Provider>
</NavigationBuilderContext.Provider>
</ScheduleUpdateContext.Provider>
); );
} }
); );

View File

@@ -4,7 +4,7 @@ type Props = {
children: React.ReactNode; 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< 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 NavigationRouteContext from './NavigationRouteContext';
import StaticContainer from './StaticContainer'; import StaticContainer from './StaticContainer';
import EnsureSingleNavigator from './EnsureSingleNavigator'; import EnsureSingleNavigator from './EnsureSingleNavigator';
import { NavigationProp, RouteConfig } from './types'; import { NavigationProp, RouteConfig, EventMapBase } from './types';
type Props<State extends NavigationState, ScreenOptions extends object> = { type Props<
screen: RouteConfig<ParamListBase, string, ScreenOptions>; State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
> = {
screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>; navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
route: Route<string> & { route: Route<string> & {
state?: NavigationState | PartialState<NavigationState>; state?: NavigationState | PartialState<NavigationState>;
@@ -28,19 +32,26 @@ type Props<State extends NavigationState, ScreenOptions extends object> = {
*/ */
export default function SceneView< export default function SceneView<
State extends NavigationState, State extends NavigationState,
ScreenOptions extends object ScreenOptions extends object,
EventMap extends EventMapBase
>({ >({
screen, screen,
route, route,
navigation, navigation,
getState, getState,
setState, setState,
}: Props<State, ScreenOptions>) { }: Props<State, ScreenOptions, EventMap>) {
const { performTransaction } = React.useContext(NavigationStateContext); const navigatorKeyRef = React.useRef<string | undefined>();
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
const setKey = React.useCallback((key: string) => {
navigatorKeyRef.current = key;
}, []);
const getCurrentState = React.useCallback(() => { const getCurrentState = React.useCallback(() => {
const state = getState(); 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; return currentRoute ? currentRoute.state : undefined;
}, [getState, route.key]); }, [getState, route.key]);
@@ -51,7 +62,7 @@ export default function SceneView<
setState({ setState({
...state, ...state,
routes: state.routes.map(r => routes: state.routes.map((r) =>
r.key === route.key ? { ...r, state: child } : r r.key === route.key ? { ...r, state: child } : r
), ),
}); });
@@ -64,16 +75,10 @@ export default function SceneView<
state: route.state, state: route.state,
getState: getCurrentState, getState: getCurrentState,
setState: setCurrentState, setState: setCurrentState,
performTransaction, getKey,
key: route.key, setKey,
}), }),
[ [getCurrentState, getKey, route.state, setCurrentState, setKey]
getCurrentState,
performTransaction,
route.key,
route.state,
setCurrentState,
]
); );
return ( return (

View File

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

View File

@@ -8,12 +8,19 @@ function StaticContainer(props: any) {
} }
export default React.memo(StaticContainer, (prevProps: any, nextProps: any) => { export default React.memo(StaticContainer, (prevProps: any, nextProps: any) => {
for (const prop in prevProps) { const prevPropKeys = Object.keys(prevProps);
if (prop === 'children') { const nextPropKeys = Object.keys(nextProps);
if (prevPropKeys.length !== nextPropKeys.length) {
return false;
}
for (const key of prevPropKeys) {
if (key === 'children') {
continue; continue;
} }
if (prevProps[prop] !== nextProps[prop]) { if (prevProps[key] !== nextProps[key]) {
return false; return false;
} }
} }

View File

@@ -28,7 +28,7 @@ it('throws when getState is accessed without a container', () => {
const element = <Test />; const element = <Test />;
expect(() => render(element).update(element)).toThrowError( 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,78 +47,7 @@ it('throws when setState is accessed without a container', () => {
const element = <Test />; const element = <Test />;
expect(() => render(element).update(element)).toThrowError( 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'?"
);
});
it('throws when performTransaction is accessed without a container', () => {
expect.assertions(1);
const Test = () => {
const { performTransaction } = React.useContext(NavigationStateContext);
// eslint-disable-next-line babel/no-unused-expressions
performTransaction;
return null;
};
const element = <Test />;
expect(() => render(element).update(element)).toThrowError(
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
);
});
it('throws when setState is called outside performTransaction', () => {
expect.assertions(1);
const Test = () => {
const { setState } = React.useContext(NavigationStateContext);
React.useEffect(() => {
setState(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};
const element = (
<BaseNavigationContainer>
<Test />
</BaseNavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
"Any 'setState' calls need to be done inside 'performTransaction'"
);
});
it('throws when nesting performTransaction', () => {
expect.assertions(1);
const Test = () => {
const { performTransaction } = React.useContext(NavigationStateContext);
React.useEffect(() => {
performTransaction(() => {
performTransaction(() => {});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};
const element = (
<BaseNavigationContainer>
<Test />
</BaseNavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
"Only one transaction can be active at a time. Did you accidentally nest 'performTransaction'?"
); );
}); });
@@ -193,7 +122,7 @@ it('handle dispatching with ref', () => {
return ( return (
<React.Fragment> <React.Fragment>
{state.routes.map(route => descriptors[route.key].render())} {state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment> </React.Fragment>
); );
}; };
@@ -291,7 +220,7 @@ it('handle resetting state with ref', () => {
return ( return (
<React.Fragment> <React.Fragment>
{state.routes.map(route => descriptors[route.key].render())} {state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment> </React.Fragment>
); );
}; };
@@ -442,7 +371,7 @@ it('emits state events when the state changes', () => {
return ( return (
<React.Fragment> <React.Fragment>
{state.routes.map(route => descriptors[route.key].render())} {state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment> </React.Fragment>
); );
}; };
@@ -501,3 +430,51 @@ it('emits state events when the state changes', () => {
], ],
}); });
}); });
it('throws if there is no navigator rendered', () => {
expect.assertions(1);
const ref = React.createRef<NavigationContainerRef>();
const element = <BaseNavigationContainer ref={ref} children={null} />;
render(element);
act(() => {
expect(() => ref.current?.dispatch({ type: 'WHATEVER' })).toThrow(
"The 'navigation' object hasn't been initialized yet."
);
});
});
it("throws if the ref hasn't finished initializing", () => {
expect.assertions(1);
const ref = React.createRef<NavigationContainerRef>();
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => {
React.useEffect(() => {
expect(() => ref.current?.dispatch({ type: 'WHATEVER' })).toThrow(
"The 'navigation' object hasn't been initialized yet."
);
}, []);
return null;
};
const element = (
<BaseNavigationContainer ref={ref}>
<TestNavigator>
<Screen name="foo" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
});

View File

@@ -49,3 +49,27 @@ it('updates element if any props changed', () => {
expect(root).toMatchInlineSnapshot(`"second"`); expect(root).toMatchInlineSnapshot(`"second"`);
}); });
it('updates element if any props are added', () => {
expect.assertions(2);
const Test = ({ label }: any) => {
return label;
};
const root = render(
<StaticContainer count={42}>
<Test label="first" />
</StaticContainer>
);
expect(root).toMatchInlineSnapshot(`"first"`);
root.update(
<StaticContainer count={42} moreCounts={12}>
<Test label="second" />
</StaticContainer>
);
expect(root).toMatchInlineSnapshot(`"second"`);
});

View File

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

View File

@@ -35,8 +35,49 @@ it('gets navigate action from state', () => {
author: 'jane', author: 'jane',
}, },
screen: 'qux', screen: 'qux',
initial: true,
}, },
screen: 'bar', screen: 'bar',
initial: true,
},
},
type: 'NAVIGATE',
});
expect(
getActionFromState({
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
})
).toEqual({
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
screen: 'quz',
initial: false,
},
}, },
}, },
type: 'NAVIGATE', type: 'NAVIGATE',
@@ -53,13 +94,7 @@ it('gets reset action from state', () => {
{ {
name: 'bar', name: 'bar',
state: { state: {
routes: [ routes: [],
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
}, },
}, },
], ],
@@ -68,8 +103,6 @@ it('gets reset action from state', () => {
], ],
}; };
expect(getActionFromState(state)).toEqual({ expect(getActionFromState(state)).toBe(undefined);
payload: state, expect(getActionFromState({ routes: [] })).toBe(undefined);
type: 'RESET_ROOT',
});
}); });

View File

@@ -42,7 +42,8 @@ it('converts state to path string with config', () => {
Baz: { Baz: {
path: 'baz/:author', path: 'baz/:author',
parse: { 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/, '')), id: (id: string) => Number(id.replace(/^x/, '')),
valid: Boolean, valid: Boolean,
}, },
@@ -128,7 +129,8 @@ it('handles state with config with nested screens', () => {
Baz: { Baz: {
path: 'baz/:author', path: 'baz/:author',
parse: { parse: {
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()), author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number, count: Number,
valid: Boolean, valid: Boolean,
}, },
@@ -192,12 +194,14 @@ it('handles state with config with nested screens and unused configs', () => {
Baz: { Baz: {
path: 'baz/:author', path: 'baz/:author',
parse: { parse: {
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()), author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number, count: Number,
valid: Boolean, valid: Boolean,
}, },
stringify: { stringify: {
author: (author: string) => author.replace(/^\w/, c => c.toLowerCase()), author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
unknown: (_: unknown) => 'x', unknown: (_: unknown) => 'x',
}, },
}, },
@@ -255,11 +259,11 @@ it('handles nested object with stringify in it', () => {
path: 'bis/:author', path: 'bis/:author',
stringify: { stringify: {
author: (author: string) => author: (author: string) =>
author.replace(/^\w/, c => c.toLowerCase()), author.replace(/^\w/, (c) => c.toLowerCase()),
}, },
parse: { parse: {
author: (author: string) => author: (author: string) =>
author.replace(/^\w/, c => c.toUpperCase()), author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number, count: Number,
valid: Boolean, valid: Boolean,
}, },
@@ -422,6 +426,49 @@ it('ignores empty string paths', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); 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', () => { it('cuts nested configs too', () => {
const path = '/baz'; const path = '/baz';
const config = { const config = {
@@ -489,3 +536,240 @@ it('handles empty path at the end', () => {
expect(getPathFromState(state, config)).toBe(path); expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('returns "/" for empty path', () => {
const path = '/';
const config = {
Foo: {
path: '',
screens: {
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', () => {
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: {
path: '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,
answer: undefined,
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: {
path: '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: undefined,
answer: undefined,
valid: undefined,
},
},
],
},
},
],
},
},
],
},
},
],
};
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(onStateChange).toBeCalledTimes(0);
expect(spy.mock.calls[0][0]).toMatch( 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(); spy.mockRestore();
}); });
it('cleans up state when the navigator unmounts', () => { it('cleans up state when the navigator unmounts', () => {
jest.useFakeTimers();
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -426,6 +428,8 @@ it('cleans up state when the navigator unmounts', () => {
<BaseNavigationContainer onStateChange={onStateChange} children={null} /> <BaseNavigationContainer onStateChange={onStateChange} children={null} />
); );
act(() => jest.runAllTimers());
expect(onStateChange).toBeCalledTimes(2); expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).lastCalledWith(undefined); 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 => { const TestNavigator = (props: any): any => {
useNavigationBuilder(MockRouter, props); useNavigationBuilder(MockRouter, props);
return null; return null;
@@ -635,7 +639,7 @@ it('handles change in route names', () => {
const onStateChange = jest.fn(); const onStateChange = jest.fn();
const root = render( const root = render(
<BaseNavigationContainer onStateChange={onStateChange}> <BaseNavigationContainer>
<TestNavigator initialRouteName="bar"> <TestNavigator initialRouteName="bar">
<Screen name="foo" component={jest.fn()} /> <Screen name="foo" component={jest.fn()} />
<Screen name="bar" 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', () => { it('gives access to internal state', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -772,6 +1136,60 @@ it('gives access to internal state', () => {
}); });
}); });
it('preserves order of screens in state with non-numeric names', () => {
const TestNavigator = (props: any): any => {
useNavigationBuilder(MockRouter, props);
return null;
};
const navigation = React.createRef<NavigationContainerRef>();
const root = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="foo" component={jest.fn()} />
<Screen name="bar" component={jest.fn()} />
<Screen name="baz" component={jest.fn()} />
</TestNavigator>
</BaseNavigationContainer>
);
render(root);
expect(navigation.current?.getRootState().routeNames).toEqual([
'foo',
'bar',
'baz',
]);
});
it('preserves order of screens in state with numeric names', () => {
const TestNavigator = (props: any): any => {
useNavigationBuilder(MockRouter, props);
return null;
};
const navigation = React.createRef<NavigationContainerRef>();
const root = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="4" component={jest.fn()} />
<Screen name="7" component={jest.fn()} />
<Screen name="1" component={jest.fn()} />
</TestNavigator>
</BaseNavigationContainer>
);
render(root);
expect(navigation.current?.getRootState().routeNames).toEqual([
'4',
'7',
'1',
]);
});
it("throws if navigator doesn't have any screens", () => { it("throws if navigator doesn't have any screens", () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props); useNavigationBuilder(MockRouter, props);
@@ -1031,7 +1449,7 @@ it('throws descriptive error for invalid screen component', () => {
); );
expect(() => render(element).update(element)).toThrowError( 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

@@ -51,20 +51,55 @@ it('returns false for non-serializable object', () => {
}); });
it('returns false for circular references', () => { it('returns false for circular references', () => {
const o = { const x = {
index: 0, a: 1,
key: '7', b: { b1: 1 },
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
},
],
}; };
// @ts-ignore // @ts-ignore
o.routes[0].state = o; x.b.b2 = x;
// @ts-ignore
x.c = x.b;
expect(isSerializable(o)).toBe(false); expect(isSerializable(x)).toBe(false);
const y = [
{
label: 'home',
children: [{ label: 'product' }],
},
{ label: 'about', extend: {} },
];
// @ts-ignore
y[0].children[0].parent = y[0];
// @ts-ignore
y[1].extend.home = y[0].children[0];
expect(isSerializable(y)).toBe(false);
const z = {
name: 'sun',
child: [{ name: 'flower' }],
};
// @ts-ignore
z.child[0].parent = z;
expect(isSerializable(z)).toBe(false);
});
it("doesn't fail if same object used multiple times", () => {
const o = { foo: 'bar' };
expect(
isSerializable({
baz: 'bax',
first: o,
second: o,
stuff: {
b: o,
},
})
).toBe(true);
}); });

View File

@@ -119,7 +119,7 @@ it('sets options with screenOptions prop as an object', () => {
return ( return (
<> <>
{state.routes.map(route => { {state.routes.map((route) => {
const { render, options } = descriptors[route.key]; const { render, options } = descriptors[route.key];
return ( return (
@@ -179,7 +179,7 @@ it('sets options with screenOptions prop as a fuction', () => {
return ( return (
<> <>
{state.routes.map(route => { {state.routes.map((route) => {
const { render, options } = descriptors[route.key]; const { render, options } = descriptors[route.key];
return ( return (
@@ -262,8 +262,10 @@ it('sets initial options with setOptions', () => {
}; };
const TestScreen = ({ navigation }: any): any => { const TestScreen = ({ navigation }: any): any => {
navigation.setOptions({ React.useEffect(() => {
title: 'Hello world', navigation.setOptions({
title: 'Hello world',
});
}); });
return 'Test screen'; return 'Test screen';
@@ -273,7 +275,7 @@ it('sets initial options with setOptions', () => {
<BaseNavigationContainer> <BaseNavigationContainer>
<TestNavigator> <TestNavigator>
<Screen name="foo" options={{ color: 'blue' }}> <Screen name="foo" options={{ color: 'blue' }}>
{props => <TestScreen {...props} />} {(props) => <TestScreen {...props} />}
</Screen> </Screen>
<Screen name="bar" component={jest.fn()} /> <Screen name="bar" component={jest.fn()} />
</TestNavigator> </TestNavigator>
@@ -315,12 +317,12 @@ it('updates options with setOptions', () => {
}; };
const TestScreen = ({ navigation }: any): any => { const TestScreen = ({ navigation }: any): any => {
navigation.setOptions({
title: 'Hello world',
description: 'Something here',
});
React.useEffect(() => { React.useEffect(() => {
navigation.setOptions({
title: 'Hello world',
description: 'Something here',
});
const timer = setTimeout(() => const timer = setTimeout(() =>
navigation.setOptions({ navigation.setOptions({
title: 'Hello again', title: 'Hello again',
@@ -338,7 +340,7 @@ it('updates options with setOptions', () => {
<BaseNavigationContainer> <BaseNavigationContainer>
<TestNavigator> <TestNavigator>
<Screen name="foo" options={{ color: 'blue' }}> <Screen name="foo" options={{ color: 'blue' }}>
{props => <TestScreen {...props} />} {(props) => <TestScreen {...props} />}
</Screen> </Screen>
<Screen name="bar" component={jest.fn()} /> <Screen name="bar" component={jest.fn()} />
</TestNavigator> </TestNavigator>

View File

@@ -15,7 +15,7 @@ it('fires focus and blur events in root navigator', () => {
React.useImperativeHandle(ref, () => navigation, [navigation]); 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(); const firstFocusCallback = jest.fn();
@@ -97,6 +97,69 @@ it('fires focus and blur events in root navigator', () => {
expect(fourthBlurCallback).toBeCalledTimes(0); 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', () => { it('fires focus and blur events in nested navigator', () => {
const TestNavigator = React.forwardRef((props: any, ref: any): any => { const TestNavigator = React.forwardRef((props: any, ref: any): any => {
const { state, navigation, descriptors } = useNavigationBuilder( const { state, navigation, descriptors } = useNavigationBuilder(
@@ -106,7 +169,7 @@ it('fires focus and blur events in nested navigator', () => {
React.useImperativeHandle(ref, () => navigation, [navigation]); 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(); const firstFocusCallback = jest.fn();
@@ -188,10 +251,12 @@ it('fires focus and blur events in nested navigator', () => {
expect(thirdFocusCallback).toBeCalledTimes(0); expect(thirdFocusCallback).toBeCalledTimes(0);
expect(secondFocusCallback).toBeCalledTimes(1); expect(secondFocusCallback).toBeCalledTimes(1);
expect(fourthBlurCallback).toBeCalledTimes(0);
act(() => parent.current.navigate('nested')); act(() => parent.current.navigate('nested'));
expect(firstBlurCallback).toBeCalledTimes(1); expect(firstBlurCallback).toBeCalledTimes(1);
expect(secondBlurCallback).toBeCalledTimes(1);
expect(thirdFocusCallback).toBeCalledTimes(0); expect(thirdFocusCallback).toBeCalledTimes(0);
expect(fourthFocusCallback).toBeCalledTimes(1); expect(fourthFocusCallback).toBeCalledTimes(1);
@@ -199,6 +264,35 @@ it('fires focus and blur events in nested navigator', () => {
expect(fourthBlurCallback).toBeCalledTimes(1); expect(fourthBlurCallback).toBeCalledTimes(1);
expect(thirdFocusCallback).toBeCalledTimes(1); expect(thirdFocusCallback).toBeCalledTimes(1);
act(() => parent.current.navigate('first'));
expect(firstFocusCallback).toBeCalledTimes(2);
expect(thirdBlurCallback).toBeCalledTimes(1);
act(() => parent.current.navigate('nested', { screen: 'fourth' }));
expect(fourthFocusCallback).toBeCalledTimes(2);
expect(thirdBlurCallback).toBeCalledTimes(1);
expect(firstBlurCallback).toBeCalledTimes(2);
act(() => parent.current.navigate('nested', { screen: 'third' }));
expect(thirdFocusCallback).toBeCalledTimes(2);
expect(fourthBlurCallback).toBeCalledTimes(2);
// Make sure nothing else has changed
expect(firstFocusCallback).toBeCalledTimes(2);
expect(firstBlurCallback).toBeCalledTimes(2);
expect(secondFocusCallback).toBeCalledTimes(1);
expect(secondBlurCallback).toBeCalledTimes(1);
expect(thirdFocusCallback).toBeCalledTimes(2);
expect(thirdBlurCallback).toBeCalledTimes(1);
expect(fourthFocusCallback).toBeCalledTimes(2);
expect(fourthBlurCallback).toBeCalledTimes(2);
}); });
it('fires blur event when a route is removed with a delay', async () => { it('fires blur event when a route is removed with a delay', async () => {
@@ -331,7 +425,7 @@ it('fires blur event when a route is removed with a delay', async () => {
expect(blurCallback).toBeCalledTimes(1); expect(blurCallback).toBeCalledTimes(1);
}); });
it('fires custom events', () => { it('fires custom events added with addListener', () => {
const eventName = 'someSuperCoolEvent'; const eventName = 'someSuperCoolEvent';
const TestNavigator = React.forwardRef((props: any, ref: any): any => { const TestNavigator = React.forwardRef((props: any, ref: any): any => {
@@ -345,7 +439,7 @@ it('fires custom events', () => {
state, state,
]); ]);
return state.routes.map(route => descriptors[route.key].render()); return state.routes.map((route) => descriptors[route.key].render());
}); });
const firstCallback = jest.fn(); const firstCallback = jest.fn();
@@ -378,10 +472,13 @@ it('fires custom events', () => {
expect(secondCallback).toBeCalledTimes(0); expect(secondCallback).toBeCalledTimes(0);
expect(thirdCallback).toBeCalledTimes(0); expect(thirdCallback).toBeCalledTimes(0);
const target =
ref.current.state.routes[ref.current.state.routes.length - 1].key;
act(() => { act(() => {
ref.current.navigation.emit({ ref.current.navigation.emit({
type: eventName, type: eventName,
target: ref.current.state.routes[ref.current.state.routes.length - 1].key, target,
data: 42, data: 42,
}); });
}); });
@@ -391,6 +488,7 @@ it('fires custom events', () => {
expect(thirdCallback).toBeCalledTimes(1); expect(thirdCallback).toBeCalledTimes(1);
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent'); expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
expect(thirdCallback.mock.calls[0][0].data).toBe(42); 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].defaultPrevented).toBe(undefined);
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined); expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
@@ -398,11 +496,280 @@ it('fires custom events', () => {
ref.current.navigation.emit({ type: eventName }); 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(firstCallback).toBeCalledTimes(1);
expect(secondCallback).toBeCalledTimes(1); expect(secondCallback).toBeCalledTimes(1);
expect(thirdCallback).toBeCalledTimes(2); 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', () => { it('has option to prevent default', () => {
expect.assertions(5); expect.assertions(5);
@@ -419,7 +786,7 @@ it('has option to prevent default', () => {
state, state,
]); ]);
return state.routes.map(route => descriptors[route.key].render()); return state.routes.map((route) => descriptors[route.key].render());
}); });
const callback = (e: any) => { const callback = (e: any) => {

View File

@@ -10,7 +10,7 @@ it('runs focus effect on focus change', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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(); const focusEffect = jest.fn();
@@ -107,7 +107,7 @@ it('runs focus effect when initial state is given', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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(); const focusEffect = jest.fn();

View File

@@ -10,7 +10,7 @@ it('renders correct focus state', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 = () => { const Test = () => {

View File

@@ -12,7 +12,7 @@ it('gets navigation prop from context', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 = () => { const Test = () => {
@@ -38,7 +38,7 @@ it("gets navigation's parent from context", () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 = () => { const Test = () => {
@@ -70,7 +70,7 @@ it("gets navigation's parent's parent from context", () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 = () => { const Test = () => {
@@ -112,7 +112,7 @@ it('throws if called outside a navigation context', () => {
const Test = () => { const Test = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
expect(() => useNavigation()).toThrow( 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; return null;

View File

@@ -1,7 +1,10 @@
import * as React from 'react'; 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 useEventEmitter from '../useEventEmitter';
import useNavigationCache from '../useNavigationCache'; import useNavigationCache from '../useNavigationCache';
import useNavigationBuilder from '../useNavigationBuilder';
import BaseNavigationContainer from '../BaseNavigationContainer';
import Screen from '../Screen';
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter'; import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
beforeEach(() => (MockRouterKey.current = 0)); beforeEach(() => (MockRouterKey.current = 0));
@@ -40,7 +43,7 @@ it('preserves reference for navigation objects', () => {
}); });
if (previous.current) { if (previous.current) {
Object.keys(navigations).forEach(key => { Object.keys(navigations).forEach((key) => {
expect(navigations[key]).toBe(previous.current[key]); expect(navigations[key]).toBe(previous.current[key]);
}); });
} }
@@ -56,3 +59,136 @@ it('preserves reference for navigation objects', () => {
root.update(<Test />); 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 TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 callback = jest.fn();
const Test = () => { const Test = () => {
const state = useNavigationState(state => state); const state = useNavigationState((state) => state);
callback(state); callback(state);
@@ -62,13 +62,13 @@ it('gets the current navigation state with selector', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 callback = jest.fn();
const Test = () => { const Test = () => {
const index = useNavigationState(state => state.index); const index = useNavigationState((state) => state.index);
callback(index); callback(index);
@@ -112,7 +112,7 @@ it('gets the correct value if selector changes', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 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).toBeCalledTimes(1);
expect(callback.mock.calls[0][0]).toBe(0); 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).toBeCalledTimes(2);
expect(callback.mock.calls[1][0]).toBe('first'); 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 ( return (
<React.Fragment> <React.Fragment>
{state.routes.map(route => descriptors[route.key].render())} {state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment> </React.Fragment>
); );
}; };
@@ -270,7 +270,7 @@ it("action doesn't bubble if target is specified", () => {
return ( return (
<React.Fragment> <React.Fragment>
{state.routes.map(route => descriptors[route.key].render())} {state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment> </React.Fragment>
); );
}; };
@@ -317,7 +317,7 @@ it('logs error if no navigator handled the action', () => {
return ( return (
<React.Fragment> <React.Fragment>
{state.routes.map(route => descriptors[route.key].render())} {state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment> </React.Fragment>
); );
}; };
@@ -374,7 +374,7 @@ it('logs error if no navigator handled the action', () => {
render(element).update(element); render(element).update(element);
expect(spy.mock.calls[0][0]).toMatch( 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(); spy.mockRestore();

View File

@@ -13,7 +13,7 @@ it('gets route prop from context', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); 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 = () => { const Test = () => {

View File

@@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { ParamListBase } from '@react-navigation/routers'; import { ParamListBase, NavigationState } from '@react-navigation/routers';
import Screen from './Screen'; import Screen from './Screen';
import { TypedNavigator } from './types'; import { TypedNavigator, EventMapBase } from './types';
/** /**
* Higher order component to create a `Navigator` and `Screen` pair. * 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. * @returns Factory method to create a `Navigator` and `Screen` pair.
*/ */
export default function createNavigatorFactory< export default function createNavigatorFactory<
State extends NavigationState,
ScreenOptions extends object, ScreenOptions extends object,
EventMap extends EventMapBase,
NavigatorComponent extends React.ComponentType<any> NavigatorComponent extends React.ComponentType<any>
>(Navigator: NavigatorComponent) { >(Navigator: NavigatorComponent) {
return function<ParamList extends ParamListBase>(): TypedNavigator< return function <ParamList extends ParamListBase>(): TypedNavigator<
ParamList, ParamList,
State,
ScreenOptions, ScreenOptions,
EventMap,
typeof Navigator typeof Navigator
> { > {
if (arguments[0] !== undefined) { if (arguments[0] !== undefined) {
throw new Error( 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/hello-react-navigation.html for usage 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,65 +3,54 @@ import { PartialState, NavigationState } from '@react-navigation/routers';
type NavigateParams = { type NavigateParams = {
screen?: string; screen?: string;
params?: NavigateParams; params?: NavigateParams;
initial?: boolean;
}; };
type Action = type NavigateAction = {
| { type: 'NAVIGATE';
type: 'NAVIGATE'; payload: { name: string; params: NavigateParams };
payload: { name: string; params: NavigateParams }; };
}
| {
type: 'RESET_ROOT';
payload: PartialState<NavigationState>;
};
export default function getActionFromState( export default function getActionFromState(
state: PartialState<NavigationState> state: PartialState<NavigationState>
): Action { ): NavigateAction | undefined {
let payload: { name: string; params: NavigateParams } | undefined; if (state.routes.length === 0) {
return undefined;
if (state.routes.length === 1) {
// Try to construct payload for a `NAVIGATE` action from the state
// This lets us preserve the navigation state and not lose it
let route = state.routes[0];
payload = {
name: route.name,
params: { ...route.params },
};
let current = state.routes[0].state;
let params = payload.params;
while (current) {
if (current.routes.length === 1) {
route = current.routes[0];
params.screen = route.name;
if (route.state) {
params.params = { ...route.params };
params = params.params;
} else {
params.params = route.params;
}
current = route.state;
} else {
payload = undefined;
break;
}
}
} }
if (payload) { // Try to construct payload for a `NAVIGATE` action from the state
return { // This lets us preserve the navigation state and not lose it
type: 'NAVIGATE', let route = state.routes[state.routes.length - 1];
payload,
}; let payload: { name: string; params: NavigateParams } = {
name: route.name,
params: { ...route.params },
};
let current = route.state;
let params = payload.params;
while (current) {
if (current.routes.length === 0) {
return undefined;
}
route = current.routes[current.routes.length - 1];
params.initial = current.routes.length === 1;
params.screen = route.name;
if (route.state) {
params.params = { ...route.params };
params = params.params;
} else {
params.params = route.params;
}
current = route.state;
} }
return { return {
type: 'RESET_ROOT', type: 'NAVIGATE',
payload: state, payload,
}; };
} }

View File

@@ -64,6 +64,8 @@ export default function getPathFromState(
}; };
let currentOptions = options; let currentOptions = options;
let pattern = route.name; let pattern = route.name;
// we keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
let nestedRouteNames = '';
while (route.name in currentOptions) { while (route.name in currentOptions) {
if (typeof currentOptions[route.name] === 'string') { if (typeof currentOptions[route.name] === 'string') {
@@ -77,11 +79,13 @@ export default function getPathFromState(
}).screens }).screens
) { ) {
pattern = (currentOptions[route.name] as { path: string }).path; pattern = (currentOptions[route.name] as { path: string }).path;
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
break; break;
} else { } else {
// if it is the end of state, we return pattern // if it is the end of state, we return pattern
if (route.state === undefined) { if (route.state === undefined) {
pattern = (currentOptions[route.name] as { path: string }).path; pattern = (currentOptions[route.name] as { path: string }).path;
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
break; break;
} else { } else {
index = index =
@@ -92,11 +96,13 @@ export default function getPathFromState(
}).screens; }).screens;
// if there is config for next route name, we go deeper // if there is config for next route name, we go deeper
if (nextRoute.name in deeperConfig) { if (nextRoute.name in deeperConfig) {
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
route = nextRoute as Route<string> & { state?: State }; route = nextRoute as Route<string> & { state?: State };
currentOptions = deeperConfig; currentOptions = deeperConfig;
} else { } else {
// if not, there is no sense in going deeper in config // if not, there is no sense in going deeper in config
pattern = (currentOptions[route.name] as { path: string }).path; pattern = (currentOptions[route.name] as { path: string }).path;
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
break; break;
} }
} }
@@ -104,60 +110,73 @@ export default function getPathFromState(
} }
} }
// we don't add empty path strings to path if (pattern === undefined) {
if (pattern !== '') { // cut the first `/`
const config = pattern = nestedRouteNames.substring(1);
currentOptions[route.name] !== undefined }
? (currentOptions[route.name] as { stringify?: StringifyConfig })
.stringify
: undefined;
const params = route.params const config =
? // Stringify all of the param values before we use them currentOptions[route.name] !== undefined
Object.entries(route.params).reduce<{ ? (currentOptions[route.name] as { stringify?: StringifyConfig })
[key: string]: string; .stringify
}>((acc, [key, value]) => {
acc[key] = config?.[key] ? config[key](value) : String(value);
return acc;
}, {})
: undefined; : undefined;
if (currentOptions[route.name] !== undefined) { const params = route.params
path += pattern ? // Stringify all of the param values before we use them
.split('/') Object.entries(route.params).reduce<{
.map(p => { [key: string]: string;
const name = p.replace(/^:/, ''); }>((acc, [key, value]) => {
acc[key] = config?.[key] ? config[key](value) : String(value);
return acc;
}, {})
: undefined;
// If the path has a pattern for a param, put the param in the path if (currentOptions[route.name] !== undefined) {
if (params && name in params && p.startsWith(':')) { path += pattern
const value = params[name]; .split('/')
// Remove the used value from the params object since we'll use the rest for query string .map((p) => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete const name = p.replace(/^:/, '').replace(/\?$/, '');
delete params[name];
return encodeURIComponent(value);
}
return encodeURIComponent(p); // If the path has a pattern for a param, put the param in the path
}) if (params && name in params && p.startsWith(':')) {
.join('/'); const value = params[name];
} else { // Remove the used value from the params object since we'll use the rest for query string
path += encodeURIComponent(route.name); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
} delete params[name];
return encodeURIComponent(value);
} else if (p.endsWith('?')) {
// optional params without value assigned in route.params should be ignored
return '';
}
return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
}
if (route.state) { if (route.state) {
path += '/'; path += '/';
} else if (params) { } else if (params) {
const query = queryString.stringify(params); for (let param in params) {
if (params[param] === 'undefined') {
if (query) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
path += `?${query}`; delete params[param];
} }
} }
const query = queryString.stringify(params);
if (query) {
path += `?${query}`;
}
} }
current = route.state; current = route.state;
} }
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; return path;
} }

View File

@@ -20,7 +20,8 @@ type Options = {
}; };
type RouteConfig = { type RouteConfig = {
match: RegExp; screen: string;
match: RegExp | null;
pattern: string; pattern: string;
routeNames: string[]; routeNames: string[];
parse: ParseConfig | undefined; parse: ParseConfig | undefined;
@@ -58,24 +59,54 @@ export default function getStateFromPath(
path: string, path: string,
options: Options = {} options: Options = {}
): ResultState | undefined { ): ResultState | undefined {
if (path === '') {
return undefined;
}
let initialRoutes: InitialRouteConfig[] = []; let initialRoutes: InitialRouteConfig[] = [];
// Create a normalized configs array which will be easier to use // Create a normalized configs array which will be easier to use
const configs = ([] as RouteConfig[]).concat( const configs = ([] as RouteConfig[]).concat(
...Object.keys(options).map(key => ...Object.keys(options).map((key) =>
createNormalizedConfigs(key, options, [], initialRoutes) createNormalizedConfigs(key, options, [], initialRoutes)
) )
); );
let result: PartialState<NavigationState> | undefined; // sort configs so the most exhaustive is always first to be chosen
let current: PartialState<NavigationState> | undefined; configs.sort(
(config1, config2) =>
config2.pattern.split('/').length - config1.pattern.split('/').length
);
let remaining = path let remaining = path
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash .replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*/, ''); // Remove query params which we will handle later .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.pattern === '' &&
config.routeNames.every(
// make sure that none of the parent configs have a non-empty path defined
(name) => !configs.find((c) => c.screen === name)?.pattern
)
);
if (match) {
return createNestedStateObject(
match.routeNames,
initialRoutes,
parseQueryParams(path, match.parse)
);
}
return undefined;
}
let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined;
while (remaining) { while (remaining) {
let routeNames: string[] | undefined; let routeNames: string[] | undefined;
@@ -83,6 +114,10 @@ export default function getStateFromPath(
// Go through all configs, and see if the next path segment matches our regex // Go through all configs, and see if the next path segment matches our regex
for (const config of configs) { for (const config of configs) {
if (!config.match) {
continue;
}
const match = remaining.match(config.match); const match = remaining.match(config.match);
// If our regex matches, we need to extract params from the path // If our regex matches, we need to extract params from the path
@@ -91,24 +126,25 @@ export default function getStateFromPath(
const paramPatterns = config.pattern const paramPatterns = config.pattern
.split('/') .split('/')
.filter(p => p.startsWith(':')); .filter((p) => p.startsWith(':'));
if (paramPatterns.length) { if (paramPatterns.length) {
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => { params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const key = p.replace(/^:/, ''); const key = p.replace(/^:/, '').replace(/\?$/, '');
const value = match[i + 1]; // The param segments start from index 1 in the regex match result const value = match[(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
acc[key] = if (value) {
config.parse && config.parse[key] acc[key] =
? config.parse[key](value) config.parse && config.parse[key]
: value; ? config.parse[key](value)
: value;
}
return acc; return acc;
}, {}); }, {});
} }
// Remove the matched segment from the remaining path remaining = remaining.replace(match[1], '');
remaining = remaining.replace(match[0], '');
break; break;
} }
@@ -123,34 +159,7 @@ export default function getStateFromPath(
remaining = segments.join('/'); remaining = segments.join('/');
} }
let state: InitialState; const state = createNestedStateObject(routeNames, initialRoutes, params);
let routeName = routeNames.shift() as string;
let initialRoute = findInitialRoute(routeName, initialRoutes);
state = createNestedState(
initialRoute,
routeName,
routeNames.length === 0,
params
);
if (routeNames.length > 0) {
let nestedState = state;
while ((routeName = routeNames.shift() as string)) {
initialRoute = findInitialRoute(routeName, initialRoutes);
nestedState.routes[nestedState.index || 0].state = createNestedState(
initialRoute,
routeName,
routeNames.length === 0,
params
);
if (routeNames.length > 0) {
nestedState = nestedState.routes[nestedState.index || 0]
.state as InitialState;
}
}
}
if (current) { if (current) {
// The state should be nested inside the deepest route we parsed before // The state should be nested inside the deepest route we parsed before
@@ -172,29 +181,13 @@ export default function getStateFromPath(
return undefined; return undefined;
} }
const query = path.split('?')[1]; const route = findFocusedRoute(current);
const params = parseQueryParams(
if (query) { path,
while (current?.routes[current.index || 0].state) { findParseConfigForRoute(route.name, configs)
// 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);
}
});
}
if (params) {
route.params = { ...route.params, ...params }; route.params = { ...route.params, ...params };
} }
@@ -215,16 +208,15 @@ function createNormalizedConfigs(
if (typeof value === 'string') { if (typeof value === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern // 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(key, routeNames, value));
configs.push(createConfigItem(routeNames, value));
}
} else if (typeof value === 'object') { } else if (typeof value === 'object') {
// if an object is specified as the value (e.g. Foo: { ... }), // if an object is specified as the value (e.g. Foo: { ... }),
// it can have `path` property and // it can have `path` property and
// it could have `screens` prop which has nested configs // it could have `screens` prop which has nested configs
if (value.path && value.path !== '') { if (typeof value.path === 'string') {
configs.push(createConfigItem(routeNames, value.path, value.parse)); configs.push(createConfigItem(key, routeNames, value.path, value.parse));
} }
if (value.screens) { if (value.screens) {
// property `initialRouteName` without `screens` has no purpose // property `initialRouteName` without `screens` has no purpose
if (value.initialRouteName) { if (value.initialRouteName) {
@@ -233,7 +225,7 @@ function createNormalizedConfigs(
connectedRoutes: Object.keys(value.screens), connectedRoutes: Object.keys(value.screens),
}); });
} }
Object.keys(value.screens).forEach(nestedConfig => { Object.keys(value.screens).forEach((nestedConfig) => {
const result = createNormalizedConfigs( const result = createNormalizedConfigs(
nestedConfig, nestedConfig,
value.screens as Options, value.screens as Options,
@@ -251,15 +243,28 @@ function createNormalizedConfigs(
} }
function createConfigItem( function createConfigItem(
screen: string,
routeNames: string[], routeNames: string[],
pattern: string, pattern: string,
parse?: ParseConfig parse?: ParseConfig
): RouteConfig { ): RouteConfig {
const match = new RegExp( const match = pattern
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?' ? new RegExp(
); `^(${pattern
.split('/')
.map((it) => {
if (it.startsWith(':')) {
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
}
return `${escape(it)}\\/`;
})
.join('')})`
)
: null;
return { return {
screen,
match, match,
pattern, pattern,
// The routeNames array is mutated, so copy it to keep the current state // The routeNames array is mutated, so copy it to keep the current state
@@ -295,9 +300,9 @@ function findInitialRoute(
return undefined; 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 // it is the end of state and if there is initialRoute for this level
function createNestedState( function createStateObject(
initialRoute: string | undefined, initialRoute: string | undefined,
routeName: string, routeName: string,
isEmpty: boolean, isEmpty: boolean,
@@ -331,3 +336,73 @@ function createNestedState(
} }
} }
} }
function createNestedStateObject(
routeNames: string[],
initialRoutes: InitialRouteConfig[],
params: object | undefined
) {
let state: InitialState;
let routeName = routeNames.shift() as string;
let initialRoute = findInitialRoute(routeName, initialRoutes);
state = createStateObject(
initialRoute,
routeName,
routeNames.length === 0,
params
);
if (routeNames.length > 0) {
let nestedState = state;
while ((routeName = routeNames.shift() as string)) {
initialRoute = findInitialRoute(routeName, initialRoutes);
nestedState.routes[nestedState.index || 0].state = createStateObject(
initialRoute,
routeName,
routeNames.length === 0,
params
);
if (routeNames.length > 0) {
nestedState = nestedState.routes[nestedState.index || 0]
.state as InitialState;
}
}
}
return state;
}
function 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;
}
function 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 BaseNavigationContainer } from './BaseNavigationContainer';
export { default as createNavigatorFactory } from './createNavigatorFactory'; export { default as createNavigatorFactory } from './createNavigatorFactory';
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
export { default as NavigationContext } from './NavigationContext'; export { default as NavigationContext } from './NavigationContext';
export { default as NavigationRouteContext } from './NavigationRouteContext'; export { default as NavigationRouteContext } from './NavigationRouteContext';

View File

@@ -1,6 +1,6 @@
const isSerializableWithoutCircularReference = ( const isSerializableWithoutCircularReference = (
o: { [key: string]: any }, o: { [key: string]: any },
seen = new Set<any>() seen: Set<any>
): boolean => { ): boolean => {
if ( if (
o === undefined || o === undefined ||
@@ -27,13 +27,13 @@ const isSerializableWithoutCircularReference = (
if (Array.isArray(o)) { if (Array.isArray(o)) {
for (const it of o) { for (const it of o) {
if (!isSerializableWithoutCircularReference(it, seen)) { if (!isSerializableWithoutCircularReference(it, new Set<any>(seen))) {
return false; return false;
} }
} }
} else { } else {
for (const key in o) { for (const key in o) {
if (!isSerializableWithoutCircularReference(o[key], seen)) { if (!isSerializableWithoutCircularReference(o[key], new Set<any>(seen))) {
return false; return false;
} }
} }
@@ -43,5 +43,5 @@ const isSerializableWithoutCircularReference = (
}; };
export default function isSerializable(o: { [key: string]: any }) { export default function isSerializable(o: { [key: string]: any }) {
return isSerializableWithoutCircularReference(o); return isSerializableWithoutCircularReference(o, new Set<any>());
} }

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