Compare commits

..

91 Commits

Author SHA1 Message Date
Brent Vatne
b9d55a6330 Release 1.5.2 2018-03-12 11:12:31 -07:00
Brent Vatne
315e43701b Fix tab icon height on horizontal / ipad 2018-03-12 11:12:22 -07:00
Hugo Dozois
1d573bc246 Fix isFocused from withNavigationFocus is a function (#3710)
Fixes #3709
2018-03-11 22:26:33 +01:00
Eric Vicenti
3bfb0b90d0 Fix onTransitionEnd props passthrough
Fixes #3647
2018-03-10 00:09:02 -08:00
Brent Vatne
8a129afe13 Release 1.5.1 2018-03-09 11:33:06 -08:00
Brent Vatne
ab2a63fe92 Update snapshot for tab icon changes 2018-03-09 11:32:53 -08:00
Brandon Smith
c411210ecc Pass initialRouteKey into StackRouter (#3540) (#3701) 2018-03-09 11:31:41 -08:00
Brent Vatne
01e7296520 Release 1.5.0 2018-03-07 17:42:38 -08:00
corupta
8f3e0997c5 Add activeLabelStyle and inactiveLabelStyle for DrawerItem (#3559)
* Add activeLabelStyle and inactiveLabelStyle

Adding activeLabelStyle, so that active items can be customized more.
My use case: Change font of the active item to bold.
Also, added inactiveLabelStyle which can be used for a similar purpose.

* prettier fix

* Update react-navigation.js

* prettier fix

* Update jest snapshot for DrawerNavigator - for adding a new style property to the styles array
2018-03-07 17:38:02 -08:00
Nicolas Beck
3f3ef6485c Add initialRouteKey for StackRouter (#3540)
* use initialRouteName as key when initializing StackRouter

* fix null headerLeft

* merge back

* fixed tests

* use config flag

* fixed snapshots

* implemented requested changes
2018-03-07 17:38:02 -08:00
Vishwesh Jainkuniya
b12abb553f Fix: tabBar icons are not visible. (#3650)
* Fix: tabBar icons are not visible.

* Fix: tests.
2018-03-07 17:38:02 -08:00
Ashoat Tevosyan
e02841a979 [Flow] Some updates, mostly from flow-typed (#3682)
1. Remove `NavigationComponent` from `NavigationScreenRouteConfig`. The only context `NavigationScreenRouteConfig` is used in is as an intersection with an object, and as such the only relevant portions of `NavigationScreenRouteConfig` are the object parts.
2. Add static `HEIGHT` variable to `Header` type.
3. In `NavigationContainerProps`, make `onNavigationStateChange` property value nullable.

PS: if in the future you guys would prefer that I separate these sort of PRs into their constituent parts, let me know.
2018-03-07 18:21:15 -05:00
Ben Styles
e147f34555 Allow passing null to onNavigationStateChange prop (#3683)
As seen here: react-navigation/react-navigation#360, we need to be able to pass null to turn off logging.
2018-03-07 18:20:58 -05:00
Sirui Li
81e0ce136e Flow type: SafeAreaView doesn't require children (#3670)
`children` prop should be optional.
2018-03-07 18:20:44 -05:00
Edward Drapkin
8ba727c2cf More specific injected Navigation props (#3645) 2018-03-07 18:19:37 -05:00
Yordis Prieto
9a86ef8362 Fix typespec of back action creator (#3659) 2018-03-07 18:19:26 -05:00
Ashoat Tevosyan
4fe7c92847 Fix NavigationEventPayload.lastState type (#3664)
Should be nullable, since it is initially called as `null` in `src/createNavigationContainer.js` (and in `react-navigation-redux-helpers`, where it is causing a Flow error)
2018-03-07 18:19:10 -05:00
Leon Miller-Out
afecaaed7f Call react-navigation-redux-helpers's initializeListeners() when mounting app. (#3666)
This makes the first navigation click work correctly, per
https://github.com/react-navigation/react-navigation-redux-helpers/issues/7
2018-03-05 15:02:34 -05:00
Brent Vatne
6373b802dd Release 1.4.0 2018-03-03 09:59:22 -08:00
Brent Vatne
138151433d Add isFocused helper to navigation and fix withNavigationFocus (1.x branch) (#3651)
* Add isFocused helper to navigation and fix withNavigationFocus accordingly

* Fix snapshots

* Make flow pass on TabsWithNavigationFocus example
2018-03-03 09:58:19 -08:00
Brent Vatne
2744cb32b7 Cache child event subscriptions (1.x branch) (#3649)
* Cache child event subscriptions on StackNavigator on 1.x branch

* Cache child event subscribers on DrawerView and withCachedChildNavigation
2018-03-03 09:01:24 -08:00
Brent Vatne
439b4222ce Release 1.3.2 2018-03-02 13:36:47 -08:00
Brent Vatne
ba0b1861e5 Use SceneView with SwitchNavigator to thread context through. Fixes #3646 2018-03-02 13:35:56 -08:00
Brent Vatne
318788ca60 Release 1.3.1 2018-03-02 12:19:05 -08:00
Brent Vatne
498a39c200 Fix regression in error message for route config validation 2018-03-02 12:18:57 -08:00
Brent Vatne
b31ebef5b0 Release 1.3.0 2018-03-01 13:44:22 -08:00
Arseny Yankovsky
f4fe588e08 Allow modification of SafeAreaView props (#3496)
* SafeAreaView fix

* Updated to only allow modification of forceInset property of SafeAreaView
2018-03-01 13:44:07 -08:00
Brent Vatne
403af82c3f Add SwitchNavigator (#3634)
* Add SwitchNavigator

* Fix type definition

* Update snapshot

* Add SwitchNavigator test coverage
2018-03-01 13:43:47 -08:00
Brent Vatne
0c2360dc36 Clarify that people should not report Redux or MobX related integration issues here 2018-02-26 15:52:31 -08:00
Brent Vatne
5f32a48c16 Release 1.2.1 2018-02-26 14:04:44 -08:00
Brent Vatne
d5a0b5912b Apply StyleSheet.flatten properly to headerStyle. Fixes #3608 2018-02-26 14:04:36 -08:00
Yordis Prieto
13fd8acb20 fix typespec (#3583) 2018-02-26 12:25:13 -08:00
Serhii Palash
8fc8f3b8f7 Fix bug #3279 “Action within RESET doesn't trigger” (#3593)
https://github.com/react-navigation/react-navigation/issues/3279
2018-02-26 12:14:29 -08:00
Brent Vatne
a37473c5e4 Release 1.2.0 2018-02-25 17:46:44 -08:00
Brent Vatne
23eb0839cb Improve crossfade configuration for header 2018-02-25 17:44:16 -08:00
Brent Vatne
e84473db78 Use a closer-to-correct spring animation when releasing a gesture in CardStack (#3601) 2018-02-25 17:20:39 -08:00
Brent Vatne
c8531d0f38 Pop to top on TabNavigator when tapping already focused tab icon (#3589)
* Pop to top when tapping the tab icon for an already focused tab

* Dispatch popToTop with key to scope the action properly

* Add test for POP_TO_TOP with key
2018-02-25 16:33:20 -08:00
Vojtech Novak
276fb8f7b3 remove support for deprecated navigation actions (#3595)
* remove support for deprecated navigation actions

* remove deprecated action from flow definitions
2018-02-24 12:08:14 -08:00
Johan Ruokangas
ab758bcaaa Fix RFC link (#3594) 2018-02-24 12:50:31 +01:00
Matt Hamil
2bb91a6740 Updated README (#3567)
Changed wording in the Web Support section of README to reflect priority for previous 1.0 release.
2018-02-22 21:52:09 -08:00
Brent Vatne
ffa3a92534 First crack at publish from circle (#3574)
* First crack at publish from circle

* s/publish/deploy

* Update snapshots

* Try installing with yarn

* Nice one

* Shhh

* Comment some things out and add a webhook

* Add proper webhook url and add back build steps
2018-02-22 16:39:41 -08:00
Brent Vatne
6dda8c30a9 Fix colors for card and header border on iOS 2018-02-22 12:58:33 -08:00
Brent Vatne
065fdf61d8 Several improvements to StackNavigation Header (#3568)
* Refactor to remove unused variables, styles, and outer Animated view

* Style cleanup

* Proof of concept blur background

* Clean it up and add flow interface

* Update snapshots
2018-02-21 18:29:43 -08:00
Brent Vatne
371a714b57 Release 1.1.2 2018-02-20 16:00:44 -08:00
Ron Arts
1d33b95c5f Bump react-native-safe-area-view dep to 0.7.0 for react-native-web support (#3553) 2018-02-20 15:44:20 -08:00
Mike Grabowski
593bc8a648 Improve Header performance a bit (#3556)
* Improve `Header` performance a bit

I have been investigating `<Header />` component performance once again today with `render` cycles in particular. I have observed that during `push` phase, a typical Header in NavigationPlayground re-renders 3 times on iOS and 2 times on Android.

The first time is obvious on both platforms since that's when we add a new scene to an array. Second on iOS was also self-explanatory - we measure title width and other layout parameters in order to provide a better animation. 

What got me thinking was the last render cycle that didn't have a clear origin. After digging around I've found it is caused by a `scenes` array changing when transition finishes. What is surprising, it's just the reference to the object that changes, the content (items inside) stay the same.

I have found out that this is caused by `Transitioner` looping through an array of scenes attempting to remove stale scenes. Since scene becomes stale when you `pop` from it, this is obviously a noop when `pushing` a new route. That, obviously, causes an extra render cycle since `filter` produces same, but with a different pointer, object.

* Update Transitioner.js

* Update Transitioner.js
2018-02-20 15:44:10 -08:00
Brent Vatne
174a6e4175 Release 1.1.1 2018-02-19 18:18:38 -08:00
Brent Vatne
af991e5512 Release 1.1.0 2018-02-19 18:16:23 -08:00
Brent Vatne
0b0e9e9df5 Fix TabRouter to support shorthand route config 2018-02-19 18:16:23 -08:00
Brent Vatne
42b0ccca79 Release 1.1.0-rc.5 2018-02-19 18:16:23 -08:00
Brent Vatne
da6a960bb1 Fix regression in modular back button 2018-02-19 18:16:23 -08:00
Ron Arts
3ca47ec778 Fix react-native-web support for #3526 (#3546)
* MaskedViewIOS use broke react-native-web support, this fixes it.

* Fallback more gracefully.

* Actually return the value ...
2018-02-19 13:48:12 +01:00
Marcin Raburski
14ee56a20d getComponentForState made more generic (#2498) 2018-02-19 04:14:13 -08:00
Matthieu Lemoine
9d36daf48e Pass prevTransitionsProps to transitionConfig (#3304)
* Pass prevTransitionsProps to transitionConfig

* Remove flow type
2018-02-19 02:29:46 -08:00
Brent Vatne
055fdb2ffc Release 1.1.0-rc.4 2018-02-17 12:50:26 -08:00
Brent Vatne
b3bf80634d Move the back button handler subscription to the NavigationContainer constructor (#3542)
* Move the back button handler subscription to the constructor to fix precedence
- See 07d92947a1/src/Navigation.js (L190-L193)

* Return the boolean result of dispatch in global back handler
2018-02-17 12:50:04 -08:00
Brent Vatne
fafd0e8702 Cleanup around StackRouter (#3487)
* Make StackRouter a bit easier to read, update and add some comments, add an explicit test around stack and navigate

* Address feedback for clarity
2018-02-17 11:14:03 -08:00
Brent Vatne
d94045817c Release 1.1.0-rc.3 2018-02-17 09:42:45 -08:00
Brent Vatne
f0acaddf05 Fix headerLeft on first screen regression 2018-02-17 09:40:59 -08:00
Brent Vatne
9beb32ecac Update snapshots 2018-02-16 18:14:07 -08:00
Brent Vatne
29a29936bf Remove withNavigationFocus example until it's fixed 2018-02-16 18:10:33 -08:00
Brent Vatne
8961fef00b Bump version 2018-02-16 18:09:40 -08:00
Brent Vatne
021c7e54be Do not use hide views when swipe or animations are enabled. Improve withNavigationFocus example 2018-02-16 18:05:39 -08:00
Brent Vatne
27538cad94 Properly thread through childNavigation 2018-02-16 17:26:01 -08:00
Brent Vatne
9a8a50ef1d Release 1.1.0-rc.0 2018-02-16 17:07:28 -08:00
Brent Vatne
2c6dc35723 Revert "Revert "Add getParam navigation helper (#3510)" (#3532)"
This reverts commit 5febb81a1c.
2018-02-16 17:06:16 -08:00
Brent Vatne
3600ecbd9b Add back support for lazy tabs and use removeClippedSubviews (#3538)
* Lazy initialization of tabs and move contents off-screen when not active

* Make subview clipping and lazy both configurable

* Record snapshots again

* Update type definition

* Remove unused log
2018-02-16 16:59:19 -08:00
Sébastien Lorber
c74f001b1c add withNavigationFocus HOC (#3512)
* add withNavigationFocus HOC

See:
- https://github.com/react-navigation/react-navigation/issues/51#issuecomment-276003658
- https://github.com/react-navigation/react-navigation/issues/51#issuecomment-278761705
- https://github.com/react-navigation/react-navigation/pull/3345#issuecomment-360260067

* typos

* remove unused import

* Add withNavigationFocus export

* add example TabsWithNavigationFocus

* add example TabsWithNavigationFocus

* withNavigationFocus: get navigation from context or props

* subs => subscriptions

* fix flow issues
2018-02-16 16:57:33 -08:00
Brent Vatne
7f4706e4cc Update snapshots 2018-02-16 12:45:57 -08:00
Brent Vatne
d0ef33b12f Several small changes related to header style commit 2018-02-16 12:42:44 -08:00
Brent Vatne
3c3668c952 Header transition presets with support for standard iOS transition style (#3526)
Header transition presets with approximate support for UIKit transition style
2018-02-16 12:41:59 -08:00
Brent Vatne
5febb81a1c Revert "Add getParam navigation helper (#3510)" (#3532)
This reverts commit 50dcb37cd7.
2018-02-15 13:24:40 -08:00
Peter Piekarczyk
50dcb37cd7 Add getParam navigation helper (#3510)
* add getParam helper

* passing tests

* check for existence of param instead of just or

* fix spacing

* use in instead of checking for null

* add test for null
2018-02-15 10:14:08 -08:00
Ron Arts
69bca191a7 The Linking and BackHandler components are now both supported by react-native-web (#3494)
so there's no need to supply dummy replacements.
2018-02-13 12:23:26 -08:00
Sébastien Lorber
4df9198979 fix doc link (#3513) 2018-02-13 10:30:20 -08:00
Ashoat Tevosyan
0fc7f87173 Run Prettier on Flow libdef on precommit (#3495) 2018-02-12 14:45:50 -08:00
Brent Vatne
5b4d11ab5d Better orientation layout on playground 2018-02-12 11:53:23 -08:00
Brent Vatne
4d1cc285b9 Slightly improved margins on the iOS header back button 2018-02-09 21:19:02 -08:00
Brent Vatne
41725afa8a Use the correct iOS back icon 2018-02-09 20:08:56 -08:00
Brent Vatne
58b77d44ae Make Router({ RouteName: Component }) a valid way to configure a router (#3486)
* Make Router({routeName: Component}) a valid way to instantiate a route

* Update App.js in NavigationPlayground

* Fix route config flow type
2018-02-09 18:20:01 -08:00
Kazato Sugimoto
1d49b6e3fe Decode URI encoded string of deep link (#3455) 2018-02-09 16:26:20 -08:00
Brent Vatne
58b8a08af6 Update snapshots for TabView/TabNavigator 2018-02-09 14:22:31 -08:00
Maxime Florent Fankam
b11ea8a1d5 Allow swipeEnabled be a function (#3378)
* Allow swipeEnabled be a function with state param
2018-02-09 14:04:17 -08:00
Brent Vatne
3c2b977eca Number of lines in label for TabBarBottom is always 1 2018-02-09 13:51:04 -08:00
Brent Vatne
95565487ec Make it possible to go back to menu from key example and improve banner on menu screen (#3479)
* Make it possible to go back to menu from key example and improve banner on menu screen

* Fix NavigationPlayground flow error
2018-02-08 19:10:54 -08:00
Brent Vatne
a26d2acfca Bump to patch version 1.0.3 2018-02-08 18:24:20 -08:00
Brent Vatne
a1b3d2213d No need to have a conditional around slicing the routes array 2018-02-08 18:22:02 -08:00
Brent Vatne
eb39df507e Prevent navigation from getting in bad state when navigating back to route by key (#3478) 2018-02-08 18:20:14 -08:00
Brent Vatne
cca06bb530 Do not use contentInsetAdjustmentBehavior on iOS in ModalStack example 2018-02-08 16:14:45 -08:00
Eric Vicenti
2187d8fe66 Consistent treatment of route keys (#3474)
This problem was found and fixed by @matthargett and @jayphelps in #3397. I’m just rebasing and cleaning a few things up
2018-02-08 15:28:27 -08:00
Eric Vicenti
67f98b69eb Simpler implementation of withNavigation (#3476)
This will allow for refs with onRef (fixes #3461), and will avoid all these warnings from throwing during our tests
2018-02-08 14:20:52 -08:00
79 changed files with 3557 additions and 1195 deletions

View File

@@ -15,9 +15,26 @@ jobs:
- v3-react-navigation-master
- run: yarn # install root deps
- run: ./scripts/test.sh # run tests
- setup_remote_docker
- deploy:
command: |
set -x
VER="17.03.0-ce"
curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz
tar -xz -C /tmp -f /tmp/docker-$VER.tgz
mv /tmp/docker/* /usr/bin
yarn global add exp
set +x
exp login -u "$EXPO_USERNAME" -p "$EXPO_PASSWORD"
set -x
cd examples/NavigationPlayground && yarn && exp publish --release-channel "${CIRCLE_SHA1}"
- save_cache:
key: v3-react-navigation-{{ .Branch }}-{{ checksum "package.json" }}
paths:
- ~/.cache/yarn
- ~/react-navigation/examples/NavigationPlayground/node_modules
- ~/react-navigation/examples/ReduxExample/node_modules
notify:
webhooks:
- url: https://react-navigation-ci.now.sh

View File

@@ -9,6 +9,8 @@ If you have a question, feature request, or an idea for improving the library or
- [Get help on Discord chat (#react-navigation on Reactiflux)](https://discord.gg/4xEK3nD) or [on StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation)
- Search for your issue - it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue.
Bugs with react-navigation must be reproducible *without any external libraries that operate on it*. This means that if you are attempting to use Redux or MobX with it and you think you have found a bug, you must be able to reproduce it without Redux or MobX in this report. Redux related issues belong in [react-navigation-redux-helpers](https://github.com/react-navigation/react-navigation-redux-helpers), and we do not have any first-class integration with MobX at the moment.
---
### Current Behavior

View File

@@ -45,8 +45,8 @@ This library is a community effort: it can only be great if we all help out in o
* Creating public example repositories or [Snacks](https://snack.expo.io/) of navigation problems you have solved and sharing the links in [Community Resources](https://github.com/react-navigation/react-navigation/blob/master/COMMUNITY_RESOURCES.md).
* Answering questions on [Stack Overflow](https://stackoverflow.com/search?q=react-navigation).
* Answering questions in our [Reactiflux](https://www.reactiflux.com/) channel.
* Providing feedback on the open [PRs](https://github.com/react-community/react-navigation/pulls).
* Providing feedback on the open [RFCs](https://github.com/react-community/rfcs/pulls).
* Providing feedback on the open [PRs](https://github.com/react-navigation/react-navigation/pulls).
* Providing feedback on the open [RFCs](https://github.com/react-navigation/rfcs).
* Improving the [website](https://github.com/react-navigation/react-navigation.github.io).
If you would like to submit a pull request, please follow the [Contributors guide](https://reactnavigation.org/docs/contributing.html) to find out how. If you don't know where to start, check the ones with the label [`good first issue`](https://github.com/react-community/react-navigation/labels/good%20first%20issue) - even [fixing a typo in the documentation](https://github.com/react-community/react-navigation/pull/2727) is a worthy contribution!
@@ -57,7 +57,7 @@ Certainly not! There other libraries - which, depending on your needs, can be be
#### Can I use this library for web?
Currently this is [not a priority](https://github.com/react-community/react-navigation/issues/2585#issuecomment-330338793), but the architecture of this library allows for it (and it has worked in the past). If you would like to lead this charge, please reach out with your ideas for how to move forward on the [RFCs repository](https://github.com/react-navigation/rfcs) and we would be happy to discuss.
Web support was [not a priority for the 1.0 release](https://github.com/react-community/react-navigation/issues/2585#issuecomment-330338793), but the architecture of this library allows for it (and it has worked in the past). If you would like to lead this charge, please reach out with your ideas for how to move forward on the [RFCs repository](https://github.com/react-navigation/rfcs) and we would be happy to discuss.
## Code of conduct

View File

@@ -65,11 +65,13 @@ module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
suppress_type=$FlowIgnore
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy

View File

@@ -1,11 +1,13 @@
/* @flow */
import React from 'react';
import { Constants, ScreenOrientation } from 'expo';
import { Asset, Constants, ScreenOrientation } from 'expo';
ScreenOrientation.allow(ScreenOrientation.Orientation.ALL);
import {
Animated,
Image,
Platform,
ScrollView,
StyleSheet,
@@ -16,7 +18,6 @@ import {
} from 'react-native';
import { SafeAreaView, StackNavigator } from 'react-navigation';
import Banner from './Banner';
import CustomTabs from './CustomTabs';
import CustomTransitioner from './CustomTransitioner';
import Drawer from './Drawer';
@@ -24,16 +25,25 @@ import MultipleDrawer from './MultipleDrawer';
import TabsInDrawer from './TabsInDrawer';
import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs';
import SwitchWithStacks from './SwitchWithStacks';
import StacksOverTabs from './StacksOverTabs';
import StacksWithKeys from './StacksWithKeys';
import SimpleStack from './SimpleStack';
import StackWithHeaderPreset from './StackWithHeaderPreset';
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
import SimpleTabs from './SimpleTabs';
import TabAnimations from './TabAnimations';
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
const ExampleInfo = {
SimpleStack: {
name: 'Stack Example',
description: 'A card stack',
},
SwitchWithStacks: {
name: 'Switch Example',
description: 'A switch with stacks inside',
},
SimpleTabs: {
name: 'Tabs Example',
description: 'Tabs following platform conventions',
@@ -42,6 +52,18 @@ const ExampleInfo = {
name: 'Drawer Example',
description: 'Android-style drawer navigation',
},
StackWithHeaderPreset: {
name: 'UIKit-style Header Transitions',
description: 'Masked back button and sliding header items. iOS only.',
},
StackWithHeaderPreset: {
name: 'UIKit-style Header Transitions',
description: 'Masked back button and sliding header items. iOS only.',
},
StackWithTranslucentHeader: {
name: 'Translucent Header',
description: 'Render arbitrary translucent content in header background.',
},
// MultipleDrawer: {
// name: 'Multiple Drawer Example',
// description: 'Add any drawer you need',
@@ -76,6 +98,10 @@ const ExampleInfo = {
name: 'Stacks over Tabs',
description: 'Nested stack navigation that pushes on top of tabs',
},
StacksWithKeys: {
name: 'Link in Stack with keys',
description: 'Use keys to link between screens',
},
LinkStack: {
name: 'Link in Stack',
description: 'Deep linking into a route in stack',
@@ -88,38 +114,29 @@ const ExampleInfo = {
name: 'Animated Tabs Example',
description: 'Tab transitions have custom animations',
},
TabsWithNavigationFocus: {
name: 'withNavigationFocus',
description: 'Receive the focus prop to know when a screen is focused',
},
};
const ExampleRoutes = {
SimpleStack: {
screen: SimpleStack,
},
SimpleTabs: {
screen: SimpleTabs,
},
Drawer: {
screen: Drawer,
},
SimpleStack,
SwitchWithStacks,
SimpleTabs,
Drawer,
// MultipleDrawer: {
// screen: MultipleDrawer,
// },
TabsInDrawer: {
screen: TabsInDrawer,
},
CustomTabs: {
screen: CustomTabs,
},
CustomTransitioner: {
screen: CustomTransitioner,
},
ModalStack: {
screen: ModalStack,
},
StacksInTabs: {
screen: StacksInTabs,
},
StacksOverTabs: {
screen: StacksOverTabs,
},
StackWithHeaderPreset,
StackWithTranslucentHeader,
TabsInDrawer,
CustomTabs,
CustomTransitioner,
ModalStack,
StacksWithKeys,
StacksInTabs,
StacksOverTabs,
LinkStack: {
screen: SimpleStack,
path: 'people/Jordan',
@@ -128,48 +145,148 @@ const ExampleRoutes = {
screen: SimpleTabs,
path: 'settings',
},
TabAnimations: {
screen: TabAnimations,
},
TabAnimations,
TabsWithNavigationFocus,
};
class MainScreen extends React.Component<*> {
type State = {
scrollY: Animated.Value,
};
class MainScreen extends React.Component<any, State> {
state = {
scrollY: new Animated.Value(0),
};
componentWillMount() {
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon-mask.png')
).downloadAsync();
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon.png')
).downloadAsync();
}
render() {
const { navigation } = this.props;
const scale = this.state.scrollY.interpolate({
inputRange: [-450, 0, 100],
outputRange: [2, 1, 0.8],
extrapolate: 'clamp',
});
const translateY = this.state.scrollY.interpolate({
inputRange: [-450, 0, 100],
outputRange: [-150, 0, 40],
});
const opacity = this.state.scrollY.interpolate({
inputRange: [0, 50],
outputRange: [1, 0],
extrapolate: 'clamp',
});
const underlayOpacity = this.state.scrollY.interpolate({
inputRange: [0, 50],
outputRange: [0, 1],
extrapolate: 'clamp',
});
const backgroundScale = this.state.scrollY.interpolate({
inputRange: [-450, 0],
outputRange: [3, 1],
extrapolate: 'clamp',
});
const backgroundTranslateY = this.state.scrollY.interpolate({
inputRange: [-450, 0],
outputRange: [0, 0],
});
return (
<View style={{ flex: 1 }}>
<ScrollView style={{ flex: 1 }}>
<Banner />
{Object.keys(ExampleRoutes).map((routeName: string) => (
<TouchableOpacity
key={routeName}
onPress={() => {
const { path, params, screen } = ExampleRoutes[routeName];
const { router } = screen;
const action =
path && router.getActionForPathAndParams(path, params);
navigation.navigate(routeName, {}, action);
}}
<Animated.ScrollView
style={{ flex: 1 }}
scrollEventThrottle={1}
onScroll={Animated.event(
[
{
nativeEvent: { contentOffset: { y: this.state.scrollY } },
},
],
{ useNativeDriver: true }
)}
>
<Animated.View
style={[
styles.backgroundUnderlay,
{
transform: [
{ scale: backgroundScale },
{ translateY: backgroundTranslateY },
],
},
]}
/>
<Animated.View
style={{ opacity, transform: [{ scale }, { translateY }] }}
>
<SafeAreaView
style={styles.bannerContainer}
forceInset={{ top: 'always', bottom: 'never' }}
>
<SafeAreaView
style={styles.itemContainer}
forceInset={{ vertical: 'never' }}
>
<View style={styles.item}>
<Text style={styles.title}>
{ExampleInfo[routeName].name}
</Text>
<Text style={styles.description}>
{ExampleInfo[routeName].description}
</Text>
</View>
</SafeAreaView>
</TouchableOpacity>
))}
</ScrollView>
<View style={styles.banner}>
<Image
source={require('./assets/NavLogo.png')}
style={styles.bannerImage}
/>
<Text style={styles.bannerTitle}>
React Navigation Examples
</Text>
</View>
</SafeAreaView>
</Animated.View>
<SafeAreaView forceInset={{ bottom: 'always', horizontal: 'never' }}>
<View style={{ backgroundColor: '#fff' }}>
{Object.keys(ExampleRoutes).map((routeName: string) => (
<TouchableOpacity
key={routeName}
onPress={() => {
let route = ExampleRoutes[routeName];
if (route.screen || route.path || route.params) {
const { path, params, screen } = route;
const { router } = screen;
const action =
path && router.getActionForPathAndParams(path, params);
navigation.navigate(routeName, {}, action);
} else {
navigation.navigate(routeName);
}
}}
>
<SafeAreaView
style={styles.itemContainer}
forceInset={{ veritcal: 'never', bottom: 'never' }}
>
<View style={styles.item}>
<Text style={styles.title}>
{ExampleInfo[routeName].name}
</Text>
<Text style={styles.description}>
{ExampleInfo[routeName].description}
</Text>
</View>
</SafeAreaView>
</TouchableOpacity>
))}
</View>
</SafeAreaView>
</Animated.ScrollView>
<StatusBar barStyle="light-content" />
<View style={styles.statusBarUnderlay} />
<Animated.View
style={[styles.statusBarUnderlay, { opacity: underlayOpacity }]}
/>
</View>
);
}
@@ -230,4 +347,35 @@ const styles = StyleSheet.create({
fontSize: 13,
color: '#999',
},
backgroundUnderlay: {
backgroundColor: '#673ab7',
position: 'absolute',
top: -100,
height: 300,
left: 0,
right: 0,
},
bannerContainer: {
// backgroundColor: '#673ab7',
alignItems: 'center',
},
banner: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
bannerImage: {
width: 36,
height: 36,
resizeMode: 'contain',
tintColor: '#fff',
margin: 8,
},
bannerTitle: {
fontSize: 18,
fontWeight: '200',
color: '#fff',
marginVertical: 8,
marginRight: 5,
},
});

View File

@@ -8,7 +8,7 @@ import { SafeAreaView, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<ScrollView>
<SafeAreaView
forceInset={{
top: navigation.state.routeName === 'HeaderTest' ? 'always' : 'never',

View File

@@ -19,14 +19,12 @@ type MyNavScreenProps = {
class MyBackButton extends React.Component<any, any> {
render() {
return (
<Button onPress={this._navigateBack} title="Custom Back" />
);
return <Button onPress={this._navigateBack} title="Custom Back" />;
}
_navigateBack = () => {
this.props.navigation.goBack(null);
}
};
}
const MyBackButtonWithNavigation = withNavigation(MyBackButton);
@@ -108,7 +106,7 @@ type MyPhotosScreenProps = {
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
headerLeft: <MyBackButtonWithNavigation />
headerLeft: <MyBackButtonWithNavigation />,
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;

View File

@@ -163,6 +163,8 @@ const SimpleTabs = TabNavigator(
},
},
{
lazy: true,
removeClippedSubviews: true,
tabBarOptions: {
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
},

View File

@@ -0,0 +1,68 @@
/**
* @flow
*/
import type { NavigationScreenProp } from 'react-navigation';
import * as React from 'react';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
type NavScreenProps = {
navigation: NavigationScreenProp<*>,
};
class HomeScreen extends React.Component<NavScreenProps> {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
class OtherScreen extends React.Component<NavScreenProps> {
static navigationOptions = {
title: 'Your title here',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
const StackWithHeaderPreset = StackNavigator(
{
Home: HomeScreen,
Other: OtherScreen,
},
{
headerTransitionPreset: 'uikit',
}
);
export default StackWithHeaderPreset;

View File

@@ -0,0 +1,234 @@
/**
* @flow
*/
import type {
NavigationScreenProp,
NavigationEventSubscription,
} from 'react-navigation';
import { isIphoneX } from 'react-native-iphone-x-helper';
import * as React from 'react';
import { BlurView, Constants } from 'expo';
import {
Button,
Dimensions,
Platform,
ScrollView,
StatusBar,
View,
} from 'react-native';
import { Header, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
type MyNavScreenProps = {
navigation: NavigationScreenProp<*>,
banner: React.Node,
};
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
return (
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Navigate to a photos screen"
/>
<Button
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</ScrollView>
);
}
// Inset to compensate for navigation bar being transparent.
// And improved abstraction for this will be built in to react-navigation
// at some point.
getHeaderInset() {
const NOTCH_HEIGHT = isIphoneX() ? 25 : 0;
// $FlowIgnore: we will remove the HEIGHT static soon enough
const BASE_HEADER_HEIGHT = Header.HEIGHT;
const HEADER_HEIGHT =
Platform.OS === 'ios'
? BASE_HEADER_HEIGHT + NOTCH_HEIGHT
: BASE_HEADER_HEIGHT + Constants.statusBarHeight;
return Platform.select({
ios: {
contentInset: { top: HEADER_HEIGHT },
contentOffset: { y: -HEADER_HEIGHT },
},
android: {
contentContainerStyle: {
paddingTop: HEADER_HEIGHT,
},
},
});
}
}
type MyHomeScreenProps = {
navigation: NavigationScreenProp<*>,
};
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
static navigationOptions = {
title: 'Welcome',
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;
_s2: NavigationEventSubscription;
_s3: NavigationEventSubscription;
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus HomeScreen', a);
};
_onDF = a => {
console.log('_didFocus HomeScreen', a);
};
_onWB = a => {
console.log('_willBlur HomeScreen', a);
};
_onDB = a => {
console.log('_didBlur HomeScreen', a);
};
render() {
const { navigation } = this.props;
return <MyNavScreen banner="Home Screen" navigation={navigation} />;
}
}
type MyPhotosScreenProps = {
navigation: NavigationScreenProp<*>,
};
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;
_s2: NavigationEventSubscription;
_s3: NavigationEventSubscription;
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus PhotosScreen', a);
};
_onDF = a => {
console.log('_didFocus PhotosScreen', a);
};
_onWB = a => {
console.log('_willBlur PhotosScreen', a);
};
_onDB = a => {
console.log('_didBlur PhotosScreen', a);
};
render() {
const { navigation } = this.props;
return (
<MyNavScreen
banner={`${navigation.state.params.name}'s Photos`}
navigation={navigation}
/>
);
}
}
const MyProfileScreen = ({ navigation }) => (
<MyNavScreen
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
navigation.state.params.name
}'s Profile`}
navigation={navigation}
/>
);
MyProfileScreen.navigationOptions = props => {
const { navigation } = props;
const { state, setParams } = navigation;
const { params } = state;
return {
headerBackImage: params.headerBackImage,
headerTitle: `${params.name}'s Profile!`,
// Render a button on the right side of the header.
// When pressed switches the screen to edit mode.
headerRight: (
<Button
title={params.mode === 'edit' ? 'Done' : 'Edit'}
onPress={() =>
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
}
/>
),
};
};
const StackWithTranslucentHeader = StackNavigator(
{
Home: {
screen: MyHomeScreen,
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
},
{
headerTransitionPreset: 'uikit',
navigationOptions: {
headerTransparent: true,
headerBackground: Platform.select({
ios: <BlurView style={{ flex: 1 }} intensity={98} />,
android: (
<View style={{ flex: 1, backgroundColor: 'rgba(255,255,255,0.7)' }} />
),
}),
},
}
);
export default StackWithTranslucentHeader;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Button, StatusBar, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
class HomeScreen extends React.Component<any, any> {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home</Text>
<Button
title="Navigate to 'Profile' with key 'A'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Profile',
key: 'A',
params: { homeKey: this.props.navigation.state.key },
})
}
/>
<Button
title="Go back to other examples"
onPress={() => this.props.navigation.goBack(null)}
/>
<StatusBar barStyle="default" />
</View>
);
}
}
class ProfileScreen extends React.Component<any, any> {
render() {
const { homeKey } = this.props.navigation.state.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile</Text>
<Button
title="Navigate to 'Settings' with key 'B'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Settings',
key: 'B',
params: { homeKey },
})
}
/>
<Button
title={`Navigate back to 'Home' with key ${homeKey}`}
onPress={() =>
this.props.navigation.navigate({ routeName: 'Home', key: homeKey })
}
/>
</View>
);
}
}
class SettingsScreen extends React.Component<any, any> {
render() {
const { homeKey } = this.props.navigation.state.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Settings</Text>
<Button
title={`Navigate back to 'Home' with key ${homeKey}`}
onPress={() =>
this.props.navigation.navigate({ routeName: 'Home', key: homeKey })
}
/>
<Button
title="Navigate back to 'Profile' with key 'A'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Profile',
key: 'A',
})
}
/>
</View>
);
}
}
const Stack = StackNavigator(
{
Home: {
screen: HomeScreen,
},
Profile: {
screen: ProfileScreen,
},
Settings: {
screen: SettingsScreen,
},
},
{
headerMode: 'none',
}
);
export default Stack;

View File

@@ -0,0 +1,121 @@
/**
* @flow
*/
import React from 'react';
import {
ActivityIndicator,
AsyncStorage,
Button,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { StackNavigator, SwitchNavigator } from 'react-navigation';
class SignInScreen extends React.Component<any, any> {
static navigationOptions = {
title: 'Please sign in',
};
render() {
return (
<View style={styles.container}>
<Button title="Sign in!" onPress={this._signInAsync} />
<Button
title="Go back to other examples"
onPress={() => this.props.navigation.goBack(null)}
/>
<StatusBar barStyle="default" />
</View>
);
}
_signInAsync = async () => {
await AsyncStorage.setItem('userToken', 'abc');
this.props.navigation.navigate('App');
};
}
class HomeScreen extends React.Component<any, any> {
static navigationOptions = {
title: 'Welcome to the app!',
};
render() {
return (
<View style={styles.container}>
<Button title="Show me more of the app" onPress={this._showMoreApp} />
<Button title="Actually, sign me out :)" onPress={this._signOutAsync} />
<StatusBar barStyle="default" />
</View>
);
}
_showMoreApp = () => {
this.props.navigation.navigate('Other');
};
_signOutAsync = async () => {
await AsyncStorage.clear();
this.props.navigation.navigate('Auth');
};
}
class OtherScreen extends React.Component<any, any> {
static navigationOptions = {
title: 'Lots of features here',
};
render() {
return (
<View style={styles.container}>
<Button title="I'm done, sign me out" onPress={this._signOutAsync} />
<StatusBar barStyle="default" />
</View>
);
}
_signOutAsync = async () => {
await AsyncStorage.clear();
this.props.navigation.navigate('Auth');
};
}
class LoadingScreen extends React.Component<any, any> {
componentDidMount() {
this._bootstrapAsync();
}
_bootstrapAsync = async () => {
const userToken = await AsyncStorage.getItem('userToken');
let initialRouteName = userToken ? 'App' : 'Auth';
this.props.navigation.navigate(initialRouteName);
};
render() {
return (
<View style={styles.container}>
<ActivityIndicator />
<StatusBar barStyle="default" />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
const AppStack = StackNavigator({ Home: HomeScreen, Other: OtherScreen });
const AuthStack = StackNavigator({ SignIn: SignInScreen });
export default SwitchNavigator({
Loading: LoadingScreen,
App: AppStack,
Auth: AuthStack,
});

View File

@@ -0,0 +1,101 @@
/**
* @flow
*/
import React from 'react';
import { Button, SafeAreaView, Text } from 'react-native';
import { TabNavigator, withNavigationFocus } from 'react-navigation';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import SampleText from './SampleText';
class Child extends React.Component<any, any> {
render() {
return (
<Text style={{ color: this.props.isFocused ? 'green' : 'maroon' }}>
{this.props.isFocused
? 'I know that my parent is focused!'
: 'My parent is not focused! :O'}
</Text>
);
}
}
const ChildWithNavigationFocus = withNavigationFocus(Child);
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
class TabScreen extends React.Component<any, any> {
static navigationOptions = {
tabBarLabel: name,
tabBarIcon: ({ tintColor, focused }) => (
<MaterialCommunityIcons
name={focused ? focusedIcon : icon}
size={26}
style={{ color: focused ? tintColor : '#ccc' }}
/>
),
};
state = { showChild: false };
render() {
const { isFocused } = this.props;
return (
<SafeAreaView
forceInset={{ horizontal: 'always', top: 'always' }}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
{'Tab ' + name.toLowerCase()}
</Text>
<Text style={{ marginBottom: 20 }}>
{'props.isFocused: ' + (isFocused ? ' true' : 'false')}
</Text>
{this.state.showChild ? (
<ChildWithNavigationFocus />
) : (
<Button
title="Press me"
onPress={() => this.setState({ showChild: true })}
/>
)}
<Button
onPress={() => this.props.navigation.goBack(null)}
title="Back to other examples"
/>
</SafeAreaView>
);
}
}
return withNavigationFocus(TabScreen);
};
const TabsWithNavigationFocus = TabNavigator(
{
One: {
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
},
Two: {
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
},
Three: {
screen: createTabScreen(
'Three',
'numeric-3-box-outline',
'numeric-3-box'
),
},
},
{
tabBarPosition: 'bottom',
animationEnabled: true,
swipeEnabled: true,
}
);
export default TabsWithNavigationFocus;

View File

@@ -14,11 +14,12 @@
"expo": "^25.0.0",
"react": "16.2.0",
"react-native": "^0.52.0",
"react-native-iphone-x-helper": "^1.0.2",
"react-navigation": "link:../.."
},
"devDependencies": {
"babel-plugin-transform-remove-console": "^6.9.0",
"babel-jest": "^21.0.0",
"babel-plugin-transform-remove-console": "^6.9.0",
"flow-bin": "^0.61.0",
"jest": "^21.0.1",
"jest-expo": "^25.1.0",

View File

@@ -568,9 +568,9 @@ babel-jest@^21.0.0, babel-jest@^21.2.0:
babel-plugin-istanbul "^4.0.0"
babel-preset-jest "^21.2.0"
babel-jest@^22.1.0, babel-jest@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.2.0.tgz#2d04087f5d149585e14f641d529551963fc9b4f8"
babel-jest@^22.1.0, babel-jest@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.2.2.tgz#eda38dca284e32cc5257f96a9b51351975de4e04"
dependencies:
babel-plugin-istanbul "^4.1.5"
babel-preset-jest "^22.2.0"
@@ -1217,8 +1217,8 @@ bplist-parser@0.1.1:
big-integer "^1.6.7"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -2093,9 +2093,9 @@ expect@^21.2.1:
jest-message-util "^21.2.1"
jest-regex-util "^21.2.0"
expect@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.2.0.tgz#dddcaab2e22ccc9f51e7c1732e0aa723f2f1f2b8"
expect@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.2.2.tgz#6cb6ae2eeb651a4187b9096de70333a018fab63f"
dependencies:
ansi-styles "^3.2.0"
jest-diff "^22.1.0"
@@ -3212,9 +3212,9 @@ jest-cli@^21.2.1:
worker-farm "^1.3.1"
yargs "^9.0.0"
jest-cli@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.2.1.tgz#f1df6675cd719e0773be77d6314c5f4b1d8af47f"
jest-cli@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.2.2.tgz#4431a93a29549da5dcb6d4a41dd03503c9198cd6"
dependencies:
ansi-escapes "^3.0.0"
chalk "^2.0.1"
@@ -3228,18 +3228,18 @@ jest-cli@^22.2.1:
istanbul-lib-instrument "^1.8.0"
istanbul-lib-source-maps "^1.2.1"
jest-changed-files "^22.2.0"
jest-config "^22.2.1"
jest-environment-jsdom "^22.2.0"
jest-config "^22.2.2"
jest-environment-jsdom "^22.2.2"
jest-get-type "^22.1.0"
jest-haste-map "^22.2.0"
jest-haste-map "^22.2.2"
jest-message-util "^22.2.0"
jest-regex-util "^22.1.0"
jest-resolve-dependencies "^22.1.0"
jest-runner "^22.2.1"
jest-runtime "^22.2.1"
jest-runner "^22.2.2"
jest-runtime "^22.2.2"
jest-snapshot "^22.2.0"
jest-util "^22.2.0"
jest-worker "^22.2.0"
jest-util "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
node-notifier "^5.2.1"
realpath-native "^1.0.0"
@@ -3266,20 +3266,20 @@ jest-config@^21.2.1:
jest-validate "^21.2.1"
pretty-format "^21.2.1"
jest-config@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.2.1.tgz#8617e99cff0544f0a5f254a5dde37f43b5383934"
jest-config@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.2.2.tgz#6b8ed615bc51239847d15460086f174dad4a7015"
dependencies:
chalk "^2.0.1"
glob "^7.1.1"
jest-environment-jsdom "^22.2.0"
jest-environment-node "^22.2.0"
jest-environment-jsdom "^22.2.2"
jest-environment-node "^22.2.2"
jest-get-type "^22.1.0"
jest-jasmine2 "^22.2.1"
jest-jasmine2 "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.2.0"
jest-util "^22.2.0"
jest-validate "^22.2.0"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
jest-validate "^22.2.2"
pretty-format "^22.1.0"
jest-diff@^21.2.1:
@@ -3310,9 +3310,9 @@ jest-docblock@^21.2.0:
version "21.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
jest-docblock@^22.1.0, jest-docblock@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.2.0.tgz#4d054eac354751e94a43a0ea2e2fe5c04cc61bbb"
jest-docblock@^22.1.0, jest-docblock@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.2.2.tgz#617f13edb16ec64202002b3c336cd14ae36c0631"
dependencies:
detect-newline "^2.1.0"
@@ -3324,12 +3324,12 @@ jest-environment-jsdom@^21.2.1:
jest-util "^21.2.1"
jsdom "^9.12.0"
jest-environment-jsdom@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.2.0.tgz#e9537400cbdef2d1e61d7196f8afa40e826fe9d8"
jest-environment-jsdom@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.2.2.tgz#3513ccdccc2bc41daf9cdee199b7069b0d9feebc"
dependencies:
jest-mock "^22.2.0"
jest-util "^22.2.0"
jest-util "^22.2.2"
jsdom "^11.5.1"
jest-environment-node@^21.2.1:
@@ -3339,12 +3339,12 @@ jest-environment-node@^21.2.1:
jest-mock "^21.2.0"
jest-util "^21.2.1"
jest-environment-node@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.2.0.tgz#ba7d0183fac076d34867367a4ac53ced69e3d3a9"
jest-environment-node@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.2.2.tgz#570896eef2dd0f939c71bd5712ef4321958c1270"
dependencies:
jest-mock "^22.2.0"
jest-util "^22.2.0"
jest-util "^22.2.2"
jest-expo@^25.1.0:
version "25.1.0"
@@ -3385,14 +3385,14 @@ jest-haste-map@^21.2.0:
sane "^2.0.0"
worker-farm "^1.3.1"
jest-haste-map@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.2.0.tgz#c9f508b8f63322490339ba02343dd688474d9ad5"
jest-haste-map@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.2.2.tgz#9d3d5a14bd5e05ab9176979f2a5fbb4ddc80eb20"
dependencies:
fb-watchman "^2.0.0"
graceful-fs "^4.1.11"
jest-docblock "^22.2.0"
jest-worker "^22.2.0"
jest-docblock "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
sane "^2.0.0"
@@ -3409,14 +3409,14 @@ jest-jasmine2@^21.2.1:
jest-snapshot "^21.2.1"
p-cancelable "^0.3.0"
jest-jasmine2@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.2.1.tgz#58d115f3f4a0a186b5e90c5db8ac68aecfc41051"
jest-jasmine2@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.2.2.tgz#9065255c8f635ae9dfa33fc66068f59adf53c9aa"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
co "^4.6.0"
expect "^22.2.0"
expect "^22.2.2"
graceful-fs "^4.1.11"
is-generator-fn "^1.0.0"
jest-diff "^22.1.0"
@@ -3501,9 +3501,9 @@ jest-resolve@^21.2.0:
chalk "^2.0.1"
is-builtin-module "^1.0.0"
jest-resolve@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.2.0.tgz#25aa8b887b31ab8c79763503e209d7c136f74ab1"
jest-resolve@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.2.2.tgz#6f49d91e3680c86a4d5e5f72ccdab3996d1cbc19"
dependencies:
browser-resolve "^1.11.2"
chalk "^2.0.1"
@@ -3523,20 +3523,20 @@ jest-runner@^21.2.1:
throat "^4.0.0"
worker-farm "^1.3.1"
jest-runner@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.2.1.tgz#539b2b7eb0ceb34e63a1ca78a1eda46ace70b940"
jest-runner@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.2.2.tgz#17fff27a61b63b58cf104c9cdcc0fdfccd3878ce"
dependencies:
exit "^0.1.2"
jest-config "^22.2.1"
jest-docblock "^22.2.0"
jest-haste-map "^22.2.0"
jest-jasmine2 "^22.2.1"
jest-config "^22.2.2"
jest-docblock "^22.2.2"
jest-haste-map "^22.2.2"
jest-jasmine2 "^22.2.2"
jest-leak-detector "^22.1.0"
jest-message-util "^22.2.0"
jest-runtime "^22.2.1"
jest-util "^22.2.0"
jest-worker "^22.2.0"
jest-runtime "^22.2.2"
jest-util "^22.2.2"
jest-worker "^22.2.2"
throat "^4.0.0"
jest-runtime@^21.2.1:
@@ -3561,22 +3561,22 @@ jest-runtime@^21.2.1:
write-file-atomic "^2.1.0"
yargs "^9.0.0"
jest-runtime@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.2.1.tgz#c5b0173a7f5274b28da30019cf7bb7b8cda68040"
jest-runtime@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.2.2.tgz#256d0efb65deae1c23b819d88cec5ab43d7a4ed6"
dependencies:
babel-core "^6.0.0"
babel-jest "^22.2.0"
babel-jest "^22.2.2"
babel-plugin-istanbul "^4.1.5"
chalk "^2.0.1"
convert-source-map "^1.4.0"
exit "^0.1.2"
graceful-fs "^4.1.11"
jest-config "^22.2.1"
jest-haste-map "^22.2.0"
jest-config "^22.2.2"
jest-haste-map "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.2.0"
jest-util "^22.2.0"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
json-stable-stringify "^1.0.1"
micromatch "^2.3.11"
realpath-native "^1.0.0"
@@ -3619,16 +3619,16 @@ jest-util@^21.2.1:
jest-validate "^21.2.1"
mkdirp "^0.5.1"
jest-util@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.2.0.tgz#afad693641447858bec7b37f32952516bf887262"
jest-util@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.2.2.tgz#335484b6aeae0c5a1ae498401630324977fe3465"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
graceful-fs "^4.1.11"
is-ci "^1.0.10"
jest-message-util "^22.2.0"
jest-validate "^22.2.0"
jest-validate "^22.2.2"
mkdirp "^0.5.1"
jest-validate@^21.2.1:
@@ -3640,9 +3640,9 @@ jest-validate@^21.2.1:
leven "^2.1.0"
pretty-format "^21.2.1"
jest-validate@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.2.0.tgz#f7ce459105a8210825e5e57279f868ab080063fa"
jest-validate@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.2.2.tgz#9cdce422c93cc28395e907ac6bbc929158d9a6ba"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
@@ -3655,9 +3655,9 @@ jest-worker@22.1.0:
dependencies:
merge-stream "^1.0.1"
jest-worker@^22.1.0, jest-worker@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.2.0.tgz#d88d6ee176d6409f206cbbf7b485250793264262"
jest-worker@^22.1.0, jest-worker@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.2.2.tgz#c1f5dc39976884b81f68ec50cb8532b2cbab3390"
dependencies:
merge-stream "^1.0.1"
@@ -3668,11 +3668,11 @@ jest@^21.0.1:
jest-cli "^21.2.1"
jest@^22.1.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.2.1.tgz#fb6524d35bd02968afe3b17f330d6f7207846147"
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.2.2.tgz#26aca0f5e4eaa76d52f2792b14033a3d1e7be2bd"
dependencies:
import-local "^1.0.0"
jest-cli "^22.2.1"
jest-cli "^22.2.2"
joi@^10.0.2:
version "10.6.0"
@@ -4156,10 +4156,6 @@ lru-memoizer@^1.11.1:
lru-cache "~4.0.0"
very-fast-args "^1.1.0"
lsmod@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b"
macos-release@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb"
@@ -4986,9 +4982,9 @@ probe-image-size@^3.1.0:
next-tick "^1.0.0"
stream-parser "~0.3.1"
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
process@~0.5.1:
version "0.5.2"
@@ -5099,11 +5095,10 @@ raven-js@^3.17.0:
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.2.tgz#85785928ebd664049e54efd0db8ff28da0cbb374"
raven@^2.1.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.0.tgz#49b7d5f838e5893f31dd72f82d05a35e42203f60"
version "2.4.1"
resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.1.tgz#7a6a6ff1c42d0a3892308f44c94273e7f88677fd"
dependencies:
cookie "0.3.1"
lsmod "1.0.0"
md5 "^2.2.1"
stack-trace "0.0.9"
timed-out "4.0.1"
@@ -5178,13 +5173,17 @@ react-native-gesture-handler@1.0.0-alpha.39:
invariant "^2.2.2"
prop-types "^15.5.10"
react-native-iphone-x-helper@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.0.2.tgz#7dbca530930f7c1ce8633cc8fd13ba94102992e1"
react-native-maps@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/react-native-maps/-/react-native-maps-0.19.0.tgz#ce94fad1cf360e335cb4338a68c95f791e869074"
react-native-safe-area-view@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.6.0.tgz#ce01eb27905a77780219537e0f53fe9c783a8b3d"
react-native-safe-area-view@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.7.0.tgz#38f5ab9368d6ef9e5d18ab64212938af3ec39421"
dependencies:
hoist-non-react-statics "^2.3.1"
@@ -5398,13 +5397,13 @@ readable-stream@1.1.x, readable-stream@^1.1.8, readable-stream@~1.1.8, readable-
string_decoder "~0.10.x"
readable-stream@2, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"

View File

@@ -2,4 +2,4 @@
## Usage
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.

View File

@@ -26,7 +26,7 @@
"react": "16.2.0",
"react-native": "^0.52.0",
"react-navigation": "link:../..",
"react-navigation-redux-helpers": "^1.0.0",
"react-navigation-redux-helpers": "^1.0.3",
"react-redux": "^5.0.6",
"redux": "^3.7.2"
},

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
import { initializeListeners } from 'react-navigation-redux-helpers';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
@@ -20,6 +21,10 @@ class AppWithNavigationState extends React.Component {
nav: PropTypes.object.isRequired,
};
componentDidMount() {
initializeListeners('root', this.props.nav);
}
render() {
const { dispatch, nav } = this.props;
return (

View File

@@ -48,6 +48,15 @@ declare module 'react-navigation' {
// react-native/Libraries/Animated/src/nodes/AnimatedValue.js
declare type AnimatedValue = Object;
declare type HeaderForceInset = {
horizontal?: string,
vertical?: string,
left?: string,
right?: string,
top?: string,
bottom?: string,
};
/**
* Next, all the type declarations
*/
@@ -71,25 +80,11 @@ declare module 'react-navigation' {
key?: string,
|};
declare type DeprecatedNavigationNavigateAction = {|
type: 'Navigate',
routeName: string,
params?: NavigationParams,
// The action to run inside the sub-router
action?: NavigationNavigateAction | DeprecatedNavigationNavigateAction,
|};
declare export type NavigationBackAction = {|
type: 'Navigation/BACK',
key?: ?string,
|};
declare type DeprecatedNavigationBackAction = {|
type: 'Back',
key?: ?string,
|};
declare export type NavigationSetParamsAction = {|
type: 'Navigation/SET_PARAMS',
@@ -100,26 +95,11 @@ declare module 'react-navigation' {
params: NavigationParams,
|};
declare type DeprecatedNavigationSetParamsAction = {|
type: 'SetParams',
// The key of the route where the params should be set
key: string,
// The new params to merge into the existing route params
params: NavigationParams,
|};
declare export type NavigationInitAction = {|
type: 'Navigation/INIT',
params?: NavigationParams,
|};
declare type DeprecatedNavigationInitAction = {|
type: 'Init',
params?: NavigationParams,
|};
declare export type NavigationResetAction = {|
type: 'Navigation/RESET',
index: number,
@@ -127,25 +107,11 @@ declare module 'react-navigation' {
actions: Array<NavigationNavigateAction>,
|};
declare type DeprecatedNavigationResetAction = {|
type: 'Reset',
index: number,
key?: ?string,
actions: Array<
NavigationNavigateAction | DeprecatedNavigationNavigateAction
>,
|};
declare export type NavigationUriAction = {|
type: 'Navigation/URI',
uri: string,
|};
declare type DeprecatedNavigationUriAction = {|
type: 'Uri',
uri: string,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
@@ -181,17 +147,6 @@ declare module 'react-navigation' {
| NavigationSetParamsAction
| NavigationResetAction;
declare type DeprecatedNavigationAction =
| DeprecatedNavigationInitAction
| DeprecatedNavigationNavigateAction
| DeprecatedNavigationBackAction
| DeprecatedNavigationSetParamsAction
| DeprecatedNavigationResetAction;
declare export type PossiblyDeprecatedNavigationAction =
| NavigationAction
| DeprecatedNavigationAction;
/**
* NavigationState is a tree of routes for a single navigator, where each
* child route may either be a NavigationScreenRoute or a
@@ -332,10 +287,12 @@ declare module 'react-navigation' {
navigationOptions?: ?NavigationScreenConfig<Options>,
};
declare export type NavigationRouteConfig = {
navigationOptions?: NavigationScreenConfig<*>,
path?: string,
} & NavigationScreenRouteConfig;
declare export type NavigationRouteConfig =
| NavigationComponent
| ({
navigationOptions?: NavigationScreenConfig<*>,
path?: string,
} & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig =
| {
@@ -378,6 +335,7 @@ declare module 'react-navigation' {
declare export type NavigationStackScreenOptions = NavigationScreenOptions & {
header?: ?(React$Node | (HeaderProps => React$Node)),
headerTransparent?: boolean,
headerTitle?: string | React$Node | React$ElementType,
headerTitleStyle?: AnimatedTextStyleProp,
headerTitleAllowFontScaling?: boolean,
@@ -390,6 +348,8 @@ declare module 'react-navigation' {
headerPressColorAndroid?: string,
headerRight?: React$Node,
headerStyle?: ViewStyleProp,
headerForceInset?: HeaderForceInset,
headerBackground?: React$Node | React$ElementType,
gesturesEnabled?: boolean,
gestureResponseDistance?: { vertical?: number, horizontal?: number },
gestureDirection?: 'default' | 'inverted',
@@ -400,11 +360,13 @@ declare module 'react-navigation' {
initialRouteParams?: NavigationParams,
paths?: NavigationPathsConfig,
navigationOptions?: NavigationScreenConfig<*>,
initialRouteKey?: string,
|};
declare export type NavigationStackViewConfig = {|
mode?: 'card' | 'modal',
headerMode?: HeaderMode,
headerTransitionPreset?: 'fade-in-place' | 'uikit',
cardStyle?: ViewStyleProp,
transitionConfig?: () => TransitionConfig,
onTransitionStart?: () => void,
@@ -416,6 +378,20 @@ declare module 'react-navigation' {
...NavigationStackRouterConfig,
|};
/**
* Switch Navigator
*/
declare export type NavigationSwitchRouterConfig = {|
initialRouteName?: string,
initialRouteParams?: NavigationParams,
paths?: NavigationPathsConfig,
navigationOptions?: NavigationScreenConfig<*>,
order?: Array<string>,
backBehavior?: 'none' | 'initialRoute', // defaults to `'none'`
resetOnBlur?: boolean, // defaults to `true`
|};
/**
* Tab Navigator
*/
@@ -427,7 +403,6 @@ declare module 'react-navigation' {
navigationOptions?: NavigationScreenConfig<*>,
// todo: type these as the real route names rather than 'string'
order?: Array<string>,
// Does the back button cause the router to switch to the initial tab
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|};
@@ -476,7 +451,7 @@ declare module 'react-navigation' {
*/
declare export type NavigationDispatch = (
action: PossiblyDeprecatedNavigationAction
action: NavigationAction
) => boolean;
declare export type NavigationProp<S> = {
@@ -495,7 +470,7 @@ declare module 'react-navigation' {
type: EventType,
action: NavigationAction,
state: NavigationState,
lastState: NavigationState,
lastState: ?NavigationState,
};
declare export type NavigationEventCallback = (
@@ -578,7 +553,7 @@ declare module 'react-navigation' {
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
uriPrefix?: string | RegExp,
onNavigationStateChange?: (
onNavigationStateChange?: ?(
NavigationState,
NavigationState,
NavigationAction
@@ -749,11 +724,11 @@ declare module 'react-navigation' {
SET_PARAMS: 'Navigation/SET_PARAMS',
URI: 'Navigation/URI',
back: {
(payload: { key?: ?string }): NavigationBackAction,
(payload?: { key?: ?string }): NavigationBackAction,
toString: () => string,
},
init: {
(payload: { params?: NavigationParams }): NavigationInitAction,
(payload?: { params?: NavigationParams }): NavigationInitAction,
toString: () => string,
},
navigate: {
@@ -783,9 +758,6 @@ declare module 'react-navigation' {
(payload: { uri: string }): NavigationUriAction,
toString: () => string,
},
mapDeprecatedActionAndWarn: (
action: PossiblyDeprecatedNavigationAction
) => NavigationAction,
};
declare type _RouterProp<S: NavigationState, O: {}> = {
@@ -829,12 +801,21 @@ declare module 'react-navigation' {
declare type _TabNavigatorConfig = {|
...NavigationTabRouterConfig,
..._TabViewConfig,
lazy?: boolean,
removeClippedSubviews?: boolean,
containerOptions?: void,
|};
declare export function TabNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _TabNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _SwitchNavigatorConfig = {|
...NavigationSwitchRouterConfig,
|};
declare export function SwitchNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _SwitchNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _DrawerViewConfig = {|
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
@@ -944,12 +925,14 @@ declare module 'react-navigation' {
vertical?: _SafeAreaViewForceInsetValue,
horizontal?: _SafeAreaViewForceInsetValue,
},
children: React$Node,
children?: React$Node,
style?: AnimatedViewStyleProp,
};
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>;
declare export var Header: React$ComponentType<HeaderProps>;
declare export var Header: React$ComponentType<HeaderProps> & {
HEIGHT: number,
};
declare type _HeaderTitleProps = {
children: React$Node,
@@ -1014,6 +997,8 @@ declare module 'react-navigation' {
itemsContainerStyle?: ViewStyleProp,
itemStyle?: ViewStyleProp,
labelStyle?: TextStyleProp,
activeLabelStyle?: TextStyleProp,
inactiveLabelStyle?: TextStyleProp,
iconContainerStyle?: ViewStyleProp,
drawerPosition: 'left' | 'right',
};
@@ -1094,9 +1079,12 @@ declare module 'react-navigation' {
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
declare type _NavigationInjectedProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<NavigationStateRoute>,
};
declare export function withNavigation<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps>
): React$ComponentType<T>;
declare export function withNavigationFocus<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps>
): React$ComponentType<T>;
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "1.0.2",
"version": "1.5.2",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -33,7 +33,7 @@
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10",
"react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.6.0",
"react-native-safe-area-view": "^0.7.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {
@@ -72,6 +72,10 @@
"modulePathIgnorePatterns": ["examples"]
},
"lint-staged": {
"*.js": ["eslint --fix", "git add"]
"*.js": [
"eslint --fix",
"prettier --write flow/react-navigation.js",
"git add"
]
}
}

View File

@@ -57,6 +57,7 @@ const pop = createAction(POP, payload => ({
const popToTop = createAction(POP_TO_TOP, payload => ({
type: POP_TO_TOP,
immediate: payload && payload.immediate,
key: payload && payload.key,
}));
const push = createAction(PUSH, payload => {
@@ -106,59 +107,6 @@ const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
key: payload && payload.key,
}));
const mapDeprecatedNavigateAction = action => {
if (action.type === 'Navigate') {
const payload = {
routeName: action.routeName,
params: action.params,
};
if (action.action) {
payload.action = mapDeprecatedNavigateAction(action.action);
}
return navigate(payload);
}
return action;
};
const mapDeprecatedAction = action => {
if (action.type === 'Back') {
return back(action);
} else if (action.type === 'Init') {
return init(action);
} else if (action.type === 'Navigate') {
return mapDeprecatedNavigateAction(action);
} else if (action.type === 'Reset') {
return reset({
index: action.index,
key: action.key,
actions: action.actions.map(mapDeprecatedNavigateAction),
});
} else if (action.type === 'SetParams') {
return setParams(action);
}
return action;
};
const mapDeprecatedActionAndWarn = action => {
const newAction = mapDeprecatedAction(action);
if (newAction !== action) {
const oldType = action.type;
const newType = newAction.type;
console.warn(
[
`The action type '${oldType}' has been renamed to '${newType}'.`,
`'${oldType}' will continue to work while in beta but will be removed`,
'in the first major release. Moving forward, you should use the',
'action constants and action creators exported by this library in',
"the 'actions' object.",
'See https://github.com/react-community/react-navigation/pull/120 for',
'more details.',
].join(' ')
);
}
return newAction;
};
export default {
// Action constants
BACK,
@@ -185,7 +133,4 @@ export default {
setParams,
uri,
completeTransition,
// TODO: Remove once old actions are deprecated
mapDeprecatedActionAndWarn,
};

View File

@@ -1,9 +1,9 @@
import {
BackAndroid as DeprecatedBackAndroid,
BackHandler as ModernBackHandler,
Linking,
MaskedViewIOS,
} from 'react-native';
const BackHandler = ModernBackHandler || DeprecatedBackAndroid;
export { BackHandler, Linking };
export { BackHandler, MaskedViewIOS };

View File

@@ -1,9 +1,6 @@
export const Linking = {
addEventListener: () => {},
removeEventListener: () => {},
getInitialURL: () => Promise.reject('Unsupported platform'),
};
import React from 'react';
import { BackHandler, View } from 'react-native';
export const BackHandler = {
addEventListener: () => {},
};
const MaskedViewIOS = () => <View>{this.props.children}</View>;
export { BackHandler, MaskedViewIOS };

View File

@@ -76,4 +76,43 @@ describe('addNavigationHelpers', () => {
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles GetParams action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings', params: { name: 'Peter' } },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name', 'Brent')
).toEqual('Peter');
});
it('handles GetParams action with default param', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name', 'Brent')
).toEqual('Brent');
});
it('handles GetParams action with param value as null', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings', params: { name: null } },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name')
).toEqual(null);
});
});

View File

@@ -61,6 +61,16 @@ export default function(navigation) {
return navigation.dispatch(NavigationActions.setParams({ params, key }));
},
getParam: (paramName, defaultValue) => {
const params = navigation.state.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
},
push: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.push({ routeName, params, action })

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { BackHandler, Linking } from './PlatformHelpers';
import { Linking } from 'react-native';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
import addNavigationHelpers from './addNavigationHelpers';
import invariant from './utils/invariant';
@@ -25,6 +26,20 @@ export default function createNavigationContainer(Component) {
this._validateProps(props);
this._initialAction = NavigationActions.init();
if (this._isStateful()) {
this.subs = BackHandler.addEventListener('hardwareBackPress', () => {
if (!this._isMounted) {
this.subs && this.subs.remove();
} else {
// dispatch returns true if the action results in a state change,
// and false otherwise. This maps well to what BackHandler expects
// from a callback -- true if handled, false if not handled
return this.dispatch(NavigationActions.back());
}
});
}
this.state = {
nav: this._isStateful()
? Component.router.getStateForAction(this._initialAction)
@@ -124,14 +139,11 @@ export default function createNavigationContainer(Component) {
}
componentDidMount() {
this._isMounted = true;
if (!this._isStateful()) {
return;
}
this.subs = BackHandler.addEventListener('hardwareBackPress', () =>
this.dispatch(NavigationActions.back())
);
Linking.addEventListener('url', this._handleOpenURL);
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
@@ -147,14 +159,14 @@ export default function createNavigationContainer(Component) {
}
componentWillUnmount() {
this._isMounted = false;
Linking.removeEventListener('url', this._handleOpenURL);
this.subs && this.subs.remove();
}
// Per-tick temporary storage for state.nav
dispatch = inputAction => {
const action = NavigationActions.mapDeprecatedActionAndWarn(inputAction);
dispatch = action => {
if (!this._isStateful()) {
return false;
}

View File

@@ -4,6 +4,7 @@
* Based on the 'action' events that get fired for this navigation state, this utility will fire
* focus and blur events for this child
*/
export default function getChildEventSubscriber(addListener, key) {
const actionSubscribers = new Set();
const willFocusSubscribers = new Set();

View File

@@ -11,10 +11,12 @@ import NavigationActions from '../NavigationActions';
export default (routeConfigMap, stackConfig = {}) => {
const {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
headerMode,
headerTransitionPreset,
mode,
cardStyle,
transitionConfig,
@@ -24,6 +26,7 @@ export default (routeConfigMap, stackConfig = {}) => {
} = stackConfig;
const stackRouterConfig = {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
@@ -38,6 +41,7 @@ export default (routeConfigMap, stackConfig = {}) => {
<CardStackTransitioner
{...props}
headerMode={headerMode}
headerTransitionPreset={headerTransitionPreset}
mode={mode}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
@@ -45,7 +49,7 @@ export default (routeConfigMap, stackConfig = {}) => {
onTransitionEnd={(lastTransition, transition) => {
const { state, dispatch } = props.navigation;
dispatch(NavigationActions.completeTransition({ key: state.key }));
onTransitionEnd && onTransitionEnd();
onTransitionEnd && onTransitionEnd(lastTransition, transition);
}}
/>
)

View File

@@ -0,0 +1,15 @@
import React from 'react';
import SwitchRouter from '../routers/SwitchRouter';
import SwitchView from '../views/SwitchView/SwitchView';
import createNavigationContainer from '../createNavigationContainer';
import createNavigator from '../navigators/createNavigator';
export default (routeConfigMap, switchConfig = {}) => {
const router = SwitchRouter(routeConfigMap, switchConfig);
const navigator = createNavigator(router, routeConfigMap, switchConfig)(
props => <SwitchView {...props} />
);
return createNavigationContainer(navigator);
};

View File

@@ -19,6 +19,8 @@ const TabNavigator = (routeConfigs, config = {}) => {
tabBarComponent,
tabBarPosition,
tabBarOptions,
lazy,
removeClippedSubviews,
swipeEnabled,
animationEnabled,
configureTransition,
@@ -31,6 +33,8 @@ const TabNavigator = (routeConfigs, config = {}) => {
const navigator = createNavigator(router, routeConfigs, config)(props => (
<TabView
{...props}
lazy={lazy}
removeClippedSubviews={removeClippedSubviews}
tabBarComponent={tabBarComponent}
tabBarPosition={tabBarPosition}
tabBarOptions={tabBarOptions}

View File

@@ -1,15 +1,22 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import StackNavigator from '../StackNavigator';
const styles = StyleSheet.create({
header: {
opacity: 0.5,
},
});
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: `Welcome ${
navigation.state.params ? navigation.state.params.user : 'anonymous'
}`,
gesturesEnabled: true,
headerStyle: [{ backgroundColor: 'red' }, styles.header],
});
render() {

View File

@@ -0,0 +1,18 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import SwitchNavigator from '../SwitchNavigator';
const A = () => <View />;
const B = () => <View />;
const routeConfig = { A, B };
describe('SwitchNavigator', () => {
it('renders successfully', () => {
const MySwitchNavigator = SwitchNavigator(routeConfig);
const rendered = renderer.create(<MySwitchNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
});

View File

@@ -224,6 +224,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
"color": "#2196f3",
},
undefined,
undefined,
]
}
>

View File

@@ -48,7 +48,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"backgroundColor": "#EFEFF4",
"bottom": 0,
"left": 0,
"opacity": 1,
@@ -75,162 +75,109 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
/>
</View>
<View
cardStyle={undefined}
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
index={0}
layout={
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"height": 0,
"initHeight": 0,
"initWidth": 0,
"isMeasured": false,
"width": 0,
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
leftInterpolator={[Function]}
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-1",
"routeName": "Home",
},
],
},
}
}
rightInterpolator={[Function]}
router={
Object {
"getActionForPathAndParams": [Function],
"getComponentForRouteName": [Function],
"getComponentForState": [Function],
"getPathAndParamsForState": [Function],
"getScreenConfig": [Function],
"getScreenOptions": [Function],
"getStateForAction": [Function],
}
}
titleInterpolator={[Function]}
transitionConfig={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "rgba(0, 0, 0, .3)",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flex": 1,
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
Object {
"flexDirection": "row",
},
]
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
}
}
>
<View
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
pointerEvents="box-none"
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "600",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"justifyContent": "center",
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
Welcome anonymous
</Text>
</View>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
>
<View />
</View>
}
>
<View />
</View>
</View>
</View>
@@ -287,7 +234,7 @@ exports[`StackNavigator renders successfully 1`] = `
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"backgroundColor": "#EFEFF4",
"bottom": 0,
"left": 0,
"opacity": 1,
@@ -314,144 +261,91 @@ exports[`StackNavigator renders successfully 1`] = `
/>
</View>
<View
cardStyle={undefined}
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
index={0}
layout={
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"height": 0,
"initHeight": 0,
"initWidth": 0,
"isMeasured": false,
"width": 0,
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
leftInterpolator={[Function]}
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-0",
"routeName": "Home",
},
],
},
}
}
rightInterpolator={[Function]}
router={
Object {
"getActionForPathAndParams": [Function],
"getComponentForRouteName": [Function],
"getComponentForState": [Function],
"getPathAndParamsForState": [Function],
"getScreenConfig": [Function],
"getScreenOptions": [Function],
"getStateForAction": [Function],
}
}
titleInterpolator={[Function]}
transitionConfig={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "rgba(0, 0, 0, .3)",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flex": 1,
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
Object {
"flexDirection": "row",
},
]
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
pointerEvents="box-none"
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "600",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
Welcome anonymous
</Text>
</View>
</View>
</View>

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwitchNavigator renders successfully 1`] = `<View />`;

View File

@@ -55,13 +55,23 @@ exports[`TabNavigator renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
/>
>
<View
style={
Object {
"flex": 1,
}
}
/>
</View>
</View>
</View>
<View
@@ -137,13 +147,12 @@ exports[`TabNavigator renders successfully 1`] = `
style={
Object {
"alignItems": "center",
"bottom": 0,
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"width": "100%",
}
}
/>
@@ -152,13 +161,12 @@ exports[`TabNavigator renders successfully 1`] = `
style={
Object {
"alignItems": "center",
"bottom": 0,
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"width": "100%",
}
}
/>
@@ -168,6 +176,7 @@ exports[`TabNavigator renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",

View File

@@ -22,6 +22,9 @@ module.exports = {
get StackNavigator() {
return require('./navigators/StackNavigator').default;
},
get SwitchNavigator() {
return require('./navigators/SwitchNavigator').default;
},
get TabNavigator() {
return require('./navigators/TabNavigator').default;
},
@@ -36,6 +39,9 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// Views
get Transitioner() {
@@ -84,8 +90,16 @@ module.exports = {
return require('./views/TabView/TabBarBottom').default;
},
// SwitchView
get SwitchView() {
return require('./views/SwitchView/SwitchView').default;
},
// HOCs
get withNavigation() {
return require('./views/withNavigation').default;
},
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
};

View File

@@ -32,4 +32,7 @@ module.exports = {
get withNavigation() {
return require('./views/withNavigation').default;
},
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
};

View File

@@ -51,6 +51,62 @@ export default (routeConfigs, stackConfig = {}) => {
const pathsByRouteNames = { ...stackConfig.paths } || {};
let paths = [];
function getInitialState(action) {
let route = {};
const childRouter = childRouters[action.routeName];
// This is a push-like action, and childRouter will be a router or null if we are responsible for this routeName
if (behavesLikePushAction(action) && childRouter !== undefined) {
let childState = {};
// The router is null for normal leaf routes
if (childRouter !== null) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [
{
params: action.params,
...childState,
key: action.key || generateKey(),
routeName: action.routeName,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params || action.params || initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
const { initialRouteKey } = stackConfig;
route = {
...route,
...(params ? { params } : {}),
routeName: initialRouteName,
key: action.key || (initialRouteKey || generateKey()),
};
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [route],
};
}
// Build paths for each route
routeNames.forEach(routeName => {
let pathPattern =
@@ -100,55 +156,11 @@ export default (routeConfigs, stackConfig = {}) => {
getStateForAction(action, state) {
// Set up the initial state if needed
if (!state) {
let route = {};
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [
{
routeName: action.routeName,
params: action.params,
key: `Init-${generateKey()}`,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params ||
action.params ||
initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
route = {
...route,
routeName: initialRouteName,
key: `Init-${generateKey()}`,
...(params ? { params } : {}),
};
// eslint-disable-next-line no-param-reassign
state = {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [route],
};
return getInitialState(action);
}
// Check if a child scene wants to handle the action as long as it is not a reset to the root stack
// Check if the focused child scene wants to handle the action, as long as
// it is not a reset to the root stack
if (action.type !== NavigationActions.RESET || action.key !== null) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
@@ -171,8 +183,137 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
// Handle explicit push navigation action. This must happen after the
// focused child router has had a chance to handle the action.
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
const childRouter = childRouters[action.routeName];
let route;
invariant(
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
if (action.key) {
const lastRouteIndex = state.routes.findIndex(
r => r.key === action.key
);
if (lastRouteIndex !== -1) {
// If index is unchanged and params are not being set, leave state identity intact
if (state.index === lastRouteIndex && !action.params) {
return state;
}
// Remove the now unused routes at the tail of the routes array
const routes = state.routes.slice(0, lastRouteIndex + 1);
// Apply params if provided, otherwise leave route identity intact
if (action.params) {
const route = state.routes.find(r => r.key === action.key);
routes[lastRouteIndex] = {
...route,
params: {
...route.params,
...action.params,
},
};
}
// Return state with new index. Change isTransitioning only if index has changed
return {
...state,
isTransitioning:
state.index !== lastRouteIndex
? action.immediate !== true
: undefined,
index: lastRouteIndex,
routes,
};
}
}
if (childRouter) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
routeName: action.routeName,
key: action.key || generateKey(),
};
} else {
route = {
params: action.params,
routeName: action.routeName,
key: action.key || generateKey(),
};
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
// If we've made it this far with a push action, we return the
// state with a new identity to prevent the action from bubbling
// back up.
return {
...state,
};
}
// Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) {
const childRouterNames = Object.keys(childRouters);
for (let i = 0; i < childRouterNames.length; i++) {
const childRouterName = childRouterNames[i];
const childRouter = childRouters[childRouterName];
if (childRouter) {
// For each child router, start with a blank state
const initChildRoute = childRouter.getStateForAction(
NavigationActions.init()
);
// Then check to see if the router handles our navigate action
const navigatedChildRoute = childRouter.getStateForAction(
action,
initChildRoute
);
let routeToPush = null;
if (navigatedChildRoute === null) {
// Push the route if the router has 'handled' the action and returned null
routeToPush = initChildRoute;
} else if (navigatedChildRoute !== initChildRoute) {
// Push the route if the state has changed in response to this navigation
routeToPush = navigatedChildRoute;
}
if (routeToPush) {
const route = {
...routeToPush,
routeName: childRouterName,
key: action.key || generateKey(),
};
return StateUtils.push(state, route);
}
}
}
}
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
// Refuse to handle pop to top if a key is given that doesn't correspond
// to this router
if (action.key && state.key !== action.key) {
return state;
}
// If we're already at the top, then we return the state with a new
// identity so that the action is handled by this router.
if (state.index === 0) {
return {
...state,
@@ -206,90 +347,14 @@ export default (routeConfigs, stackConfig = {}) => {
params: action.params,
// merge the child state in this order to allow params override
...childState,
key: action.newKey || generateKey(),
routeName: action.routeName,
key: action.newKey || generateKey(),
};
return { ...state, routes };
}
}
// Handle explicit push navigation action. Make sure this happens after children have had a chance to handle the action
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
const childRouter = childRouters[action.routeName];
let route;
invariant(
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
if (action.key) {
const lastRouteIndex = state.routes.findIndex(
r => r.key === action.key
);
if (lastRouteIndex !== -1) {
// If index is unchanged and params are not being set, leave state identity intact
if (state.index === lastRouteIndex && !action.params) {
return state;
}
const routes = [...state.routes];
// Apply params if provided, otherwise leave route identity intact
if (action.params) {
const route = state.routes.find(r => r.key === action.key);
routes[lastRouteIndex] = {
...route,
params: {
...route.params,
...action.params,
},
};
}
// Return state with new index. Change isTransitioning only if index has changed
return {
...state,
isTransitioning:
state.index !== lastRouteIndex
? action.immediate !== true
: undefined,
index: lastRouteIndex,
routes,
};
}
}
const key = action.key || generateKey();
if (childRouter) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
key,
routeName: action.routeName,
};
} else {
route = {
params: action.params,
key,
routeName: action.routeName,
};
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
return {
...state,
};
}
// Update transitioning state
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
@@ -301,41 +366,6 @@ export default (routeConfigs, stackConfig = {}) => {
};
}
// Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) {
const childRouterNames = Object.keys(childRouters);
for (let i = 0; i < childRouterNames.length; i++) {
const childRouterName = childRouterNames[i];
const childRouter = childRouters[childRouterName];
if (childRouter) {
// For each child router, start with a blank state
const initChildRoute = childRouter.getStateForAction(
NavigationActions.init()
);
// Then check to see if the router handles our navigate action
const navigatedChildRoute = childRouter.getStateForAction(
action,
initChildRoute
);
let routeToPush = null;
if (navigatedChildRoute === null) {
// Push the route if the router has 'handled' the action and returned null
routeToPush = initChildRoute;
} else if (navigatedChildRoute !== initChildRoute) {
// Push the route if the state has changed in response to this navigation
routeToPush = navigatedChildRoute;
}
if (routeToPush) {
return StateUtils.push(state, {
...routeToPush,
key: generateKey(),
routeName: childRouterName,
});
}
}
}
}
if (action.type === NavigationActions.SET_PARAMS) {
const key = action.key;
const lastRoute = state.routes.find(route => route.key === key);
@@ -363,26 +393,29 @@ export default (routeConfigs, stackConfig = {}) => {
// undefined on either the state or the action
return state;
}
const resetAction = action;
const newStackActions = action.actions;
return {
...state,
routes: resetAction.actions.map(childAction => {
const router = childRouters[childAction.routeName];
routes: newStackActions.map(newStackAction => {
const router = childRouters[newStackAction.routeName];
let childState = {};
if (router) {
return {
...childAction,
...router.getStateForAction(childAction),
routeName: childAction.routeName,
key: generateKey(),
};
const childAction =
newStackAction.action ||
NavigationActions.init({ params: newStackAction.params });
childState = router.getStateForAction(childAction);
}
const route = {
...childAction,
key: generateKey(),
return {
params: newStackAction.params,
...childState,
routeName: newStackAction.routeName,
key: newStackAction.key || generateKey(),
};
delete route.type;
return route;
}),
index: action.index,
};
@@ -521,7 +554,15 @@ export default (routeConfigs, stackConfig = {}) => {
}
const nextResult = result || {};
const paramName = key.name;
nextResult[paramName] = matchResult;
let decodedMatchResult;
try {
decodedMatchResult = decodeURIComponent(matchResult);
} catch (e) {
// ignore `URIError: malformed URI`
}
nextResult[paramName] = decodedMatchResult || matchResult;
return nextResult;
}, queryParams);

360
src/routers/SwitchRouter.js Normal file
View File

@@ -0,0 +1,360 @@
import invariant from '../utils/invariant';
import getScreenForRouteName from './getScreenForRouteName';
import createConfigGetter from './createConfigGetter';
import NavigationActions from '../NavigationActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
function childrenUpdateWithoutSwitchingIndex(actionType) {
return [
NavigationActions.SET_PARAMS,
NavigationActions.COMPLETE_TRANSITION,
].includes(actionType);
}
export default (routeConfigs, config = {}) => {
// Fail fast on invalid route definitions
validateRouteConfigMap(routeConfigs);
const order = config.order || Object.keys(routeConfigs);
const paths = config.paths || {};
const initialRouteParams = config.initialRouteParams;
const initialRouteName = config.initialRouteName || order[0];
const backBehavior = config.backBehavior || 'none';
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
const resetOnBlur = config.hasOwnProperty('resetOnBlur')
? config.resetOnBlur
: true;
const initialRouteIndex = order.indexOf(initialRouteName);
const childRouters = {};
order.forEach(routeName => {
const routeConfig = routeConfigs[routeName];
paths[routeName] =
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
childRouters[routeName] = null;
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen.router) {
childRouters[routeName] = screen.router;
}
});
if (initialRouteIndex === -1) {
throw new Error(
`Invalid initialRouteName '${initialRouteName}'.` +
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
);
}
function resetChildRoute(routeName) {
const params =
routeName === initialRouteName ? initialRouteParams : undefined;
const childRouter = childRouters[routeName];
if (childRouter) {
const childAction = NavigationActions.init();
return {
...childRouter.getStateForAction(childAction),
key: routeName,
routeName,
params,
};
}
return {
key: routeName,
routeName,
params,
};
}
return {
getInitialState() {
const routes = order.map(resetChildRoute);
return {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
},
getNextState(prevState, possibleNextState) {
let nextState;
if (prevState.index !== possibleNextState.index && resetOnBlur) {
const prevRouteName = prevState.routes[prevState.index].routeName;
const nextRoutes = [...possibleNextState.routes];
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
return {
...possibleNextState,
routes: nextRoutes,
};
} else {
nextState = possibleNextState;
}
return nextState;
},
getStateForAction(action, inputState) {
let prevState = inputState ? { ...inputState } : inputState;
let state = inputState || this.getInitialState();
let activeChildIndex = state.index;
if (action.type === NavigationActions.INIT) {
// NOTE(brentvatne): this seems weird... why are we merging these
// params into child routes?
// ---------------------------------------------------------------
// Merge any params from the action into all the child routes
const { params } = action;
if (params) {
state.routes = state.routes.map(route => ({
...route,
params: {
...route.params,
...params,
...(route.routeName === initialRouteName
? initialRouteParams
: null),
},
}));
}
}
// Let the current child handle it
const activeChildLastState = state.routes[state.index];
const activeChildRouter = childRouters[order[state.index]];
if (activeChildRouter) {
const activeChildState = activeChildRouter.getStateForAction(
action,
activeChildLastState
);
if (!activeChildState && inputState) {
return null;
}
if (activeChildState && activeChildState !== activeChildLastState) {
const routes = [...state.routes];
routes[state.index] = activeChildState;
return this.getNextState(prevState, {
...state,
routes,
});
}
}
// Handle tab changing. Do this after letting the current tab try to
// handle the action, to allow inner children to change first
if (backBehavior !== 'none') {
const isBackEligible =
action.key == null || action.key === activeChildLastState.key;
if (action.type === NavigationActions.BACK) {
if (isBackEligible && shouldBackNavigateToInitialRoute) {
activeChildIndex = initialRouteIndex;
} else {
return state;
}
}
}
let didNavigate = false;
if (action.type === NavigationActions.NAVIGATE) {
const navigateAction = action;
didNavigate = !!order.find((childId, i) => {
if (childId === navigateAction.routeName) {
activeChildIndex = i;
return true;
}
return false;
});
if (didNavigate) {
const childState = state.routes[activeChildIndex];
let newChildState;
const childRouter = childRouters[action.routeName];
if (action.action) {
newChildState = childRouter
? childRouter.getStateForAction(action.action, childState)
: null;
} else if (!childRouter && action.params) {
newChildState = {
...childState,
params: {
...(childState.params || {}),
...action.params,
},
};
}
if (newChildState && newChildState !== childState) {
const routes = [...state.routes];
routes[activeChildIndex] = newChildState;
return this.getNextState(prevState, {
...state,
routes,
index: activeChildIndex,
});
}
}
}
if (action.type === NavigationActions.SET_PARAMS) {
const key = action.key;
const lastRoute = state.routes.find(route => route.key === key);
if (lastRoute) {
const params = {
...lastRoute.params,
...action.params,
};
const routes = [...state.routes];
routes[state.routes.indexOf(lastRoute)] = {
...lastRoute,
params,
};
return this.getNextState(prevState, {
...state,
routes,
});
}
}
if (activeChildIndex !== state.index) {
return this.getNextState(prevState, {
...state,
index: activeChildIndex,
});
} else if (didNavigate && !inputState) {
return state;
} else if (didNavigate) {
return null;
}
// Let other children handle it and switch to the first child that returns a new state
let index = state.index;
let routes = state.routes;
order.find((childId, i) => {
const childRouter = childRouters[childId];
if (i === index) {
return false;
}
let childState = routes[i];
if (childRouter) {
childState = childRouter.getStateForAction(action, childState);
}
if (!childState) {
index = i;
return true;
}
if (childState !== routes[i]) {
routes = [...routes];
routes[i] = childState;
index = i;
return true;
}
return false;
});
// Nested routers can be updated after switching children with actions such as SET_PARAMS
// and COMPLETE_TRANSITION.
// NOTE: This may be problematic with custom routers because we whitelist the actions
// that can be handled by child routers without automatically changing index.
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
index = state.index;
}
if (index !== state.index || routes !== state.routes) {
return this.getNextState(prevState, {
...state,
index,
routes,
});
}
return state;
},
getComponentForState(state) {
const routeName = state.routes[state.index].routeName;
invariant(
routeName,
`There is no route defined for index ${state.index}. Check that
that you passed in a navigation state with a valid tab/screen index.`
);
const childRouter = childRouters[routeName];
if (childRouter) {
return childRouter.getComponentForState(state.routes[state.index]);
}
return getScreenForRouteName(routeConfigs, routeName);
},
getComponentForRouteName(routeName) {
return getScreenForRouteName(routeConfigs, routeName);
},
getPathAndParamsForState(state) {
const route = state.routes[state.index];
const routeName = order[state.index];
const subPath = paths[routeName];
const screen = getScreenForRouteName(routeConfigs, routeName);
let path = subPath;
let params = route.params;
if (screen && screen.router) {
const stateRoute = route;
// If it has a router it's a navigator.
// If it doesn't have router it's an ordinary React component.
const child = screen.router.getPathAndParamsForState(stateRoute);
path = subPath ? `${subPath}/${child.path}` : child.path;
params = child.params ? { ...params, ...child.params } : params;
}
return {
path,
params,
};
},
/**
* Gets an optional action, based on a relative path and query params.
*
* This will return null if there is no action matched
*/
getActionForPathAndParams(path, params) {
return (
order
.map(childId => {
const parts = path.split('/');
const pathToTest = paths[childId];
if (parts[0] === pathToTest) {
const childRouter = childRouters[childId];
const action = NavigationActions.navigate({
routeName: childId,
});
if (childRouter && childRouter.getActionForPathAndParams) {
action.action = childRouter.getActionForPathAndParams(
parts.slice(1).join('/'),
params
);
} else if (params) {
action.params = params;
}
return action;
}
return null;
})
.find(action => !!action) ||
order
.map(childId => {
const childRouter = childRouters[childId];
return (
childRouter && childRouter.getActionForPathAndParams(path, params)
);
})
.find(action => !!action) ||
null
);
},
getScreenOptions: createConfigGetter(
routeConfigs,
config.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@@ -30,8 +30,9 @@ export default (routeConfigs, config = {}) => {
paths[routeName] =
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
tabRouters[routeName] = null;
if (routeConfig.screen && routeConfig.screen.router) {
tabRouters[routeName] = routeConfig.screen.router;
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen.router) {
tabRouters[routeName] = screen.router;
}
});
if (initialRouteIndex === -1) {
@@ -239,7 +240,7 @@ export default (routeConfigs, config = {}) => {
},
getComponentForState(state) {
const routeName = order[state.index];
const routeName = state.routes[state.index].routeName;
invariant(
routeName,
`There is no route defined for index ${state.index}. Check that

View File

@@ -7,7 +7,6 @@ import TabRouter from '../TabRouter';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
@@ -19,7 +18,7 @@ const ROUTERS = {
StackRouter,
};
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
const dummyEventSubscriber = (name, handler) => ({
remove: () => {},
});
@@ -111,8 +110,8 @@ test('Handles no-op actions with tabs within stack router', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
expect(state1.routes[0].key).toEqual('id-0');
expect(state2.routes[0].key).toEqual('id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -140,7 +139,7 @@ test('Handles deep action', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -180,8 +179,8 @@ test('Supports lazily-evaluated getScreen', () => {
immediate: true,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
expect(state1.routes[0].key).toEqual('id-0');
expect(state2.routes[0].key).toEqual('id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(

View File

@@ -355,7 +355,7 @@ describe('StackRouter', () => {
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [{ key: 'Init-id-0', routeName: 'foo' }],
routes: [{ key: 'id-0', routeName: 'foo' }],
});
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'qux' }),
@@ -457,6 +457,35 @@ describe('StackRouter', () => {
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('popToTop targets StackRouter by key if specified', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.POP_TO_TOP,
key: state2.key,
},
state2
);
expect(state3 && state3.index).toEqual(0);
});
test('popToTop works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -513,6 +542,57 @@ describe('StackRouter', () => {
expect(pushedTwiceState.routes[2].routeName).toEqual('bar');
});
test('Navigate from top propagates to any arbitary depth of stacks', () => {
const GrandChildNavigator = () => <div />;
GrandChildNavigator.router = StackRouter({
Quux: { screen: () => <div /> },
Corge: { screen: () => <div /> },
});
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Woo: { screen: () => <div /> },
Qux: { screen: GrandChildNavigator },
});
const Parent = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = Parent.getStateForAction({ type: NavigationActions.INIT });
const state2 = Parent.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Corge',
},
state
);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge');
});
test('Navigate to initial screen is possible', () => {
const TestRouter = StackRouter(
{
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
},
{ initialRouteKey: 'foo' }
);
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
initState
);
expect(pushedState.index).toEqual(0);
expect(pushedState.routes[0].routeName).toEqual('foo');
});
test('Navigate with key is idempotent', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -553,6 +633,37 @@ describe('StackRouter', () => {
}).toThrow();
});
test('Navigate backwards with key removes leading routes', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
initState
);
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'b`' }),
pushedState
);
const pushedThriceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo', key: 'c`' }),
pushedTwiceState
);
expect(pushedThriceState.routes.length).toEqual(4);
const navigatedBackToFirstRouteState = TestRouter.getStateForAction(
NavigationActions.navigate({
routeName: 'foo',
key: pushedThriceState.routes[0].key,
}),
pushedThriceState
);
expect(navigatedBackToFirstRouteState.index).toEqual(0);
expect(navigatedBackToFirstRouteState.routes.length).toEqual(1);
});
test('Handle basic stack logic for plain components', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -571,7 +682,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -599,7 +710,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -696,7 +807,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -724,7 +835,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -798,7 +909,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -907,14 +1018,14 @@ describe('StackRouter', () => {
{
type: NavigationActions.SET_PARAMS,
params: { name: 'foobar' },
key: 'Init-id-0',
key: 'id-0',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Quux',
params: { name: 'foobar' },
},
@@ -1133,6 +1244,126 @@ describe('StackRouter', () => {
]);
});
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: 'test',
foo: 'bar',
},
},
},
});
expect(state).toEqual({
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-2',
params: { code: 'test', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-1',
params: { code: 'test', foo: 'bar', id: '4' },
routeName: 'profile',
routes: [
{
key: 'id-0',
params: { code: 'test', foo: 'bar', id: '10259959195' },
routeName: 'list',
type: undefined,
},
],
},
],
},
],
});
const state2 = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: '',
foo: 'bar',
},
},
},
});
expect(state2).toEqual({
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-5',
params: { code: '', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-4',
params: { code: '', foo: 'bar', id: '4' },
routeName: 'profile',
routes: [
{
key: 'id-3',
params: { code: '', foo: 'bar', id: '10259959195' },
routeName: 'list',
type: undefined,
},
],
},
],
},
],
});
});
test('Handles the navigate action with params and nested TabRouter', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = TabRouter({
@@ -1321,38 +1552,26 @@ describe('StackRouter', () => {
}
});
test('Maps old actions (uses "Handles the reset action" test)', () => {
global.console.warn = jest.fn();
const router = StackRouter({
Foo: {
screen: () => <div />,
test('URI encoded string get passed to deep link', () => {
const uri = 'people/2018%2F02%2F07';
const action = TestStackRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
routeName: 'person',
params: {
id: '2018/02/07',
},
Bar: {
screen: () => <div />,
type: NavigationActions.NAVIGATE,
});
const malformedUri = 'people/%E0%A4%A';
const action2 = TestStackRouter.getActionForPathAndParams(malformedUri);
expect(action2).toEqual({
routeName: 'person',
params: {
id: '%E0%A4%A',
},
type: NavigationActions.NAVIGATE,
});
const initAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Init',
});
const state = router.getStateForAction(initAction);
const resetAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Reset',
actions: [
{ type: 'Navigate', routeName: 'Foo', params: { bar: '42' } },
{ type: 'Navigate', routeName: 'Bar' },
],
index: 1,
});
const state2 = router.getStateForAction(resetAction, state);
expect(state2 && state2.index).toEqual(1);
expect(state2 && state2.routes[0].params).toEqual({ bar: '42' });
expect(state2 && state2.routes[0].routeName).toEqual('Foo');
expect(state2 && state2.routes[1].routeName).toEqual('Bar');
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"The action type 'Init' has been renamed to 'Navigation/INIT'"
)
);
});
test('Querystring params get passed to nested deep link', () => {

View File

@@ -0,0 +1,109 @@
/* eslint react/display-name:0 */
import React from 'react';
import SwitchRouter from '../SwitchRouter';
import StackRouter from '../StackRouter';
import NavigationActions from '../../NavigationActions';
describe('SwitchRouter', () => {
test('resets the route when unfocusing a tab by default', () => {
const router = getExampleRouter();
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
state
);
expect(state2.routes[0].index).toEqual(1);
expect(state2.routes[0].routes.length).toEqual(2);
const state3 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state2
);
expect(state3.routes[0].index).toEqual(0);
expect(state3.routes[0].routes.length).toEqual(1);
});
test('does not reset the route on unfocus if resetOnBlur is false', () => {
const router = getExampleRouter({ resetOnBlur: false });
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
state
);
expect(state2.routes[0].index).toEqual(1);
expect(state2.routes[0].routes.length).toEqual(2);
const state3 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state2
);
expect(state3.routes[0].index).toEqual(1);
expect(state3.routes[0].routes.length).toEqual(2);
});
test('ignores back by default', () => {
const router = getExampleRouter();
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state
);
expect(state2.index).toEqual(1);
const state3 = router.getStateForAction(
{ type: NavigationActions.BACK },
state2
);
expect(state3.index).toEqual(1);
});
test('handles back if given a backBehavior', () => {
const router = getExampleRouter({ backBehavior: 'initialRoute' });
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state
);
expect(state2.index).toEqual(1);
const state3 = router.getStateForAction(
{ type: NavigationActions.BACK },
state2
);
expect(state3.index).toEqual(0);
});
});
const getExampleRouter = (config = {}) => {
const PlainScreen = () => <div />;
const StackA = () => <div />;
const StackB = () => <div />;
StackA.router = StackRouter({
A1: PlainScreen,
A2: PlainScreen,
});
StackB.router = StackRouter({
B1: PlainScreen,
B2: PlainScreen,
});
const router = SwitchRouter(
{
A: StackA,
B: StackB,
},
{
initialRouteName: 'A',
...config,
}
);
return router;
};

View File

@@ -593,29 +593,6 @@ describe('TabRouter', () => {
expect(path).toEqual('f/Baz');
});
test('Maps old actions (uses "getStateForAction returns null when navigating to same tab" test)', () => {
global.console.warn = jest.fn();
const router = TabRouter(
{ Foo: BareLeafRouteConfig, Bar: BareLeafRouteConfig },
{ initialRouteName: 'Bar' }
);
const initAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Init',
});
const state = router.getStateForAction(initAction);
const navigateAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Navigate',
routeName: 'Bar',
});
const state2 = router.getStateForAction(navigateAction, state);
expect(state2).toEqual(null);
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"The action type 'Init' has been renamed to 'Navigation/INIT'"
)
);
});
test('Can navigate to other tab (no router) with params', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;

View File

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateRouteConfigMap Fails if both screen and getScreen are defined 1`] = `"Route 'Home' should declare a screen or a getScreen, not both."`;
exports[`validateRouteConfigMap Fails on bad object 1`] = `
"The component for route 'Home' must be a React component. For example:
import MyScreen from './MyScreen';
...
Home: MyScreen,
}
You can also use a navigator:
import MyNavigator from './MyNavigator';
...
Home: MyNavigator,
}"
`;
exports[`validateRouteConfigMap Fails on empty bare screen 1`] = `
"The component for route 'Home' must be a React component. For example:
import MyScreen from './MyScreen';
...
Home: MyScreen,
}
You can also use a navigator:
import MyNavigator from './MyNavigator';
...
Home: MyNavigator,
}"
`;
exports[`validateRouteConfigMap Fails on empty config 1`] = `"Please specify at least one route when configuring a navigator."`;

View File

@@ -175,23 +175,3 @@ test('should throw if the route does not exist', () => {
"There is no route defined for key Settings.\nMust be one of: 'Home'"
);
});
test('should throw if the screen is not defined under the route config', () => {
/* eslint-disable react/no-multi-comp */
const getScreenOptions = createConfigGetter({
Home: {},
});
const routes = [{ key: 'B', routeName: 'Home' }];
expect(() =>
getScreenOptions(
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
})
)
).toThrowError('Route Home must define a screen or a getScreen.');
});

View File

@@ -13,9 +13,19 @@ ProfileNavigator.router = StackRouter({
});
describe('validateRouteConfigMap', () => {
test('Fails on empty bare screen', () => {
const invalidMap = {
Home: undefined,
};
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails on empty config', () => {
const invalidMap = {};
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails on bad object', () => {
const invalidMap = {
@@ -23,7 +33,9 @@ describe('validateRouteConfigMap', () => {
foo: 'bar',
},
};
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails if both screen and getScreen are defined', () => {
const invalidMap = {
@@ -32,17 +44,17 @@ describe('validateRouteConfigMap', () => {
getScreen: () => ListScreen,
},
};
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Succeeds on a valid config', () => {
const invalidMap = {
const validMap = {
Home: {
screen: ProfileNavigator,
},
Chat: {
screen: ListScreen,
},
Chat: ListScreen,
};
validateRouteConfigMap(invalidMap);
validateRouteConfigMap(validMap);
});
});

View File

@@ -32,5 +32,5 @@ export default function getScreenForRouteName(routeConfigs, routeName) {
return screen;
}
throw new Error(`Route ${routeName} must define a screen or a getScreen.`);
return routeConfig;
}

View File

@@ -13,46 +13,44 @@ function validateRouteConfigMap(routeConfigs) {
routeNames.forEach(routeName => {
const routeConfig = routeConfigs[routeName];
if (!routeConfig.screen && !routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen. ` +
'For example:\n\n' +
"import MyScreen from './MyScreen';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyScreen,\n' +
'}'
);
} else if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
);
}
const screenComponent = getScreenComponent(routeConfig);
if (
routeConfig.screen &&
typeof routeConfig.screen !== 'function' &&
typeof routeConfig.screen !== 'string'
!screenComponent ||
(typeof screenComponent !== 'function' &&
typeof screenComponent !== 'string' &&
!routeConfig.getScreen)
) {
throw new Error(
`The component for route '${routeName}' must be a ` +
'React component. For example:\n\n' +
"import MyScreen from './MyScreen';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyScreen,\n' +
`${routeName}: MyScreen,\n` +
'}\n\n' +
'You can also use a navigator:\n\n' +
"import MyNavigator from './MyNavigator';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyNavigator,\n' +
`${routeName}: MyNavigator,\n` +
'}'
);
}
if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
);
}
});
}
function getScreenComponent(routeConfig) {
if (!routeConfig) {
return null;
}
return routeConfig.screen ? routeConfig.screen : routeConfig;
}
export default validateRouteConfigMap;

View File

@@ -22,7 +22,7 @@ class Card extends React.Component {
const styles = StyleSheet.create({
main: {
backgroundColor: '#E9E9EF',
backgroundColor: '#EFEFF4',
bottom: 0,
left: 0,
position: 'absolute',

View File

@@ -19,6 +19,7 @@ import getChildEventSubscriber from '../../getChildEventSubscriber';
import SceneView from '../SceneView';
import TransitionConfigs from './TransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
@@ -81,11 +82,13 @@ class CardStack extends React.Component {
_screenDetails = {};
_childEventSubscribers = {};
componentWillReceiveProps(props) {
if (props.screenProps !== this.props.screenProps) {
this._screenDetails = {};
}
props.scenes.forEach(newScene => {
props.transitionProps.scenes.forEach(newScene => {
if (
this._screenDetails[newScene.key] &&
this._screenDetails[newScene.key].state !== newScene.route
@@ -95,17 +98,39 @@ class CardStack extends React.Component {
});
}
componentDidUpdate() {
const activeKeys = this.props.transitionProps.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
_isRouteFocused = route => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_getScreenDetails = scene => {
const { screenProps, navigation, router } = this.props;
const { screenProps, transitionProps: { navigation }, router } = this.props;
let screenDetails = this._screenDetails[scene.key];
if (!screenDetails || screenDetails.state !== scene.route) {
if (!this._childEventSubscribers[scene.route.key]) {
this._childEventSubscribers[scene.route.key] = getChildEventSubscriber(
navigation.addListener,
scene.route.key
);
}
const screenNavigation = addNavigationHelpers({
dispatch: navigation.dispatch,
state: scene.route,
addListener: getChildEventSubscriber(
navigation.addListener,
scene.route.key
),
isFocused: this._isRouteFocused.bind(this, scene.route),
addListener: this._childEventSubscribers[scene.route.key],
});
screenDetails = {
state: scene.route,
@@ -131,12 +156,19 @@ class CardStack extends React.Component {
headerRightInterpolator,
} = this._getTransitionConfig();
const { mode, ...passProps } = this.props;
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
getScreenDetails: this._getScreenDetails,
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
@@ -153,34 +185,42 @@ class CardStack extends React.Component {
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.layout.width);
animatedSubscribeValue(props.layout.height);
animatedSubscribeValue(props.position);
animatedSubscribeValue(props.transitionProps.layout.width);
animatedSubscribeValue(props.transitionProps.layout.height);
animatedSubscribeValue(props.transitionProps.position);
}
_reset(resetToIndex, duration) {
Animated.timing(this.props.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.position.__isNative,
}).start();
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(this.props.transitionProps.position, {
toValue: resetToIndex,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
} else {
Animated.timing(this.props.transitionProps.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
}
}
_goBack(backFromIndex, duration) {
const { navigation, position, scenes } = this.props;
const { navigation, position, scenes } = this.props.transitionProps;
const toValue = Math.max(backFromIndex - 1, 0);
// set temporary index for gesture handler to respect until the action is
// dispatched at the end of the transition.
this._immediateIndex = toValue;
Animated.timing(position, {
toValue,
duration,
easing: EaseInOut,
useNativeDriver: position.__isNative,
}).start(() => {
const onCompleteAnimation = () => {
this._immediateIndex = null;
const backFromScene = scenes.find(s => s.index === toValue + 1);
if (!this._isResponding && backFromScene) {
@@ -191,16 +231,42 @@ class CardStack extends React.Component {
})
);
}
});
};
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(position, {
toValue,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
} else {
Animated.timing(position, {
toValue,
duration,
easing: EaseInOut,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
}
}
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
floatingHeader = this._renderHeader(this.props.scene, headerMode);
floatingHeader = this._renderHeader(
this.props.transitionProps.scene,
headerMode
);
}
const { navigation, position, layout, scene, scenes, mode } = this.props;
const {
transitionProps: { navigation, position, layout, scene, scenes },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = this._getScreenDetails(scene);
@@ -363,6 +429,21 @@ class CardStack extends React.Component {
return 'float';
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
_renderInnerScene(SceneComponent, scene) {
const { navigation } = this._getScreenDetails(scene);
const { screenProps } = this.props;
@@ -395,8 +476,8 @@ class CardStack extends React.Component {
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
{},
{},
this.props.transitionProps,
this.props.prevTransitionProps,
isModal
);
};
@@ -404,15 +485,19 @@ class CardStack extends React.Component {
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator && screenInterpolator({ ...this.props, scene });
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
const SceneComponent = this.props.router.getComponentForRouteName(
scene.route.routeName
);
const { transitionProps, ...props } = this.props;
return (
<Card
{...this.props}
{...props}
{...transitionProps}
key={`card_${scene.key}`}
style={[style, this.props.cardStyle]}
scene={scene}

View File

@@ -53,10 +53,11 @@ class CardStackTransitioner extends React.Component {
return transitionSpec;
};
_render = props => {
_render = (props, prevProps) => {
const {
screenProps,
headerMode,
headerTransitionPreset,
mode,
router,
cardStyle,
@@ -66,11 +67,13 @@ class CardStackTransitioner extends React.Component {
<CardStack
screenProps={screenProps}
headerMode={headerMode}
headerTransitionPreset={headerTransitionPreset}
mode={mode}
router={router}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
{...props}
transitionProps={props}
prevTransitionProps={prevProps}
/>
);
};

View File

@@ -21,6 +21,8 @@ const DrawerNavigatorItems = ({
itemsContainerStyle,
itemStyle,
labelStyle,
activeLabelStyle,
inactiveLabelStyle,
iconContainerStyle,
drawerPosition,
}) => (
@@ -34,6 +36,7 @@ const DrawerNavigatorItems = ({
const scene = { route, index, focused, tintColor: color };
const icon = renderIcon(scene);
const label = getLabel(scene);
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
return (
<TouchableItem
key={route.key}
@@ -63,7 +66,9 @@ const DrawerNavigatorItems = ({
</View>
) : null}
{typeof label === 'string' ? (
<Text style={[styles.label, { color }, labelStyle]}>
<Text
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
>
{label}
</Text>
) : (

View File

@@ -17,6 +17,8 @@ export default class DrawerView extends React.PureComponent {
: this.props.drawerWidth,
};
_childEventSubscribers = {};
componentWillMount() {
this._updateScreenNavigation(this.props.navigation);
@@ -27,6 +29,17 @@ export default class DrawerView extends React.PureComponent {
Dimensions.removeEventListener('change', this._updateWidth);
}
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
componentWillReceiveProps(nextProps) {
if (
this.props.navigation.state.index !== nextProps.navigation.state.index
@@ -68,6 +81,12 @@ export default class DrawerView extends React.PureComponent {
}
};
_isRouteFocused = route => () => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_updateScreenNavigation = navigation => {
const { drawerCloseRoute } = this.props;
const navigationState = navigation.state.routes.find(
@@ -79,13 +98,18 @@ export default class DrawerView extends React.PureComponent {
) {
return;
}
if (!this._childEventSubscribers[navigationState.key]) {
this._childEventSubscribers[
navigationState.key
] = getChildEventSubscriber(navigation.addListener, navigationState.key);
}
this._screenNavigationProp = addNavigationHelpers({
dispatch: navigation.dispatch,
state: navigationState,
addListener: getChildEventSubscriber(
navigation.addListener,
navigationState.key
),
isFocused: this._isRouteFocused.bind(this, navigationState),
addListener: this._childEventSubscribers[navigationState.key],
});
};

View File

@@ -3,15 +3,18 @@ import React from 'react';
import {
Animated,
Dimensions,
Image,
Platform,
StyleSheet,
View,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import withOrientation from '../withOrientation';
@@ -19,9 +22,18 @@ const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad ? 32 : 44
: 56;
};
class Header extends React.PureComponent {
static defaultProps = {
leftInterpolator: HeaderStyleInterpolator.forLeft,
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
titleInterpolator: HeaderStyleInterpolator.forCenter,
rightInterpolator: HeaderStyleInterpolator.forRight,
};
@@ -116,15 +128,18 @@ class Header extends React.PureComponent {
_renderLeftComponent = props => {
const { options } = this.props.getScreenDetails(props.scene);
if (
React.isValidElement(options.headerLeft) ||
options.headerLeft === null
) {
return options.headerLeft;
}
if (props.scene.index === 0) {
return null;
return;
}
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
@@ -147,6 +162,36 @@ class Header extends React.PureComponent {
);
};
_renderModularLeftComponent = (
props,
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options } = this.props.getScreenDetails(props.scene);
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
return (
<ModularHeaderBackButton
onPress={this._navigateBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderRightComponent = props => {
const details = this.props.getScreenDetails(props.scene);
const { headerRight } = details.options;
@@ -154,16 +199,38 @@ class Header extends React.PureComponent {
};
_renderLeft(props) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
const { options } = this.props.getScreenDetails(props.scene);
const { transitionPreset } = this.props;
// On Android, or if we have a custom header left, or if we have a custom back image, we
// do not use the modular header (which is the one that imitates UINavigationController)
if (
transitionPreset !== 'uikit' ||
options.headerBackImage ||
options.headerLeft ||
options.headerLeft === null
) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
} else {
return this._renderModularSubView(
props,
'left',
this._renderModularLeftComponent,
this.props.leftLabelInterpolator,
this.props.leftButtonInterpolator
);
}
}
_renderTitle(props, options) {
const style = {};
const { transitionPreset } = this.props;
if (Platform.OS === 'android') {
if (!options.hasLeftComponent) {
@@ -185,7 +252,9 @@ class Header extends React.PureComponent {
{ ...props, style },
'title',
this._renderTitleComponent,
this.props.titleInterpolator
transitionPreset === 'uikit'
? this.props.titleFromLeftInterpolator
: this.props.titleInterpolator
);
}
@@ -198,6 +267,64 @@ class Header extends React.PureComponent {
);
}
_renderModularSubView(
props,
name,
renderer,
labelStyleInterpolator,
buttonStyleInterpolator
) {
const { scene } = props;
const { index, isStale, key } = scene;
// Never render a modular back button on the first screen in a stack.
if (index === 0) {
return;
}
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const ButtonContainer = ({ children }) => (
<Animated.View
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const LabelContainer = ({ children }) => (
<Animated.View
style={[labelStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const subView = renderer(props, ButtonContainer, LabelContainer);
if (subView === null) {
return subView;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<View
key={`${name}_${key}`}
pointerEvents={pointerEvents}
style={[styles.item, styles[name], props.style]}
>
{subView}
</View>
);
}
_renderSubView(props, name, renderer, styleInterpolator) {
const { scene } = props;
const { index, isStale, key } = scene;
@@ -246,22 +373,54 @@ class Header extends React.PureComponent {
hasRightComponent: !!right,
});
return (
<View
style={[StyleSheet.absoluteFill, styles.header]}
key={`scene_${props.scene.key}`}
>
{title}
{left}
{right}
</View>
);
const { isLandscape, transitionPreset } = this.props;
const { options } = this.props.getScreenDetails(props.scene);
const wrapperProps = {
style: styles.header,
key: `scene_${props.scene.key}`,
};
if (
options.headerLeft ||
options.headerBackImage ||
Platform.OS !== 'ios' ||
transitionPreset !== 'uikit'
) {
return (
<View {...wrapperProps}>
{title}
{left}
{right}
</View>
);
} else {
return (
<MaskedViewIOS
{...wrapperProps}
maskElement={
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
/>
<View style={styles.iconMaskFillerRect} />
</View>
}
>
{title}
{left}
{right}
</MaskedViewIOS>
);
}
}
render() {
let appBar;
const { mode, scene, isLandscape } = this.props;
if (this.props.mode === 'float') {
if (mode === 'float') {
const scenesByIndex = {};
this.props.scenes.forEach(scene => {
scenesByIndex[scene.index] = scene;
@@ -280,38 +439,59 @@ class Header extends React.PureComponent {
});
}
// eslint-disable-next-line no-unused-vars
const {
scenes,
scene,
position,
screenProps,
progress,
isLandscape,
...rest
} = this.props;
const { options } = this.props.getScreenDetails(scene);
const { headerStyle } = options;
const appBarHeight =
Platform.OS === 'ios' ? (isLandscape && !Platform.isPad ? 32 : 44) : 56;
const { headerStyle = {} } = options;
const headerStyleObj = StyleSheet.flatten(headerStyle);
const appBarHeight = getAppBarHeight(isLandscape);
const {
alignItems,
justifyContent,
flex,
flexDirection,
flexGrow,
flexShrink,
flexBasis,
flexWrap,
...safeHeaderStyle
} = headerStyleObj;
if (__DEV__) {
warnIfHeaderStyleDefined(alignItems, 'alignItems');
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
warnIfHeaderStyleDefined(flex, 'flex');
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
}
// TODO: warn if any unsafe styles are provided
const containerStyles = [
styles.container,
{
height: appBarHeight,
},
headerStyle,
options.headerTransparent
? styles.transparentContainer
: styles.container,
{ height: appBarHeight },
safeHeaderStyle,
];
const { headerForceInset } = options;
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
return (
<Animated.View {...rest}>
<SafeAreaView
style={containerStyles}
forceInset={{ top: 'always', bottom: 'never' }}
>
<View style={styles.appBar}>{appBar}</View>
</SafeAreaView>
</Animated.View>
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
<View style={{ flex: 1 }}>{appBar}</View>
</SafeAreaView>
);
}
}
function warnIfHeaderStyleDefined(value, styleProp) {
if (value !== undefined) {
console.warn(
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
);
}
}
@@ -320,7 +500,7 @@ let platformContainerStyles;
if (Platform.OS === 'ios') {
platformContainerStyles = {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0, 0, 0, .3)',
borderBottomColor: '#A7A7AA',
};
} else {
platformContainerStyles = {
@@ -339,36 +519,64 @@ const styles = StyleSheet.create({
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
...platformContainerStyles,
},
appBar: {
flex: 1,
transparentContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
...platformContainerStyles,
},
header: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
},
item: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
},
iconMaskContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
iconMaskFillerRect: {
flex: 1,
backgroundColor: '#d8d8d8',
marginLeft: -3,
},
iconMask: {
// These are mostly the same as the icon in ModularHeaderBackButton
height: 21,
width: 12,
marginLeft: 9,
marginTop: -0.5, // resizes down to 20.5
alignSelf: 'center',
resizeMode: 'contain',
},
title: {
bottom: 0,
top: 0,
left: TITLE_OFFSET,
right: TITLE_OFFSET,
top: 0,
position: 'absolute',
alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start',
alignItems: 'center',
flexDirection: 'row',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
},
left: {
left: 0,
bottom: 0,
top: 0,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
},
right: {
right: 0,
bottom: 0,
top: 0,
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
},
});

View File

@@ -107,7 +107,7 @@ const styles = StyleSheet.create({
? {
height: 21,
width: 13,
marginLeft: 10,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
@@ -123,7 +123,7 @@ const styles = StyleSheet.create({
iconWithTitle:
Platform.OS === 'ios'
? {
marginRight: 5,
marginRight: 6,
}
: {},
});

View File

@@ -1,7 +1,11 @@
import { I18nManager } from 'react-native';
import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
const crossFadeInterpolation = (first, index, last) => ({
inputRange: [first, index - 0.9, index - 0.2, index, last],
outputRange: [0, 0, 0.3, 1, 0],
});
/**
* Utility that builds the style for the navigation header.
*
@@ -23,16 +27,7 @@ function forLeft(props) {
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0, 1, 0, 0],
}),
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -44,21 +39,9 @@ function forCenter(props) {
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index, last];
return {
opacity: position.interpolate({
inputRange,
outputRange: [0, 1, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange,
outputRange: I18nManager.isRTL ? [-200, 0, 200] : [200, 0, -200],
}),
},
],
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -70,16 +53,125 @@ function forRight(props) {
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
/**
* iOS UINavigationController style interpolators
*/
function forLeftButton(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [first, index, last],
outputRange: [0, 1, 0],
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0.5, 1, 0.5, 0],
}),
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. See the comment on title for more information.
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
function forLeftLabel(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const offset = LEFT_LABEL_OFFSET;
return {
// For now we fade out the label before fading in the title, so the
// differences between the label and title position can be hopefully not so
// noticable to the user
opacity: position.interpolate({
inputRange: [first, index - 0.35, index, index + 0.5, last],
outputRange: [0, 0, 1, 0.5, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset * 1.5],
}),
},
],
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. We want the back button label to transition
* smoothly into the title text and to do this we need to understand
* where the title is positioned within the title container (since it is
* centered).
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
function forCenterFromLeft(props) {
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index - 0.5, index, index + 0.5, last];
const offset = TITLE_OFFSET_IOS;
return {
opacity: position.interpolate({
inputRange: [first, index - 0.5, index, index + 0.7, last],
outputRange: [0, 0, 1, 0, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset],
}),
},
],
};
}
export default {
forLeft,
forLeftButton,
forLeftLabel,
forCenterFromLeft,
forCenter,
forRight,
};

View File

@@ -15,7 +15,7 @@ const HeaderTitle = ({ style, ...rest }) => (
const styles = StyleSheet.create({
title: {
fontSize: Platform.OS === 'ios' ? 17 : 20,
fontWeight: Platform.OS === 'ios' ? '600' : '500',
fontWeight: Platform.OS === 'ios' ? '700' : '500',
color: 'rgba(0, 0, 0, .9)',
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
marginHorizontal: 16,

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { I18nManager, Image, Text, View, StyleSheet } from 'react-native';
import TouchableItem from '../TouchableItem';
class ModularHeaderBackButton extends React.PureComponent {
static defaultProps = {
tintColor: '#037aff',
truncatedTitle: 'Back',
// eslint-disable-next-line global-require
buttonImage: require('../assets/back-icon.png'),
};
state = {};
_onTextLayout = e => {
if (this.state.initialTextWidth) {
return;
}
this.setState({
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
});
};
render() {
const {
buttonImage,
onPress,
width,
title,
titleStyle,
tintColor,
truncatedTitle,
} = this.props;
const renderTruncated =
this.state.initialTextWidth && width
? this.state.initialTextWidth > width
: false;
let backButtonTitle = renderTruncated ? truncatedTitle : title;
// TODO: When we've sorted out measuring in the header, let's revisit this.
// This is clearly a bad workaround.
if (backButtonTitle && backButtonTitle.length > 8) {
backButtonTitle = truncatedTitle;
}
const { ButtonContainerComponent, LabelContainerComponent } = this.props;
return (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={backButtonTitle}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
style={styles.container}
borderless
>
<View style={styles.container}>
<ButtonContainerComponent>
<Image
style={[
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
]}
source={buttonImage}
/>
</ButtonContainerComponent>
{typeof backButtonTitle === 'string' && (
<LabelContainerComponent>
<Text
onLayout={this._onTextLayout}
style={[
styles.title,
!!tintColor && { color: tintColor },
titleStyle,
]}
numberOfLines={1}
>
{backButtonTitle}
</Text>
</LabelContainerComponent>
)}
</View>
</TouchableItem>
);
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
backgroundColor: 'transparent',
},
title: {
fontSize: 17,
paddingRight: 10,
},
icon: {
height: 21,
width: 12,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
iconWithTitle: {
marginRight: 3,
},
});
export default ModularHeaderBackButton;

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import SceneView from './SceneView';
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
export default class ResourceSavingSceneView extends React.PureComponent {
constructor(props) {
super();
const key = props.childNavigation.state.key;
const focusedIndex = props.navigation.state.index;
const focusedKey = props.navigation.state.routes[focusedIndex].key;
const isFocused = key === focusedKey;
this.state = {
awake: props.lazy ? isFocused : true,
visible: isFocused,
};
}
componentWillMount() {
this._actionListener = this.props.navigation.addListener(
'action',
this._onAction
);
}
componentWillUnmount() {
this._actionListener.remove();
}
render() {
const { awake, visible } = this.state;
const {
childNavigation,
navigation,
removeClippedSubviews,
lazy,
...rest
} = this.props;
return (
<View
style={styles.container}
collapsable={false}
removeClippedSubviews={
Platform.OS === 'android'
? removeClippedSubviews
: !visible && removeClippedSubviews
}
>
<View
style={
this._mustAlwaysBeVisible() || visible
? styles.innerAttached
: styles.innerDetached
}
>
{awake ? <SceneView {...rest} navigation={childNavigation} /> : null}
</View>
</View>
);
}
_mustAlwaysBeVisible = () => {
return this.props.animationEnabled || this.props.swipeEnabled;
};
_onAction = payload => {
// We do not care about transition complete events, they won't actually change the state
if (
payload.action.type == 'Navigation/COMPLETE_TRANSITION' ||
!payload.state
) {
return;
}
const { routes, index } = payload.state;
const key = this.props.childNavigation.state.key;
if (routes[index].key === key) {
if (!this.state.visible) {
let nextState = { visible: true };
if (!this.state.awake) {
nextState.awake = true;
}
this.setState(nextState);
}
} else {
if (this.state.visible) {
this.setState({ visible: false });
}
}
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'hidden',
},
innerAttached: {
flex: 1,
},
innerDetached: {
flex: 1,
top: FAR_FAR_AWAY,
},
});

View File

@@ -0,0 +1,27 @@
import React from 'react';
import SceneView from '../SceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
class SwitchContainer extends React.Component {
render() {
const { screenProps } = this.props;
const route = this.props.navigation.state.routes[
this.props.navigation.state.index
];
const childNavigation = this.props.childNavigationProps[route.key];
const ChildComponent = this.props.router.getComponentForRouteName(
route.routeName
);
return (
<SceneView
component={ChildComponent}
navigation={childNavigation}
screenProps={screenProps}
/>
);
}
}
export default withCachedChildNavigation(SwitchContainer);

View File

@@ -10,6 +10,7 @@ import {
import SafeAreaView from 'react-native-safe-area-view';
import TabBarIcon from './TabBarIcon';
import NavigationActions from '../../NavigationActions';
import withOrientation from '../withOrientation';
const majorVersion = parseInt(Platform.Version, 10);
@@ -63,6 +64,7 @@ class TabBarBottom extends React.PureComponent {
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color },
@@ -98,6 +100,7 @@ class TabBarBottom extends React.PureComponent {
if (showIcon === false) {
return null;
}
return (
<TabBarIcon
position={position}
@@ -106,7 +109,11 @@ class TabBarBottom extends React.PureComponent {
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
scene={scene}
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
style={
showLabel && this._shouldUseHorizontalTabs()
? styles.horizontalIcon
: styles.icon
}
/>
);
};
@@ -175,6 +182,24 @@ class TabBarBottom extends React.PureComponent {
}
}
_handleTabPress = index => {
const { jumpToIndex, navigation } = this.props;
const currentIndex = navigation.state.index;
if (currentIndex === index) {
let childRoute = navigation.state.routes[index];
if (childRoute.hasOwnProperty('index') && childRoute.index > 0) {
navigation.dispatch(
NavigationActions.popToTop({ key: childRoute.key })
);
} else {
// TODO: do something to scroll to top
}
} else {
jumpToIndex(index);
}
};
render() {
const {
position,
@@ -234,8 +259,13 @@ class TabBarBottom extends React.PureComponent {
accessibilityLabel={accessibilityLabel}
onPress={() =>
onPress
? onPress({ previousScene, scene, jumpToIndex })
: jumpToIndex(index)
? onPress({
previousScene,
scene,
jumpToIndex,
defaultHandler: this._handleTabPress,
})
: this._handleTabPress(index)
}
>
<Animated.View style={[styles.tab, { backgroundColor }]}>
@@ -261,6 +291,9 @@ class TabBarBottom extends React.PureComponent {
}
}
const DEFAULT_HEIGHT = 49;
const COMPACT_HEIGHT = 29;
const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10
@@ -269,10 +302,10 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
tabBarCompact: {
height: 29,
height: COMPACT_HEIGHT,
},
tabBarRegular: {
height: 49,
height: DEFAULT_HEIGHT,
},
tab: {
flex: 1,
@@ -289,6 +322,9 @@ const styles = StyleSheet.create({
icon: {
flexGrow: 1,
},
horizontalIcon: {
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',

View File

@@ -23,6 +23,7 @@ export default class TabBarIcon extends React.PureComponent {
inputRange,
outputRange: inputRange.map(i => (i === index ? 0 : 1)),
});
// We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them.
return (
@@ -53,12 +54,11 @@ const styles = StyleSheet.create({
// We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them:
// Cover the whole iconContainer:
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
alignSelf: 'center',
height: '100%',
justifyContent: 'center',
position: 'absolute',
width: '100%',
},
});

View File

@@ -91,7 +91,15 @@ export default class TabBarTop extends React.PureComponent {
const onPress = getOnPress(previousScene, scene);
if (onPress) {
onPress({ previousScene, scene, jumpToIndex });
// To maintain the same API as `TabbarBottom`, we pass in a `defaultHandler`
// even though I don't believe in this case it should be any different
// than `jumpToIndex`.
onPress({
previousScene,
scene,
jumpToIndex,
defaultHandler: jumpToIndex,
});
} else {
jumpToIndex(scene.index);
}

View File

@@ -3,11 +3,13 @@ import { View, StyleSheet, Platform } from 'react-native';
import { TabViewAnimated, TabViewPagerPan } from 'react-native-tab-view';
import SafeAreaView from 'react-native-safe-area-view';
import SceneView from '../SceneView';
import ResourceSavingSceneView from '../ResourceSavingSceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
class TabView extends React.PureComponent {
static defaultProps = {
lazy: true,
removedClippedSubviews: true,
// fix for https://github.com/react-native-community/react-native-tab-view/issues/312
initialLayout: Platform.select({
android: { width: 1, height: 0 },
@@ -25,14 +27,18 @@ class TabView extends React.PureComponent {
const TabComponent = this.props.router.getComponentForRouteName(
route.routeName
);
return (
<View style={styles.page}>
<SceneView
screenProps={screenProps}
component={TabComponent}
navigation={childNavigation}
/>
</View>
<ResourceSavingSceneView
lazy={this.props.lazy}
removeClippedSubViews={this.props.removeClippedSubviews}
animationEnabled={this.props.animationEnabled}
swipeEnabled={this.props.swipeEnabled}
screenProps={screenProps}
component={TabComponent}
navigation={this.props.navigation}
childNavigation={childNavigation}
/>
);
};
@@ -140,11 +146,15 @@ class TabView extends React.PureComponent {
const tabBarVisible =
options.tabBarVisible == null ? true : options.tabBarVisible;
const swipeEnabled =
let swipeEnabled =
options.swipeEnabled == null
? this.props.swipeEnabled
: options.swipeEnabled;
if (typeof swipeEnabled === 'function') {
swipeEnabled = swipeEnabled(state);
}
if (tabBarComponent !== undefined && tabBarVisible) {
if (tabBarPosition === 'bottom') {
renderFooter = this._renderTabBar;
@@ -185,9 +195,4 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
page: {
flex: 1,
overflow: 'hidden',
},
});

View File

@@ -179,9 +179,19 @@ class Transitioner extends React.Component {
const prevTransitionProps = this._prevTransitionProps;
this._prevTransitionProps = null;
const scenes = this.state.scenes.filter(isSceneNotStale);
const nextState = {
...this.state,
scenes: this.state.scenes.filter(isSceneNotStale),
/**
* Array.prototype.filter creates a new instance of an array
* even if there were no elements removed. There are cases when
* `this.state.scenes` will have no stale scenes (typically when
* pushing a new route). As a result, components that rely on this prop
* might enter an unnecessary render cycle.
*/
scenes:
this.state.scenes.length === scenes.length ? this.state.scenes : scenes,
};
this._transitionProps = buildTransitionProps(this.props, nextState);

View File

@@ -93,13 +93,12 @@ exports[`TabBarBottom renders successfully 1`] = `
style={
Object {
"alignItems": "center",
"bottom": 0,
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"width": "100%",
}
}
/>
@@ -108,13 +107,12 @@ exports[`TabBarBottom renders successfully 1`] = `
style={
Object {
"alignItems": "center",
"bottom": 0,
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"width": "100%",
}
}
/>
@@ -124,6 +122,7 @@ exports[`TabBarBottom renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",
@@ -224,6 +223,8 @@ exports[`TabBarBottom renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
@@ -232,25 +233,35 @@ exports[`TabBarBottom renders successfully 1`] = `
}
>
<View
navigation={
style={
Object {
"addListener": [Function],
"dispatch": undefined,
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
"routeName": "s1",
},
"flex": 1,
}
}
screenProps={undefined}
/>
>
<View
navigation={
Object {
"addListener": [Function],
"dispatch": undefined,
"getParam": [Function],
"goBack": [Function],
"isFocused": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
"routeName": "s1",
},
}
}
screenProps={undefined}
/>
</View>
</View>
</View>
</RCTScrollContentView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -3,16 +3,25 @@ import propTypes from 'prop-types';
import hoistStatics from 'hoist-non-react-statics';
export default function withNavigation(Component) {
const componentWithNavigation = (props, { navigation }) => (
<Component {...props} navigation={navigation} />
);
class ComponentWithNavigation extends React.Component {
static displayName = `withNavigation(${Component.displayName ||
Component.name})`;
const displayName = Component.displayName || Component.name;
componentWithNavigation.displayName = `withNavigation(${displayName})`;
static contextTypes = {
navigation: propTypes.object.isRequired,
};
componentWithNavigation.contextTypes = {
navigation: propTypes.object.isRequired,
};
render() {
const { navigation } = this.context;
return (
<Component
{...this.props}
navigation={navigation}
ref={this.props.onRef}
/>
);
}
}
return hoistStatics(componentWithNavigation, Component);
return hoistStatics(ComponentWithNavigation, Component);
}

View File

@@ -0,0 +1,60 @@
import React from 'react';
import propTypes from 'prop-types';
import hoistStatics from 'hoist-non-react-statics';
import invariant from '../utils/invariant';
export default function withNavigationFocus(Component) {
class ComponentWithNavigationFocus extends React.Component {
static displayName = `withNavigationFocus(${Component.displayName ||
Component.name})`;
static contextTypes = {
navigation: propTypes.object.isRequired,
};
constructor(props, context) {
super();
this.state = {
isFocused: this.getNavigation(props, context).isFocused(),
};
}
componentDidMount() {
const navigation = this.getNavigation();
this.subscriptions = [
navigation.addListener('didFocus', () =>
this.setState({ isFocused: true })
),
navigation.addListener('willBlur', () =>
this.setState({ isFocused: false })
),
];
}
componentWillUnmount() {
this.subscriptions.forEach(sub => sub.remove());
}
getNavigation = (props = this.props, context = this.context) => {
const navigation = props.navigation || context.navigation;
invariant(
!!navigation,
'withNavigationFocus can only be used on a view hierarchy of a navigator. The wrapped component is unable to get access to navigation from props or context.'
);
return navigation;
};
render() {
return (
<Component
{...this.props}
isFocused={this.state.isFocused}
ref={this.props.onRef}
/>
);
}
}
return hoistStatics(ComponentWithNavigationFocus, Component);
}

View File

@@ -10,6 +10,8 @@ export default function withCachedChildNavigation(Comp) {
return class extends React.PureComponent {
static displayName = `withCachedChildNavigation(${displayName})`;
_childEventSubscribers = {};
componentWillMount() {
this._updateNavigationProps(this.props.navigation);
}
@@ -18,6 +20,23 @@ export default function withCachedChildNavigation(Comp) {
this._updateNavigationProps(nextProps.navigation);
}
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
_isRouteFocused = route => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_updateNavigationProps = navigation => {
// Update props for each child route
if (!this._childNavigationProps) {
@@ -28,13 +47,19 @@ export default function withCachedChildNavigation(Comp) {
if (childNavigation && childNavigation.state === route) {
return;
}
if (!this._childEventSubscribers[route.key]) {
this._childEventSubscribers[route.key] = getChildEventSubscriber(
navigation.addListener,
route.key
);
}
this._childNavigationProps[route.key] = addNavigationHelpers({
dispatch: navigation.dispatch,
state: route,
addListener: getChildEventSubscriber(
navigation.addListener,
route.key
),
isFocused: () => this._isRouteFocused(route),
addListener: this._childEventSubscribers[route.key],
});
});
};

312
yarn.lock
View File

@@ -52,7 +52,7 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
acorn@^5.0.0, acorn@^5.3.0, acorn@^5.4.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
@@ -363,8 +363,8 @@ babel-eslint@^7.2.3:
babylon "^6.17.0"
babel-generator@^6.18.0, babel-generator@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
version "6.26.1"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
dependencies:
babel-messages "^6.23.0"
babel-runtime "^6.26.0"
@@ -372,7 +372,7 @@ babel-generator@^6.18.0, babel-generator@^6.26.0:
detect-indent "^4.0.0"
jsesc "^1.3.0"
lodash "^4.17.4"
source-map "^0.5.6"
source-map "^0.5.7"
trim-right "^1.0.1"
babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
@@ -492,12 +492,12 @@ babel-jest@^20.0.3:
babel-plugin-istanbul "^4.0.0"
babel-preset-jest "^20.0.3"
babel-jest@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.1.0.tgz#7fae6f655fffe77e818a8c2868c754a42463fdfd"
babel-jest@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.2.2.tgz#eda38dca284e32cc5257f96a9b51351975de4e04"
dependencies:
babel-plugin-istanbul "^4.1.5"
babel-preset-jest "^22.1.0"
babel-preset-jest "^22.2.0"
babel-messages@^6.23.0:
version "6.23.0"
@@ -529,9 +529,9 @@ babel-plugin-jest-hoist@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767"
babel-plugin-jest-hoist@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.1.0.tgz#c1281dd7887d77a1711dc760468c3b8285dde9ee"
babel-plugin-jest-hoist@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.2.0.tgz#bd34f39d652406669713b8c89e23ef25c890b993"
babel-plugin-react-transform@2.0.2:
version "2.0.2"
@@ -860,11 +860,11 @@ babel-preset-jest@^20.0.3:
dependencies:
babel-plugin-jest-hoist "^20.0.3"
babel-preset-jest@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.1.0.tgz#ff4e704102f9642765e2254226050561d8942ec9"
babel-preset-jest@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.2.0.tgz#f77b43f06ef4d8547214b2e206cc76a25c3ba0e2"
dependencies:
babel-plugin-jest-hoist "^22.1.0"
babel-plugin-jest-hoist "^22.2.0"
babel-plugin-syntax-object-rest-spread "^6.13.0"
babel-preset-react-native@^2.1.0:
@@ -1095,8 +1095,8 @@ bplist-parser@0.1.1:
big-integer "^1.6.7"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -1325,9 +1325,9 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
commander@^2.11.0, commander@^2.9.0, commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
commander@^2.11.0, commander@^2.9.0, commander@~2.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
common-tags@^1.4.0:
version "1.7.2"
@@ -1852,8 +1852,8 @@ eslint-visitor-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
eslint@^4.2.0, eslint@^4.5.0:
version "4.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1"
version "4.17.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.17.0.tgz#dc24bb51ede48df629be7031c71d9dc0ee4f3ddf"
dependencies:
ajv "^5.3.0"
babel-code-frame "^6.22.0"
@@ -1894,10 +1894,10 @@ eslint@^4.2.0, eslint@^4.5.0:
text-table "~0.2.0"
espree@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
version "3.5.3"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6"
dependencies:
acorn "^5.2.1"
acorn "^5.4.0"
acorn-jsx "^3.0.0"
esprima@^3.1.3:
@@ -1938,8 +1938,8 @@ event-target-shim@^1.0.5:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-1.1.1.tgz#a86e5ee6bdaa16054475da797ccddf0c55698491"
eventemitter3@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.0.0.tgz#fc29ecf233bd19fbd527bb4089bbf665dc90c1e3"
version "3.0.1"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.0.1.tgz#4ce66c3fc5b5a6b9f2245e359e1938f1ab10f960"
exec-sh@^0.2.0:
version "0.2.1"
@@ -1991,15 +1991,15 @@ expand-range@^1.8.1:
dependencies:
fill-range "^2.1.0"
expect@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.1.0.tgz#f8f9b019ab275d859cbefed531fbaefe8972431d"
expect@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.2.2.tgz#6cb6ae2eeb651a4187b9096de70333a018fab63f"
dependencies:
ansi-styles "^3.2.0"
jest-diff "^22.1.0"
jest-get-type "^22.1.0"
jest-matcher-utils "^22.1.0"
jest-message-util "^22.1.0"
jest-matcher-utils "^22.2.0"
jest-message-util "^22.2.0"
jest-regex-util "^22.1.0"
express-session@~1.11.3:
@@ -2916,15 +2916,15 @@ istanbul-reports@^1.1.3:
dependencies:
handlebars "^4.0.3"
jest-changed-files@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.1.4.tgz#1f7844bcb739dec07e5899a633c0cb6d5069834e"
jest-changed-files@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.2.0.tgz#517610c4a8ca0925bdc88b0ca53bd678aa8d019e"
dependencies:
throat "^4.0.0"
jest-cli@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.1.4.tgz#0fe9f3ac881b0cdc00227114c58583a2ebefcc04"
jest-cli@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.2.2.tgz#4431a93a29549da5dcb6d4a41dd03503c9198cd6"
dependencies:
ansi-escapes "^3.0.0"
chalk "^2.0.1"
@@ -2937,21 +2937,21 @@ jest-cli@^22.1.4:
istanbul-lib-coverage "^1.1.1"
istanbul-lib-instrument "^1.8.0"
istanbul-lib-source-maps "^1.2.1"
jest-changed-files "^22.1.4"
jest-config "^22.1.4"
jest-environment-jsdom "^22.1.4"
jest-changed-files "^22.2.0"
jest-config "^22.2.2"
jest-environment-jsdom "^22.2.2"
jest-get-type "^22.1.0"
jest-haste-map "^22.1.0"
jest-message-util "^22.1.0"
jest-haste-map "^22.2.2"
jest-message-util "^22.2.0"
jest-regex-util "^22.1.0"
jest-resolve-dependencies "^22.1.0"
jest-runner "^22.1.4"
jest-runtime "^22.1.4"
jest-snapshot "^22.1.2"
jest-util "^22.1.4"
jest-worker "^22.1.0"
jest-runner "^22.2.2"
jest-runtime "^22.2.2"
jest-snapshot "^22.2.0"
jest-util "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
node-notifier "^5.1.2"
node-notifier "^5.2.1"
realpath-native "^1.0.0"
rimraf "^2.5.4"
slash "^1.0.0"
@@ -2960,20 +2960,20 @@ jest-cli@^22.1.4:
which "^1.2.12"
yargs "^10.0.3"
jest-config@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.1.4.tgz#075ffacce83c3e38cf85b1b9ba0d21bd3ee27ad0"
jest-config@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.2.2.tgz#6b8ed615bc51239847d15460086f174dad4a7015"
dependencies:
chalk "^2.0.1"
glob "^7.1.1"
jest-environment-jsdom "^22.1.4"
jest-environment-node "^22.1.4"
jest-environment-jsdom "^22.2.2"
jest-environment-node "^22.2.2"
jest-get-type "^22.1.0"
jest-jasmine2 "^22.1.4"
jest-jasmine2 "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.1.4"
jest-util "^22.1.4"
jest-validate "^22.1.2"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
jest-validate "^22.2.2"
pretty-format "^22.1.0"
jest-diff@^22.1.0:
@@ -2985,7 +2985,7 @@ jest-diff@^22.1.0:
jest-get-type "^22.1.0"
pretty-format "^22.1.0"
jest-docblock@22.1.0, jest-docblock@^22.1.0:
jest-docblock@22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.1.0.tgz#3fe5986d5444cbcb149746eb4b07c57c5a464dfd"
dependencies:
@@ -2995,20 +2995,26 @@ jest-docblock@^21.0.0:
version "21.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
jest-environment-jsdom@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.1.4.tgz#704518ce8375f7ec5de048d1e9c4268b08a03e00"
jest-docblock@^22.1.0, jest-docblock@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.2.2.tgz#617f13edb16ec64202002b3c336cd14ae36c0631"
dependencies:
jest-mock "^22.1.0"
jest-util "^22.1.4"
detect-newline "^2.1.0"
jest-environment-jsdom@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.2.2.tgz#3513ccdccc2bc41daf9cdee199b7069b0d9feebc"
dependencies:
jest-mock "^22.2.0"
jest-util "^22.2.2"
jsdom "^11.5.1"
jest-environment-node@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.1.4.tgz#0f2946e8f8686ce6c5d8fa280ce1cd8d58e869eb"
jest-environment-node@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.2.2.tgz#570896eef2dd0f939c71bd5712ef4321958c1270"
dependencies:
jest-mock "^22.1.0"
jest-util "^22.1.4"
jest-mock "^22.2.0"
jest-util "^22.2.2"
jest-get-type@^21.2.0:
version "21.2.0"
@@ -3018,7 +3024,7 @@ jest-get-type@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.1.0.tgz#4e90af298ed6181edc85d2da500dbd2753e0d5a9"
jest-haste-map@22.1.0, jest-haste-map@^22.1.0:
jest-haste-map@22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.1.0.tgz#1174c6ff393f9818ebf1163710d8868b5370da2a"
dependencies:
@@ -3029,20 +3035,31 @@ jest-haste-map@22.1.0, jest-haste-map@^22.1.0:
micromatch "^2.3.11"
sane "^2.0.0"
jest-jasmine2@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.1.4.tgz#cada0baf50a220c616a9575728b80d4ddedebe8b"
jest-haste-map@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.2.2.tgz#9d3d5a14bd5e05ab9176979f2a5fbb4ddc80eb20"
dependencies:
fb-watchman "^2.0.0"
graceful-fs "^4.1.11"
jest-docblock "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
sane "^2.0.0"
jest-jasmine2@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.2.2.tgz#9065255c8f635ae9dfa33fc66068f59adf53c9aa"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
co "^4.6.0"
expect "^22.1.0"
expect "^22.2.2"
graceful-fs "^4.1.11"
is-generator-fn "^1.0.0"
jest-diff "^22.1.0"
jest-matcher-utils "^22.1.0"
jest-message-util "^22.1.0"
jest-snapshot "^22.1.2"
jest-matcher-utils "^22.2.0"
jest-message-util "^22.2.0"
jest-snapshot "^22.2.0"
source-map-support "^0.5.0"
jest-leak-detector@^22.1.0:
@@ -3051,17 +3068,17 @@ jest-leak-detector@^22.1.0:
dependencies:
pretty-format "^22.1.0"
jest-matcher-utils@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.1.0.tgz#e164665b5d313636ac29f7f6fe9ef0a6ce04febc"
jest-matcher-utils@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.2.0.tgz#5390f823c18c748543d463825aa8e4df0db253ca"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
pretty-format "^22.1.0"
jest-message-util@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.1.0.tgz#51ba0794cb6e579bfc4e9adfac452f9f1a0293fc"
jest-message-util@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.2.0.tgz#84a6bb34186d8b9af7e0732fabbef63f7355f7b2"
dependencies:
"@babel/code-frame" "^7.0.0-beta.35"
chalk "^2.0.1"
@@ -3069,9 +3086,9 @@ jest-message-util@^22.1.0:
slash "^1.0.0"
stack-utils "^1.0.1"
jest-mock@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.1.0.tgz#87ec21c0599325671c9a23ad0e05c86fb5879b61"
jest-mock@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.2.0.tgz#444b3f9488a7473adae09bc8a77294afded397a7"
jest-regex-util@^22.1.0:
version "22.1.0"
@@ -3083,45 +3100,45 @@ jest-resolve-dependencies@^22.1.0:
dependencies:
jest-regex-util "^22.1.0"
jest-resolve@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.1.4.tgz#72b9b371eaac48f84aad4ad732222ffe37692602"
jest-resolve@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.2.2.tgz#6f49d91e3680c86a4d5e5f72ccdab3996d1cbc19"
dependencies:
browser-resolve "^1.11.2"
chalk "^2.0.1"
jest-runner@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.1.4.tgz#e039039110cb1b31febc0f99e349bf7c94304a2f"
jest-runner@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.2.2.tgz#17fff27a61b63b58cf104c9cdcc0fdfccd3878ce"
dependencies:
exit "^0.1.2"
jest-config "^22.1.4"
jest-docblock "^22.1.0"
jest-haste-map "^22.1.0"
jest-jasmine2 "^22.1.4"
jest-config "^22.2.2"
jest-docblock "^22.2.2"
jest-haste-map "^22.2.2"
jest-jasmine2 "^22.2.2"
jest-leak-detector "^22.1.0"
jest-message-util "^22.1.0"
jest-runtime "^22.1.4"
jest-util "^22.1.4"
jest-worker "^22.1.0"
jest-message-util "^22.2.0"
jest-runtime "^22.2.2"
jest-util "^22.2.2"
jest-worker "^22.2.2"
throat "^4.0.0"
jest-runtime@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.1.4.tgz#1474d9f5cda518b702e0b25a17d4ef3fc563a20c"
jest-runtime@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.2.2.tgz#256d0efb65deae1c23b819d88cec5ab43d7a4ed6"
dependencies:
babel-core "^6.0.0"
babel-jest "^22.1.0"
babel-jest "^22.2.2"
babel-plugin-istanbul "^4.1.5"
chalk "^2.0.1"
convert-source-map "^1.4.0"
exit "^0.1.2"
graceful-fs "^4.1.11"
jest-config "^22.1.4"
jest-haste-map "^22.1.0"
jest-config "^22.2.2"
jest-haste-map "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.1.4"
jest-util "^22.1.4"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
json-stable-stringify "^1.0.1"
micromatch "^2.3.11"
realpath-native "^1.0.0"
@@ -3130,27 +3147,27 @@ jest-runtime@^22.1.4:
write-file-atomic "^2.1.0"
yargs "^10.0.3"
jest-snapshot@^22.1.2:
version "22.1.2"
resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.1.2.tgz#b270cf6e3098f33aceeafda02b13eb0933dc6139"
jest-snapshot@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.2.0.tgz#0c0ba152d296ef70fa198cc84977a2cc269ee4cf"
dependencies:
chalk "^2.0.1"
jest-diff "^22.1.0"
jest-matcher-utils "^22.1.0"
jest-matcher-utils "^22.2.0"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
pretty-format "^22.1.0"
jest-util@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.1.4.tgz#ac8cbd43ee654102f1941f3f0e9d1d789a8b6a9b"
jest-util@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.2.2.tgz#335484b6aeae0c5a1ae498401630324977fe3465"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
graceful-fs "^4.1.11"
is-ci "^1.0.10"
jest-message-util "^22.1.0"
jest-validate "^22.1.2"
jest-message-util "^22.2.0"
jest-validate "^22.2.2"
mkdirp "^0.5.1"
jest-validate@^21.1.0:
@@ -3162,26 +3179,33 @@ jest-validate@^21.1.0:
leven "^2.1.0"
pretty-format "^21.2.1"
jest-validate@^22.1.2:
version "22.1.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.1.2.tgz#c3b06bcba7bd9a850919fe336b5f2a8c3a239404"
jest-validate@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.2.2.tgz#9cdce422c93cc28395e907ac6bbc929158d9a6ba"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
leven "^2.1.0"
pretty-format "^22.1.0"
jest-worker@22.1.0, jest-worker@^22.1.0:
jest-worker@22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.1.0.tgz#0987832fe58fbdc205357f4c19b992446368cafb"
dependencies:
merge-stream "^1.0.1"
jest@^22.1.3:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.1.4.tgz#9ec71373a38f40ff92a3e5e96ae85687c181bb72"
jest-worker@^22.1.0, jest-worker@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.2.2.tgz#c1f5dc39976884b81f68ec50cb8532b2cbab3390"
dependencies:
jest-cli "^22.1.4"
merge-stream "^1.0.1"
jest@^22.1.3:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.2.2.tgz#26aca0f5e4eaa76d52f2792b14033a3d1e7be2bd"
dependencies:
import-local "^1.0.0"
jest-cli "^22.2.2"
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
@@ -3495,8 +3519,8 @@ lodash.keys@^3.0.0:
lodash.isarray "^3.0.0"
lodash.merge@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
lodash.pad@^4.1.0:
version "4.5.1"
@@ -3548,8 +3572,8 @@ lodash@^3.5.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.6.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
log-symbols@^1.0.2:
version "1.0.2"
@@ -3853,7 +3877,7 @@ node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
node-notifier@^5.1.2:
node-notifier@^5.1.2, node-notifier@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
dependencies:
@@ -4299,9 +4323,9 @@ private@^0.1.6, private@^0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
process@~0.5.1:
version "0.5.2"
@@ -4561,13 +4585,13 @@ read-pkg@^2.0.0:
path-type "^2.0.0"
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
@@ -4874,8 +4898,8 @@ safe-buffer@~5.0.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
sane@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.3.0.tgz#3f3df584abf69e63d4bb74f0f8c42468e4d7d46b"
version "2.4.1"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.4.1.tgz#29f991208cf28636720efdc584293e7fd66663a5"
dependencies:
anymatch "^1.3.0"
exec-sh "^0.2.0"
@@ -5044,7 +5068,7 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6:
source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -5163,8 +5187,8 @@ string_decoder@~1.0.3:
safe-buffer "~5.1.0"
stringify-object@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
version "3.2.2"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd"
dependencies:
get-own-enumerable-property-symbols "^2.0.1"
is-obj "^1.0.1"
@@ -5374,10 +5398,10 @@ ua-parser-js@^0.7.9:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
uglify-es@^3.1.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
version "3.3.10"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.10.tgz#8b0b7992cebe20edc26de1bf325cef797b8f3fa5"
dependencies:
commander "~2.13.0"
commander "~2.14.1"
source-map "~0.6.1"
uglify-js@^2.6: