Compare commits

..

80 Commits

Author SHA1 Message Date
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
140 changed files with 5796 additions and 4294 deletions

1
.gitattributes vendored Normal file
View File

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

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

@@ -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."

1
.gitignore vendored
View File

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

View File

@@ -7,7 +7,7 @@
"slug": "react-navigation-example", "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,4 +1,4 @@
/* eslint-disable jest/no-jasmine-globals, import/no-commonjs */ /* eslint-disable import/no-commonjs */
const detox = require('detox'); const detox = require('detox');
const config = require('../../package.json').detox; const config = require('../../package.json').detox;

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

@@ -12,31 +12,30 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.0.0",
"@react-native-community/masked-view": "0.1.6", "@react-native-community/masked-view": "^0.1.7",
"@types/react-native-restart": "^0.0.0",
"color": "^3.1.2", "color": "^3.1.2",
"expo": "^36.0.2", "expo": "^37.0.0",
"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.7.0",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.7.0",
"react-native-restart": "^0.0.13", "react-native-restart": "^0.0.14",
"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.3.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.8.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/react": "^16.9.23",
"@types/react-native": "^0.60.30", "@types/react-native": "^0.60.22",
"babel-preset-expo": "^8.0.0", "babel-preset-expo": "^8.1.0",
"expo-cli": "^3.11.9", "expo-cli": "^3.17.18",
"typescript": "^3.7.5" "typescript": "^3.8.3"
} }
} }

View File

@@ -28,7 +28,7 @@ export default function BottomTabsScreen() {
return ( return (
<BottomTabs.Navigator <BottomTabs.Navigator
screenOptions={{ screenOptions={{
tabBarButton: props => <TouchableBounce {...props} />, tabBarButton: (props) => <TouchableBounce {...props} />,
}} }}
> >
<BottomTabs.Screen <BottomTabs.Screen
@@ -38,7 +38,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

@@ -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

@@ -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

@@ -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

@@ -6,8 +6,14 @@ import {
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,
@@ -17,7 +23,6 @@ import {
List, List,
Divider, Divider,
} from 'react-native-paper'; } from 'react-native-paper';
import { Asset } from 'expo-asset';
import { import {
InitialState, InitialState,
useLinking, useLinking,
@@ -49,10 +54,11 @@ 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 SettingsItem from './Shared/SettingsItem';
import { Updates } from 'expo';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']); YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
enableScreens();
type RootDrawerParamList = { type RootDrawerParamList = {
Root: undefined; Root: undefined;
Another: undefined; Another: undefined;
@@ -110,7 +116,7 @@ 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>(); const containerRef = React.useRef<NavigationContainerRef>(null);
// To test deep linking on, run the following in the Terminal: // 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" // Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
@@ -192,10 +198,24 @@ 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 > 900;
return ( return (
<PaperProvider theme={paperTheme}> <PaperProvider theme={paperTheme}>
{Platform.OS === 'ios' && ( {Platform.OS === 'ios' && (
@@ -204,7 +224,7 @@ export default function App() {
<NavigationContainer <NavigationContainer
ref={containerRef} 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)
@@ -212,7 +232,7 @@ export default function App() {
} }
theme={theme} theme={theme}
> >
<Drawer.Navigator> <Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen <Drawer.Screen
name="Root" name="Root"
options={{ options={{
@@ -236,13 +256,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()}
/>
),
}} }}
> >
{({ {({
@@ -276,12 +298,12 @@ 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}
title={SCREENS[name].title} title={SCREENS[name].title}
@@ -293,7 +315,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}

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,7 +20,7 @@ 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, {
@@ -30,7 +30,7 @@ module.exports = async function(env, argv) {
'@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]
) )

View File

@@ -18,32 +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", "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.0",
"@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.2",
"@commitlint/config-conventional": "^8.3.4", "@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^25.1.2", "@types/jest": "^25.2.1",
"babel-jest": "^25.2.6",
"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.4",
"detox": "^15.1.4",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-satya164": "^3.1.5", "eslint-config-satya164": "^3.1.6",
"husky": "^4.2.1", "husky": "^4.2.3",
"jest": "^25.1.0", "jest": "^25.2.7",
"lerna": "^3.20.2", "lerna": "^3.20.2",
"prettier": "^1.19.1", "prettier": "^2.0.4",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"resolutions": { "resolutions": {
"react": "~16.9.0", "react": "~16.9.0",

View File

@@ -3,6 +3,91 @@
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.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) ## [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 **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/bottom-tabs", "name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines", "description": "Bottom tab navigator following iOS design guidelines",
"version": "5.0.7", "version": "5.2.6",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -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.10.0",
"@react-navigation/native": "^5.0.7", "@react-navigation/native": "^5.1.5",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.19", "@types/react": "^16.9.23",
"@types/react-native": "^0.60.30", "@types/react-native": "^0.61.22",
"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.3.0",
"typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.5", "@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,7 +12,7 @@ export { default as BottomTabBar } from './views/BottomTabBar';
/** /**
* Types * Types
*/ */
export { export type {
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationProp, BottomTabNavigationProp,
BottomTabBarProps, BottomTabBarProps,

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

@@ -11,6 +11,7 @@ import {
ParamListBase, ParamListBase,
Descriptor, Descriptor,
TabNavigationState, TabNavigationState,
TabActionHelpers,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type BottomTabNavigationEventMap = { export type BottomTabNavigationEventMap = {
@@ -40,19 +41,8 @@ export type BottomTabNavigationProp<
TabNavigationState, TabNavigationState,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap BottomTabNavigationEventMap
> & { > &
/** TabActionHelpers<ParamList>;
* Jump to an existing tab.
*
* @param name Name of the route for the tab.
* @param [params] Params object for the route.
*/
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends undefined | any
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
};
export type BottomTabNavigationOptions = { export type BottomTabNavigationOptions = {
/** /**
@@ -148,7 +138,7 @@ export type BottomTabBarOptions = {
*/ */
inactiveTintColor?: string; inactiveTintColor?: string;
/** /**
* Background olor for the active tab. * Background color for the active tab.
*/ */
activeBackgroundColor?: string; activeBackgroundColor?: string;
/** /**
@@ -184,6 +174,16 @@ export type BottomTabBarOptions = {
* 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.
*/ */

View File

@@ -15,7 +15,7 @@ import {
CommonActions, CommonActions,
useTheme, useTheme,
} 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';
@@ -43,6 +43,7 @@ export default function BottomTabBar({
keyboardHidesTabBar = false, keyboardHidesTabBar = false,
labelPosition, labelPosition,
labelStyle, labelStyle,
safeAreaInsets,
showIcon, showIcon,
showLabel, showLabel,
style, style,
@@ -50,7 +51,12 @@ export default function BottomTabBar({
}: Props) { }: Props) {
const { colors } = useTheme(); const { colors } = useTheme();
const [dimensions, setDimensions] = React.useState(Dimensions.get('window')); const [dimensions, setDimensions] = React.useState(() => {
const { height = 0, width = 0 } = Dimensions.get('window');
return { height, width };
});
const [layout, setLayout] = React.useState({ const [layout, setLayout] = React.useState({
height: 0, height: 0,
width: dimensions.width, width: dimensions.width,
@@ -115,7 +121,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 {
@@ -158,116 +164,122 @@ 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}
> >
<NavigationRouteContext.Provider value={route}> <NavigationRouteContext.Provider value={route}>
<BottomTabItem <BottomTabItem
route={route} route={route}
focused={focused} focused={focused}
horizontal={shouldUseHorizontalLabels()} horizontal={shouldUseHorizontalLabels()}
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
testID={options.tabBarTestID} testID={options.tabBarTestID}
allowFontScaling={allowFontScaling} allowFontScaling={allowFontScaling}
activeTintColor={activeTintColor} activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor} inactiveTintColor={inactiveTintColor}
activeBackgroundColor={activeBackgroundColor} activeBackgroundColor={activeBackgroundColor}
inactiveBackgroundColor={inactiveBackgroundColor} inactiveBackgroundColor={inactiveBackgroundColor}
button={options.tabBarButton} button={options.tabBarButton}
icon={options.tabBarIcon} icon={options.tabBarIcon}
label={label} label={label}
showIcon={showIcon} showIcon={showIcon}
showLabel={showLabel} showLabel={showLabel}
labelStyle={labelStyle} labelStyle={labelStyle}
style={tabStyle} style={tabStyle}
/> />
</NavigationRouteContext.Provider> </NavigationRouteContext.Provider>
</NavigationContext.Provider> </NavigationContext.Provider>
); );
})} })}
</View> </View>
</Animated.View> </Animated.View>
)}
</SafeAreaConsumer>
); );
} }

View File

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

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,84 @@
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.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) ## [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 **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.7", "version": "5.1.8",
"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,11 +25,11 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.9.3", "@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.0.7", "@react-navigation/native": "^5.1.5",
"@types/react": "^16.9.19", "@types/react": "^16.9.23",
"react": "~16.9.0", "react": "~16.9.0",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.5", "@react-navigation/native": "^5.0.5",

View File

@@ -6,6 +6,7 @@ import {
TypedNavigator, TypedNavigator,
NavigationProp, NavigationProp,
RouteProp, RouteProp,
EventMapBase,
} 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 +16,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) {
@@ -66,7 +69,7 @@ export default function createCompatNavigatorFactory<
function Navigator({ screenProps }: { screenProps?: unknown }) { function Navigator({ screenProps }: { screenProps?: unknown }) {
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;

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

@@ -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,96 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.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) ## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.1.5...@react-navigation/core@5.1.6) (2020-02-21)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/core", "name": "@react-navigation/core",
"description": "Core utilities for building navigators", "description": "Core utilities for building navigators",
"version": "5.1.6", "version": "5.3.3",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -29,24 +29,23 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.2", "@react-navigation/routers": "^5.3.0",
"escape-string-regexp": "^2.0.0", "escape-string-regexp": "^2.0.0",
"query-string": "^6.10.1", "nanoid": "^3.0.2",
"react-is": "^16.12.0", "query-string": "^6.12.0",
"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.10.0",
"@types/react": "^16.9.19", "@types/react": "^16.9.23",
"@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.12.0",
"react-test-renderer": "~16.12.0", "react-test-renderer": "~16.13.1",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"

View File

@@ -9,22 +9,23 @@ 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';
import useSyncState from './useSyncState';
type State = NavigationState | PartialState<NavigationState> | undefined; type State = NavigationState | PartialState<NavigationState> | undefined;
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 = const NOT_INITIALIZED_ERROR =
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html#handling-initialization for more details."; "The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
export const NavigationStateContext = React.createContext<{ export const NavigationStateContext = React.createContext<{
isDefault?: true; isDefault?: true;
@@ -73,7 +74,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>;
@@ -102,7 +103,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);
@@ -112,7 +113,13 @@ const BaseNavigationContainer = React.forwardRef(
); );
} }
const [state, getState, setState] = useSyncState<State>(() => const [
state,
getState,
setState,
scheduleUpdate,
flushUpdates,
] = useSyncState<State>(() =>
getPartialState(initialState == null ? undefined : initialState) getPartialState(initialState == null ? undefined : initialState)
); );
@@ -136,6 +143,7 @@ const BaseNavigationContainer = React.forwardRef(
); );
const { trackState, trackAction } = useDevTools({ const { trackState, trackAction } = useDevTools({
enabled: false,
name: '@react-navigation', name: '@react-navigation',
reset, reset,
state, state,
@@ -155,7 +163,7 @@ const BaseNavigationContainer = React.forwardRef(
throw new Error(NOT_INITIALIZED_ERROR); throw new Error(NOT_INITIALIZED_ERROR);
} }
listeners[0](navigation => navigation.dispatch(action)); listeners[0]((navigation) => navigation.dispatch(action));
}; };
const canGoBack = () => { const canGoBack = () => {
@@ -163,7 +171,7 @@ const BaseNavigationContainer = React.forwardRef(
return false; return false;
} }
const { result, handled } = listeners[0](navigation => const { result, handled } = listeners[0]((navigation) =>
navigation.canGoBack() navigation.canGoBack()
); );
@@ -217,6 +225,11 @@ 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,
@@ -238,7 +251,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/troubleshooting.html#i-get-the-warning-we-found-non-serializable-values-in-the-navigation-state for more details." "Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details."
); );
} }
} }
@@ -262,11 +275,13 @@ const BaseNavigationContainer = React.forwardRef(
}, [onStateChange, trackState, getRootState, emitter, state]); }, [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

@@ -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,14 +32,15 @@ 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 navigatorKeyRef = React.useRef<string | undefined>(); const navigatorKeyRef = React.useRef<string | undefined>();
const getKey = React.useCallback(() => navigatorKeyRef.current, []); const getKey = React.useCallback(() => navigatorKeyRef.current, []);
@@ -46,7 +51,7 @@ export default function SceneView<
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]);
@@ -57,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
), ),
}); });

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

@@ -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,7 +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'?"
); );
}); });
@@ -122,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>
); );
}; };
@@ -220,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>
); );
}; };
@@ -371,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>
); );
}; };

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

@@ -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,
}, },
@@ -517,3 +521,209 @@ it('returns "/" for empty path', () => {
expect(getPathFromState(state, config)).toBe('/'); expect(getPathFromState(state, config)).toBe('/');
}); });
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);
});

View File

@@ -42,7 +42,8 @@ it('converts path string to initial state 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()),
count: Number, count: Number,
valid: Boolean, valid: Boolean,
}, },
@@ -153,7 +154,8 @@ it('converts path string to initial 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,
}, },
@@ -217,7 +219,8 @@ it('converts path string to initial state with config with nested screens and un
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,
id: Boolean, id: Boolean,
@@ -277,11 +280,11 @@ it('handles nested object with unused configs and with parse 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,
}, },
@@ -528,11 +531,11 @@ it('handles two initialRouteNames', () => {
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,
}, },
@@ -610,11 +613,11 @@ it('accepts initialRouteName without config for 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,
}, },

View File

@@ -372,7 +372,7 @@ 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();
@@ -626,7 +626,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 +635,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()} />
@@ -1085,7 +1085,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

@@ -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();
@@ -362,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 => {
@@ -376,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();
@@ -409,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,
}); });
}); });
@@ -422,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);
@@ -429,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);
@@ -450,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/upgrading-from-4.x.html for migration guide." "Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5? See https://reactnavigation.org/docs/upgrading-from-4.x for migration guide."
); );
} }

View File

@@ -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,6 +110,11 @@ export default function getPathFromState(
} }
} }
if (pattern === undefined) {
// cut the first `/`
pattern = nestedRouteNames.substring(1);
}
// we don't add empty path strings to path // we don't add empty path strings to path
if (pattern !== '') { if (pattern !== '') {
const config = const config =
@@ -125,7 +136,7 @@ export default function getPathFromState(
if (currentOptions[route.name] !== undefined) { if (currentOptions[route.name] !== undefined) {
path += pattern path += pattern
.split('/') .split('/')
.map(p => { .map((p) => {
const name = p.replace(/^:/, ''); const name = p.replace(/^:/, '');
// If the path has a pattern for a param, put the param in the path // If the path has a pattern for a param, put the param in the path
@@ -147,6 +158,12 @@ export default function getPathFromState(
if (route.state) { if (route.state) {
path += '/'; path += '/';
} else if (params) { } else if (params) {
for (let param in params) {
if (params[param] === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete params[param];
}
}
const query = queryString.stringify(params); const query = queryString.stringify(params);
if (query) { if (query) {

View File

@@ -64,7 +64,7 @@ export default function getStateFromPath(
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)
) )
); );
@@ -91,7 +91,7 @@ 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) => {
@@ -188,7 +188,7 @@ export default function getStateFromPath(
const parseFunction = findParseConfigForRoute(route.name, configs); const parseFunction = findParseConfigForRoute(route.name, configs);
if (parseFunction) { if (parseFunction) {
Object.keys(params).forEach(name => { Object.keys(params).forEach((name) => {
if (parseFunction[name] && typeof params[name] === 'string') { if (parseFunction[name] && typeof params[name] === 'string') {
params[name] = parseFunction[name](params[name] as string); params[name] = parseFunction[name](params[name] as string);
} }
@@ -233,7 +233,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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import {
RouterFactory, RouterFactory,
PartialState, PartialState,
NavigationAction, NavigationAction,
Route,
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import { NavigationStateContext } from './BaseNavigationContainer'; import { NavigationStateContext } from './BaseNavigationContainer';
import NavigationRouteContext from './NavigationRouteContext'; import NavigationRouteContext from './NavigationRouteContext';
@@ -27,9 +28,11 @@ import {
DefaultNavigatorOptions, DefaultNavigatorOptions,
RouteConfig, RouteConfig,
PrivateValueStore, PrivateValueStore,
EventMapBase,
} from './types'; } from './types';
import useStateGetters from './useStateGetters'; import useStateGetters from './useStateGetters';
import useOnGetState from './useOnGetState'; import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
// This is to make TypeScript compiler happy // This is to make TypeScript compiler happy
// eslint-disable-next-line babel/no-unused-expressions // eslint-disable-next-line babel/no-unused-expressions
@@ -55,18 +58,28 @@ const isArrayEqual = (a: any[], b: any[]) =>
* *
* @param children React Elements to extract the config from. * @param children React Elements to extract the config from.
*/ */
const getRouteConfigsFromChildren = <ScreenOptions extends object>( const getRouteConfigsFromChildren = <
State extends NavigationState,
ScreenOptions extends object,
EventMap extends EventMapBase
>(
children: React.ReactNode children: React.ReactNode
) => { ) => {
const configs = React.Children.toArray(children).reduce< const configs = React.Children.toArray(children).reduce<
RouteConfig<ParamListBase, string, ScreenOptions>[] RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>[]
>((acc, child) => { >((acc, child) => {
if (React.isValidElement(child)) { if (React.isValidElement(child)) {
if (child.type === Screen) { if (child.type === Screen) {
// We can only extract the config from `Screen` elements // We can only extract the config from `Screen` elements
// If something else was rendered, it's probably a bug // If something else was rendered, it's probably a bug
acc.push( acc.push(
child.props as RouteConfig<ParamListBase, string, ScreenOptions> child.props as RouteConfig<
ParamListBase,
string,
State,
ScreenOptions,
EventMap
>
); );
return acc; return acc;
} }
@@ -75,7 +88,9 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
// When we encounter a fragment, we need to dive into its children to extract the configs // When we encounter a fragment, we need to dive into its children to extract the configs
// This is handy to conditionally define a group of screens // This is handy to conditionally define a group of screens
acc.push( acc.push(
...getRouteConfigsFromChildren<ScreenOptions>(child.props.children) ...getRouteConfigsFromChildren<State, ScreenOptions, EventMap>(
child.props.children
)
); );
return acc; return acc;
} }
@@ -90,7 +105,7 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
}, []); }, []);
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
configs.forEach(config => { configs.forEach((config) => {
const { name, children, component } = config as any; const { name, children, component } = config as any;
if (typeof name !== 'string' || !name) { if (typeof name !== 'string' || !name) {
@@ -116,7 +131,7 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
if (component !== undefined && !isValidElementType(component)) { if (component !== undefined && !isValidElementType(component)) {
throw new Error( throw new Error(
`Got an invalid value for 'component' prop for the screen '${name}'. It must be a a valid React Component.` `Got an invalid value for 'component' prop for the screen '${name}'. It must be a valid React Component.`
); );
} }
@@ -177,9 +192,17 @@ export default function useNavigationBuilder<
}) })
); );
const routeConfigs = getRouteConfigsFromChildren<ScreenOptions>(children); const routeConfigs = getRouteConfigsFromChildren<
State,
ScreenOptions,
EventMap
>(children);
const screens = routeConfigs.reduce< const screens = routeConfigs.reduce<
Record<string, RouteConfig<ParamListBase, string, ScreenOptions>> Record<
string,
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
>
>((acc, config) => { >((acc, config) => {
if (config.name in acc) { if (config.name in acc) {
throw new Error( throw new Error(
@@ -191,7 +214,7 @@ export default function useNavigationBuilder<
return acc; return acc;
}, {}); }, {});
const routeNames = routeConfigs.map(config => config.name); const routeNames = routeConfigs.map((config) => config.name);
const routeParamList = routeNames.reduce<Record<string, object | undefined>>( const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
(acc, curr) => { (acc, curr) => {
const { initialParams } = screens[curr]; const { initialParams } = screens[curr];
@@ -220,12 +243,12 @@ export default function useNavigationBuilder<
} }
const isStateValid = React.useCallback( const isStateValid = React.useCallback(
state => state.type === undefined || state.type === router.type, (state) => state.type === undefined || state.type === router.type,
[router.type] [router.type]
); );
const isStateInitialized = React.useCallback( const isStateInitialized = React.useCallback(
state => (state) =>
state !== undefined && state.stale === false && isStateValid(state), state !== undefined && state.stale === false && isStateValid(state),
[isStateValid] [isStateValid]
); );
@@ -286,16 +309,13 @@ export default function useNavigationBuilder<
} }
if ( if (
previousRouteRef.current && typeof route?.params?.screen === 'string' &&
route && route.params !== previousRouteRef.current?.params
route.params &&
typeof route.params.screen === 'string' &&
route.params !== previousRouteRef.current.params
) { ) {
// If the route was updated with new name and/or params, we should navigate there // If the route was updated with new name and/or params, we should navigate there
// The update should be limited to current navigator only, so we call the router manually // The update should be limited to current navigator only, so we call the router manually
const updatedState = router.getStateForAction( const updatedState = router.getStateForAction(
state, nextState,
CommonActions.navigate(route.params.screen, route.params.params), CommonActions.navigate(route.params.screen, route.params.params),
{ {
routeNames, routeNames,
@@ -309,15 +329,17 @@ export default function useNavigationBuilder<
routeNames, routeNames,
routeParamList, routeParamList,
}) })
: state; : nextState;
} }
if (state !== nextState) { const shouldUpdate = state !== nextState;
// If the state needs to be updated, we'll schedule an update with React
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState useScheduleUpdate(() => {
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops if (shouldUpdate) {
setState(nextState); // If the state needs to be updated, we'll schedule an update
} setState(nextState);
}
});
// The up-to-date state will come in next render, but we don't need to wait for it // The up-to-date state will come in next render, but we don't need to wait for it
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config // We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
@@ -344,7 +366,50 @@ export default function useNavigationBuilder<
: (initializedStateRef.current as State); : (initializedStateRef.current as State);
}, [getCurrentState, isStateInitialized]); }, [getCurrentState, isStateInitialized]);
const emitter = useEventEmitter(); const emitter = useEventEmitter((e) => {
let routeNames = [];
let route: Route<string> | undefined;
if (e.target) {
route = state.routes.find((route) => route.key === e.target);
if (route?.name) {
routeNames.push(route.name);
}
} else {
route = state.routes[state.index];
routeNames.push(
...Object.keys(screens).filter((name) => route?.name === name)
);
}
if (route == null) {
return;
}
const navigation = descriptors[route.key].navigation;
const listeners = ([] as (((e: any) => void) | undefined)[])
.concat(
...routeNames.map((name) => {
const { listeners } = screens[name];
const map =
typeof listeners === 'function'
? listeners({ route: route as any, navigation })
: listeners;
return map
? Object.keys(map)
.filter((type) => type === e.type)
.map((type) => map?.[type])
: undefined;
})
)
.filter((cb, i, self) => cb && self.lastIndexOf(cb) === i);
listeners.forEach((listener) => listener?.(e));
});
useFocusEvents({ state, emitter }); useFocusEvents({ state, emitter });
@@ -400,7 +465,7 @@ export default function useNavigationBuilder<
getStateForRoute, getStateForRoute,
}); });
const descriptors = useDescriptors<State, ScreenOptions>({ const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
state, state,
screens, screens,
navigation, navigation,

View File

@@ -63,7 +63,7 @@ export default function useNavigationCache<
}; };
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>( cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(
(acc, route, index) => { (acc, route) => {
const previous = cache.current[route.key]; const previous = cache.current[route.key];
if (previous) { if (previous) {
@@ -103,14 +103,14 @@ export default function useNavigationCache<
dangerouslyGetState: getState, dangerouslyGetState: getState,
dispatch, dispatch,
setOptions: (options: object) => setOptions: (options: object) =>
setOptions(o => ({ setOptions((o) => ({
...o, ...o,
[route.key]: { ...o[route.key], ...options }, [route.key]: { ...o[route.key], ...options },
})), })),
isFocused: () => { isFocused: () => {
const state = getState(); const state = getState();
if (index !== state.index) { if (state.routes[state.index].key !== route.key) {
return false; return false;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,113 @@
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.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.0...@react-navigation/drawer@5.4.1) (2020-04-08)
### Bug Fixes
* don't hide content from accessibility with permanent drawer ([cb2f157](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/cb2f157a561a2ce3f073eb4ccb567532c77bd869)), closes [#7976](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7976)
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.4...@react-navigation/drawer@5.4.0) (2020-03-30)
### Bug Fixes
* disable only swipe gesture on safari ([105da6a](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/105da6ab2fe69847b676c4d4117638212cda1f9a))
### Features
* add swipeEnabled option to disable swipe gesture in drawer ([#7834](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7834)) ([ac7f972](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/ac7f972e922a82cd32d943356941d100b68bd8b0))
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.3...@react-navigation/drawer@5.3.4) (2020-03-23)
**Note:** Version bump only for package @react-navigation/drawer
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.2...@react-navigation/drawer@5.3.3) (2020-03-22)
**Note:** Version bump only for package @react-navigation/drawer
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.1...@react-navigation/drawer@5.3.2) (2020-03-19)
### Bug Fixes
* close drawer on pressing Esc on web ([5c4afc5](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/5c4afc5cb40c1206a9d8c40efe3cf947030da48e)), closes [#6745](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6745)
* don't use react-native-screens on web ([b1a65fc](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/b1a65fc73e8603ae2c06ef101a74df31e80bb9b2)), closes [#7485](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7485)
* fix permanent sidebar position ([#7830](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7830)) ([3ea8eec](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/3ea8eec4324ea82f0ed427f4662e68e1115e60ab))
* initialize height and width to zero if undefined ([3df65e2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/3df65e28197db3bb8371059146546d57661c5ba3)), closes [#6789](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6789)
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.3.0...@react-navigation/drawer@5.3.1) (2020-03-17)
**Note:** Version bump only for package @react-navigation/drawer
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.2.0...@react-navigation/drawer@5.3.0) (2020-03-17)
### Features
* add permanent drawer type ([#7818](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7818)) ([6a5d0a0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/6a5d0a035afae60d91aef78401ec8826295746fe))
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.1.1...@react-navigation/drawer@5.2.0) (2020-03-16)
### Features
* make useIsDrawerOpen workable inside drawer content ([#7746](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7746)) ([cb46d0b](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/cb46d0bca4e17e847fff46ac94276213ac9697bf))
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.1.0...@react-navigation/drawer@5.1.1) (2020-03-03)
**Note:** Version bump only for package @react-navigation/drawer
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.7...@react-navigation/drawer@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/drawer/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6756)
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.6...@react-navigation/drawer@5.0.7) (2020-02-21) ## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.6...@react-navigation/drawer@5.0.7) (2020-02-21)
**Note:** Version bump only for package @react-navigation/drawer **Note:** Version bump only for package @react-navigation/drawer

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/drawer", "name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess", "description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.0.7", "version": "5.4.1",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -39,18 +39,18 @@
"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.10.0",
"@react-navigation/native": "^5.0.7", "@react-navigation/native": "^5.1.5",
"@types/react": "^16.9.19", "@types/react": "^16.9.23",
"@types/react-native": "^0.60.30", "@types/react-native": "^0.61.22",
"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-gesture-handler": "^1.5.6", "react-native-gesture-handler": "^1.6.0",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.7.0",
"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.3.0",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.5", "@react-navigation/native": "^5.0.5",
@@ -59,7 +59,7 @@
"react-native-gesture-handler": ">= 1.0.0", "react-native-gesture-handler": ">= 1.0.0",
"react-native-reanimated": ">= 1.0.0", "react-native-reanimated": ">= 1.0.0",
"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" "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

@@ -22,7 +22,7 @@ export { default as useIsDrawerOpen } from './utils/useIsDrawerOpen';
/** /**
* Types * Types
*/ */
export { export type {
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationProp, DrawerNavigationProp,
DrawerContentOptions, DrawerContentOptions,

View File

@@ -49,6 +49,8 @@ function DrawerNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
DrawerNavigationState,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap,
typeof DrawerNavigator typeof DrawerNavigator
>(DrawerNavigator); >(DrawerNavigator);

View File

@@ -7,6 +7,7 @@ import {
Descriptor, Descriptor,
NavigationHelpers, NavigationHelpers,
DrawerNavigationState, DrawerNavigationState,
DrawerActionHelpers,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { PanGestureHandler } from 'react-native-gesture-handler'; import { PanGestureHandler } from 'react-native-gesture-handler';
@@ -26,8 +27,9 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
* - `front`: Traditional drawer which covers the screen with a overlay behind it. * - `front`: Traditional drawer which covers the screen with a overlay behind it.
* - `back`: The drawer is revealed behind the screen on swipe. * - `back`: The drawer is revealed behind the screen on swipe.
* - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer. * - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
* - `permanent`: A permanent drawer is shown as a sidebar.
*/ */
drawerType?: 'front' | 'back' | 'slide'; drawerType?: 'front' | 'back' | 'slide' | 'permanent';
/** /**
* How far from the edge of the screen the swipe gesture should activate. * How far from the edge of the screen the swipe gesture should activate.
*/ */
@@ -109,9 +111,18 @@ export type DrawerNavigationOptions = {
/** /**
* Whether you can use gestures to open or close the drawer. * Whether you can use gestures to open or close the drawer.
* Setting this to `false` disables swipe gestures as well as tap on overlay to close.
* See `swipeEnabled` to disable only the swipe gesture.
* Defaults to `true` * Defaults to `true`
*/ */
gestureEnabled?: boolean; gestureEnabled?: boolean;
/**
* Whether you can use swipe gestures to open or close the drawer.
* Defaults to `true`
*/
swipeEnabled?: boolean;
/** /**
* Whether this screen should be unmounted when navigating away from it. * Whether this screen should be unmounted when navigating away from it.
* Defaults to `false`. * Defaults to `false`.
@@ -190,22 +201,8 @@ export type DrawerNavigationProp<
DrawerNavigationState, DrawerNavigationState,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap DrawerNavigationEventMap
> & { > &
/** DrawerActionHelpers<ParamList>;
* Open the drawer sidebar.
*/
openDrawer(): void;
/**
* Close the drawer sidebar.
*/
closeDrawer(): void;
/**
* Open the drawer sidebar if closed, or close if opened.
*/
toggleDrawer(): void;
};
export type DrawerDescriptor = Descriptor< export type DrawerDescriptor = Descriptor<
ParamListBase, ParamListBase,

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
const DrawerOpenContext = React.createContext<boolean | null>(null);
export default DrawerOpenContext;

View File

@@ -1,38 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { useNavigation, ParamListBase } from '@react-navigation/native'; import DrawerOpenContext from './DrawerOpenContext';
import { DrawerNavigationProp } from '../types';
/** /**
* Hook to detect if the drawer is open in a parent navigator. * Hook to detect if the drawer is open in a parent navigator.
*/ */
export default function useIsDrawerOpen() { export default function useIsDrawerOpen() {
const navigation = useNavigation(); const isDrawerOpen = React.useContext(DrawerOpenContext);
let drawer = navigation as DrawerNavigationProp<ParamListBase>; if (typeof isDrawerOpen !== 'boolean') {
throw new Error(
// The screen might be inside another navigator such as stack nested in drawer "Couldn't find a drawer. Is your component inside a drawer navigator?"
// We need to find the closest drawer navigator and add the listener there );
while (drawer && drawer.dangerouslyGetState().type !== 'drawer') {
drawer = drawer.dangerouslyGetParent();
} }
const [isDrawerOpen, setIsDrawerOpen] = React.useState(() =>
drawer
? Boolean(
drawer.dangerouslyGetState().history.find(it => it.type === 'drawer')
)
: false
);
React.useEffect(() => {
const unsubscribe = drawer.addListener('state', e => {
setIsDrawerOpen(
Boolean(e.data.state.history.find(it => it.type === 'drawer'))
);
});
return unsubscribe;
}, [drawer, isDrawerOpen]);
return isDrawerOpen; return isDrawerOpen;
} }

View File

@@ -68,6 +68,8 @@ const SPRING_CONFIG = {
restSpeedThreshold: 0.01, restSpeedThreshold: 0.01,
}; };
const ANIMATED_ONE = new Animated.Value(1);
type Binary = 0 | 1; type Binary = 0 | 1;
type Renderer = (props: { progress: Animated.Node<number> }) => React.ReactNode; type Renderer = (props: { progress: Animated.Node<number> }) => React.ReactNode;
@@ -78,8 +80,9 @@ type Props = {
onClose: () => void; onClose: () => void;
onGestureRef?: (ref: PanGestureHandler | null) => void; onGestureRef?: (ref: PanGestureHandler | null) => void;
gestureEnabled: boolean; gestureEnabled: boolean;
swipeEnabled: boolean;
drawerPosition: 'left' | 'right'; drawerPosition: 'left' | 'right';
drawerType: 'front' | 'back' | 'slide'; drawerType: 'front' | 'back' | 'slide' | 'permanent';
keyboardDismissMode: 'none' | 'on-drag'; keyboardDismissMode: 'none' | 'on-drag';
swipeEdgeWidth: number; swipeEdgeWidth: number;
swipeDistanceThreshold?: number; swipeDistanceThreshold?: number;
@@ -98,7 +101,7 @@ type Props = {
* Disables the pan gesture by default on Apple devices in the browser. * Disables the pan gesture by default on Apple devices in the browser.
* https://stackoverflow.com/a/9039885 * https://stackoverflow.com/a/9039885
*/ */
function shouldEnableGesture(): boolean { function shouldEnableSwipeGesture(): boolean {
if ( if (
Platform.OS === 'web' && Platform.OS === 'web' &&
typeof navigator !== 'undefined' && typeof navigator !== 'undefined' &&
@@ -113,11 +116,12 @@ function shouldEnableGesture(): boolean {
return true; return true;
} }
export default class DrawerView extends React.PureComponent<Props> { export default class DrawerView extends React.Component<Props> {
static defaultProps = { static defaultProps = {
drawerPostion: I18nManager.isRTL ? 'left' : 'right', drawerPostion: I18nManager.isRTL ? 'left' : 'right',
drawerType: 'front', drawerType: 'front',
gestureEnabled: shouldEnableGesture(), gestureEnabled: true,
swipeEnabled: shouldEnableSwipeGesture(),
swipeEdgeWidth: 32, swipeEdgeWidth: 32,
swipeVelocityThreshold: 500, swipeVelocityThreshold: 500,
keyboardDismissMode: 'on-drag', keyboardDismissMode: 'on-drag',
@@ -125,21 +129,22 @@ export default class DrawerView extends React.PureComponent<Props> {
statusBarAnimation: 'slide', statusBarAnimation: 'slide',
}; };
componentDidMount() {
if (Platform.OS === 'web') {
document?.body?.addEventListener?.('keyup', this.handleEscape);
}
}
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
const { const {
open, open,
drawerPosition, drawerPosition,
drawerType, drawerType,
gestureEnabled,
swipeDistanceThreshold, swipeDistanceThreshold,
swipeVelocityThreshold, swipeVelocityThreshold,
hideStatusBar, hideStatusBar,
} = this.props; } = this.props;
if (prevProps.gestureEnabled !== gestureEnabled) {
this.isGestureEnabled.setValue(gestureEnabled ? TRUE : FALSE);
}
if ( if (
// If we're not in the middle of a transition, sync the drawer's open state // If we're not in the middle of a transition, sync the drawer's open state
typeof this.pendingOpenValue !== 'boolean' || typeof this.pendingOpenValue !== 'boolean' ||
@@ -180,8 +185,22 @@ export default class DrawerView extends React.PureComponent<Props> {
componentWillUnmount() { componentWillUnmount() {
this.toggleStatusBar(false); this.toggleStatusBar(false);
this.handleEndInteraction(); this.handleEndInteraction();
if (Platform.OS === 'web') {
document?.body?.removeEventListener?.('keyup', this.handleEscape);
}
} }
private handleEscape = (e: KeyboardEvent) => {
const { open, onClose } = this.props;
if (e.key === 'Escape') {
if (open) {
onClose();
}
}
};
private handleEndInteraction = () => { private handleEndInteraction = () => {
if (this.interactionHandle !== undefined) { if (this.interactionHandle !== undefined) {
InteractionManager.clearInteractionHandle(this.interactionHandle); InteractionManager.clearInteractionHandle(this.interactionHandle);
@@ -201,9 +220,6 @@ export default class DrawerView extends React.PureComponent<Props> {
private isDrawerTypeFront = new Value<Binary>( private isDrawerTypeFront = new Value<Binary>(
this.props.drawerType === 'front' ? TRUE : FALSE this.props.drawerType === 'front' ? TRUE : FALSE
); );
private isGestureEnabled = new Value(
this.props.gestureEnabled ? TRUE : FALSE
);
private isOpen = new Value<Binary>(this.props.open ? TRUE : FALSE); private isOpen = new Value<Binary>(this.props.open ? TRUE : FALSE);
private nextIsOpen = new Value<Binary | -1>(UNSET); private nextIsOpen = new Value<Binary | -1>(UNSET);
@@ -532,6 +548,7 @@ export default class DrawerView extends React.PureComponent<Props> {
const { const {
open, open,
gestureEnabled, gestureEnabled,
swipeEnabled,
drawerPosition, drawerPosition,
drawerType, drawerType,
swipeEdgeWidth, swipeEdgeWidth,
@@ -544,6 +561,7 @@ export default class DrawerView extends React.PureComponent<Props> {
gestureHandlerProps, gestureHandlerProps,
} = this.props; } = this.props;
const isOpen = drawerType === 'permanent' ? true : open;
const isRight = drawerPosition === 'right'; const isRight = drawerPosition === 'right';
const contentTranslateX = drawerType === 'front' ? 0 : this.translateX; const contentTranslateX = drawerType === 'front' ? 0 : this.translateX;
@@ -569,8 +587,10 @@ export default class DrawerView extends React.PureComponent<Props> {
const hitSlop = isRight const hitSlop = isRight
? // Extend hitSlop to the side of the screen when drawer is closed ? // Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen // This lets the user drag the drawer from the side of the screen
{ right: 0, width: open ? undefined : swipeEdgeWidth } { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
: { left: 0, width: open ? undefined : swipeEdgeWidth }; : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
const progress = drawerType === 'permanent' ? ANIMATED_ONE : this.progress;
return ( return (
<PanGestureHandler <PanGestureHandler
@@ -580,62 +600,87 @@ export default class DrawerView extends React.PureComponent<Props> {
onGestureEvent={this.handleGestureEvent} onGestureEvent={this.handleGestureEvent}
onHandlerStateChange={this.handleGestureStateChange} onHandlerStateChange={this.handleGestureStateChange}
hitSlop={hitSlop} hitSlop={hitSlop}
enabled={gestureEnabled} enabled={drawerType !== 'permanent' && gestureEnabled && swipeEnabled}
{...gestureHandlerProps} {...gestureHandlerProps}
> >
<Animated.View <Animated.View
onLayout={this.handleContainerLayout} onLayout={this.handleContainerLayout}
style={styles.main} style={[
styles.main,
{
flexDirection:
drawerType === 'permanent' && !isRight ? 'row-reverse' : 'row',
},
]}
> >
<Animated.View <Animated.View
style={[ style={[
styles.content, styles.content,
{ drawerType !== 'permanent' && {
transform: [{ translateX: contentTranslateX }], transform: [{ translateX: contentTranslateX }],
}, },
sceneContainerStyle as any, sceneContainerStyle as any,
]} ]}
> >
<View <View
accessibilityElementsHidden={open} accessibilityElementsHidden={isOpen && drawerType !== 'permanent'}
importantForAccessibility={open ? 'no-hide-descendants' : 'auto'} importantForAccessibility={
isOpen && drawerType !== 'permanent'
? 'no-hide-descendants'
: 'auto'
}
style={styles.content} style={styles.content}
> >
{renderSceneContent({ progress: this.progress })} {renderSceneContent({ progress })}
</View> </View>
<TapGestureHandler {
enabled={gestureEnabled} // Disable overlay if sidebar is permanent
onHandlerStateChange={this.handleTapStateChange} drawerType === 'permanent' ? null : (
> <TapGestureHandler
<Overlay progress={this.progress} style={overlayStyle} /> enabled={gestureEnabled}
</TapGestureHandler> onHandlerStateChange={this.handleTapStateChange}
>
<Overlay progress={progress} style={overlayStyle} />
</TapGestureHandler>
)
}
</Animated.View> </Animated.View>
<Animated.Code {drawerType === 'permanent' ? null : (
exec={block([ <Animated.Code
onChange(this.manuallyTriggerSpring, [ exec={block([
cond(eq(this.manuallyTriggerSpring, TRUE), [ onChange(this.manuallyTriggerSpring, [
set(this.nextIsOpen, FALSE), cond(eq(this.manuallyTriggerSpring, TRUE), [
call([], () => (this.currentOpenValue = false)), set(this.nextIsOpen, FALSE),
call([], () => (this.currentOpenValue = false)),
]),
]), ]),
]), ])}
])} />
/> )}
<Animated.View <Animated.View
accessibilityViewIsModal={open} accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
removeClippedSubviews={Platform.OS !== 'ios'} removeClippedSubviews={Platform.OS !== 'ios'}
onLayout={this.handleDrawerLayout} onLayout={this.handleDrawerLayout}
style={[ style={[
styles.container, styles.container,
isRight ? { right: offset } : { left: offset }, drawerType === 'permanent'
{ ? // Without this, the `left`/`right` values don't get reset
transform: [{ translateX: drawerTranslateX }], isRight
opacity: this.drawerOpacity, ? { right: 0 }
zIndex: drawerType === 'back' ? -1 : 0, : { left: 0 }
}, : [
styles.nonPermanent,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
},
isRight ? { right: offset } : { left: offset },
{ zIndex: drawerType === 'back' ? -1 : 0 },
],
drawerStyle as any, drawerStyle as any,
]} ]}
> >
{renderDrawerContent({ progress: this.progress })} {renderDrawerContent({ progress })}
</Animated.View> </Animated.View>
</Animated.View> </Animated.View>
</PanGestureHandler> </PanGestureHandler>
@@ -646,11 +691,13 @@ export default class DrawerView extends React.PureComponent<Props> {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: 'white', backgroundColor: 'white',
maxWidth: '100%',
},
nonPermanent: {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
bottom: 0, bottom: 0,
width: '80%', width: '80%',
maxWidth: '100%',
}, },
content: { content: {
flex: 1, flex: 1,

View File

@@ -72,14 +72,8 @@ export default function DrawerItem(props: Props) {
labelStyle, labelStyle,
focused = false, focused = false,
activeTintColor = colors.primary, activeTintColor = colors.primary,
inactiveTintColor = Color(colors.text) inactiveTintColor = Color(colors.text).alpha(0.68).rgb().string(),
.alpha(0.68) activeBackgroundColor = Color(activeTintColor).alpha(0.12).rgb().string(),
.rgb()
.string(),
activeBackgroundColor = Color(activeTintColor)
.alpha(0.12)
.rgb()
.string(),
inactiveBackgroundColor = 'transparent', inactiveBackgroundColor = 'transparent',
style, style,
onPress, onPress,

View File

@@ -32,6 +32,7 @@ import {
DrawerNavigationHelpers, DrawerNavigationHelpers,
DrawerContentComponentProps, DrawerContentComponentProps,
} from '../types'; } from '../types';
import DrawerOpenContext from '../utils/DrawerOpenContext';
import DrawerPositionContext from '../utils/DrawerPositionContext'; import DrawerPositionContext from '../utils/DrawerPositionContext';
type Props = DrawerNavigationConfig & { type Props = DrawerNavigationConfig & {
@@ -88,15 +89,17 @@ export default function DrawerView({
sceneContainerStyle, sceneContainerStyle,
}: Props) { }: Props) {
const [loaded, setLoaded] = React.useState([state.index]); const [loaded, setLoaded] = React.useState([state.index]);
const [drawerWidth, setDrawerWidth] = React.useState(() => const [drawerWidth, setDrawerWidth] = React.useState(() => {
getDefaultDrawerWidth(Dimensions.get('window')) const { height = 0, width = 0 } = Dimensions.get('window');
);
return getDefaultDrawerWidth({ height, width });
});
const drawerGestureRef = React.useRef<PanGestureHandler>(null); const drawerGestureRef = React.useRef<PanGestureHandler>(null);
const { colors } = useTheme(); const { colors } = useTheme();
const isDrawerOpen = Boolean(state.history.find(it => it.type === 'drawer')); const isDrawerOpen = state.history.some((it) => it.type === 'drawer');
const handleDrawerOpen = React.useCallback(() => { const handleDrawerOpen = React.useCallback(() => {
navigation.dispatch({ navigation.dispatch({
@@ -197,42 +200,55 @@ export default function DrawerView({
}; };
const activeKey = state.routes[state.index].key; const activeKey = state.routes[state.index].key;
const { gestureEnabled } = descriptors[activeKey].options; const { gestureEnabled, swipeEnabled } = descriptors[activeKey].options;
return ( return (
<GestureHandlerWrapper style={styles.content}> <GestureHandlerWrapper style={styles.content}>
<SafeAreaProviderCompat> <SafeAreaProviderCompat>
<DrawerGestureContext.Provider value={drawerGestureRef}> <DrawerGestureContext.Provider value={drawerGestureRef}>
<Drawer <DrawerOpenContext.Provider value={isDrawerOpen}>
open={isDrawerOpen} <Drawer
gestureEnabled={gestureEnabled} open={isDrawerOpen}
onOpen={handleDrawerOpen} gestureEnabled={gestureEnabled}
onClose={handleDrawerClose} swipeEnabled={swipeEnabled}
onGestureRef={ref => { onOpen={handleDrawerOpen}
// @ts-ignore onClose={handleDrawerClose}
drawerGestureRef.current = ref; onGestureRef={(ref) => {
}} // @ts-ignore
gestureHandlerProps={gestureHandlerProps} drawerGestureRef.current = ref;
drawerType={drawerType} }}
drawerPosition={drawerPosition} gestureHandlerProps={gestureHandlerProps}
sceneContainerStyle={[ drawerType={drawerType}
{ backgroundColor: colors.background }, drawerPosition={drawerPosition}
sceneContainerStyle, sceneContainerStyle={[
]} { backgroundColor: colors.background },
drawerStyle={[ sceneContainerStyle,
{ width: drawerWidth, backgroundColor: colors.card }, ]}
drawerStyle, drawerStyle={[
]} { width: drawerWidth, backgroundColor: colors.card },
overlayStyle={{ backgroundColor: overlayColor }} drawerType === 'permanent' &&
swipeEdgeWidth={edgeWidth} (drawerPosition === 'left'
swipeDistanceThreshold={minSwipeDistance} ? {
hideStatusBar={hideStatusBar} borderRightColor: colors.border,
statusBarAnimation={statusBarAnimation} borderRightWidth: StyleSheet.hairlineWidth,
renderDrawerContent={renderNavigationView} }
renderSceneContent={renderContent} : {
keyboardDismissMode={keyboardDismissMode} borderLeftColor: colors.border,
drawerPostion={drawerPosition} borderLeftWidth: StyleSheet.hairlineWidth,
/> }),
drawerStyle,
]}
overlayStyle={{ backgroundColor: overlayColor }}
swipeEdgeWidth={edgeWidth}
swipeDistanceThreshold={minSwipeDistance}
hideStatusBar={hideStatusBar}
statusBarAnimation={statusBarAnimation}
renderDrawerContent={renderNavigationView}
renderSceneContent={renderContent}
keyboardDismissMode={keyboardDismissMode}
drawerPostion={drawerPosition}
/>
</DrawerOpenContext.Provider>
</DrawerGestureContext.Provider> </DrawerGestureContext.Provider>
</SafeAreaProviderCompat> </SafeAreaProviderCompat>
</GestureHandlerWrapper> </GestureHandlerWrapper>

View File

@@ -9,21 +9,29 @@ 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} />;
} }
const { isVisible, children, style, ...rest } = this.props; const { isVisible, children, style, ...rest } = this.props;
return ( return (
<View <View
style={[styles.container, style]} 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,84 @@
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.8](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.7...@react-navigation/material-bottom-tabs@5.1.8) (2020-04-08)
### Bug Fixes
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.6...@react-navigation/material-bottom-tabs@5.1.7) (2020-03-30)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.5...@react-navigation/material-bottom-tabs@5.1.6) (2020-03-23)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.4...@react-navigation/material-bottom-tabs@5.1.5) (2020-03-22)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.3...@react-navigation/material-bottom-tabs@5.1.4) (2020-03-19)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.2...@react-navigation/material-bottom-tabs@5.1.3) (2020-03-17)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.1...@react-navigation/material-bottom-tabs@5.1.2) (2020-03-16)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.0...@react-navigation/material-bottom-tabs@5.1.1) (2020-03-03)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.7...@react-navigation/material-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/material-bottom-tabs/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/issues/6756)
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.6...@react-navigation/material-bottom-tabs@5.0.7) (2020-02-21) ## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.6...@react-navigation/material-bottom-tabs@5.0.7) (2020-02-21)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-bottom-tabs", "name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper", "description": "Integration for bottom navigation component from react-native-paper",
"version": "5.0.7", "version": "5.1.8",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -35,17 +35,17 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.9.3", "@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.0.7", "@react-navigation/native": "^5.1.5",
"@types/react": "^16.9.19", "@types/react": "^16.9.23",
"@types/react-native": "^0.60.30", "@types/react-native": "^0.61.22",
"@types/react-native-vector-icons": "^6.4.5", "@types/react-native-vector-icons": "^6.4.5",
"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-paper": "^3.6.0", "react-native-paper": "^3.7.0",
"react-native-vector-icons": "^6.6.0", "react-native-vector-icons": "^6.6.0",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.5", "@react-navigation/native": "^5.0.5",

View File

@@ -11,7 +11,7 @@ export { default as MaterialBottomTabView } from './views/MaterialBottomTabView'
/** /**
* Types * Types
*/ */
export { export type {
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationProp, MaterialBottomTabNavigationProp,
} from './types'; } from './types';

View File

@@ -49,6 +49,8 @@ function MaterialBottomTabNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap,
typeof MaterialBottomTabNavigator typeof MaterialBottomTabNavigator
>(MaterialBottomTabNavigator); >(MaterialBottomTabNavigator);

View File

@@ -5,6 +5,7 @@ import {
NavigationProp, NavigationProp,
NavigationHelpers, NavigationHelpers,
TabNavigationState, TabNavigationState,
TabActionHelpers,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type MaterialBottomTabNavigationEventMap = { export type MaterialBottomTabNavigationEventMap = {
@@ -28,19 +29,8 @@ export type MaterialBottomTabNavigationProp<
TabNavigationState, TabNavigationState,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
> & { > &
/** TabActionHelpers<ParamList>;
* Jump to an existing tab.
*
* @param name Name of the route for the tab.
* @param [params] Params object for the route.
*/
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends undefined | any
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
};
export type MaterialBottomTabNavigationOptions = { export type MaterialBottomTabNavigationOptions = {
/** /**

View File

@@ -3,6 +3,84 @@
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.8](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.7...@react-navigation/material-top-tabs@5.1.8) (2020-04-08)
### Bug Fixes
* mark type exports for all packages ([b71de6c](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/commit/b71de6cc799143f1d0e8a0cfcc34f0a2381f9840))
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.6...@react-navigation/material-top-tabs@5.1.7) (2020-03-30)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.5...@react-navigation/material-top-tabs@5.1.6) (2020-03-23)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.4...@react-navigation/material-top-tabs@5.1.5) (2020-03-22)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.3...@react-navigation/material-top-tabs@5.1.4) (2020-03-19)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.2...@react-navigation/material-top-tabs@5.1.3) (2020-03-17)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.1...@react-navigation/material-top-tabs@5.1.2) (2020-03-16)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.0...@react-navigation/material-top-tabs@5.1.1) (2020-03-03)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.7...@react-navigation/material-top-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/material-top-tabs/commit/162410843c4f175ae107756de1c3af04d1d47aa7)), closes [#6756](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/issues/6756)
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.6...@react-navigation/material-top-tabs@5.0.7) (2020-02-21) ## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.6...@react-navigation/material-top-tabs@5.0.7) (2020-02-21)
**Note:** Version bump only for package @react-navigation/material-top-tabs **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-top-tabs", "name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view", "description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.0.7", "version": "5.1.8",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -38,17 +38,17 @@
"color": "^3.1.2" "color": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.9.3", "@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.0.7", "@react-navigation/native": "^5.1.5",
"@types/react": "^16.9.19", "@types/react": "^16.9.23",
"@types/react-native": "^0.60.30", "@types/react-native": "^0.61.22",
"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-gesture-handler": "^1.5.6", "react-native-gesture-handler": "^1.6.0",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.7.0",
"react-native-tab-view": "^2.13.0", "react-native-tab-view": "^2.14.0",
"typescript": "^3.7.5" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.5", "@react-navigation/native": "^5.0.5",

View File

@@ -12,7 +12,7 @@ export { default as MaterialTopTabBar } from './views/MaterialTopTabBar';
/** /**
* Types * Types
*/ */
export { export type {
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationProp, MaterialTopTabNavigationProp,
MaterialTopTabBarProps, MaterialTopTabBarProps,

View File

@@ -48,6 +48,8 @@ function MaterialTopTabNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap,
typeof MaterialTopTabNavigator typeof MaterialTopTabNavigator
>(MaterialTopTabNavigator); >(MaterialTopTabNavigator);

View File

@@ -7,6 +7,7 @@ import {
Route, Route,
NavigationProp, NavigationProp,
TabNavigationState, TabNavigationState,
TabActionHelpers,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type MaterialTopTabNavigationEventMap = { export type MaterialTopTabNavigationEventMap = {
@@ -42,19 +43,8 @@ export type MaterialTopTabNavigationProp<
TabNavigationState, TabNavigationState,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
> & { > &
/** TabActionHelpers<ParamList>;
* Jump to an existing tab.
*
* @param name Name of the route for the tab.
* @param [params] Params object for the route.
*/
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends undefined | any
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;
};
export type MaterialTopTabNavigationOptions = { export type MaterialTopTabNavigationOptions = {
/** /**

View File

@@ -14,17 +14,11 @@ export default function TabBarTop(props: MaterialTopTabBarProps) {
navigation, navigation,
descriptors, descriptors,
activeTintColor = colors.text, activeTintColor = colors.text,
inactiveTintColor = Color(activeTintColor) inactiveTintColor = Color(activeTintColor).alpha(0.5).rgb().string(),
.alpha(0.5)
.rgb()
.string(),
allowFontScaling = true, allowFontScaling = true,
showIcon = false, showIcon = false,
showLabel = true, showLabel = true,
pressColor = Color(activeTintColor) pressColor = Color(activeTintColor).alpha(0.08).rgb().string(),
.alpha(0.08)
.rgb()
.string(),
iconStyle, iconStyle,
labelStyle, labelStyle,
indicatorStyle, indicatorStyle,

View File

@@ -47,7 +47,7 @@ export default function MaterialTopTabView({
return ( return (
<TabView <TabView
{...rest} {...rest}
onIndexChange={index => onIndexChange={(index) =>
navigation.dispatch({ navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name), ...TabActions.jumpTo(state.routes[index].name),
target: state.key, target: state.key,

View File

@@ -3,6 +3,84 @@
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.5](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.4...@react-navigation/native@5.1.5) (2020-04-08)
**Note:** Version bump only for package @react-navigation/native
## [5.1.4](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.3...@react-navigation/native@5.1.4) (2020-03-30)
**Note:** Version bump only for package @react-navigation/native
## [5.1.3](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.2...@react-navigation/native@5.1.3) (2020-03-23)
### Bug Fixes
* add info about android launchMode in useLinking error ([d94e43c](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/d94e43c3c8625b209a5c883b8cb560496d07fda7))
## [5.1.2](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.1...@react-navigation/native@5.1.2) (2020-03-22)
**Note:** Version bump only for package @react-navigation/native
## [5.1.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.0...@react-navigation/native@5.1.1) (2020-03-19)
**Note:** Version bump only for package @react-navigation/native
# [5.1.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.0.10...@react-navigation/native@5.1.0) (2020-03-17)
### Features
* add permanent drawer type ([#7818](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/7818)) ([6a5d0a0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/6a5d0a035afae60d91aef78401ec8826295746fe))
## [5.0.10](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.0.9...@react-navigation/native@5.0.10) (2020-03-16)
**Note:** Version bump only for package @react-navigation/native
## [5.0.9](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.0.8...@react-navigation/native@5.0.9) (2020-03-03)
**Note:** Version bump only for package @react-navigation/native
## [5.0.8](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.0.7...@react-navigation/native@5.0.8) (2020-02-26)
**Note:** Version bump only for package @react-navigation/native
## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.0.6...@react-navigation/native@5.0.7) (2020-02-21) ## [5.0.7](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.0.6...@react-navigation/native@5.0.7) (2020-02-21)
**Note:** Version bump only for package @react-navigation/native **Note:** Version bump only for package @react-navigation/native

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