Compare commits

...

158 Commits

Author SHA1 Message Date
Brent Vatne
3dd3f5b804 Release 2.5.4 2018-06-27 15:24:34 -07:00
Julian Paas
3d8d5a0634 Adds getNavigation to web exports (#4551) 2018-06-27 13:34:45 -07:00
Brent Vatne
54448ed070 Prevent flicker in header when header is null on initial mount (when using default header sizes) (#4577)
* Prevent flicker in header in most common cases. Fixes https://github.com/react-navigation/react-navigation/issues/4264

* Update snapshots
2018-06-26 13:27:43 -07:00
Julian Paas
369ac2b568 Adds SwitchNavigator to react-navigation-web (#4550)
* Adds SwitchNavigator to react-navigation-web
2018-06-26 11:24:23 -07:00
Vojtech Novak
3dc592f679 Update ISSUE_TEMPLATE.md (#4575) 2018-06-25 15:41:56 -07:00
Brent Vatne
4f93200c91 Release 2.5.3 2018-06-25 14:37:26 -07:00
Brent Vatne
665736d754 Hoist navigation action creators for router above those for child router 2018-06-25 14:33:01 -07:00
Brent Vatne
5598c3e28f Update changelog 2018-06-23 11:03:38 -07:00
Brent Vatne
cde6e845cd Release 2.5.2 2018-06-23 11:02:14 -07:00
Brent Vatne
fb8c712ad8 Update react-navigation-drawer to 0.4.2 to fix toggle regression 2018-06-23 11:01:47 -07:00
Brent Vatne
350b7e0aed Release 2.5.1 2018-06-22 13:21:22 -07:00
Brent Vatne
de112565d3 Fix name of prop, should be lastTransitionProps instead of prevTransitionProps in StackViewLayout
- Fixes #4542
2018-06-22 13:18:26 -07:00
Brent Vatne
acdd515c13 Update example app.json 2018-06-22 12:50:50 -07:00
Rodrigo Bermúdez Schettino
452a6d2004 Improve changelog format (#4559)
Bug fixes should be listed in the "Fixed" section instead of "Changed" according to keepachangelog.
2018-06-22 10:41:58 -07:00
Brent Vatne
08c8031a71 Release 2.5.0 2018-06-22 10:32:11 -07:00
Eric Vicenti
608365266a @ericvicenti/universe (#4493)
* Isolate modules for uncontainerized navigators

* Clean up prop-types

* Fix warnings and web import friendlyness

* strip a flow

* Standalone provider/consumer navigation context

* export shallowEqual as module

* address various lint

# Conflicts:
#	src/navigators/createStackNavigator.js

* Get tests to pass
2018-06-22 10:20:27 -07:00
Rodrigo Bermúdez Schettino
247fba56e6 Fix typo in Pull Request Template (#4558)
Also link to the "Unreleased" section in CHANGELOG.
2018-06-22 10:19:41 -07:00
Brent Vatne
060f5dcecf Update PR template for changelog 2018-06-22 08:25:38 -07:00
Rodrigo Bermúdez Schettino
fdec05c87a Create CHANGELOG.md (#4544)
List all changes between versions to notify about notable changes in releases.
2018-06-22 08:22:15 -07:00
Brent Vatne
76da804574 Fixes #4491 2018-06-21 15:24:52 -07:00
Brent Vatne
dde091848a Release 2.4.1 2018-06-20 15:35:07 -07:00
Brent Vatne
824fa32416 Improve playground on Android 2018-06-20 14:20:00 -07:00
Brent Vatne
c518e7f36c Improve example 2018-06-20 13:51:34 -07:00
Brent Vatne
1cfe01dbdb Release 2.4.0 2018-06-20 13:38:05 -07:00
Brent Vatne
e62a9050fd Fix withDefaultValue 2018-06-20 13:37:33 -07:00
Christophe Hurpeau
310b909ba8 createNavigationContainer: rethrow the error instead of creating a new one (#4533)
Creating a new error makes the stack unreadable in sentry, because the stack is stringified when the error is cast to string to create a new one.
Is there another reason to do that ?
Alternative possible solution would be to add a method to be able to handle the error ourselves ?
2018-06-20 13:03:02 -07:00
Eric Vicenti
aebe8a5c23 Fix isTransitioning on nested navigate (#4520)
This bug wasnt apparent until we fixed the transitioner to fully respect isTransitioning. The router did not handle this properly until now.

Enhanced a test to verify this in the future
2018-06-20 12:17:14 -07:00
Vladislav Shabanov
e1df2c6c4a Make headers of non-standard heights work correctly in StackNavigator when header is null (Fixes #4208) (#4353)
* Measure header height dynamically

* Add comment

* StacksOverTopTabs playground example
2018-06-15 08:10:43 -07:00
Ashoat Tevosyan
fa86718a24 Revert "Export getNavigationActionCreators (#4258)" (#4495)
This reverts commit 1e7d8d55c3.
2018-06-14 11:34:38 -07:00
Ashoat Tevosyan
c8e5673183 [flow] Type getNavigation (#4505)
Unrelated change in here: typing `tabBarOnPress`'s `defaultHandler`. Wasn't sure if I should make a separate PR for a single unrelated line, let me know if you'd prefer that going forward.
2018-06-14 11:33:50 -07:00
Ashoat Tevosyan
b312a5e307 [ReduxExample] Update to use react-navigation-redux-helpers@2.0.0 (#4504) 2018-06-14 11:15:11 -07:00
Brent Vatne
ee6a6c53b1 Fix incorrect methods being called in createKeyboardAwareNavigator 2018-06-13 14:50:03 -07:00
Eric Vicenti
8edec88341 Export getNavigation as public API, test redux (#4489)
This unblocks the fixing of react-navigation-redux-helpers to unbreak redux support in 2.3

https://github.com/react-navigation/react-navigation-redux-helpers/pull/37

All redux users will need to change their code to reflect the changes made here to `ReduxExample/AppNavigator`
2018-06-12 11:59:09 -07:00
Brent Vatne
b8d6d4253d Release 2.3.1 2018-06-12 11:32:08 -07:00
Eric Vicenti
0adb1ba9f1 Export getNavigation as public API, test redux (#4488)
This unblocks the fixing of react-navigation-redux-helpers to unbreak redux support in 2.3

https://github.com/react-navigation/react-navigation-redux-helpers/pull/37

All redux users will need to change their code to reflect the changes made here to `ReduxExample/AppNavigator`
2018-06-12 11:30:23 -07:00
Brent Vatne
d3ef3d1271 Release 2.3.0 2018-06-11 15:54:41 -07:00
k-murakami0609
89a24bdc12 fix broken link to Common mistakes (#4468) 2018-06-10 10:41:18 -07:00
Brent Vatne
128a95b496 Release 2.3.0-beta.1 2018-06-09 14:18:09 -07:00
Brent Vatne
470eaf3b08 Make the actions available on the navigator navigation prop the same as its children (#4467)
* Make the actions available on the navigator navigation prop the same as its children

* Fix test and improve code clarity
2018-06-09 14:13:21 -07:00
Brent Vatne
c91e8206a5 Fix ScenesReducer test 2018-06-08 17:44:32 -07:00
Brent Vatne
da283915f8 Release 2.3.0-beta.0 2018-06-08 17:36:42 -07:00
Brent Vatne
3031e7bd80 Update header when screenProps change (Fixes #4271) 2018-06-08 17:35:17 -07:00
Brent Vatne
98a4f26f26 Bump react-navigation-drawer. Fixes #4416 2018-06-08 16:27:36 -07:00
Brent Vatne
b7f5435c93 Fix withNavigation in non-floating headers 2018-06-08 15:48:21 -07:00
Brent Vatne
21ef4fcb82 Return result of dispatch from action helpers 2018-06-08 14:33:01 -07:00
Eric Vicenti
5f64a2a9cb Refactor redo, child navigation to navigation prop (#4453) 2018-06-08 14:20:07 -07:00
Brent Vatne
992d0fb267 Release 2.2.5 2018-06-08 09:40:39 -07:00
ensecoz
6f41379ed1 extract correct property for onTransitionEnd (#4459) 2018-06-08 09:35:36 -07:00
Tom Klaver
267af01e72 throw readable error when wrong navigation prop is passed (#4455)
* add warning when passing wrong navigation prop: https://github.com/react-navigation/react-navigation/issues/3598

* add test for throwing when passing wrong navigation prop

* wrong nav prop error: update snapshots, throw => throw new, update docs reference url
2018-06-08 09:31:05 -07:00
Arthur Levoyer
b68c3a755d Add missing tests about DeepLinking (#4451)
* Fix #1950 (Deep link not working when nesting have more than two levels.)

* Fix test
2018-06-07 11:37:57 -07:00
Brent Vatne
7345634493 Release 2.2.4 2018-06-07 11:33:14 -07:00
Brent Vatne
6517169119 Re-compute options when screenProps change 2018-06-07 11:31:31 -07:00
Brent Vatne
ea5d14a720 Release 2.2.3 2018-06-06 21:13:00 -07:00
Brent Vatne
313d0726a8 Add some logs to NavigationPlayground around helper dispatch return value
This is a poor substitute for tests
2018-06-06 21:12:18 -07:00
Brent Vatne
b52f153747 Return the result of dispatch from helpers. Fixes #4445 2018-06-06 21:01:51 -07:00
Brent Vatne
44621005ff Release 2.2.2 2018-06-06 14:34:59 -07:00
Brent Vatne
bf58364c3d Bump react-navigation-drawer 2018-06-06 14:34:42 -07:00
Brent Vatne
b55053cde6 Release 2.2.1 2018-06-06 13:24:50 -07:00
Brent Vatne
9abb2644a9 Bump react-navigation-tabs and rebuild yarn.lock 2018-06-06 13:24:29 -07:00
Brent Vatne
6a946d6ab7 Revert "Refactor, move child navigation to navigation prop (#4425)"
This reverts commit ba62509ff4.
2018-06-06 13:19:08 -07:00
陈然
395abe5200 Change 'React' import writing, Prevent warnings in old version React (#4428) 2018-06-06 12:51:09 -07:00
Eric Vicenti
ba62509ff4 Refactor, move child navigation to navigation prop (#4425)
* Refactor, move child navigation to navigation prop

This fixes our descriptor caching issue, and unblocks explicit nested navigation options.

As a side effect, the following APIs are introduced:

- navigation.getChildNavigation(routeKey) , which is useful for explicitly getting children config info
- navigation.router, access to the static router

- router.childRouters[routeName] , an optional way to access the children routers directly. If childRouters are not provided in a router, we will fall back on getComponentForRouteName(routeName).router, which is the previous external API for this (although it may be slower because it will require the whole screen component).

* supporting tests and top level actions

* cleanup
2018-06-06 12:50:30 -07:00
Brent Vatne
45391db7d9 Move all drawer code into react-navigation-drawer (#4435)
* Move all drawer code into react-navigation-drawer

* Remove react-native-drawer-layout-polyfill from package.json
2018-06-06 12:11:28 -07:00
Brent Vatne
7f86362e86 Release 2.2.0 2018-06-05 12:21:09 -07:00
Brent Vatne
99605737e9 Bump react-navigation-tabs to latest 2018-06-05 12:20:47 -07:00
Serge Lebedev
842f5eb7b2 [flow] Fix NavigationScreenComponent's navigationOptions (#4317) 2018-06-05 12:19:12 -07:00
Richárd Biró
183ea82416 Pass getCustomActionCreators to Drawer route (#4433) 2018-06-05 11:12:03 -07:00
Rodrigo Bermúdez Schettino
108a6504a7 Use consistent naming in README (#4427)
Use the same casing (camel case) as in the README's title for the License section.
2018-06-05 10:55:20 -07:00
Eric Vicenti
f92d671746 Fix getCustomActionCreators in drawer navigator 2018-06-05 10:51:05 -07:00
Brent Vatne
e0c4a8f7d3 Release 2.1.0 2018-06-04 14:04:00 -07:00
Rodrigo Bermúdez Schettino
bc881c8aa1 Update deprecated StackNavigator usage (#4389)
Use createStackNavigator instead.
2018-06-04 13:59:55 -07:00
Ashoat Tevosyan
118c19dcce [flow] Update NavigationActions and introduce StackActions and DrawerActions (#4345) 2018-06-04 13:58:14 -07:00
James Bechet
01b43974e6 fix swipe to go back by providing the index (#4409) 2018-06-04 13:55:26 -07:00
Brent Vatne
2f90899620 Cache descriptors in createNavigator (#4424) 2018-06-04 13:54:09 -07:00
Eric Vicenti
6cc86f66e1 Fix stack router state change without isTransitioning change (#4368) 2018-06-04 12:47:40 -07:00
Eric Vicenti
4be99b6645 Transitioner: Fix instantaneous transitions (#4404)
* Transitioner: Fix instantaneous transitions

The transitioner will always perform an animation on state change. Instead we should check if the navigation state is requesting a transition with isTransitioning. If not, we follow a similar codepath to transitioning, without any animation.

* Update .eslintrc
2018-06-04 12:46:08 -07:00
Eric Vicenti
80016b7218 Fix race condition in DrawerView (#4392) 2018-06-03 13:35:55 -07:00
Eric Vicenti
f555a9ec9a Fix drawer router logic to close drawer (#4394) 2018-06-03 13:35:30 -07:00
Brent Vatne
05cbd85d5c Temporarily stop running flow on navigation playground due to internal react-native flow errors 2018-06-01 15:02:12 -07:00
Hunkyo Jung
51965eac38 Pass getCustomActionCreators to StackRouter (#4366) 2018-05-31 19:21:11 -07:00
Brent Vatne
a3956bf3ce Update tab snapshots 2018-05-31 13:53:00 -07:00
Brent Vatne
ce24c66b5a Disable react/no-deprecated to pass CI without breaking old versions of React 2018-05-31 13:47:28 -07:00
Brent Vatne
5467f0e22d Release 2.0.4 2018-05-25 23:05:54 -07:00
Ashoat Tevosyan
1e7d8d55c3 Export getNavigationActionCreators (#4258)
* Export getNavigationActionCreators

`react-navigation-redux-helpers` generally needs to duplicate whatever `createNavigationCreator` does. As of `react-navigation@2.0`, that includes using `getNavigationActionCreators` to construct the top-level `NavigationScreenProp`.

This PR isn't strictly needed, as it's possible to import from the file directly. But that's a brittle approach, and not particularly discoverable for anybody attempting to use a "non-stateful" navigator with their state management library of choice.

* Further refinements after writing the new helper

* Final refinement of NavigationScreenProp's relationship with _DefaultActionCreators

* Resolve merge conflicts
2018-05-25 22:59:11 -07:00
Brent Vatne
1d2ce862c2 Fix Stacks over Tabs example 2018-05-25 16:07:55 -07:00
Louis Lagrange
d778479e4a Fix drawer router initial state (#4219)
* Fix drawer router initial state

* Add test

* Be concise
2018-05-25 16:01:53 -07:00
Sébastien BARBIER
352dae50e1 Apply drawerlockmode from screen options to DrawerLayout component (#4202)
Fix #4201
2018-05-25 15:50:39 -07:00
Louis Lagrange
61385cae59 Export missing Drawer components (#4221) 2018-05-25 15:49:33 -07:00
Brent Vatne
aa3c13891e Bump some versions 2018-05-25 15:26:25 -07:00
Andrei Xavier de Oliveira Calazans
9696d7220d fix(create-nav-container): pass up error on catch (#4298) 2018-05-25 15:13:18 -07:00
Louis Lagrange
2b83b44816 Remove obsolete DrawerScreen (#4222) 2018-05-25 14:45:45 -07:00
Louis Lagrange
ec749023ed [Web] Fix header height margin (#4206) 2018-05-25 14:44:46 -07:00
Sébastien Lorber
adc9389eb3 Add CircleCI Badge to README (#4318)
Displaying a badge is helpful for users but also contributors (for example to know why my upstream-rebased PR is not passing tests)
2018-05-25 14:41:27 -07:00
Sébastien Lorber
54d143fee2 Fix Codecov link -> react-community vs react-navigation (#4319) 2018-05-25 14:39:51 -07:00
Louis Lagrange
d50e74d0c7 Fix NavigatorContainer test (fixes CI) (#4327) 2018-05-24 17:43:58 +02:00
Nicolas Charpentier
22926c5230 Optimize images (#4251) 2018-05-23 16:54:23 -07:00
Ashoat Tevosyan
f6c47a6c66 [examples] Update ReduxExample (#4267)
Using two new utils:
1. `createNavigationPropConstructor`, which is now preferred over `createReduxBoundAddListener`.
2. `initializeListeners`, which is necessary to trigger the event listeners with the initial state.
2018-05-23 10:20:14 -07:00
Ashoat Tevosyan
046a9f8930 [flow] Make StackRouter action creators optional and add DrawerRouter ones (#4257)
This will force all Flow users to do runtime assertions if they want to use these action creators. Open to any better solutions.
2018-05-23 10:18:35 -07:00
Ashoat Tevosyan
72f17538c2 [flow] Fix tabBarOnPress type for react-navigation@2.0 (#4256) 2018-05-23 10:18:11 -07:00
Ashoat Tevosyan
550001b053 [flow] Fixes for v2 libdef (#4230)
* [flow] Remove "any" type from NavigationComponent

"any" cripples the typechecker, so it's best to avoid. It was introduced in #3392, but I don't think the intention was to keep it there.

* [flow] Remove `any` type from `createNavigator` return

And use objects with spread sub-types instead of unions for `React$ComponentType` type param
2018-05-23 10:17:45 -07:00
Brent Vatne
d168ab26f9 Release 2.0.2 2018-05-21 17:56:24 -07:00
Brent Vatne
99916328a1 Don't warn about multiple navigation containers on Android
This really is not ideal but it's an upstream bug on react-native Android
that we can't work around. Users may experience other unexpected behavior as a
result of this upstream bug.
2018-05-21 17:55:05 -07:00
Eric Vicenti
08e1b53f2e Update playground yarn lockfile 2018-05-12 10:48:45 -07:00
Eric Vicenti
2243528e97 Cache panResponder in StackView (#4183)
* fix swipe gesture bug

* panresponder as instance prop
2018-05-09 17:36:57 -07:00
Steven Kabbes
931271198b Remove unused code addNavigationHelpers (#4179) 2018-05-09 14:53:06 -07:00
Brent Vatne
1876706bad Release 2.0.1 2018-05-08 17:45:02 -07:00
Eric Vicenti
e97d6b26a8 Fix dispatch implementation (#4168)
Previously we had been emiting an action event with a null state when the router handles the action by returning null from getStateForAction.

- Fixes the case of null state getting emitted
- Renames a few things for clarity
- Refactors conditionals to read easier
- Comments to explain intent
2018-05-08 17:41:08 -07:00
Brent Vatne
ec1694f909 Release 2.0.0 2018-05-07 13:47:13 -07:00
Brent Vatne
93db7d0c95 Release 2.0.0-rc.14 2018-05-07 13:03:21 -07:00
Brent Vatne
589b80b2fa Update drawer example 2018-05-07 13:01:22 -07:00
Eric Vicenti
364144d639 Navigation Action Helpers at root (#4151) 2018-05-07 12:56:35 -07:00
Brent Vatne
2fd7284fe2 Release candidate lucky number 13 2018-05-07 11:54:50 -07:00
Brent Vatne
6499f02157 Only blur input when index changes on transition start (Fixes #4148) 2018-05-07 11:47:56 -07:00
Brent Vatne
39d5714ac0 Release 2.0.0-rc.12 2018-05-07 11:14:03 -07:00
Brent Vatne
1ceda5f04d Update react-navigation-deprecated-tab-navigator. Closes #4147 2018-05-07 11:13:34 -07:00
Brent Vatne
4533883ba7 Release 2.0.0-rc.11 2018-05-06 13:41:53 -07:00
spaceye
8ec2466fef Fix for broken swipe back gesture in RTL mode (#4119)
* 3127: Fixed gestureDirectionInverted variable declaration and condition in onPanResponderMove method in regards to swipe back action in RTL mode.

* - 4118: Put axis check first;
- Fixed default BackButton mask in RTL mode.
2018-05-06 12:42:22 -07:00
Brent Vatne
1bd6593ede Release 2.0.0-rc.10 2018-05-06 12:00:02 -07:00
Eric Vicenti
4e8d8ce12f Fixes to drawer router handling of child routers (#4131)
Addresses #4129, and also lets child routers swallow actions with null. Tests added
2018-05-06 11:59:40 -07:00
Eric Vicenti
9f95a7f10b Fix CompletionAction trigger (#4115) 2018-05-06 11:57:51 -07:00
Serge Lebedev
f6bd3e4306 Fix setting undefined value to isTransitioning (#4124) 2018-05-06 10:04:57 -07:00
Eric Vicenti
c1a94895f5 Fix drawer stack reset-to-top behavior (#4132)
“reset” is not a NavigationAction anymore.

The correct fix, for v3, to this is to navigate to the first screen inside the stack. With the less-pushy navigate behavior in v2, this will result in the first route getting selected if you specify it by routeName.
2018-05-06 09:51:18 -07:00
Brent Vatne
ad7cde9eb9 Release 2.0.0-rc.9 2018-05-04 11:16:34 -07:00
Brent Vatne
2643f690a9 Pull createMaterialBottomTabNavigator into separate repo 2018-05-04 11:06:51 -07:00
Brent Vatne
8e52995ef3 Release 2.0.0-rc.8 2018-05-04 10:14:34 -07:00
Eric Vicenti
8ed3817c90 Fix event unsubscription logic (#4116)
Events were getting unsubscribed too early, so that the inner willBlur event was getting skipped when trying to “dismiss” a deep navigation stack.
2018-05-04 10:14:22 -07:00
Brent Vatne
eda9bfd567 Update react-lifecycles-compat 2018-05-04 10:10:28 -07:00
Eric Vicenti
723c5f2149 Fix drawer navigation dispatch (#4121) 2018-05-04 09:48:45 -07:00
Brent Vatne
ab5481a290 Add context around a test in switchrouter 2018-05-03 15:43:37 -07:00
Brent Vatne
df281cfed0 Add router tests to clarify route resolution 2018-05-03 15:42:23 -07:00
Eric Vicenti
e0df3cf74a Fix issue with nested navigation actions in stack (#4114) 2018-05-03 10:13:43 -07:00
Vojtech Novak
2440af66e4 add a test so that #2856 can be closed (#4102)
* add test for 2856

* rename variables
2018-05-02 20:02:03 -07:00
simonbuerger
47fe858d4e StateUtils: Use Array.prototype.findIndex instead of map => indexOf (#4106)
* Use Array.prototype.findIndex instead of map => indexOf

Creating a new array and iterating over all the routes is inefficient compared to findIndex, which does not create a new array and exits as soon as it finds a match. Since the indexOf method is used extensively this should provide a minor performance improvement

* reverted yarn.lock edits
2018-05-02 20:01:20 -07:00
Eric Vicenti
c641bee11b Fix initial action dispatch for nonPersisted apps (#4104) 2018-05-02 09:17:00 -07:00
Brent Vatne
4b39e2db3c Release 2.0.0-rc.7 2018-04-30 17:08:23 -07:00
Eric Vicenti
f7533a790f Add tests to confirm setParam behavior (#4099)
Adding these tests because reports from this issue make me nervous:

https://github.com/react-navigation/react-navigation/issues/1274
2018-04-30 16:37:41 -07:00
Brent Vatne
c56122466f Release 2.0.0-rc.6 2018-04-30 16:19:28 -07:00
Brent Vatne
7fc992dc58 Container state can be out of date if we have async functions between getting it and using it (#4098) 2018-04-30 15:59:49 -07:00
Rob Allsopp
32922cdd7d go back to React$, and fix real error (#4095) 2018-04-30 12:04:48 -07:00
Rob Allsopp
eda51b3b79 Fix missing/incorrect flow types (#4085)
* don’t use private global

* add missing method ‘getParam’ to navigation screen prop

* correct return type for ‘withNavigation’

* add usage of `getParam` method
2018-04-27 14:30:23 -07:00
Jed Mao
921ee09587 Fix "npm test" on Windows 10 (#4066)
* Fix Windows

* update prettier

* Explain need for assetsTransformer in comment
2018-04-27 08:57:07 -07:00
Brent Vatne
7ae4c60eb8 Release 2.0.0-rc.5 2018-04-25 17:49:17 -07:00
Brent Vatne
5fff7ef5c6 Give inactive routes in stack opportunity to handle action (#4064) 2018-04-26 00:48:55 +00:00
Brent Vatne
42bb1cc317 Release 2.0.0-rc.4 2018-04-25 17:04:15 -07:00
Brent Vatne
337fd89ad5 Bump react-navigation-deprecated-tab-navigator 2018-04-25 16:57:42 -07:00
Brent Vatne
acf9b92ff7 Bump react-navigation-tabs 2018-04-25 16:49:57 -07:00
Rich Gilbank
5072130d6f Typo in deprecation notice (#4051) 2018-04-24 11:10:48 -07:00
Brent Vatne
20bbbd62ff Release 2.0.0-rc.3 2018-04-20 17:40:02 +03:00
Adam Miskiewicz
0890896824 Make StackNavigator keyboard aware (#3951)
* Make StackNavigator keyboard aware

One thing that has always annoyed me in React Navigation is the handling of the keyboard. When a keyboard is visible on screen and a navigation action occurs (either by tapping a button or using a gesture), the keyboard tends to stay on screen until the transition completes. This feels janky and broken. On native iOS, for instance, the keyboard hides immediately when the navigation starts, and if the transition is cancelled (say, when the user releases the gesture), the keyboard reappears.

This PR introduces a "KeyboardAwareNavigator" higher order component that is enabled on the StackNavigator, unless a `disableKeyboardHandling` prop is passed into the StackNavigator's configuration.

* Set status bar in keyboard handling example

* Call gesture props in keyboard aware navigator if available

* Fix formatting
2018-04-20 17:03:25 +03:00
Serhii Palash
0cf14f8e1e Fix TabNavigator export ( Issue #3962 ) (#3979) 2018-04-20 16:40:18 +03:00
Brent Vatne
e5e434c9e2 Fix _isRouteFocused so it takes a route and returns bool rather than fn 2018-04-20 16:39:03 +03:00
Janic Duplessis
e5d8d2c216 Fix header hardcoded height to accound for iPhone X and orientation changes (#4017) 2018-04-20 16:34:33 +03:00
Eric Vicenti
abd5200739 Fix header ModularLeftComponent to goBack from child navigation (#4023) 2018-04-20 16:33:36 +03:00
Brent Vatne
202609d9cf Release 2.0.0-rc.2 2018-04-09 17:26:32 -07:00
Yao Hui Chua
7b4dd98255 Shift tests to isolate routers (#3876) 2018-04-09 13:00:01 -07:00
Janic Duplessis
70c644f522 Fix transition between 2 screens with no header (#3939) 2018-04-09 12:32:27 -07:00
Janic Duplessis
5274d16e3b Use Header.HEIGHT instead of measuring to avoid flicker (#3940) 2018-04-09 12:31:43 -07:00
Janic Duplessis
e5e2cbb86d Fix header transition when mode is set to screen (#3927) 2018-04-09 11:22:51 -07:00
102 changed files with 7402 additions and 6053 deletions

View File

@@ -7,18 +7,18 @@
"prettier/react"
],
"parser": "babel-eslint",
"plugins": [
"react",
"prettier"
],
"plugins": ["react", "prettier"],
"env": {
"jasmine": true
},
"rules": {
"prettier/prettier": ["error", {
"trailingComma": "es5",
"singleQuote": true
}],
"prettier/prettier": [
"error",
{
"trailingComma": "es5",
"singleQuote": true
}
],
"no-underscore-dangle": "off",
"no-use-before-define": "off",
@@ -27,24 +27,20 @@
"no-plusplus": "off",
"no-class-assign": "off",
"no-duplicate-imports": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": "off",
"import/no-unresolved": "off",
"react/jsx-filename-extension": [
"off", { "extensions": [".js", ".jsx"] }
],
"react/jsx-filename-extension": ["off", { "extensions": [".js", ".jsx"] }],
"react/sort-comp": "off",
"react/prefer-stateless-function": "off",
"react/no-deprecated": "off",
"react/forbid-prop-types": "warn",
"react/prop-types": "off",
"react/require-default-props": "off",
"react/no-unused-prop-types": "off",
},
"settings": {
"react/no-unused-prop-types": "off"
},
"parserOptions": {
"ecmaVersion": 6,

View File

@@ -25,7 +25,7 @@ Bugs with react-navigation must be reproducible *without any external libraries
### How to reproduce
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repoistory as that is outside of the scope of Rect Navigation.
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repoistory as that is outside of the scope of React Navigation.
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.

View File

@@ -1,17 +1,21 @@
Please provide enough information so that others can review your pull request:
## Motivation
Explain the **motivation** for making this change. What existing problem does the pull request solve?
Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise split it.
## Test plan
**Test plan (required)**
Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI.
Demonstrate the code is solid. Example: the exact commands you ran and their output, screenshots / videos if the pull request changes UI.
Make sure you test on both platforms if your change affects both platforms.
The code must pass tests.
**Code formatting**
## Code formatting
Look around. Match the style of the rest of the codebase.
Look around. Match the style of the rest of the codebase. Run `yarn format` before committing.
## Changelog
Add an entry under the "Unreleased" heading in [CHANGELOG.md](https://github.com/react-navigation/react-navigation/blob/master/CHANGELOG.md#unreleased) which explains your change.

45
CHANGELOG.md Normal file
View File

@@ -0,0 +1,45 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [2.5.4] - [2018-06-27](https://github.com/react-navigation/react-navigation/releases/tag/2.5.4)
### Fixed
- Header no longer sometimes flashes for 1 frame when using `header: null` on initial route of stack with floating header.
- Export `createSwitchNavigator` in react-navigation.web.js
## [2.5.3] - [2018-06-23](https://github.com/react-navigation/react-navigation/releases/tag/2.5.3)
### Fixed
- `setParams` applies to the navigation object it is called on even if that is the navigation object for a navigation view (more details in https://github.com/react-navigation/react-navigation/issues/4497)
## [2.5.2] - [2018-06-23](https://github.com/react-navigation/react-navigation/releases/tag/2.5.2)
### Fixed
- Update react-navigation-drawer to fix regression in toggleDrawer
## [2.5.1] - [2018-06-22](https://github.com/react-navigation/react-navigation/releases/tag/2.5.1)
### Fixed
- `transitionConfig` in stack navigator no longer passes incorrect `fromTransitionProps` when navigating back
## [2.5.0] - [2018-06-22](https://github.com/react-navigation/react-navigation/releases/tag/2.5.0)
### Changed
- Refactor internals to make it play more nicely with web
### Fixed
- `const defaultGetStateForAction = SwitchBasedNavigator.router.getStateForAction` no longer throws error.
- Updated react-navigation-drawer to 0.4.1 which should fix issues related to automatically closing drawer when changing routes.
## [2.4.1] - [2018-06-21](https://github.com/react-navigation/react-navigation/releases/tag/2.4.1)
### Changed
- Improved examples
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.5.4...HEAD
[2.5.4]: https://github.com/react-navigation/react-navigation/compare/2.5.3...2.5.4
[2.5.3]: https://github.com/react-navigation/react-navigation/compare/2.5.2...2.5.3
[2.5.2]: https://github.com/react-navigation/react-navigation/compare/2.5.1...2.5.2
[2.5.1]: https://github.com/react-navigation/react-navigation/compare/2.5.0...2.5.1
[2.5.0]: https://github.com/react-navigation/react-navigation/compare/2.4.1...2.5.0
[2.4.1]: https://github.com/react-navigation/react-navigation/compare/2.4.0...2.4.1

View File

@@ -1,6 +1,6 @@
# React Navigation
[![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-community/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-community/react-navigation) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/contributing.html)
[![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-navigation/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-navigation/react-navigation) [![CircleCI badge](https://circleci.com/gh/react-navigation/react-navigation/tree/master.svg?style=shield)](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/contributing.html)
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.
@@ -55,4 +55,4 @@ This library has adopted a Code of Conduct that we expect project participants t
## License
React-navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
React Navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).

12
assetsTransformer.js Normal file
View File

@@ -0,0 +1,12 @@
/**
* This file is needed to hijack asset imports so that test files don't attempt
* to import them as JavaScript modules.
* See https://github.com/facebook/jest/issues/2663#issuecomment-317109798
*/
const path = require('path');
module.exports = {
process(src, filename, config, options) {
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
},
};

View File

@@ -55,8 +55,6 @@ module.system=haste
emoji=true
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
@@ -77,7 +75,5 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.61.0
^0.67.0

View File

@@ -11,16 +11,17 @@
"splash": {
"image": "./assets/icons/splash.png"
},
"sdkVersion": "26.0.0",
"sdkVersion": "27.0.0",
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"packagerOpts": {
"assetExts": [
"ttf",
"mp4"
]
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"bundleIdentifier": "com.reactnavigation.example",
"supportsTablet": true
},
"android": {
"package": "com.reactnavigation.example"
}
}
}

View File

@@ -26,7 +26,9 @@ import TabsInDrawer from './TabsInDrawer';
import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs';
import StacksOverTabs from './StacksOverTabs';
import StacksOverTopTabs from './StacksOverTopTabs';
import StacksWithKeys from './StacksWithKeys';
import InactiveStack from './InactiveStack';
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
import SimpleStack from './SimpleStack';
import StackWithHeaderPreset from './StackWithHeaderPreset';
@@ -34,6 +36,7 @@ import StackWithTranslucentHeader from './StackWithTranslucentHeader';
import SimpleTabs from './SimpleTabs';
import SwitchWithStacks from './SwitchWithStacks';
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
import KeyboardHandlingExample from './KeyboardHandlingExample';
const ExampleInfo = {
SimpleStack: {
@@ -44,6 +47,11 @@ const ExampleInfo = {
name: 'Switch between routes',
description: 'Jump between routes',
},
InactiveStack: {
name: 'Navigate idempotently to stacks in inactive routes',
description:
'An inactive route in a stack should be given the opportunity to handle actions',
},
StackWithCustomHeaderBackImage: {
name: 'Custom header back image',
description: 'Stack with custom header back image',
@@ -98,6 +106,10 @@ const ExampleInfo = {
name: 'Stacks over Tabs',
description: 'Nested stack navigation that pushes on top of tabs',
},
StacksOverTopTabs: {
name: 'Stacks with non-standard header height',
description: 'Tab navigator in stack with custom header heights',
},
StacksWithKeys: {
name: 'Link in Stack with keys',
description: 'Use keys to link between screens',
@@ -114,6 +126,11 @@ const ExampleInfo = {
name: 'withNavigationFocus',
description: 'Receive the focus prop to know when a screen is focused',
},
KeyboardHandlingExample: {
name: 'Keyboard Handling Example',
description:
'Demo automatic handling of keyboard showing/hiding inside StackNavigator',
},
};
const ExampleRoutes = {
@@ -125,7 +142,12 @@ const ExampleRoutes = {
// screen: MultipleDrawer,
// },
StackWithCustomHeaderBackImage: StackWithCustomHeaderBackImage,
StackWithHeaderPreset: StackWithHeaderPreset,
...Platform.select({
ios: {
StackWithHeaderPreset: StackWithHeaderPreset,
},
android: {},
}),
StackWithTranslucentHeader: StackWithTranslucentHeader,
TabsInDrawer: TabsInDrawer,
CustomTabs: CustomTabs,
@@ -134,6 +156,7 @@ const ExampleRoutes = {
StacksWithKeys: StacksWithKeys,
StacksInTabs: StacksInTabs,
StacksOverTabs: StacksOverTabs,
StacksOverTopTabs: StacksOverTopTabs,
LinkStack: {
screen: SimpleStack,
path: 'people/Jordan',
@@ -143,6 +166,9 @@ const ExampleRoutes = {
path: 'settings',
},
TabsWithNavigationFocus,
KeyboardHandlingExample,
// This is commented out because it's rarely useful
// InactiveStack,
};
type State = {
@@ -307,7 +333,7 @@ const AppNavigator = createStackNavigator(
}
);
export default () => <AppNavigator persistenceKey="NavState" />;
export default AppNavigator;
const styles = StyleSheet.create({
item: {

View File

@@ -32,14 +32,7 @@ const InboxScreen = ({ navigation }) => (
<MyNavScreen banner={'Inbox Screen'} navigation={navigation} />
);
InboxScreen.navigationOptions = {
drawerLabel: 'Inbox',
drawerIcon: ({ tintColor }) => (
<MaterialIcons
name="move-to-inbox"
size={24}
style={{ color: tintColor }}
/>
),
headerTitle: 'Inbox',
};
const EmailScreen = ({ navigation }) => (
@@ -50,10 +43,7 @@ const DraftsScreen = ({ navigation }) => (
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
);
DraftsScreen.navigationOptions = {
drawerLabel: 'Drafts',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="drafts" size={24} style={{ color: tintColor }} />
),
headerTitle: 'Drafts',
};
const InboxStack = createStackNavigator({
@@ -61,11 +51,29 @@ const InboxStack = createStackNavigator({
Email: { screen: EmailScreen },
});
InboxStack.navigationOptions = {
drawerLabel: 'Inbox',
drawerIcon: ({ tintColor }) => (
<MaterialIcons
name="move-to-inbox"
size={24}
style={{ color: tintColor }}
/>
),
};
const DraftsStack = createStackNavigator({
Drafts: { screen: DraftsScreen },
Email: { screen: EmailScreen },
});
DraftsStack.navigationOptions = {
drawerLabel: 'Drafts',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="drafts" size={24} style={{ color: tintColor }} />
),
};
const DrawerExample = createDrawerNavigator(
{
Inbox: {

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { Button, Text, StatusBar, View, StyleSheet } from 'react-native';
import {
SafeAreaView,
createStackNavigator,
createSwitchNavigator,
NavigationActions,
} from 'react-navigation';
const runSubRoutes = navigation => {
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
navigation.dispatch(NavigationActions.navigate({ routeName: 'Second2' }));
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
};
const runSubRoutesWithIntermediate = navigation => {
navigation.dispatch(toFirst1);
navigation.dispatch(toSecond2);
navigation.dispatch(toFirst);
navigation.dispatch(toFirst2);
};
const runSubAction = navigation => {
navigation.dispatch(toFirst2);
navigation.dispatch(toSecond2);
navigation.dispatch(toFirstChild1);
};
const DummyScreen = ({ routeName, navigation, style }) => {
return (
<SafeAreaView
style={[
StyleSheet.absoluteFill,
{
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
},
style,
]}
>
<Text style={{ fontWeight: '800' }}>
{routeName}({navigation.state.key})
</Text>
<View>
<Button title="back" onPress={() => navigation.goBack()} />
<Button title="dismiss" onPress={() => navigation.dismiss()} />
<Button
title="between sub-routes"
onPress={() => runSubRoutes(navigation)}
/>
<Button
title="between sub-routes (with intermediate)"
onPress={() => runSubRoutesWithIntermediate(navigation)}
/>
<Button
title="with sub-action"
onPress={() => runSubAction(navigation)}
/>
</View>
<StatusBar barStyle="default" />
</SafeAreaView>
);
};
const createDummyScreen = routeName => {
const BoundDummyScreen = props => DummyScreen({ ...props, routeName });
return BoundDummyScreen;
};
const toFirst = NavigationActions.navigate({ routeName: 'First' });
const toFirst1 = NavigationActions.navigate({ routeName: 'First1' });
const toFirst2 = NavigationActions.navigate({ routeName: 'First2' });
const toSecond2 = NavigationActions.navigate({ routeName: 'Second2' });
const toFirstChild1 = NavigationActions.navigate({
routeName: 'First',
action: NavigationActions.navigate({ routeName: 'First1' }),
});
export default createStackNavigator(
{
Other: createDummyScreen('Leaf'),
First: createStackNavigator({
First1: createDummyScreen('First1'),
First2: createDummyScreen('First2'),
}),
Second: createStackNavigator({
Second1: createDummyScreen('Second1'),
Second2: createDummyScreen('Second2'),
}),
},
{
headerMode: 'none',
}
);

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { StatusBar, View, TextInput, InteractionManager } from 'react-native';
import { createStackNavigator, withNavigationFocus } from 'react-navigation';
import { Button } from './commonComponents/ButtonWithMargin';
class ScreenOne extends React.Component {
static navigationOptions = {
title: 'Home',
};
render() {
const { navigation } = this.props;
return (
<View style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('ScreenTwo')}
title="Push screen with focused text input"
/>
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
<StatusBar barStyle="default" />
</View>
);
}
}
class ScreenTwo extends React.Component {
static navigationOptions = ({ navigation }) => ({
title: navigation.getParam('inputValue', 'Screen w/ Input'),
});
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this._textInput.focus();
});
}
render() {
const { navigation } = this.props;
return (
<View style={{ paddingTop: 30 }}>
<View style={{ alignSelf: 'center', paddingVertical: 20 }}>
<TextInput
ref={c => (this._textInput = c)}
onChangeText={inputValue => navigation.setParams({ inputValue })}
style={{
backgroundColor: 'white',
height: 24,
width: 150,
borderColor: '#555',
borderWidth: 1,
}}
/>
</View>
<Button onPress={() => navigation.pop()} title="Pop" />
</View>
);
}
}
export default createStackNavigator({
ScreenOne,
ScreenTwo: withNavigationFocus(ScreenTwo),
});

View File

@@ -4,6 +4,8 @@
import type {
NavigationScreenProp,
NavigationState,
NavigationStateRoute,
NavigationEventSubscription,
} from 'react-navigation';
@@ -13,17 +15,25 @@ import {
createStackNavigator,
SafeAreaView,
withNavigation,
NavigationActions,
StackActions,
} from 'react-navigation';
import invariant from 'invariant';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
type MyNavScreenProps = {
navigation: NavigationScreenProp<*>,
navigation: NavigationScreenProp<NavigationState>,
banner: React.Node,
};
class MyBackButton extends React.Component<any, any> {
type BackButtonProps = {
navigation: NavigationScreenProp<NavigationStateRoute>,
};
class MyBackButton extends React.Component<BackButtonProps, any> {
render() {
return (
<HeaderButtons>
@@ -42,25 +52,55 @@ const MyBackButtonWithNavigation = withNavigation(MyBackButton);
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
const { push, replace, popToTop, pop, dismiss } = navigation;
invariant(
push && replace && popToTop && pop && dismiss,
'missing action creators for StackNavigator'
);
return (
<SafeAreaView>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.push('Profile', { name: 'Jane' })}
onPress={() => push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
onPress={() =>
navigation.dispatch(
StackActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName: 'Photos',
params: { name: 'Jane' },
}),
],
})
)
}
title="Reset photos"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Navigate to a photos screen"
/>
<Button
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
onPress={() => 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()} title="Go back" />
<Button onPress={() => navigation.dismiss()} title="Dismiss" />
<Button onPress={() => popToTop()} title="Pop to top" />
<Button onPress={() => pop()} title="Pop" />
<Button
onPress={() => {
if (navigation.goBack()) {
console.log('goBack handled');
} else {
console.log('goBack unhandled');
}
}}
title="Go back"
/>
<Button onPress={() => dismiss()} title="Dismiss" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
@@ -68,7 +108,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
}
type MyHomeScreenProps = {
navigation: NavigationScreenProp<*>,
navigation: NavigationScreenProp<NavigationState>,
};
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
@@ -112,7 +152,7 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
}
type MyPhotosScreenProps = {
navigation: NavigationScreenProp<*>,
navigation: NavigationScreenProp<NavigationState>,
};
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
@@ -153,7 +193,7 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
const { navigation } = this.props;
return (
<MyNavScreen
banner={`${navigation.state.params.name}'s Photos`}
banner={`${navigation.getParam('name')}'s Photos`}
navigation={navigation}
/>
);
@@ -162,9 +202,9 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
const MyProfileScreen = ({ navigation }) => (
<MyNavScreen
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
navigation.state.params.name
}'s Profile`}
banner={`${
navigation.getParam('mode') === 'edit' ? 'Now Editing ' : ''
}${navigation.getParam('name')}'s Profile`}
navigation={navigation}
/>
);

View File

@@ -165,7 +165,7 @@ const SimpleTabs = createBottomTabNavigator(
},
{
tabBarOptions: {
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
activeTintColor: '#e91e63',
},
}
);

View File

@@ -6,6 +6,8 @@ import type { NavigationScreenProp } from 'react-navigation';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { createStackNavigator, SafeAreaView } from 'react-navigation';
import invariant from 'invariant';
import { Button } from './commonComponents/ButtonWithMargin';
type NavScreenProps = {
@@ -19,15 +21,14 @@ class HomeScreen extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { push } = navigation;
invariant(push, 'missing `push` action creator for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button onPress={() => push('Other')} title="Push another screen" />
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button
onPress={() => navigation.push('ScreenWithNoHeader')}
onPress={() => push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
@@ -44,18 +45,20 @@ class OtherScreen extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { push, pop } = navigation;
invariant(push && pop, 'missing action creators for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('ScreenWithLongTitle')}
onPress={() => push('ScreenWithLongTitle')}
title="Push another screen"
/>
<Button
onPress={() => navigation.push('ScreenWithNoHeader')}
onPress={() => push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
@@ -70,10 +73,12 @@ class ScreenWithLongTitle extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { pop } = navigation;
invariant(pop, 'missing `pop` action creator for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
@@ -89,14 +94,13 @@ class ScreenWithNoHeader extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { push, pop } = navigation;
invariant(push && pop, 'missing action creators for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => push('Other')} title="Push another screen" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>

View File

@@ -19,6 +19,8 @@ import {
View,
} from 'react-native';
import { Header, createStackNavigator } from 'react-navigation';
import invariant from 'invariant';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
@@ -31,11 +33,16 @@ type MyNavScreenProps = {
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
const { push, replace, popToTop, pop } = navigation;
invariant(
push && replace && popToTop && pop,
'missing action creators for StackNavigator'
);
return (
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.push('Profile', { name: 'Jane' })}
onPress={() => push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
@@ -43,11 +50,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
title="Navigate to a photos screen"
/>
<Button
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
onPress={() => replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => popToTop()} title="Pop to top" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</ScrollView>

View File

@@ -94,6 +94,19 @@ const TabNav = createBottomTabNavigator(
}
);
TabNav.navigationOptions = ({ navigation }) => {
let { routeName } = navigation.state.routes[navigation.state.index];
let title;
if (routeName === 'SettingsTab') {
title = 'Settings';
} else if (routeName === 'MainTab') {
title = 'Home';
}
return {
title,
};
};
const StacksOverTabs = createStackNavigator({
Root: {
screen: TabNav,
@@ -107,9 +120,9 @@ const StacksOverTabs = createStackNavigator({
Profile: {
screen: MyProfileScreen,
path: '/people/:name',
navigationOptions: ({ navigation }) => {
title: `${navigation.state.params.name}'s Profile!`;
},
navigationOptions: ({ navigation }) => ({
title: `${navigation.state.params.name}'s Profile!`,
}),
},
});

View File

@@ -0,0 +1,142 @@
/**
* @flow
*/
import React from 'react';
import { View, ScrollView, StatusBar, StyleSheet } from 'react-native';
import {
SafeAreaView,
createStackNavigator,
createMaterialTopTabNavigator,
} from 'react-navigation';
import { Constants } from 'expo';
import { MaterialTopTabBar } from 'react-navigation-tabs';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
const HEADER_HEIGHT = 64;
const MyNavScreen = ({ navigation, banner, statusBarStyle }) => (
<ScrollView>
<SafeAreaView forceInset={{ horizontal: 'always' }}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('Profile', { name: 'Jordan' })}
title="Open profile screen"
/>
<Button
onPress={() => navigation.navigate('NotifSettings')}
title="Open notifications screen"
/>
<Button
onPress={() => navigation.navigate('SettingsTab')}
title="Go to settings tab"
/>
<Button onPress={() => navigation.goBack(null)} title="Go back" />
</SafeAreaView>
<StatusBar barStyle={statusBarStyle || 'default'} />
</ScrollView>
);
const MyHomeScreen = ({ navigation }) => (
<MyNavScreen
banner="Home Screen"
navigation={navigation}
statusBarStyle="light-content"
/>
);
const MyProfileScreen = ({ navigation }) => (
<MyNavScreen
banner={`${navigation.state.params.name}s Profile`}
navigation={navigation}
/>
);
const MyNotificationsSettingsScreen = ({ navigation }) => (
<MyNavScreen banner="Notifications Screen" navigation={navigation} />
);
const MySettingsScreen = ({ navigation }) => (
<MyNavScreen
banner="Settings Screen"
navigation={navigation}
statusBarStyle="light-content"
/>
);
const styles = StyleSheet.create({
stackHeader: {
height: HEADER_HEIGHT,
},
tab: {
height: HEADER_HEIGHT,
},
});
function MaterialTopTabBarWithStatusBar(props) {
return (
<View
style={{
paddingTop: Constants.statusBarHeight,
backgroundColor: '#2196f3',
}}
>
<MaterialTopTabBar {...props} jumpToIndex={() => {}} />
</View>
);
}
const TabNavigator = createMaterialTopTabNavigator(
{
MainTab: {
screen: MyHomeScreen,
navigationOptions: {
title: 'Welcome',
},
},
SettingsTab: {
screen: MySettingsScreen,
navigationOptions: {
title: 'Settings',
},
},
},
{
tabBarComponent: MaterialTopTabBarWithStatusBar,
tabBarOptions: {
tabStyle: styles.tab,
},
}
);
const StackNavigator = createStackNavigator(
{
Root: {
screen: TabNavigator,
navigationOptions: {
header: null,
},
},
NotifSettings: {
screen: MyNotificationsSettingsScreen,
navigationOptions: {
title: 'Notifications',
},
},
Profile: {
screen: MyProfileScreen,
navigationOptions: ({ navigation }) => ({
title: `${navigation.state.params.name}'s Profile!`,
}),
},
},
{
navigationOptions: {
headerStyle: styles.stackHeader,
},
}
);
export default StackNavigator;

View File

@@ -4,10 +4,8 @@
import React from 'react';
import { SafeAreaView, StatusBar, Text, View } from 'react-native';
import {
createBottomTabNavigator,
withNavigationFocus,
} from 'react-navigation';
import { withNavigationFocus } from 'react-navigation';
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Button } from './commonComponents/ButtonWithMargin';
@@ -80,16 +78,26 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
return withNavigationFocus(TabScreen);
};
const TabsWithNavigationFocus = createBottomTabNavigator({
One: {
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
const TabsWithNavigationFocus = createMaterialBottomTabNavigator(
{
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'
),
},
},
Two: {
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
},
Three: {
screen: createTabScreen('Three', 'numeric-3-box-outline', 'numeric-3-box'),
},
});
{
shifting: false,
activeTintColor: '#F44336',
}
);
export default TabsWithNavigationFocus;

View File

@@ -11,17 +11,20 @@
"test": "flow"
},
"dependencies": {
"expo": "^26.0.0",
"react": "16.3.0-alpha.1",
"react-native": "^0.54.0",
"expo": "^27.0.0",
"invariant": "^2.2.4",
"react": "16.3.1",
"react-native": "^0.55.0",
"react-native-iphone-x-helper": "^1.0.2",
"react-navigation": "link:../..",
"react-navigation-header-buttons": "^0.0.4"
"react-navigation-header-buttons": "^0.0.4",
"react-navigation-material-bottom-tabs": "0.1.3",
"react-navigation-tabs": "^0.5.1"
},
"devDependencies": {
"babel-jest": "^22.4.1",
"babel-plugin-transform-remove-console": "^6.9.0",
"flow-bin": "^0.61.0",
"flow-bin": "^0.67.0",
"jest": "^22.1.3",
"jest-expo": "^26.0.0",
"react-native-scripts": "^1.5.0",

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,15 @@ import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import AppReducer from './src/reducers';
import AppWithNavigationState from './src/navigators/AppNavigator';
import { middleware } from './src/utils/redux';
import { AppNavigator, middleware } from './src/navigators/AppNavigator';
const store = createStore(
AppReducer,
applyMiddleware(middleware),
);
const store = createStore(AppReducer, applyMiddleware(middleware));
class ReduxExampleApp extends React.Component {
render() {
return (
<Provider store={store}>
<AppWithNavigationState />
<AppNavigator />
</Provider>
);
}

View File

@@ -12,7 +12,7 @@
"icon": "./assets/icons/react-navigation.png",
"hideExponentText": false
},
"sdkVersion": "25.0.0",
"sdkVersion": "27.0.0",
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"packagerOpts": {
"assetExts": ["ttf", "mp4"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -21,17 +21,18 @@
]
},
"dependencies": {
"expo": "^25.0.0",
"expo": "^27.0.0",
"prop-types": "^15.5.10",
"react": "16.2.0",
"react-native": "^0.52.0",
"react": "16.3.1",
"react-native": "^0.55.0",
"react-navigation": "link:../..",
"react-navigation-redux-helpers": "^1.0.0",
"react-navigation-redux-helpers": "^2.0.0-beta.1",
"react-redux": "^5.0.6",
"redux": "^3.7.2"
},
"devDependencies": {
"babel-jest": "^22.4.1",
"flow-bin": "^0.74.0",
"jest": "^22.1.3",
"jest-expo": "^25.1.0",
"react-native-scripts": "^1.3.1",

View File

@@ -1,41 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StackNavigator } from 'react-navigation';
import { createStackNavigator } from 'react-navigation';
import {
reduxifyNavigator,
createReactNavigationReduxMiddleware,
} from 'react-navigation-redux-helpers';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
import ProfileScreen from '../components/ProfileScreen';
import { addListener } from '../utils/redux';
export const AppNavigator = StackNavigator({
const middleware = createReactNavigationReduxMiddleware(
'root',
state => state.nav
);
const RootNavigator = createStackNavigator({
Login: { screen: LoginScreen },
Main: { screen: MainScreen },
Profile: { screen: ProfileScreen },
});
class AppWithNavigationState extends React.Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
};
render() {
const { dispatch, nav } = this.props;
return (
<AppNavigator
navigation={{
dispatch,
state: nav,
addListener,
}}
/>
);
}
}
const AppWithNavigationState = reduxifyNavigator(RootNavigator, 'root');
const mapStateToProps = state => ({
nav: state.nav,
state: state.nav,
});
export default connect(mapStateToProps)(AppWithNavigationState);
const AppNavigator = connect(mapStateToProps)(AppWithNavigationState);
export { RootNavigator, AppNavigator, middleware };

View File

@@ -1,13 +1,13 @@
import { combineReducers } from 'redux';
import { NavigationActions } from 'react-navigation';
import { AppNavigator } from '../navigators/AppNavigator';
import { RootNavigator } from '../navigators/AppNavigator';
// Start with two routes: The Main screen, with the Login screen on top.
const firstAction = AppNavigator.router.getActionForPathAndParams('Main');
const tempNavState = AppNavigator.router.getStateForAction(firstAction);
const secondAction = AppNavigator.router.getActionForPathAndParams('Login');
const initialNavState = AppNavigator.router.getStateForAction(
const firstAction = RootNavigator.router.getActionForPathAndParams('Main');
const tempNavState = RootNavigator.router.getStateForAction(firstAction);
const secondAction = RootNavigator.router.getActionForPathAndParams('Login');
const initialNavState = RootNavigator.router.getStateForAction(
secondAction,
tempNavState
);
@@ -16,19 +16,19 @@ function nav(state = initialNavState, action) {
let nextState;
switch (action.type) {
case 'Login':
nextState = AppNavigator.router.getStateForAction(
nextState = RootNavigator.router.getStateForAction(
NavigationActions.back(),
state
);
break;
case 'Logout':
nextState = AppNavigator.router.getStateForAction(
nextState = RootNavigator.router.getStateForAction(
NavigationActions.navigate({ routeName: 'Login' }),
state
);
break;
default:
nextState = AppNavigator.router.getStateForAction(action, state);
nextState = RootNavigator.router.getStateForAction(action, state);
break;
}

View File

@@ -1,15 +0,0 @@
import {
createReactNavigationReduxMiddleware,
createReduxBoundAddListener,
} from 'react-navigation-redux-helpers';
const middleware = createReactNavigationReduxMiddleware(
"root",
state => state.nav,
);
const addListener = createReduxBoundAddListener("root");
export {
middleware,
addListener,
};

File diff suppressed because it is too large Load Diff

View File

@@ -69,6 +69,14 @@ declare module 'react-navigation' {
[key: string]: mixed,
};
declare export type NavigationBackAction = {|
type: 'Navigation/BACK',
key?: ?string,
|};
declare export type NavigationInitAction = {|
type: 'Navigation/INIT',
params?: NavigationParams,
|};
declare export type NavigationNavigateAction = {|
type: 'Navigation/NAVIGATE',
routeName: string,
@@ -79,12 +87,6 @@ declare module 'react-navigation' {
key?: string,
|};
declare export type NavigationBackAction = {|
type: 'Navigation/BACK',
key?: ?string,
|};
declare export type NavigationSetParamsAction = {|
type: 'Navigation/SET_PARAMS',
@@ -95,30 +97,6 @@ declare module 'react-navigation' {
params: NavigationParams,
|};
declare export type NavigationInitAction = {|
type: 'Navigation/INIT',
params?: NavigationParams,
|};
declare export type NavigationResetAction = {|
type: 'Navigation/RESET',
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
|};
declare export type NavigationUriAction = {|
type: 'Navigation/URI',
uri: string,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationPopAction = {|
+type: 'Navigation/POP',
+n?: number,
@@ -135,17 +113,51 @@ declare module 'react-navigation' {
+action?: NavigationNavigateAction,
+key?: string,
|};
declare export type NavigationResetAction = {|
type: 'Navigation/RESET',
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationCompleteTransitionAction = {|
+type: 'Navigation/COMPLETE_TRANSITION',
+key?: string,
|};
declare export type NavigationOpenDrawerAction = {|
+type: 'Navigation/OPEN_DRAWER',
+key?: string,
|};
declare export type NavigationCloseDrawerAction = {|
+type: 'Navigation/CLOSE_DRAWER',
+key?: string,
|};
declare export type NavigationToggleDrawerAction = {|
+type: 'Navigation/TOGGLE_DRAWER',
+key?: string,
|};
declare export type NavigationAction =
| NavigationBackAction
| NavigationInitAction
| NavigationNavigateAction
| NavigationReplaceAction
| NavigationSetParamsAction
| NavigationPopAction
| NavigationPopToTopAction
| NavigationPushAction
| NavigationBackAction
| NavigationSetParamsAction
| NavigationResetAction;
| NavigationResetAction
| NavigationReplaceAction
| NavigationCompleteTransitionAction
| NavigationOpenDrawerAction
| NavigationCloseDrawerAction
| NavigationToggleDrawerAction;
/**
* NavigationState is a tree of routes for a single navigator, where each
@@ -269,24 +281,36 @@ declare module 'react-navigation' {
declare export type NavigationComponent =
| NavigationScreenComponent<NavigationRoute, *, *>
| NavigationContainer<*, *, *>
| any;
| NavigationContainer<*, *, *>;
declare interface withOptionalNavigationOptions<Options> {
navigationOptions?: NavigationScreenConfig<Options>;
}
declare export type NavigationScreenComponent<
Route: NavigationRoute,
Options: {},
Props: {}
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
({} | { navigationOptions: NavigationScreenConfig<Options> });
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, Route>,
}> &
withOptionalNavigationOptions<Options>;
declare interface withRouter<State, Options> {
router: NavigationRouter<State, Options>;
}
declare export type NavigationNavigator<
State: NavigationState,
Options: {},
Props: {}
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
router: NavigationRouter<State, Options>,
navigationOptions?: ?NavigationScreenConfig<Options>,
};
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, State>,
}> &
withRouter<State, Options> &
withOptionalNavigationOptions<Options>;
declare export type NavigationRouteConfig =
| NavigationComponent
@@ -426,10 +450,10 @@ declare module 'react-navigation' {
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
tabBarVisible?: boolean,
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
tabBarOnPress?: (
scene: TabScene,
jumpToIndex: (index: number) => void
) => void,
tabBarOnPress?: ({
navigation: NavigationScreenProp<NavigationRoute>,
defaultHandler: () => void,
}) => void,
|};
/**
@@ -485,37 +509,46 @@ declare module 'react-navigation' {
declare export type NavigationScreenProp<+S> = {
+state: S,
dispatch: NavigationDispatch,
goBack: (routeKey?: ?string) => boolean,
dismiss: () => boolean,
navigate: (
routeName: | string
| {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
},
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
setParams: (newParams: NavigationParams) => boolean,
addListener: (
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
push: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
getParam: (paramName: string, fallback?: any) => any,
isFocused: () => boolean,
// Shared action creators that exist for all routers
goBack: (routeKey?: ?string) => boolean,
navigate: (
routeName:
| string
| {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
},
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
setParams: (newParams: NavigationParams) => boolean,
// StackRouter action creators
pop?: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop?: (params?: { immediate?: boolean }) => boolean,
push?: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace?: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
reset?: (actions: NavigationAction[], index: number) => boolean,
dismiss?: () => boolean,
// DrawerRouter action creators
openDrawer?: () => boolean,
closeDrawer?: () => boolean,
toggleDrawer?: () => boolean,
};
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
@@ -532,10 +565,12 @@ declare module 'react-navigation' {
State: NavigationState,
Options: {},
Props: {}
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
router: NavigationRouter<State, Options>,
navigationOptions?: ?NavigationScreenConfig<Options>,
};
> = React$ComponentType<{
...Props,
...NavigationContainerProps<State, Options>,
}> &
withRouter<State, Options> &
withOptionalNavigationOptions<Options>;
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
uriPrefix?: string | RegExp,
@@ -704,44 +739,73 @@ declare module 'react-navigation' {
BACK: 'Navigation/BACK',
INIT: 'Navigation/INIT',
NAVIGATE: 'Navigation/NAVIGATE',
RESET: 'Navigation/RESET',
SET_PARAMS: 'Navigation/SET_PARAMS',
URI: 'Navigation/URI',
back: {
(payload?: { key?: ?string }): NavigationBackAction,
toString: () => string,
},
init: {
(payload?: { params?: NavigationParams }): NavigationInitAction,
toString: () => string,
},
navigate: {
(payload: {
routeName: string,
params?: ?NavigationParams,
action?: ?NavigationNavigateAction,
}): NavigationNavigateAction,
toString: () => string,
},
reset: {
(payload: {
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
}): NavigationResetAction,
toString: () => string,
},
setParams: {
(payload: {
key: string,
params: NavigationParams,
}): NavigationSetParamsAction,
toString: () => string,
},
uri: {
(payload: { uri: string }): NavigationUriAction,
toString: () => string,
},
back: (payload?: { key?: ?string }) => NavigationBackAction,
init: (payload?: { params?: NavigationParams }) => NavigationInitAction,
navigate: (payload: {
routeName: string,
params?: ?NavigationParams,
action?: ?NavigationNavigateAction,
key?: string,
}) => NavigationNavigateAction,
setParams: (payload: {
key: string,
params: NavigationParams,
}) => NavigationSetParamsAction,
};
declare export var StackActions: {
POP: 'Navigation/POP',
POP_TO_TOP: 'Navigation/POP_TO_TOP',
PUSH: 'Navigation/PUSH',
RESET: 'Navigation/RESET',
REPLACE: 'Navigation/REPLACE',
COMPLETE_TRANSITION: 'Navigation/COMPLETE_TRANSITION',
pop: (payload: {
n?: number,
immediate?: boolean,
}) => NavigationPopAction,
popToTop: (payload: {
immediate?: boolean,
}) => NavigationPopToTopAction,
push: (payload: {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
}) => NavigationPushAction,
reset: (payload: {
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
}) => NavigationResetAction,
replace: (payload: {
key: string,
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
}) => NavigationReplaceAction,
completeTransition: (payload: {
key?: string,
}) => NavigationCompleteTransitionAction,
};
declare export var DrawerActions: {
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
openDrawer: (payload: {
key?: string,
}) => NavigationOpenDrawerAction,
closeDrawer: (payload: {
key?: string,
}) => NavigationCloseDrawerAction,
toggleDrawer: (payload: {
key?: string,
}) => NavigationToggleDrawerAction,
};
declare type _RouterProp<S: NavigationState, O: {}> = {
@@ -764,7 +828,7 @@ declare module 'react-navigation' {
view: NavigationView<O, S>,
router: NavigationRouter<S, O>,
navigatorConfig?: NavigatorConfig
): any;
): NavigationNavigator<S, O, *>;
declare export function StackNavigator(
routeConfigMap: NavigationRouteConfigMap,
@@ -807,10 +871,6 @@ declare module 'react-navigation' {
routeConfigs: NavigationRouteConfigMap,
config?: _TabNavigatorConfig
): NavigationContainer<*, *, *>;
declare export function createMaterialBottomTabNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _TabNavigatorConfig
): NavigationContainer<*, *, *>;
declare export function createMaterialTopTabNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _TabNavigatorConfig
@@ -1086,13 +1146,26 @@ declare module 'react-navigation' {
};
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
declare type _NavigationInjectedProps = {
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>;
declare export function withNavigation<Props: {}>(
Component: React$ComponentType<Props>
): React$ComponentType<
$Diff<
Props,
{
navigation: NavigationScreenProp<NavigationStateRoute> | void,
}
>
>;
declare export function withNavigationFocus<Props: {}>(
Component: React$ComponentType<Props>
): React$ComponentType<$Diff<Props, { isFocused: boolean | void }>>;
declare export function getNavigation<State: NavigationState, Options: {}>(
router: NavigationRouter<State, Options>,
state: State,
dispatch: NavigationDispatch,
actionSubscribers: Set<NavigationEventCallback>,
getScreenProps: () => {},
getCurrentNavigation: () => NavigationScreenProp<State>
): NavigationScreenProp<State>;
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "2.0.0-rc.1",
"version": "2.5.4",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -33,12 +33,11 @@
"create-react-context": "^0.2.1",
"hoist-non-react-statics": "^2.2.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10",
"react-lifecycles-compat": "^1.0.2",
"react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.7.0",
"react-navigation-deprecated-tab-navigator": "1.0.1",
"react-navigation-tabs": "0.1.0-alpha.6"
"react-lifecycles-compat": "^3",
"react-native-safe-area-view": "^0.8.0",
"react-navigation-deprecated-tab-navigator": "1.3.0",
"react-navigation-drawer": "0.4.2",
"react-navigation-tabs": "0.5.1"
},
"devDependencies": {
"babel-cli": "^6.24.1",
@@ -48,17 +47,17 @@
"babel-preset-react-native": "^2.1.0",
"codecov": "^2.2.0",
"eslint": "^4.2.0",
"eslint-config-prettier": "^2.3.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-prettier": "^2.1.2",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.1.0",
"husky": "^0.14.3",
"jest": "^22.1.3",
"jest-expo": "^25.1.0",
"lint-staged": "^4.2.1",
"prettier": "^1.5.3",
"prettier-eslint": "^6.4.2",
"prettier": "^1.12.1",
"prettier-eslint": "^8.8.1",
"react": "16.2.0",
"react-native": "^0.52.0",
"react-native-vector-icons": "^4.2.0",
@@ -67,7 +66,7 @@
"jest": {
"notify": true,
"preset": "react-native",
"testRegex": "./src/.*\\-test\\.js$",
"testRegex": "/__tests__/[^/]+-test\\.js$",
"setupFiles": [
"<rootDir>/jest-setup.js"
],
@@ -82,8 +81,11 @@
"coveragePathIgnorePatterns": [
"jest-setup.js"
],
"moduleNameMapper": {
"\\.png$": "<rootDir>/assetsTransformer.js"
},
"modulePathIgnorePatterns": [
"examples"
"<rootDir>/examples/"
],
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"

View File

@@ -4,6 +4,6 @@ set -eo pipefail
case $CIRCLE_NODE_INDEX in
0) yarn test && yarn codecov ;;
1) cd examples/NavigationPlayground && yarn && yarn test ;;
#1) cd examples/NavigationPlayground && yarn && yarn test ;;
#2) cd examples/ReduxExample && yarn && yarn test ;;
esac

View File

@@ -21,7 +21,7 @@ const StateUtils = {
* routes of the navigation state, or -1 if it is not present.
*/
indexOf(state, key) {
return state.routes.map(route => route.key).indexOf(key);
return state.routes.findIndex(route => route.key === key);
},
/**
@@ -116,8 +116,23 @@ const StateUtils = {
/**
* Replace a route by a key.
* Note that this moves the index to the positon to where the new route in the
* stack is at.
* Note that this moves the index to the position to where the new route in the
* stack is at and updates the routes array accordingly.
*/
replaceAndPrune(state, key, route) {
const index = StateUtils.indexOf(state, key);
const replaced = StateUtils.replaceAtIndex(state, index, route);
return {
...replaced,
routes: replaced.routes.slice(0, index + 1),
};
},
/**
* Replace a route by a key.
* Note that this moves the index to the position to where the new route in the
* stack is at. Does not prune the routes.
*/
replaceAt(state, key, route) {
const index = StateUtils.indexOf(state, key);
@@ -137,7 +152,7 @@ const StateUtils = {
route.key
);
if (state.routes[index] === route) {
if (state.routes[index] === route && index === state.index) {
return state;
}
@@ -153,7 +168,7 @@ const StateUtils = {
/**
* Resets all routes.
* Note that this moves the index to the positon to where the last route in the
* Note that this moves the index to the position to where the last route in the
* stack is at if the param `index` isn't provided.
*/
reset(state, routes, index) {

View File

@@ -1,12 +1,13 @@
import React from 'react';
import 'react-native';
import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import NavigationActions from '../NavigationActions';
import createStackNavigator from '../navigators/createStackNavigator';
import { _TESTING_ONLY_reset_container_count } from '../createNavigationContainer';
import createNavigationContainer, {
_TESTING_ONLY_reset_container_count,
} from '../createNavigationContainer';
describe('NavigationContainer', () => {
jest.useFakeTimers();
@@ -20,7 +21,7 @@ describe('NavigationContainer', () => {
const CarScreen = () => <div />;
const DogScreen = () => <div />;
const ElkScreen = () => <div />;
const NavigationContainer = createStackNavigator(
const Stack = createStackNavigator(
{
foo: {
screen: FooScreen,
@@ -45,6 +46,7 @@ describe('NavigationContainer', () => {
initialRouteName: 'foo',
}
);
const NavigationContainer = createNavigationContainer(Stack);
describe('state.nav', () => {
it("should be preloaded with the router's initial state", () => {
@@ -209,7 +211,7 @@ describe('NavigationContainer', () => {
let spy = {};
beforeEach(() => {
spy.console = jest.spyOn(console, 'error').mockImplementation(() => {});
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
@@ -226,7 +228,7 @@ describe('NavigationContainer', () => {
let spy = spyConsole();
it('warns when you render more than one navigator explicitly', () => {
it('warns when you render more than one container explicitly', () => {
class BlankScreen extends React.Component {
render() {
return <View />;
@@ -243,13 +245,17 @@ describe('NavigationContainer', () => {
}
}
const ChildNavigator = createStackNavigator({
Child: BlankScreen,
});
const ChildNavigator = createNavigationContainer(
createStackNavigator({
Child: BlankScreen,
})
);
const RootStack = createStackNavigator({
Root: RootScreen,
});
const RootStack = createNavigationContainer(
createStackNavigator({
Root: RootScreen,
})
);
renderer.create(<RootStack />).toJSON();
expect(spy).toMatchSnapshot();

View File

@@ -202,7 +202,7 @@ describe('StateUtils', () => {
).toEqual(newState);
});
it('Returns the state if index matches the route', () => {
it('Returns the state with updated index if route is unchanged but index changes', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
@@ -210,7 +210,7 @@ describe('StateUtils', () => {
};
expect(
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
).toEqual(state);
).toEqual({ ...state, index: 1 });
});
// Reset

View File

@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NavigationContainer warnings detached navigators warns when you render more than one navigator explicitly 1`] = `
exports[`NavigationContainer warnings detached navigators warns when you render more than one container explicitly 1`] = `
Object {
"console": [MockFunction] {
"calls": Array [
Array [
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://v2.reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
],
],
},

View File

@@ -1,144 +0,0 @@
import NavigationActions from '../NavigationActions';
import addNavigationHelpers from '../addNavigationHelpers';
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
remove: () => {},
});
describe('addNavigationHelpers', () => {
it('handles dismiss action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
const child = { key: 'A', routeName: 'Home' };
expect(
addNavigationHelpers({
state: child,
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
dangerouslyGetParent: () => ({
state: {
key: 'P',
routeName: 'Parent',
routes: [child],
},
}),
}).dismiss()
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
type: NavigationActions.BACK,
key: 'P',
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles Back action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'A', routeName: 'Home' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).goBack('A')
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
type: NavigationActions.BACK,
key: 'A',
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles Back action when the key is not defined', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { routeName: 'Home' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).goBack()
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({ type: NavigationActions.BACK });
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles Navigate action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { routeName: 'Home' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).navigate('Profile', { name: 'Matt' })
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
type: NavigationActions.NAVIGATE,
params: { name: 'Matt' },
routeName: 'Profile',
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles SetParams action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).setParams({ notificationsEnabled: 'yes' })
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
type: NavigationActions.SET_PARAMS,
key: 'B',
params: { notificationsEnabled: 'yes' },
});
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

@@ -0,0 +1,102 @@
import getNavigation from '../getNavigation';
test('getNavigation provides default action helpers', () => {
const router = {
getActionCreators: () => ({}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const dispatch = jest.fn();
const topNav = getNavigation(
router,
{},
dispatch,
new Set(),
() => ({}),
() => topNav
);
topNav.navigate('GreatRoute');
expect(dispatch.mock.calls.length).toBe(1);
expect(dispatch.mock.calls[0][0].type).toBe('Navigation/NAVIGATE');
expect(dispatch.mock.calls[0][0].routeName).toBe('GreatRoute');
});
test('getNavigation provides router action helpers', () => {
const router = {
getActionCreators: () => ({
foo: bar => ({ type: 'FooBarAction', bar }),
}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const dispatch = jest.fn();
const topNav = getNavigation(
router,
{},
dispatch,
new Set(),
() => ({}),
() => topNav
);
topNav.foo('Great');
expect(dispatch.mock.calls.length).toBe(1);
expect(dispatch.mock.calls[0][0].type).toBe('FooBarAction');
expect(dispatch.mock.calls[0][0].bar).toBe('Great');
});
test('getNavigation get child navigation with router', () => {
const actionSubscribers = new Set();
let navigation = null;
const routerA = {
getActionCreators: () => ({}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const router = {
childRouters: {
RouteA: routerA,
},
getActionCreators: () => ({}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const initState = {
index: 0,
routes: [
{
key: 'a',
routeName: 'RouteA',
routes: [{ key: 'c', routeName: 'RouteC' }],
index: 0,
},
{ key: 'b', routeName: 'RouteB' },
],
};
const topNav = getNavigation(
router,
initState,
() => {},
actionSubscribers,
() => ({}),
() => navigation
);
const childNavA = topNav.getChildNavigation('a');
expect(childNavA.router).toBe(routerA);
});

View File

@@ -1,105 +0,0 @@
/* Helpers for navigation */
import NavigationActions from './NavigationActions';
import invariant from './utils/invariant';
export default function(navigation) {
return {
...navigation,
// Go back from the given key, default to active key
goBack: key => {
let actualizedKey = key;
if (key === undefined && navigation.state.key) {
invariant(
typeof navigation.state.key === 'string',
'key should be a string'
);
actualizedKey = navigation.state.key;
}
return navigation.dispatch(
NavigationActions.back({ key: actualizedKey })
);
},
// Go back from the parent key. If this is a nested stack, the entire
// stack will be dismissed.
dismiss: () => {
let parent = navigation.dangerouslyGetParent();
if (parent && parent.state) {
return navigation.dispatch(
NavigationActions.back({ key: parent.state.key })
);
} else {
return false;
}
},
navigate: (navigateTo, params, action) => {
if (typeof navigateTo === 'string') {
return navigation.dispatch(
NavigationActions.navigate({ routeName: navigateTo, params, action })
);
}
invariant(
typeof navigateTo === 'object',
'Must navigateTo an object or a string'
);
invariant(
params == null,
'Params must not be provided to .navigate() when specifying an object'
);
invariant(
action == null,
'Child action must not be provided to .navigate() when specifying an object'
);
return navigation.dispatch(NavigationActions.navigate(navigateTo));
},
pop: (n, params) =>
navigation.dispatch(
NavigationActions.pop({ n, immediate: params && params.immediate })
),
popToTop: params =>
navigation.dispatch(
NavigationActions.popToTop({ immediate: params && params.immediate })
),
/**
* For updating current route params. For example the nav bar title and
* buttons are based on the route params.
* This means `setParams` can be used to update nav bar for example.
*/
setParams: params => {
invariant(
navigation.state.key && typeof navigation.state.key === 'string',
'setParams cannot be called by root navigator'
);
const key = navigation.state.key;
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 })
),
replace: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.replace({
routeName,
params,
action,
key: navigation.state.key,
})
),
openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()),
closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()),
toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()),
};
}

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Linking, AsyncStorage } from 'react-native';
import withLifecyclePolyfill from 'react-lifecycles-compat';
import { AsyncStorage, Linking, Platform } from 'react-native';
import { polyfill } from 'react-lifecycles-compat';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
import getNavigation from './getNavigation';
import invariant from './utils/invariant';
import docsUrl from './utils/docsUrl';
@@ -184,9 +185,9 @@ export default function createNavigationContainer(Component) {
}
componentDidUpdate() {
// Clear cached _nav every tick
if (this._nav === this.state.nav) {
this._nav = null;
// Clear cached _navState every tick
if (this._navState === this.state.nav) {
this._navState = null;
}
}
@@ -198,20 +199,39 @@ export default function createNavigationContainer(Component) {
if (__DEV__ && !this.props.detached) {
if (_statefulContainerCount > 0) {
console.error(
`You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl(
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
)}`
);
// Temporarily only show this on iOS due to this issue:
// https://github.com/react-navigation/react-navigation/issues/4196#issuecomment-390827829
if (Platform.OS === 'ios') {
console.warn(
`You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl(
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
)}`
);
}
}
}
_statefulContainerCount++;
Linking.addEventListener('url', this._handleOpenURL);
// Pull out anything that can impact state
const { persistenceKey } = this.props;
const startupStateJSON =
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
const url = await Linking.getInitialURL();
const parsedUrl = url && this._urlToPathAndParams(url);
// Initialize state. This must be done *after* any async code
// so we don't end up with a different value for this.state.nav
// due to changes while async function was resolving
let action = this._initialAction;
let startupState = this.state.nav;
if (!startupState) {
!!process.env.REACT_NAV_LOGGING &&
console.log('Init new Navigation State');
startupState = Component.router.getStateForAction(action);
}
// Pull persisted state from AsyncStorage
if (startupStateJSON) {
try {
startupState = JSON.parse(startupStateJSON);
@@ -219,15 +239,7 @@ export default function createNavigationContainer(Component) {
} catch (e) {}
}
let action = this._initialAction;
if (!startupState) {
!!process.env.REACT_NAV_LOGGING &&
console.log('Init new Navigation State');
startupState = Component.router.getStateForAction(action);
}
const url = await Linking.getInitialURL();
const parsedUrl = url && this._urlToPathAndParams(url);
// Pull state out of URL
if (parsedUrl) {
const { path, params } = parsedUrl;
const urlAction = Component.router.getActionForPathAndParams(
@@ -244,11 +256,8 @@ export default function createNavigationContainer(Component) {
);
}
}
if (startupState === this.state.nav) {
return;
}
this.setState({ nav: startupState }, () => {
_reactNavigationIsHydratingState = false;
const dispatchActions = () =>
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
@@ -257,6 +266,15 @@ export default function createNavigationContainer(Component) {
lastState: null,
})
);
if (startupState === this.state.nav) {
dispatchActions();
return;
}
this.setState({ nav: startupState }, () => {
_reactNavigationIsHydratingState = false;
dispatchActions();
});
}
@@ -267,6 +285,8 @@ export default function createNavigationContainer(Component) {
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
);
this.dispatch(NavigationActions.init());
} else {
throw e;
}
}
@@ -294,58 +314,68 @@ export default function createNavigationContainer(Component) {
if (this.props.navigation) {
return this.props.navigation.dispatch(action);
}
this._nav = this._nav || this.state.nav;
const oldNav = this._nav;
invariant(oldNav, 'should be set in constructor if stateful');
const nav = Component.router.getStateForAction(action, oldNav);
// navState will have the most up-to-date value, because setState sometimes behaves asyncronously
this._navState = this._navState || this.state.nav;
const lastNavState = this._navState;
invariant(lastNavState, 'should be set in constructor if stateful');
const reducedState = Component.router.getStateForAction(
action,
lastNavState
);
const navState = reducedState === null ? lastNavState : reducedState;
const dispatchActionEvents = () => {
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action,
state: nav,
lastState: oldNav,
state: navState,
lastState: lastNavState,
})
);
};
if (nav && nav !== oldNav) {
if (reducedState === null) {
// The router will return null when action has been handled and the state hasn't changed.
// dispatch returns true when something has been handled.
dispatchActionEvents();
return true;
}
if (navState !== lastNavState) {
// Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
this._nav = nav;
this.setState({ nav }, () => {
this._onNavigationStateChange(oldNav, nav, action);
this._navState = navState;
this.setState({ nav: navState }, () => {
this._onNavigationStateChange(lastNavState, navState, action);
dispatchActionEvents();
this._persistNavigationState(nav);
this._persistNavigationState(navState);
});
return true;
} else {
dispatchActionEvents();
}
dispatchActionEvents();
return false;
};
_getScreenProps = () => this.props.screenProps;
render() {
let navigation = this.props.navigation;
if (this._isStateful()) {
const nav = this.state.nav;
if (!nav) {
const navState = this.state.nav;
if (!navState) {
return this._renderLoading();
}
if (!this._navigation || this._navigation.state !== nav) {
this._navigation = {
dispatch: this.dispatch,
state: nav,
addListener: (eventName, handler) => {
if (eventName !== 'action') {
return { remove: () => {} };
}
this._actionEventSubscribers.add(handler);
return {
remove: () => {
this._actionEventSubscribers.delete(handler);
},
};
},
};
if (!this._navigation || this._navigation.state !== navState) {
this._navigation = getNavigation(
Component.router,
navState,
this.dispatch,
this._actionEventSubscribers,
this._getScreenProps,
() => this._navigation
);
}
navigation = this._navigation;
}
@@ -354,5 +384,5 @@ export default function createNavigationContainer(Component) {
}
}
return withLifecyclePolyfill(NavigationContainer);
return polyfill(NavigationContainer);
}

View File

@@ -138,11 +138,14 @@ export default function getChildEventSubscriber(addListener, key) {
emit((lastEmittedEvent = 'didBlur'), childPayload);
}
}
if (lastEmittedEvent === 'didBlur' && !newRoute) {
removeAll();
}
})
);
return {
removeAll,
addListener(eventName, eventHandler) {
const subscribers = getChildSubscribers(eventName);
if (!subscribers) {

105
src/getChildNavigation.js Normal file
View File

@@ -0,0 +1,105 @@
import getChildEventSubscriber from './getChildEventSubscriber';
import getChildRouter from './getChildRouter';
import getNavigationActionCreators from './routers/getNavigationActionCreators';
import invariant from './utils/invariant';
const createParamGetter = route => (paramName, defaultValue) => {
const params = route.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
};
function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
const children =
navigation._childrenNavigation || (navigation._childrenNavigation = {});
const childRoute = navigation.state.routes.find(r => r.key === childKey);
if (children[childKey] && children[childKey].state === childRoute) {
return children[childKey];
}
const childRouter = getChildRouter(navigation.router, childRoute.routeName);
// If the route has children, we'll use this to pass in to the action creators
// for the childRouter so that any action that depends on the active route will
// behave as expected. We don't explicitly require that routers implement routes
// and index properties, but if we did then we would put an invariant here to
// ensure that a focusedGrandChildRoute exists if childRouter is defined.
const focusedGrandChildRoute =
childRoute.routes && typeof childRoute.index === 'number'
? childRoute.routes[childRoute.index]
: null;
const actionCreators = {
...navigation.actions,
...navigation.router.getActionCreators(childRoute, navigation.state.key),
...(childRouter
? childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key)
: {}),
...getNavigationActionCreators(childRoute),
};
const actionHelpers = {};
Object.keys(actionCreators).forEach(actionName => {
actionHelpers[actionName] = (...args) => {
const actionCreator = actionCreators[actionName];
const action = actionCreator(...args);
return navigation.dispatch(action);
};
});
if (children[childKey]) {
children[childKey] = {
...children[childKey],
...actionHelpers,
state: childRoute,
router: childRouter,
actions: actionCreators,
getParam: createParamGetter(childRoute),
};
return children[childKey];
}
const childSubscriber = getChildEventSubscriber(
navigation.addListener,
childKey
);
children[childKey] = {
...actionHelpers,
state: childRoute,
router: childRouter,
actions: actionCreators,
getParam: createParamGetter(childRoute),
getChildNavigation: grandChildKey =>
getChildNavigation(children[childKey], grandChildKey, () =>
getCurrentParentNavigation().getChildNavigation(childKey)
),
isFocused: () => {
const currentNavigation = getCurrentParentNavigation();
const { routes, index } = currentNavigation.state;
if (!currentNavigation.isFocused()) {
return false;
}
if (routes[index].key === childKey) {
return true;
}
return false;
},
dispatch: navigation.dispatch,
getScreenProps: navigation.getScreenProps,
dangerouslyGetParent: getCurrentParentNavigation,
addListener: childSubscriber.addListener,
};
return children[childKey];
}
export default getChildNavigation;

9
src/getChildRouter.js Normal file
View File

@@ -0,0 +1,9 @@
export default function getChildRouter(router, routeName) {
if (router.childRouters && router.childRouters[routeName]) {
return router.childRouters[routeName];
}
const Component = router.getComponentForRouteName(routeName);
return Component.router;
}

54
src/getNavigation.js Normal file
View File

@@ -0,0 +1,54 @@
import getNavigationActionCreators from './routers/getNavigationActionCreators';
import getChildNavigation from './getChildNavigation';
export default function getNavigation(
router,
state,
dispatch,
actionSubscribers,
getScreenProps,
getCurrentNavigation
) {
const actions = router.getActionCreators(state, null);
const navigation = {
actions,
router,
state,
dispatch,
getScreenProps,
getChildNavigation: childKey =>
getChildNavigation(navigation, childKey, getCurrentNavigation),
isFocused: childKey => {
const { routes, index } = getCurrentNavigation().state;
if (childKey == null || routes[index].key === childKey) {
return true;
}
return false;
},
addListener: (eventName, handler) => {
if (eventName !== 'action') {
return { remove: () => {} };
}
actionSubscribers.add(handler);
return {
remove: () => {
actionSubscribers.delete(handler);
},
};
},
dangerouslyGetParent: () => null,
};
const actionCreators = {
...getNavigationActionCreators(navigation.state),
...actions,
};
Object.keys(actionCreators).forEach(actionName => {
navigation[actionName] = (...args) =>
navigation.dispatch(actionCreators[actionName](...args));
});
return navigation;
}

View File

@@ -1,33 +0,0 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import DrawerNavigator from '../createDrawerNavigator';
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: `Welcome ${
navigation.state.params ? navigation.state.params.user : 'anonymous'
}`,
gesturesEnabled: true,
});
render() {
return null;
}
}
const routeConfig = {
Home: {
screen: HomeScreen,
},
};
describe('DrawerNavigator', () => {
it('renders successfully', () => {
const MyDrawerNavigator = DrawerNavigator(routeConfig);
const rendered = renderer.create(<MyDrawerNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import renderer from 'react-test-renderer';
import StackNavigator from '../createContainedStackNavigator';
const SubNavigator = StackNavigator({
Home: {
screen: () => null,
},
});
const NavNestedDirect = StackNavigator({
Sub: {
screen: SubNavigator,
},
});
const NavNestedIndirect = StackNavigator({
Sub: {
// eslint-disable-next-line react/display-name
screen: props => <SubNavigator {...props} />,
},
});
/* Prevent React error boundaries from swallowing the errors */
NavNestedIndirect.prototype.componentDidCatch = null;
SubNavigator.prototype.componentDidCatch = null;
describe('Nested navigators', () => {
it('renders succesfully as direct child', () => {
const rendered = renderer.create(<NavNestedDirect />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('throw when trying to pass navigation prop', () => {
const tryRender = () => {
renderer.create(<NavNestedIndirect />);
};
expect(tryRender).toThrowErrorMatchingSnapshot();
});
});

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import StackNavigator from '../createStackNavigator';
import StackNavigator from '../createContainedStackNavigator';
import withNavigation from '../../views/withNavigation';
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import SwitchNavigator from '../createSwitchNavigator';
import SwitchNavigator from '../createContainedSwitchNavigator';
const A = () => <View />;
const B = () => <View />;

View File

@@ -1,243 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DrawerNavigator renders successfully 1`] = `
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Object {
"backgroundColor": "transparent",
"flex": 1,
}
}
>
<View
collapsable={undefined}
style={
Object {
"flex": 1,
"zIndex": 0,
}
}
/>
<View
accessibilityComponentType={undefined}
accessibilityLabel={undefined}
accessibilityTraits={undefined}
accessible={true}
collapsable={undefined}
hitSlop={undefined}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
pointerEvents="none"
style={
Object {
"backgroundColor": "#000",
"bottom": 0,
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"zIndex": 1000,
}
}
testID={undefined}
/>
<View
accessibilityViewIsModal={false}
collapsable={undefined}
style={
Object {
"backgroundColor": "white",
"bottom": 0,
"left": 0,
"position": "absolute",
"right": null,
"top": 0,
"transform": Array [
Object {
"translateX": -320,
},
],
"width": 320,
"zIndex": 1001,
}
}
>
<View
style={
Array [
Object {
"flex": 1,
},
undefined,
]
}
>
<RCTScrollView
DEPRECATED_sendUpdatedChildFrames={false}
alwaysBounceHorizontal={undefined}
alwaysBounceVertical={false}
onContentSizeChange={null}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onResponderGrant={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={undefined}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
scrollEventThrottle={undefined}
sendMomentumEvents={false}
style={
Array [
Object {
"flexDirection": "column",
"flexGrow": 1,
"flexShrink": 1,
"overflow": "scroll",
},
undefined,
]
}
>
<RCTScrollContentView
collapsable={false}
removeClippedSubviews={undefined}
style={
Array [
undefined,
undefined,
]
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Array [
Object {
"paddingVertical": 4,
},
undefined,
]
}
>
<View
accessibilityComponentType={undefined}
accessibilityLabel={undefined}
accessibilityTraits={undefined}
accessible={true}
collapsable={undefined}
hasTVPreferredFocus={undefined}
hitSlop={undefined}
isTVSelectable={true}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
testID={undefined}
tvParallaxProperties={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "rgba(0, 0, 0, .04)",
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 0,
}
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
},
undefined,
]
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Array [
Object {
"fontWeight": "bold",
"margin": 16,
},
Object {
"color": "#2196f3",
},
undefined,
undefined,
]
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>
</View>
</View>
</RCTScrollContentView>
</RCTScrollView>
</View>
</View>
</View>
`;

View File

@@ -0,0 +1,371 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nested navigators renders succesfully as direct child 1`] = `
<View
onLayout={[Function]}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
>
<View
onLayout={[Function]}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
/>
</View>
<View
onLayout={[Function]}
pointerEvents="box-none"
>
<View
collapsable={undefined}
style={
Object {
"backgroundColor": "#F7F7F7",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<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": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
<View
onLayout={[Function]}
pointerEvents="box-none"
>
<View
collapsable={undefined}
style={
Object {
"backgroundColor": "#F7F7F7",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<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": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;
exports[`Nested navigators throw when trying to pass navigation prop 1`] = `"No \\"routes\\" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop"`;

View File

@@ -76,51 +76,37 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
/>
</View>
<View
collapsable={undefined}
onLayout={[Function]}
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
pointerEvents="box-none"
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
"backgroundColor": "#F7F7F7",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
@@ -128,70 +114,89 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
}
}
>
<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": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"opacity": 1,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View />
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
}
}
>
<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": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
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>
@@ -277,51 +282,37 @@ exports[`StackNavigator renders successfully 1`] = `
/>
</View>
<View
collapsable={undefined}
onLayout={[Function]}
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
pointerEvents="box-none"
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
"backgroundColor": "#F7F7F7",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
@@ -329,52 +320,71 @@ exports[`StackNavigator renders successfully 1`] = `
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
<View
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
Welcome anonymous
</Text>
<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": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>
</View>

View File

@@ -2,12 +2,7 @@
exports[`TabNavigator renders successfully 1`] = `
<View
loaded={
Array [
0,
]
}
onLayout={[Function]}
collapsable={false}
style={
Array [
Object {
@@ -21,56 +16,65 @@ exports[`TabNavigator renders successfully 1`] = `
}
>
<View
collapsable={undefined}
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onLayout={[Function]}
style={
Object {
"alignItems": "stretch",
"flex": 1,
"flexDirection": "row",
}
}
>
<View
collapsable={undefined}
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
"alignItems": "stretch",
"flex": 1,
"flexDirection": "row",
}
}
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
/>
>
<View
style={
Object {
"flex": 1,
}
}
/>
</View>
</View>
</View>
</View>
@@ -143,7 +147,7 @@ exports[`TabNavigator renders successfully 1`] = `
},
false,
Object {
"flexGrow": 1,
"flex": 1,
},
]
}
@@ -156,6 +160,7 @@ exports[`TabNavigator renders successfully 1`] = `
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"minWidth": 30,
"opacity": 1,
"position": "absolute",
"width": "100%",
@@ -170,6 +175,7 @@ exports[`TabNavigator renders successfully 1`] = `
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"minWidth": 30,
"opacity": 0,
"position": "absolute",
"width": "100%",

View File

@@ -0,0 +1,9 @@
import createNavigationContainer from '../createNavigationContainer';
import createStackNavigator from './createStackNavigator';
const StackNavigator = (routeConfigs, config = {}) => {
const navigator = createStackNavigator(routeConfigs, config);
return createNavigationContainer(navigator);
};
export default StackNavigator;

View File

@@ -0,0 +1,9 @@
import createNavigationContainer from '../createNavigationContainer';
import createSwitchNavigator from './createSwitchNavigator';
const SwitchNavigator = (routeConfigs, config = {}) => {
const navigator = createSwitchNavigator(routeConfigs, config);
return createNavigationContainer(navigator);
};
export default SwitchNavigator;

View File

@@ -1,71 +0,0 @@
import React from 'react';
import { Dimensions, Platform, ScrollView } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
import DrawerRouter from '../routers/DrawerRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)
// and the view's props
const defaultContentComponent = props => (
<ScrollView alwaysBounceVertical={false}>
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
<DrawerItems {...props} />
</SafeAreaView>
</ScrollView>
);
const DefaultDrawerConfig = {
drawerWidth: () => {
/*
* Default drawer width is screen width - header height
* with a max width of 280 on mobile and 320 on tablet
* https://material.io/guidelines/patterns/navigation-drawer.html
*/
const { height, width } = Dimensions.get('window');
const smallerAxisSize = Math.min(height, width);
const isLandscape = width > height;
const isTablet = smallerAxisSize >= 600;
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
const maxWidth = isTablet ? 320 : 280;
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
},
contentComponent: defaultContentComponent,
drawerPosition: 'left',
drawerBackgroundColor: 'white',
useNativeAnimations: true,
};
const DrawerNavigator = (routeConfigs, config = {}) => {
const mergedConfig = { ...DefaultDrawerConfig, ...config };
const {
order,
paths,
initialRouteName,
backBehavior,
...drawerConfig
} = mergedConfig;
const routerConfig = {
order,
paths,
initialRouteName,
backBehavior,
};
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
return createNavigationContainer(navigator);
};
export default DrawerNavigator;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { TextInput } from 'react-native';
export default Navigator =>
class KeyboardAwareNavigator extends React.Component {
static router = Navigator.router;
_previouslyFocusedTextInput = null;
render() {
return (
<Navigator
{...this.props}
onGestureBegin={this._handleGestureBegin}
onGestureCanceled={this._handleGestureCanceled}
onGestureFinish={this._handleGestureFinish}
onTransitionStart={this._handleTransitionStart}
/>
);
}
_handleGestureBegin = () => {
this._previouslyFocusedTextInput = TextInput.State.currentlyFocusedField();
if (this._previouslyFocusedTextInput) {
TextInput.State.blurTextInput(this._previouslyFocusedTextInput);
}
this.props.onGestureBegin && this.props.onGestureBegin();
};
_handleGestureCanceled = () => {
if (this._previouslyFocusedTextInput) {
TextInput.State.focusTextInput(this._previouslyFocusedTextInput);
}
this.props.onGestureCanceled && this.props.onGestureCanceled();
};
_handleGestureFinish = () => {
this._previouslyFocusedTextInput = null;
this.props.onGestureFinish && this.props.onGestureFinish();
};
_handleTransitionStart = (transitionProps, prevTransitionProps) => {
// TODO: We should not even have received the transition start event
// in the case where the index did not change, I believe. We
// should revisit this after 2.0 release.
if (transitionProps.index !== prevTransitionProps.index) {
const currentField = TextInput.State.currentlyFocusedField();
if (currentField) {
TextInput.State.blurTextInput(currentField);
}
}
this.props.onTransitionStart &&
this.props.onTransitionStart(transitionProps, prevTransitionProps);
};
};

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { polyfill } from 'react-lifecycles-compat';
import getChildEventSubscriber from '../getChildEventSubscriber';
@@ -7,82 +8,37 @@ function createNavigator(NavigatorView, router, navigationConfig) {
static router = router;
static navigationOptions = null;
childEventSubscribers = {};
// Cleanup subscriptions for routes that no longer exist
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
Object.keys(this.childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
this.childEventSubscribers[key].removeAll();
delete this.childEventSubscribers[key];
}
});
}
// Remove all subscriptions
componentWillUnmount() {
Object.values(this.childEventSubscribers).map(s => s.removeAll());
}
_isRouteFocused = route => () => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
state = {
descriptors: {},
screenProps: this.props.screenProps,
};
_dangerouslyGetParent = () => {
return this.props.navigation;
};
render() {
const { navigation, screenProps } = this.props;
static getDerivedStateFromProps(nextProps, prevState) {
const prevDescriptors = prevState.descriptors;
const { navigation, screenProps } = nextProps;
const { dispatch, state, addListener } = navigation;
const { routes } = state;
if (typeof routes === 'undefined') {
throw new TypeError(
'No "routes" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop'
);
}
const descriptors = { ...prevState.descriptors };
const descriptors = {};
routes.forEach(route => {
if (
prevDescriptors &&
prevDescriptors[route.key] &&
route === prevDescriptors[route.key].state &&
screenProps === prevState.screenProps
) {
descriptors[route.key] = prevDescriptors[route.key];
return;
}
const getComponent = () =>
router.getComponentForRouteName(route.routeName);
if (!this.childEventSubscribers[route.key]) {
this.childEventSubscribers[route.key] = getChildEventSubscriber(
addListener,
route.key
);
}
const actionCreators = {
...navigation.actions,
...router.getActionCreators(route, state.key),
};
const actionHelpers = {};
Object.keys(actionCreators).forEach(actionName => {
actionHelpers[actionName] = (...args) => {
const actionCreator = actionCreators[actionName];
const action = actionCreator(...args);
dispatch(action);
};
});
const childNavigation = {
...actionHelpers,
actions: actionCreators,
dispatch,
state: route,
isFocused: () => this._isRouteFocused(route),
dangerouslyGetParent: this._dangerouslyGetParent,
addListener: this.childEventSubscribers[route.key].addListener,
getParam: (paramName, defaultValue) => {
const params = route.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
},
};
const childNavigation = navigation.getChildNavigation(route.key);
const options = router.getScreenOptions(childNavigation, screenProps);
descriptors[route.key] = {
key: route.key,
@@ -93,17 +49,23 @@ function createNavigator(NavigatorView, router, navigationConfig) {
};
});
return { descriptors, screenProps };
}
render() {
return (
<NavigatorView
screenProps={screenProps}
navigation={navigation}
{...this.props}
screenProps={this.state.screenProps}
navigation={this.props.navigation}
navigationConfig={navigationConfig}
descriptors={descriptors}
descriptors={this.state.descriptors}
/>
);
}
}
return Navigator;
return polyfill(Navigator);
}
export default createNavigator;

View File

@@ -1,5 +1,4 @@
import * as React from 'react';
import createNavigationContainer from '../createNavigationContainer';
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
import createNavigator from './createNavigator';
import StackView from '../views/StackView/StackView';
import StackRouter from '../routers/StackRouter';
@@ -11,6 +10,8 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
initialRouteParams,
paths,
navigationOptions,
disableKeyboardHandling,
getCustomActionCreators,
} = stackConfig;
const stackRouterConfig = {
@@ -19,15 +20,18 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
initialRouteParams,
paths,
navigationOptions,
getCustomActionCreators,
};
const router = StackRouter(routeConfigMap, stackRouterConfig);
// Create a navigator with StackView as the view
const Navigator = createNavigator(StackView, router, stackConfig);
let Navigator = createNavigator(StackView, router, stackConfig);
if (!disableKeyboardHandling) {
Navigator = createKeyboardAwareNavigator(Navigator);
}
// HOC to provide the navigation prop for the top-level navigator (when the prop is missing)
return createNavigationContainer(Navigator);
return Navigator;
}
export default createStackNavigator;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import createNavigationContainer from '../createNavigationContainer';
import createNavigator from '../navigators/createNavigator';
import SwitchRouter from '../routers/SwitchRouter';
import SwitchView from '../views/SwitchView/SwitchView';
@@ -7,7 +6,7 @@ import SwitchView from '../views/SwitchView/SwitchView';
function createSwitchNavigator(routeConfigMap, switchConfig = {}) {
const router = SwitchRouter(routeConfigMap, switchConfig);
const Navigator = createNavigator(SwitchView, router, switchConfig);
return createNavigationContainer(Navigator);
return Navigator;
}
export default createSwitchNavigator;

View File

@@ -8,57 +8,58 @@ module.exports = {
get StateUtils() {
return require('./StateUtils').default;
},
get getNavigation() {
return require('./getNavigation').default;
},
// Navigators
get createNavigator() {
return require('./navigators/createNavigator').default;
},
get createStackNavigator() {
return require('./navigators/createStackNavigator').default;
return require('./navigators/createContainedStackNavigator').default;
},
get StackNavigator() {
console.warn(
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
);
return require('./navigators/createStackNavigator').default;
return require('./navigators/createContainedStackNavigator').default;
},
get createSwitchNavigator() {
return require('./navigators/createSwitchNavigator').default;
return require('./navigators/createContainedSwitchNavigator').default;
},
get SwitchNavigator() {
console.warn(
'The SwitchNavigator function name is deprecated, please use createSwitchNavigator instead'
);
return require('./navigators/createSwitchNavigator').default;
return require('./navigators/createContainedSwitchNavigator').default;
},
get createDrawerNavigator() {
return require('./navigators/createDrawerNavigator').default;
return require('react-navigation-drawer').createDrawerNavigator;
},
get DrawerNavigator() {
console.warn(
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
);
return require('./navigators/createDrawerNavigator').default;
return require('react-navigation-drawer').createDrawerNavigator;
},
get createTabNavigator() {
console.warn(
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopNavigator instead.'
'createTabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
);
return require('react-navigation-deprecated-tab-navigator')
.createTabNavigator;
},
get TabNavigator() {
console.warn(
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopNavigator instead.'
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
);
return require('react-navigation-deprecated-tab-navigator').default;
return require('react-navigation-deprecated-tab-navigator')
.createTabNavigator;
},
get createBottomTabNavigator() {
return require('react-navigation-tabs').createBottomTabNavigator;
},
get createMaterialBottomTabNavigator() {
return require('react-navigation-tabs').createMaterialBottomTabNavigator;
},
get createMaterialTopTabNavigator() {
return require('react-navigation-tabs').createMaterialTopTabNavigator;
},
@@ -71,7 +72,7 @@ module.exports = {
return require('./routers/StackActions').default;
},
get DrawerActions() {
return require('./routers/DrawerActions').default;
return require('react-navigation-drawer').DrawerActions;
},
// Routers
@@ -81,6 +82,9 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get DrawerRouter() {
return require('react-navigation-drawer').DrawerRouter;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
@@ -118,10 +122,13 @@ module.exports = {
// DrawerView
get DrawerView() {
return require('./views/Drawer/DrawerView').default;
return require('react-navigation-drawer').DrawerView;
},
get DrawerItems() {
return require('./views/Drawer/DrawerNavigatorItems').default;
return require('react-navigation-drawer').DrawerNavigatorItems;
},
get DrawerSidebar() {
return require('react-navigation-drawer').DrawerSidebar;
},
// TabView

View File

@@ -8,11 +8,17 @@ module.exports = {
get StateUtils() {
return require('./StateUtils').default;
},
get getNavigation() {
return require('./getNavigation').default;
},
// Navigators
get createNavigator() {
return require('./navigators/createNavigator').default;
},
get createSwitchNavigator() {
return require('./navigators/createSwitchNavigator').default;
},
// Actions
get NavigationActions() {

View File

@@ -1,28 +0,0 @@
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
const openDrawer = payload => ({
type: OPEN_DRAWER,
...payload,
});
const closeDrawer = payload => ({
type: CLOSE_DRAWER,
...payload,
});
const toggleDrawer = payload => ({
type: TOGGLE_DRAWER,
...payload,
});
export default {
OPEN_DRAWER,
CLOSE_DRAWER,
TOGGLE_DRAWER,
openDrawer,
closeDrawer,
toggleDrawer,
};

View File

@@ -1,77 +0,0 @@
import SwitchRouter from './SwitchRouter';
import NavigationActions from '../NavigationActions';
import invariant from '../utils/invariant';
import withDefaultValue from '../utils/withDefaultValue';
import DrawerActions from './DrawerActions';
export default (routeConfigs, config = {}) => {
config = { ...config };
config = withDefaultValue(config, 'resetOnBlur', false);
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
const switchRouter = SwitchRouter(routeConfigs, config);
return {
...switchRouter,
getActionCreators(route, navStateKey) {
return {
openDrawer: () => DrawerActions.openDrawer({ key: navStateKey }),
closeDrawer: () => DrawerActions.closeDrawer({ key: navStateKey }),
toggleDrawer: () => DrawerActions.toggleDrawer({ key: navStateKey }),
...switchRouter.getActionCreators(route, navStateKey),
};
},
getStateForAction(action, lastState) {
const state = lastState || {
...switchRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
const isRouterTargeted = action.key == null || action.key === state.key;
if (
isRouterTargeted &&
action.type === DrawerActions.CLOSE_DRAWER &&
state.isDrawerOpen
) {
return {
...state,
isDrawerOpen: false,
};
}
if (
isRouterTargeted &&
action.type === DrawerActions.OPEN_DRAWER &&
!state.isDrawerOpen
) {
return {
...state,
isDrawerOpen: true,
};
}
if (isRouterTargeted && action.type === DrawerActions.TOGGLE_DRAWER) {
return {
...state,
isDrawerOpen: !state.isDrawerOpen,
};
}
// Fall back on tab router for screen switching logic
const childState = switchRouter.getStateForAction(action, state);
if (childState !== null && childState !== state) {
// If the tabs have changed, make sure to close the drawer
return {
...childState,
isDrawerOpen: false,
};
}
return state;
},
};
};

View File

@@ -8,7 +8,6 @@ import StateUtils from '../StateUtils';
import validateRouteConfigMap from './validateRouteConfigMap';
import invariant from '../utils/invariant';
import { generateKey } from './KeyGenerator';
import getNavigationActionCreators from './getNavigationActionCreators';
function isEmpty(obj) {
if (!obj) return true;
@@ -27,6 +26,10 @@ function behavesLikePushAction(action) {
const defaultActionCreators = (route, navStateKey) => ({});
function isResetToRootStack(action) {
return action.type === StackActions.RESET && action.key === null;
}
export default (routeConfigs, stackConfig = {}) => {
// Fail fast on invalid route definitions
validateRouteConfigMap(routeConfigs);
@@ -145,6 +148,8 @@ export default (routeConfigs, stackConfig = {}) => {
paths.sort((a, b) => b[1].priority - a[1].priority);
return {
childRouters,
getComponentForState(state) {
const activeChildRoute = state.routes[state.index];
const { routeName } = activeChildRoute;
@@ -160,7 +165,6 @@ export default (routeConfigs, stackConfig = {}) => {
getActionCreators(route, navStateKey) {
return {
...getNavigationActionCreators(route, navStateKey),
...getCustomActionCreators(route, navStateKey),
pop: (n, params) =>
StackActions.pop({
@@ -223,7 +227,10 @@ export default (routeConfigs, stackConfig = {}) => {
// 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 !== StackActions.RESET || action.key !== null) {
if (
!isResetToRootStack(action) &&
action.type !== NavigationActions.NAVIGATE
) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
: -1;
@@ -243,6 +250,38 @@ export default (routeConfigs, stackConfig = {}) => {
return StateUtils.replaceAt(state, childRoute.key, route);
}
}
} else if (action.type === NavigationActions.NAVIGATE) {
// Traverse routes from the top of the stack to the bottom, so the
// active route has the first opportunity, then the one before it, etc.
for (let childRoute of state.routes.slice().reverse()) {
let childRouter = childRouters[childRoute.routeName];
let childAction =
action.routeName === childRoute.routeName && action.action
? action.action
: action;
if (childRouter) {
const nextRouteState = childRouter.getStateForAction(
childAction,
childRoute
);
if (nextRouteState === null || nextRouteState !== childRoute) {
const newState = StateUtils.replaceAndPrune(
state,
nextRouteState ? nextRouteState.key : childRoute.key,
nextRouteState ? nextRouteState : childRoute
);
return {
...newState,
isTransitioning:
state.index !== newState.index
? action.immediate !== true
: state.isTransitioning,
};
}
}
}
}
// Handle explicit push navigation action. This must happen after the
@@ -295,7 +334,7 @@ export default (routeConfigs, stackConfig = {}) => {
isTransitioning:
state.index !== lastRouteIndex
? action.immediate !== true
: undefined,
: state.isTransitioning,
index: lastRouteIndex,
routes,
};
@@ -360,7 +399,10 @@ export default (routeConfigs, stackConfig = {}) => {
routeName: childRouterName,
key: action.key || generateKey(),
};
return StateUtils.push(state, route);
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
}
}
}

View File

@@ -5,7 +5,6 @@ import createConfigGetter from './createConfigGetter';
import NavigationActions from '../NavigationActions';
import StackActions from './StackActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getNavigationActionCreators from './getNavigationActionCreators';
const defaultActionCreators = (route, navStateKey) => ({});
@@ -74,48 +73,47 @@ export default (routeConfigs, config = {}) => {
};
}
return {
getInitialState() {
const routes = order.map(resetChildRoute);
function getNextState(prevState, possibleNextState) {
if (!prevState) {
return 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 {
routes,
index: initialRouteIndex,
isTransitioning: false,
...possibleNextState,
routes: nextRoutes,
};
},
} else {
nextState = possibleNextState;
}
getNextState(prevState, possibleNextState) {
if (!prevState) {
return possibleNextState;
}
return nextState;
}
let nextState;
if (prevState.index !== possibleNextState.index && resetOnBlur) {
const prevRouteName = prevState.routes[prevState.index].routeName;
const nextRoutes = [...possibleNextState.routes];
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
function getInitialState() {
const routes = order.map(resetChildRoute);
return {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
}
return {
...possibleNextState,
routes: nextRoutes,
};
} else {
nextState = possibleNextState;
}
return nextState;
},
return {
childRouters,
getActionCreators(route, stateKey) {
return {
...getNavigationActionCreators(route, stateKey),
...getCustomActionCreators(route, stateKey),
};
return getCustomActionCreators(route, stateKey);
},
getStateForAction(action, inputState) {
let prevState = inputState ? { ...inputState } : inputState;
let state = inputState || this.getInitialState();
let state = inputState || getInitialState();
let activeChildIndex = state.index;
if (action.type === NavigationActions.INIT) {
@@ -152,7 +150,7 @@ export default (routeConfigs, config = {}) => {
if (activeChildState && activeChildState !== activeChildLastState) {
const routes = [...state.routes];
routes[state.index] = activeChildState;
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
routes,
});
@@ -202,7 +200,7 @@ export default (routeConfigs, config = {}) => {
if (newChildState && newChildState !== childState) {
const routes = [...state.routes];
routes[activeChildIndex] = newChildState;
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
routes,
index: activeChildIndex,
@@ -230,7 +228,7 @@ export default (routeConfigs, config = {}) => {
...lastRoute,
params,
};
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
routes,
});
@@ -238,7 +236,7 @@ export default (routeConfigs, config = {}) => {
}
if (activeChildIndex !== state.index) {
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
index: activeChildIndex,
});
@@ -282,7 +280,7 @@ export default (routeConfigs, config = {}) => {
}
if (index !== state.index || routes !== state.routes) {
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
index,
routes,

View File

@@ -1,93 +0,0 @@
/* eslint react/display-name:0 */
import React from 'react';
import DrawerRouter from '../DrawerRouter';
import NavigationActions from '../../NavigationActions';
import DrawerActions from '../../routers/DrawerActions';
const INIT_ACTION = { type: NavigationActions.INIT };
describe('DrawerRouter', () => {
test('Handles basic tab logic', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
state
);
const expectedState2 = {
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
});
test('Drawer opens closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction(
{ type: DrawerActions.CLOSE_DRAWER },
state2
);
expect(state3.isDrawerOpen).toEqual(false);
const state4 = router.getStateForAction(
{ type: DrawerActions.TOGGLE_DRAWER },
state3
);
expect(state4.isDrawerOpen).toEqual(true);
});
test('Drawer opens closes with key targeted', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
state
);
expect(state2.isDrawerOpen).toEqual(false);
const state3 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
state2
);
expect(state3.isDrawerOpen).toEqual(true);
});
});

View File

@@ -1,9 +1,10 @@
/* eslint react/no-multi-comp:0 */
/* eslint react/no-multi-comp:0, react/display-name:0 */
import React from 'react';
import StackRouter from '../StackRouter';
import TabRouter from '../TabRouter';
import SwitchRouter from '../SwitchRouter';
import NavigationActions from '../../NavigationActions';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
@@ -15,6 +16,7 @@ beforeEach(() => {
const ROUTERS = {
TabRouter,
StackRouter,
SwitchRouter,
};
const dummyEventSubscriber = (name, handler) => ({
@@ -25,7 +27,7 @@ Object.keys(ROUTERS).forEach(routerName => {
const Router = ROUTERS[routerName];
describe(`General router features - ${routerName}`, () => {
test('title is configurable using navigationOptions and getScreenOptions', () => {
test(`title is configurable using navigationOptions and getScreenOptions - ${routerName}`, () => {
class FooView extends React.Component {
render() {
return <div />;
@@ -86,9 +88,103 @@ Object.keys(ROUTERS).forEach(routerName => {
).title
).toEqual('Baz-123');
});
test(`set params works in ${routerName}`, () => {
class FooView extends React.Component {
render() {
return <div />;
}
}
const router = Router({
Foo: { screen: FooView },
Bar: { screen: FooView },
});
const initState = router.getStateForAction(NavigationActions.init());
const initRoute = initState.routes[initState.index];
expect(initRoute.params).toEqual(undefined);
const state0 = router.getStateForAction(
NavigationActions.setParams({
params: { foo: 42 },
key: initRoute.key,
}),
initState
);
expect(state0.routes[state0.index].params.foo).toEqual(42);
});
});
});
test('Nested navigate behavior test', () => {
const Leaf = () => <div />;
const First = () => <div />;
First.router = StackRouter({
First1: Leaf,
First2: Leaf,
});
const Second = () => <div />;
Second.router = StackRouter({
Second1: Leaf,
Second2: Leaf,
});
const Main = () => <div />;
Main.router = StackRouter({
First,
Second,
});
const TestRouter = SwitchRouter({
Login: Leaf,
Main,
});
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const state2 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'First' },
state1
);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(0);
expect(state2.routes[1].routes[0].index).toEqual(0);
const state3 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Second2' },
state2
);
expect(state3.index).toEqual(1);
expect(state3.routes[1].index).toEqual(1); // second
expect(state3.routes[1].routes[1].index).toEqual(1); //second.second2
const state4 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'First',
action: { type: NavigationActions.NAVIGATE, routeName: 'First2' },
},
state3,
true
);
expect(state4.index).toEqual(1); // main
expect(state4.routes[1].index).toEqual(0); // first
expect(state4.routes[1].routes[0].index).toEqual(1); // first2
const state5 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'First',
action: { type: NavigationActions.NAVIGATE, routeName: 'First1' },
},
state3 // second.second2 is active on state3
);
expect(state5.index).toEqual(1); // main
expect(state5.routes[1].index).toEqual(0); // first
expect(state5.routes[1].routes[0].index).toEqual(0); // first.first1
});
test('Handles no-op actions with tabs within stack router', () => {
const BarView = () => <div />;
const FooTabNavigator = () => <div />;
@@ -157,6 +253,70 @@ test('Handles deep action', () => {
expect(state2 && state2.routes[1].index).toEqual(1);
});
test('Handles the navigate action with params', () => {
const FooTabNavigator = () => <div />;
FooTabNavigator.router = TabRouter({
Baz: { screen: () => <div /> },
Boo: { screen: () => <div /> },
});
const TestRouter = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: FooTabNavigator },
});
const state = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const state2 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Bar',
params: { foo: '42' },
},
state
);
expect(state2 && state2.routes[1].params).toEqual({ foo: '42' });
expect(state2 && state2.routes[1].routes).toEqual([
{
key: 'Baz',
routeName: 'Baz',
params: { foo: '42' },
},
{
key: 'Boo',
routeName: 'Boo',
params: { foo: '42' },
},
]);
});
test('Handles the setParams action', () => {
const FooTabNavigator = () => <div />;
FooTabNavigator.router = TabRouter({
Baz: { screen: () => <div /> },
});
const TestRouter = StackRouter({
Foo: { screen: FooTabNavigator },
Bar: { screen: () => <div /> },
});
const state = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const state2 = TestRouter.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
params: { name: 'foobar' },
key: 'Baz',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes).toEqual([
{
key: 'Baz',
routeName: 'Baz',
params: { name: 'foobar' },
},
]);
});
test('Supports lazily-evaluated getScreen', () => {
const BarView = () => <div />;
const FooTabNavigator = () => <div />;
@@ -249,3 +409,62 @@ test('Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION
expect(stateAfterCompleteTransition.index).toEqual(1);
expect(stateAfterSetParams.index).toEqual(1);
});
test('Inner actions are only unpacked if the current tab matches', () => {
const PlainScreen = () => <div />;
const ScreenA = () => <div />;
const ScreenB = () => <div />;
ScreenB.router = StackRouter({
Baz: { screen: PlainScreen },
Zoo: { screen: PlainScreen },
});
ScreenA.router = StackRouter({
Bar: { screen: PlainScreen },
Boo: { screen: ScreenB },
});
const TestRouter = TabRouter({
Foo: { screen: ScreenA },
});
const screenApreState = {
index: 0,
key: 'Init',
isTransitioning: false,
routeName: 'Foo',
routes: [{ key: 'Init', routeName: 'Bar' }],
};
const preState = {
index: 0,
isTransitioning: false,
routes: [screenApreState],
};
const comparable = state => {
let result = {};
if (typeof state.routeName === 'string') {
result = { ...result, routeName: state.routeName };
}
if (state.routes instanceof Array) {
result = {
...result,
routes: state.routes.map(comparable),
};
}
return result;
};
const action = NavigationActions.navigate({
routeName: 'Boo',
action: NavigationActions.navigate({ routeName: 'Zoo' }),
});
const expectedState = ScreenA.router.getStateForAction(
action,
screenApreState
);
const state = TestRouter.getStateForAction(action, preState);
const innerState = state ? state.routes[0] : state;
expect(expectedState && comparable(expectedState)).toEqual(
innerState && comparable(innerState)
);
});

View File

@@ -5,7 +5,6 @@ import React from 'react';
import StackRouter from '../StackRouter';
import StackActions from '../StackActions';
import NavigationActions from '../../NavigationActions';
import TabRouter from '../TabRouter';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
@@ -427,12 +426,47 @@ describe('StackRouter', () => {
expect(state3 && state3.index).toEqual(0);
});
test('Handle navigation to nested navigator', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.INIT,
});
const action = TestStackRouter.getActionForPathAndParams('fo/22/b/hello');
/* $FlowFixMe */
const state2 = TestStackRouter.getStateForAction(action);
expect(state2).toEqual({
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [
{
index: 0,
key: 'id-4',
isTransitioning: false,
routeName: 'foo',
params: {
fooThing: '22',
},
routes: [
{
routeName: 'bar',
key: 'id-3',
params: {
barThing: 'hello',
},
},
],
},
],
});
});
test('popToTop bubbles up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
@@ -484,6 +518,44 @@ describe('StackRouter', () => {
expect(state3 && state3.index).toEqual(0);
});
test('pop action works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const state = {
index: 3,
isTransitioning: false,
routes: [
{ key: 'A', routeName: 'foo' },
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
{ key: 'C', routeName: 'foo' },
{ key: 'D', routeName: 'bar' },
],
};
const poppedState = TestRouter.getStateForAction(StackActions.pop(), state);
expect(poppedState.routes.length).toBe(3);
expect(poppedState.index).toBe(2);
expect(poppedState.isTransitioning).toBe(true);
const poppedState2 = TestRouter.getStateForAction(
StackActions.pop({ n: 2, immediate: true }),
state
);
expect(poppedState2.routes.length).toBe(2);
expect(poppedState2.index).toBe(1);
expect(poppedState2.isTransitioning).toBe(false);
const poppedState3 = TestRouter.getStateForAction(
StackActions.pop({ n: 5 }),
state
);
expect(poppedState3.routes.length).toBe(1);
expect(poppedState3.index).toBe(0);
expect(poppedState3.isTransitioning).toBe(true);
});
test('popToTop works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -620,6 +692,7 @@ describe('StackRouter', () => {
state
);
expect(state2.isTransitioning).toEqual(true);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].index).toEqual(1);
@@ -642,23 +715,74 @@ describe('StackRouter', () => {
expect(pushedState).toEqual(null);
});
test('Navigate with key is idempotent', () => {
test('Navigate with key and without it is idempotent', () => {
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
for (key of ['a', null]) {
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
initState
);
expect(pushedState.index).toEqual(1);
expect(pushedState.routes[1].routeName).toEqual('bar');
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
pushedState
);
expect(pushedTwiceState).toEqual(null);
}
});
// https://github.com/react-navigation/react-navigation/issues/4063
test('Navigate on inactive stackrouter is idempotent', () => {
const FirstChildNavigator = () => <div />;
FirstChildNavigator.router = StackRouter({
First1: () => <div />,
First2: () => <div />,
});
const SecondChildNavigator = () => <div />;
SecondChildNavigator.router = StackRouter({
Second1: () => <div />,
Second2: () => <div />,
});
const router = StackRouter({
Leaf: () => <div />,
First: FirstChildNavigator,
Second: SecondChildNavigator,
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const first = router.getStateForAction(
NavigationActions.navigate({ routeName: 'First2' }),
state
);
expect(pushedState.index).toEqual(1);
expect(pushedState.routes[1].routeName).toEqual('bar');
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
pushedState
const second = router.getStateForAction(
NavigationActions.navigate({ routeName: 'Second2' }),
first
);
expect(pushedTwiceState).toEqual(null);
const firstAgain = router.getStateForAction(
NavigationActions.navigate({
routeName: 'First2',
params: { debug: true },
}),
second
);
expect(first.routes.length).toEqual(2);
expect(first.index).toEqual(1);
expect(second.routes.length).toEqual(3);
expect(second.index).toEqual(2);
expect(firstAgain.index).toEqual(1);
expect(firstAgain.routes.length).toEqual(2);
});
test('Navigate to current routeName returns null to indicate handled action', () => {
@@ -1079,15 +1203,51 @@ describe('StackRouter', () => {
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
});
test('Handles the SetParams action for inactive routes', () => {
const router = StackRouter(
{
Foo: {
screen: () => <div />,
},
Bar: {
screen: () => <div />,
},
},
{
initialRouteName: 'Bar',
initialRouteParams: { name: 'Zoo' },
}
);
const initialState = {
index: 1,
routes: [
{
key: 'RouteA',
routeName: 'Foo',
params: { name: 'InitialParam', other: 'Unchanged' },
},
{ key: 'RouteB', routeName: 'Bar', params: {} },
],
};
const state = router.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
params: { name: 'NewParam' },
key: 'RouteA',
},
initialState
);
expect(state.index).toEqual(1);
expect(state.routes[0].params).toEqual({
name: 'NewParam',
other: 'Unchanged',
});
});
test('Handles the setParams action with nested routers', () => {
const ChildNavigator = () => <div />;
const GrandChildNavigator = () => <div />;
GrandChildNavigator.router = StackRouter({
Quux: { screen: () => <div /> },
Corge: { screen: () => <div /> },
});
ChildNavigator.router = TabRouter({
Baz: { screen: GrandChildNavigator },
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
@@ -1104,10 +1264,10 @@ describe('StackRouter', () => {
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
expect(state2 && state2.routes[0].routes).toEqual([
{
key: 'id-0',
routeName: 'Quux',
routeName: 'Baz',
params: { name: 'foobar' },
},
]);
@@ -1193,7 +1353,7 @@ describe('StackRouter', () => {
});
test('Handles the reset action with nested Router', () => {
const ChildRouter = TabRouter({
const ChildRouter = StackRouter({
baz: {
screen: () => <div />,
},
@@ -1214,6 +1374,7 @@ describe('StackRouter', () => {
const state2 = router.getStateForAction(
{
type: StackActions.RESET,
key: null,
actions: [
{
type: NavigationActions.NAVIGATE,
@@ -1325,6 +1486,34 @@ describe('StackRouter', () => {
]);
});
test('Navigate action to previous nested StackRouter causes isTransitioning start', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
});
const router = StackRouter({
Bar: { screen: ChildNavigator },
Foo: { screen: () => <div /> },
});
const state = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Foo',
},
router.getStateForAction({ type: NavigationActions.INIT })
);
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state
);
expect(state2.index).toEqual(0);
expect(state2.isTransitioning).toEqual(true);
});
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
@@ -1445,42 +1634,6 @@ describe('StackRouter', () => {
});
});
test('Handles the navigate action with params and nested TabRouter', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = TabRouter({
Baz: { screen: () => <div /> },
Boo: { 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,
immediate: true,
routeName: 'Bar',
params: { foo: '42' },
},
state
);
expect(state2 && state2.routes[1].params).toEqual({ foo: '42' });
expect(state2 && state2.routes[1].routes).toEqual([
{
key: 'Baz',
routeName: 'Baz',
params: { foo: '42' },
},
{
key: 'Boo',
routeName: 'Boo',
params: { foo: '42' },
},
]);
});
test('Handles empty URIs', () => {
const router = StackRouter(
{
@@ -1761,3 +1914,120 @@ test('Handles deep navigate completion action', () => {
expect(state3 && state3.routes[0].index).toEqual(1);
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
});
test('order of handling navigate action is correct for nested stackrouters', () => {
const Screen = () => <div />;
const NestedStack = () => <div />;
let nestedRouter = StackRouter({
Foo: Screen,
Bar: Screen,
});
NestedStack.router = nestedRouter;
let router = StackRouter(
{
NestedStack,
Bar: Screen,
Baz: Screen,
},
{
initialRouteName: 'Baz',
}
);
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routes[state.index].routeName).toEqual('Baz');
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
expect(state2.routes[state2.index].routeName).toEqual('Bar');
const state3 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state2
);
expect(state3.routes[state3.index].routeName).toEqual('Baz');
const state4 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
},
state3
);
let activeState4 = state4.routes[state4.index];
expect(activeState4.routeName).toEqual('NestedStack');
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
const state5 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state4
);
let activeState5 = state5.routes[state5.index];
expect(activeState5.routeName).toEqual('NestedStack');
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
});
test('order of handling navigate action is correct for nested stackrouters', () => {
const Screen = () => <div />;
const NestedStack = () => <div />;
const OtherNestedStack = () => <div />;
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
let otherNestedRouter = StackRouter({ Foo: Screen });
NestedStack.router = nestedRouter;
OtherNestedStack.router = otherNestedRouter;
let router = StackRouter(
{
NestedStack,
OtherNestedStack,
Bar: Screen,
},
{
initialRouteName: 'OtherNestedStack',
}
);
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
expect(state2.routes[state2.index].routeName).toEqual('Bar');
const state3 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'NestedStack',
},
state2
);
const state4 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state3
);
let activeState4 = state4.routes[state4.index];
expect(activeState4.routeName).toEqual('NestedStack');
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
});

View File

@@ -127,6 +127,93 @@ describe('SwitchRouter', () => {
},
});
});
test('order of handling navigate action is correct for nested switchrouters', () => {
// router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
// if we are focused on Other and navigate to Bar, what should happen?
const Screen = () => <div />;
const NestedSwitch = () => <div />;
const OtherNestedSwitch = () => <div />;
let nestedRouter = SwitchRouter({ Foo: Screen, Bar: Screen });
let otherNestedRouter = SwitchRouter({ Foo: Screen });
NestedSwitch.router = nestedRouter;
OtherNestedSwitch.router = otherNestedRouter;
let router = SwitchRouter(
{
NestedSwitch,
OtherNestedSwitch,
Bar: Screen,
},
{
initialRouteName: 'OtherNestedSwitch',
}
);
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routes[state.index].routeName).toEqual('OtherNestedSwitch');
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
expect(state2.routes[state2.index].routeName).toEqual('Bar');
const state3 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'NestedSwitch',
},
state2
);
const state4 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state3
);
let activeState4 = state4.routes[state4.index];
expect(activeState4.routeName).toEqual('NestedSwitch');
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
});
// https://github.com/react-navigation/react-navigation.github.io/issues/117#issuecomment-385597628
test('order of handling navigate action is correct for nested stackrouters', () => {
const Screen = () => <div />;
const MainStack = () => <div />;
const LoginStack = () => <div />;
MainStack.router = StackRouter({ Home: Screen, Profile: Screen });
LoginStack.router = StackRouter({ Form: Screen, ForgotPassword: Screen });
let router = SwitchRouter(
{
Home: Screen,
Login: LoginStack,
Main: MainStack,
},
{
initialRouteName: 'Login',
}
);
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routes[state.index].routeName).toEqual('Login');
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Home',
},
state
);
expect(state2.routes[state2.index].routeName).toEqual('Home');
});
});
const getExampleRouter = (config = {}) => {

View File

@@ -2,7 +2,6 @@
import React from 'react';
import TabRouter from '../TabRouter';
import StackRouter from '../StackRouter';
import StackActions from '../../routers/StackActions';
import NavigationActions from '../../NavigationActions';
@@ -141,6 +140,46 @@ describe('TabRouter', () => {
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
});
test('Handles the SetParams action for inactive routes', () => {
const router = TabRouter(
{
Foo: {
screen: () => <div />,
},
Bar: {
screen: () => <div />,
},
},
{
initialRouteName: 'Bar',
}
);
const initialState = {
index: 1,
routes: [
{
key: 'RouteA',
routeName: 'Foo',
params: { name: 'InitialParam', other: 'Unchanged' },
},
{ key: 'RouteB', routeName: 'Bar', params: {} },
],
};
const state = router.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
params: { name: 'NewParam' },
key: 'RouteA',
},
initialState
);
expect(state.index).toEqual(1);
expect(state.routes[0].params).toEqual({
name: 'NewParam',
other: 'Unchanged',
});
});
test('getStateForAction returns null when navigating to same tab', () => {
const router = TabRouter(
{ Foo: BareLeafRouteConfig, Bar: BareLeafRouteConfig },
@@ -695,53 +734,15 @@ describe('TabRouter', () => {
expect(state2).toEqual(state0);
});
test('pop action works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const state = {
index: 3,
isTransitioning: false,
routes: [
{ key: 'A', routeName: 'foo' },
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
{ key: 'C', routeName: 'foo' },
{ key: 'D', routeName: 'bar' },
],
};
const poppedState = TestRouter.getStateForAction(StackActions.pop(), state);
expect(poppedState.routes.length).toBe(3);
expect(poppedState.index).toBe(2);
expect(poppedState.isTransitioning).toBe(true);
const poppedState2 = TestRouter.getStateForAction(
StackActions.pop({ n: 2, immediate: true }),
state
);
expect(poppedState2.routes.length).toBe(2);
expect(poppedState2.index).toBe(1);
expect(poppedState2.isTransitioning).toBe(false);
const poppedState3 = TestRouter.getStateForAction(
StackActions.pop({ n: 5 }),
state
);
expect(poppedState3.routes.length).toBe(1);
expect(poppedState3.index).toBe(0);
expect(poppedState3.isTransitioning).toBe(true);
});
test('Inner actions are only unpacked if the current tab matches', () => {
const PlainScreen = () => <div />;
const ScreenA = () => <div />;
const ScreenB = () => <div />;
ScreenB.router = StackRouter({
ScreenB.router = TabRouter({
Baz: { screen: PlainScreen },
Zoo: { screen: PlainScreen },
});
ScreenA.router = StackRouter({
ScreenA.router = TabRouter({
Bar: { screen: PlainScreen },
Boo: { screen: ScreenB },
});
@@ -750,10 +751,10 @@ describe('TabRouter', () => {
});
const screenApreState = {
index: 0,
key: 'Init',
key: 'Foo',
isTransitioning: false,
routeName: 'Foo',
routes: [{ key: 'Init', routeName: 'Bar' }],
routes: [{ key: 'Bar', routeName: 'Bar' }],
};
const preState = {
index: 0,
@@ -779,7 +780,6 @@ describe('TabRouter', () => {
routeName: 'Boo',
action: NavigationActions.navigate({ routeName: 'Zoo' }),
});
const expectedState = ScreenA.router.getStateForAction(
action,
screenApreState
@@ -787,8 +787,25 @@ describe('TabRouter', () => {
const state = router.getStateForAction(action, preState);
const innerState = state ? state.routes[0] : state;
expect(innerState.routes[1].index).toEqual(1);
expect(expectedState && comparable(expectedState)).toEqual(
innerState && comparable(innerState)
);
const noMatchAction = NavigationActions.navigate({
routeName: 'Qux',
action: NavigationActions.navigate({ routeName: 'Zoo' }),
});
const expectedState2 = ScreenA.router.getStateForAction(
noMatchAction,
screenApreState
);
const state2 = router.getStateForAction(noMatchAction, preState);
const innerState2 = state2 ? state2.routes[0] : state2;
expect(innerState2.routes[1].index).toEqual(0);
expect(expectedState2 && comparable(expectedState2)).toEqual(
innerState2 && comparable(innerState2)
);
});
});

View File

@@ -1,6 +1,5 @@
import { Component } from 'react';
import createConfigGetter from '../createConfigGetter';
import addNavigationHelpers from '../../addNavigationHelpers';
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
remove: () => {},
@@ -67,81 +66,81 @@ test('should get config for screen', () => {
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).title
).toEqual('Welcome anonymous');
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[1],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).title
).toEqual('Welcome jane');
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).gesturesEnabled
).toEqual(true);
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).title
).toEqual('Settings!!!');
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).gesturesEnabled
).toEqual(false);
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[3],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).title
).toEqual('10 new notifications');
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[3],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).gesturesEnabled
).toEqual(true);
expect(
getScreenOptions(
addNavigationHelpers({
{
state: routes[4],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
).gesturesEnabled
).toEqual(false);
@@ -164,11 +163,11 @@ test('should throw if the route does not exist', () => {
expect(() =>
getScreenOptions(
addNavigationHelpers({
{
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
},
{}
)
).toThrowError(

View File

@@ -21,25 +21,24 @@ function validateRouteConfigMap(routeConfigs) {
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}: MyScreen,\n` +
'}\n\n' +
'You can also use a navigator:\n\n' +
"import MyNavigator from './MyNavigator';\n" +
'...\n' +
`${routeName}: MyNavigator,\n` +
'}'
);
throw new Error(`The component for route '${routeName}' must be a React component. For example:
import MyScreen from './MyScreen';
...
${routeName}: MyScreen,
}
You can also use a navigator:
import MyNavigator from './MyNavigator';
...
${routeName}: MyNavigator,
}`);
}
if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
`Route '${routeName}' should declare a screen or a getScreen, not both.`
);
}
});

View File

@@ -1,3 +1,3 @@
export default function docsUrl(path) {
return `https://v2.reactnavigation.org/docs/${path}`;
return `https://reactnavigation.org/docs/${path}`;
}

View File

@@ -1,14 +1,3 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
/**
* Use invariant() to assert state which your program assumes to be true.
*

View File

@@ -1,19 +1,5 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @typechecks
*
*/
/*eslint-disable no-self-compare */
'use strict';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
@@ -71,4 +57,4 @@ function shallowEqual(objA, objB) {
return true;
}
module.exports = shallowEqual;
export default shallowEqual;

View File

@@ -1,5 +1,5 @@
export default (obj, key, defaultValue) => {
if (obj.hasOwnProperty(key)) {
if (obj.hasOwnProperty(key) && typeof obj[key] !== 'undefined') {
return obj;
}

View File

@@ -1,119 +0,0 @@
import React from 'react';
import { View, Text, Platform, StyleSheet } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TouchableItem from '../TouchableItem';
/**
* Component that renders the navigation list in the drawer.
*/
const DrawerNavigatorItems = ({
navigation: { state, navigate },
items,
activeItemKey,
activeTintColor,
activeBackgroundColor,
inactiveTintColor,
inactiveBackgroundColor,
getLabel,
renderIcon,
onItemPress,
itemsContainerStyle,
itemStyle,
labelStyle,
activeLabelStyle,
inactiveLabelStyle,
iconContainerStyle,
drawerPosition,
}) => (
<View style={[styles.container, itemsContainerStyle]}>
{items.map((route, index) => {
const focused = activeItemKey === route.key;
const color = focused ? activeTintColor : inactiveTintColor;
const backgroundColor = focused
? activeBackgroundColor
: inactiveBackgroundColor;
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}
onPress={() => {
onItemPress({ route, focused });
}}
delayPressIn={0}
>
<SafeAreaView
style={{ backgroundColor }}
forceInset={{
[drawerPosition]: 'always',
[drawerPosition === 'left' ? 'right' : 'left']: 'never',
vertical: 'never',
}}
>
<View style={[styles.item, itemStyle]}>
{icon ? (
<View
style={[
styles.icon,
focused ? null : styles.inactiveIcon,
iconContainerStyle,
]}
>
{icon}
</View>
) : null}
{typeof label === 'string' ? (
<Text
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
>
{label}
</Text>
) : (
label
)}
</View>
</SafeAreaView>
</TouchableItem>
);
})}
</View>
);
/* Material design specs - https://material.io/guidelines/patterns/navigation-drawer.html#navigation-drawer-specs */
DrawerNavigatorItems.defaultProps = {
activeTintColor: '#2196f3',
activeBackgroundColor: 'rgba(0, 0, 0, .04)',
inactiveTintColor: 'rgba(0, 0, 0, .87)',
inactiveBackgroundColor: 'transparent',
};
const styles = StyleSheet.create({
container: {
paddingVertical: 4,
},
item: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginHorizontal: 16,
width: 24,
alignItems: 'center',
},
inactiveIcon: {
/*
* Icons have 0.54 opacity according to guidelines
* 100/87 * 54 ~= 62
*/
opacity: 0.62,
},
label: {
margin: 16,
fontWeight: 'bold',
},
});
export default DrawerNavigatorItems;

View File

@@ -1,24 +0,0 @@
import React from 'react';
import SceneView from '../SceneView';
/**
* Component that renders the child screen of the drawer.
*/
class DrawerScreen extends React.PureComponent {
render() {
const { descriptors, navigation, screenProps } = this.props;
const { routes, index } = navigation.state;
const descriptor = descriptors[routes[index].key];
const Content = descriptor.getComponent();
return (
<SceneView
screenProps={screenProps}
component={Content}
navigation={descriptor.navigation}
/>
);
}
}
export default DrawerScreen;

View File

@@ -1,98 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import NavigationActions from '../../NavigationActions';
import invariant from '../../utils/invariant';
/**
* Component that renders the sidebar screen of the drawer.
*/
class DrawerSidebar extends React.PureComponent {
_getScreenOptions = routeKey => {
const descriptor = this.props.descriptors[routeKey];
invariant(
descriptor.options,
'Cannot access screen descriptor options from drawer sidebar'
);
return descriptor.options;
};
_getLabel = ({ focused, tintColor, route }) => {
const { drawerLabel, title } = this._getScreenOptions(route.key);
if (drawerLabel) {
return typeof drawerLabel === 'function'
? drawerLabel({ tintColor, focused })
: drawerLabel;
}
if (typeof title === 'string') {
return title;
}
return route.routeName;
};
_renderIcon = ({ focused, tintColor, route }) => {
const { drawerIcon } = this._getScreenOptions(route.key);
if (drawerIcon) {
return typeof drawerIcon === 'function'
? drawerIcon({ tintColor, focused })
: drawerIcon;
}
return null;
};
_onItemPress = ({ route, focused }) => {
if (!focused) {
let subAction;
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
if (route.index !== undefined && route.index !== 0) {
subAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName: route.routes[0].routeName,
}),
],
});
}
this.props.navigation.navigate(route.routeName, undefined, subAction);
}
};
render() {
const ContentComponent = this.props.contentComponent;
if (!ContentComponent) {
return null;
}
const { state } = this.props.navigation;
invariant(typeof state.index === 'number', 'should be set');
return (
<View style={[styles.container, this.props.style]}>
<ContentComponent
{...this.props.contentOptions}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
items={state.routes}
activeItemKey={
state.routes[state.index] ? state.routes[state.index].key : null
}
screenProps={this.props.screenProps}
getLabel={this._getLabel}
renderIcon={this._renderIcon}
onItemPress={this._onItemPress}
drawerPosition={this.props.drawerPosition}
/>
</View>
);
}
}
export default DrawerSidebar;
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -1,120 +0,0 @@
import React from 'react';
import { Dimensions } from 'react-native';
import DrawerLayout from 'react-native-drawer-layout-polyfill';
import DrawerSidebar from './DrawerSidebar';
import NavigationActions from '../../NavigationActions';
import DrawerActions from '../../routers/DrawerActions';
/**
* Component that renders the drawer.
*/
export default class DrawerView extends React.PureComponent {
state = {
drawerWidth:
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
: this.props.navigationConfig.drawerWidth,
};
componentDidMount() {
Dimensions.addEventListener('change', this._updateWidth);
}
componentWillUnmount() {
Dimensions.removeEventListener('change', this._updateWidth);
}
componentDidUpdate(prevProps, prevState) {
const { isDrawerOpen } = this.props.navigation.state;
const wasDrawerOpen = prevProps.navigation.state.isDrawerOpen;
if (isDrawerOpen && !wasDrawerOpen) {
this._drawer.openDrawer();
} else if (wasDrawerOpen && !isDrawerOpen) {
this._drawer.closeDrawer();
}
}
_handleDrawerOpen = () => {
const { navigation } = this.props;
const { isDrawerOpen } = navigation.state;
if (!isDrawerOpen) {
navigation.dispatch({ type: DrawerActions.OPEN_DRAWER });
}
};
_handleDrawerClose = () => {
const { navigation } = this.props;
const { isDrawerOpen } = navigation.state;
if (isDrawerOpen) {
navigation.dispatch({ type: DrawerActions.CLOSE_DRAWER });
}
};
_updateWidth = () => {
const drawerWidth =
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
: this.props.navigationConfig.drawerWidth;
if (this.state.drawerWidth !== drawerWidth) {
this.setState({ drawerWidth });
}
};
_renderNavigationView = () => {
return (
<DrawerSidebar
screenProps={this.props.screenProps}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
contentComponent={this.props.navigationConfig.contentComponent}
contentOptions={this.props.navigationConfig.contentOptions}
drawerPosition={this.props.navigationConfig.drawerPosition}
style={this.props.navigationConfig.style}
{...this.props.navigationConfig}
/>
);
};
render() {
const { state } = this.props.navigation;
const activeKey = state.routes[state.index].key;
const descriptor = this.props.descriptors[activeKey];
const DrawerScreen = descriptor.getComponent();
const { drawerLockMode } = descriptor.options;
return (
<DrawerLayout
ref={c => {
this._drawer = c;
}}
drawerLockMode={
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
this.props.navigationConfig.drawerLockMode
}
drawerBackgroundColor={
this.props.navigationConfig.drawerBackgroundColor
}
drawerWidth={this.state.drawerWidth}
onDrawerOpen={this._handleDrawerOpen}
onDrawerClose={this._handleDrawerClose}
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
renderNavigationView={this._renderNavigationView}
drawerPosition={
this.props.navigationConfig.drawerPosition === 'right'
? DrawerLayout.positions.Right
: DrawerLayout.positions.Left
}
>
<DrawerScreen
screenProps={this.props.screenProps}
navigation={descriptor.navigation}
/>
</DrawerLayout>
);
}
}

View File

@@ -7,6 +7,7 @@ import {
Platform,
StyleSheet,
View,
I18nManager,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
@@ -24,7 +25,9 @@ const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad ? 32 : 44
? isLandscape && !Platform.isPad
? 32
: 44
: 56;
};
@@ -47,12 +50,6 @@ class Header extends React.PureComponent {
widths: {},
};
_handleOnLayout = e => {
if (typeof this.props.onLayout === 'function') {
this.props.onLayout(e.nativeEvent.layout);
}
};
_getHeaderTitleString(scene) {
const options = scene.descriptor.options;
if (typeof options.headerTitle === 'string') {
@@ -172,7 +169,7 @@ class Header extends React.PureComponent {
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options } = props.scene.descriptor;
const { options, navigation } = props.scene.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
@@ -184,7 +181,7 @@ class Header extends React.PureComponent {
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
this.props.navigation.goBack(props.scene.descriptor.key);
navigation.goBack(props.scene.descriptor.key);
});
};
@@ -495,8 +492,10 @@ class Header extends React.PureComponent {
return (
<Animated.View
style={this.props.layoutInterpolator(this.props)}
onLayout={this._handleOnLayout}
style={[
this.props.layoutInterpolator(this.props),
{ backgroundColor: DEFAULT_BACKGROUND_COLOR },
]}
>
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>
@@ -535,9 +534,11 @@ if (Platform.OS === 'ios') {
};
}
const DEFAULT_BACKGROUND_COLOR = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF';
const styles = StyleSheet.create({
container: {
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
backgroundColor: DEFAULT_BACKGROUND_COLOR,
...platformContainerStyles,
},
transparentContainer: {
@@ -572,6 +573,7 @@ const styles = StyleSheet.create({
marginTop: -0.5, // resizes down to 20.5
alignSelf: 'center',
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
title: {
bottom: 0,

View File

@@ -47,7 +47,10 @@ function isGoingBack(scenes) {
}
function forLayout(props) {
const { layout, position, scene, scenes } = props;
const { layout, position, scene, scenes, mode } = props;
if (mode !== 'float') {
return {};
}
const isBack = isGoingBack(scenes);
const interpolate = getSceneIndicesForInterpolationInputRange(props);
@@ -56,7 +59,26 @@ function forLayout(props) {
const { first, last } = interpolate;
const index = scene.index;
const width = layout.initWidth;
// We really shouldn't render the scene at all until we know the width of the
// stack. That said, in every case that I have ever seen, this has just been
// the full width of the window. This won't continue to be true if we support
// layouts like iPad master-detail. For now, in order to solve
// https://github.com/react-navigation/react-navigation/issues/4264, I have
// opted for the heuristic that we will use the window width until we have
// measured (and they will usually be the same).
const width = layout.initWidth || Dimensions.get('window').width;
// Make sure the header stays hidden when transitioning between 2 screens
// with no header.
if (
(isBack && !hasHeader(scenes[index]) && !hasHeader(scenes[last])) ||
(!isBack && !hasHeader(scenes[first]) && !hasHeader(scenes[index]))
) {
return {
transform: [{ translateX: width }],
};
}
const rtlMult = I18nManager.isRTL ? -1 : 1;
const translateX = position.interpolate({
inputRange: [first, index, last],
@@ -163,11 +185,11 @@ function forLeftButton(props) {
};
}
/*
/*
* NOTE: this offset calculation is 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)
*/
@@ -234,14 +256,14 @@ function forLeftLabel(props) {
};
}
/*
/*
* 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)
*/

View File

@@ -0,0 +1,3 @@
import { NavigationConsumer } from './NavigationContext';
export default NavigationConsumer;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import propTypes from 'prop-types';
import createReactContext from 'create-react-context';
const NavigationContext = createReactContext();

View File

@@ -0,0 +1,3 @@
import { NavigationProvider } from './NavigationContext';
export default NavigationProvider;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import withLifecyclePolyfill from 'react-lifecycles-compat';
import { polyfill } from 'react-lifecycles-compat';
import SceneView from './SceneView';
@@ -77,4 +77,4 @@ const styles = StyleSheet.create({
},
});
export default withLifecyclePolyfill(ResourceSavingSceneView);
export default polyfill(ResourceSavingSceneView);

View File

@@ -1,5 +1,4 @@
import React from 'react';
import propTypes from 'prop-types';
import { NavigationProvider } from './NavigationContext';
export default class SceneView extends React.PureComponent {

View File

@@ -65,6 +65,17 @@ export default function ScenesReducer(
prevState,
descriptors
) {
// Always update the descriptors
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
// It will be resolved in a better way when we re-write Transitioner
scenes.forEach(scene => {
const { route } = scene;
if (descriptors && descriptors[route.key]) {
scene.descriptor = descriptors[route.key];
}
});
// Bail out early if we didn't update the state
if (prevState === nextState) {
return scenes;
}

View File

@@ -1,9 +1,8 @@
import * as React from 'react';
import React from 'react';
import { NativeModules } from 'react-native';
import StackViewLayout from './StackViewLayout';
import Transitioner from '../Transitioner';
import NavigationActions from '../../NavigationActions';
import StackActions from '../../routers/StackActions';
import TransitionConfigs from './StackViewTransitionConfigs';
@@ -22,15 +21,14 @@ class StackView extends React.Component {
<Transitioner
render={this._render}
configureTransition={this._configureTransition}
screenProps={this.props.screenProps}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
onTransitionStart={this.props.onTransitionStart}
onTransitionEnd={(transition, lastTransition) => {
const { onTransitionEnd, navigation } = this.props;
if (
transition.navigation.state.isTransitioning &&
!lastTransition.navigation.state.isTransitioning
) {
const { navigationConfig, navigation } = this.props;
const { onTransitionEnd } = navigationConfig;
if (transition.navigation.state.isTransitioning) {
navigation.dispatch(
StackActions.completeTransition({
key: navigation.state.key,
@@ -60,6 +58,9 @@ class StackView extends React.Component {
return (
<StackViewLayout
{...navigationConfig}
onGestureBegin={this.props.onGestureBegin}
onGestureCanceled={this.props.onGestureCanceled}
onGestureEnd={this.props.onGestureEnd}
screenProps={screenProps}
descriptors={this.props.descriptors}
transitionProps={transitionProps}

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import clamp from 'clamp';
import {
@@ -9,6 +9,7 @@ import {
View,
I18nManager,
Easing,
Dimensions,
} from 'react-native';
import Card from './StackViewCard';
@@ -16,13 +17,21 @@ import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import StackActions from '../../routers/StackActions';
import SceneView from '../SceneView';
import withOrientation from '../withOrientation';
import { NavigationProvider } from '../NavigationContext';
import TransitionConfigs from './StackViewTransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
const IS_IPHONE_X =
Platform.OS === 'ios' &&
!Platform.isPad &&
!Platform.isTVOS &&
(WINDOW_HEIGHT === 812 || WINDOW_WIDTH === 812);
const EaseInOut = Easing.inOut(Easing.ease);
/**
@@ -59,6 +68,20 @@ const animatedSubscribeValue = animatedValue => {
}
};
const getDefaultHeaderHeight = isLandscape => {
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
return 32;
} else if (IS_IPHONE_X) {
return 88;
} else {
return 64;
}
} else {
return 56;
}
};
class StackViewLayout extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
@@ -80,6 +103,19 @@ class StackViewLayout extends React.Component {
*/
_immediateIndex = null;
constructor(props) {
super(props);
this.state = {
// Used when card's header is null and mode is float to make transition
// between screens with headers and those without headers smooth.
// This is not a great heuristic here. We don't know synchronously
// on mount what the header height is so we have just used the most
// common cases here.
floatingHeaderHeight: getDefaultHeaderHeight(props.isLandscape),
};
}
_renderHeader(scene, headerMode) {
const { options } = scene.descriptor;
const { header } = options;
@@ -94,11 +130,7 @@ class StackViewLayout extends React.Component {
}
// Handle the case where the header option is a function, and provide the default
const renderHeader =
header ||
(props => (
<Header onLayout={layout => (this._headerLayout = layout)} {...props} />
));
const renderHeader = header || (props => <Header {...props} />);
const {
headerLeftInterpolator,
@@ -109,20 +141,24 @@ class StackViewLayout extends React.Component {
const {
mode,
transitionProps,
prevTransitionProps,
lastTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
});
return (
<NavigationProvider value={scene.descriptor.navigation}>
{renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
})}
</NavigationProvider>
);
}
// eslint-disable-next-line class-methods-use-this
@@ -140,10 +176,7 @@ class StackViewLayout extends React.Component {
}
_reset(resetToIndex, duration) {
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
Animated.spring(this.props.transitionProps.position, {
toValue: resetToIndex,
stiffness: 5000,
@@ -183,10 +216,7 @@ class StackViewLayout extends React.Component {
}
};
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
Animated.spring(position, {
toValue,
stiffness: 5000,
@@ -204,160 +234,216 @@ class StackViewLayout extends React.Component {
}
}
_panResponder = PanResponder.create({
onPanResponderTerminate: () => {
const { navigation } = this.props.transitionProps;
const { index } = navigation.state;
this._isResponding = false;
this._reset(index, 0);
this.props.onGestureCanceled && this.props.onGestureCanceled();
},
onPanResponderGrant: () => {
const {
transitionProps: { navigation, position, scene },
} = this.props;
const { index } = navigation.state;
if (index !== scene.index) {
return false;
}
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
this.props.onGestureBegin && this.props.onGestureBegin();
},
onMoveShouldSetPanResponder: (event, gesture) => {
const {
transitionProps: { navigation, layout, scene },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
return false;
}
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
const isOnFirstCard = immediateIndex === 0;
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
onPanResponderMove: (event, gesture) => {
const {
transitionProps: { navigation, position, layout, scene },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
axis === 'dx' && gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
onPanResponderRelease: (event, gesture) => {
const {
transitionProps: { navigation, position, layout, scene },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
if (!this._isResponding) {
return;
}
this._isResponding = false;
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const movementDirection = gestureDirectionInverted ? -1 : 1;
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
const defaultVelocity = axisDistance / ANIMATION_DURATION;
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run stopAnimation:
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this.props.onGestureCanceled && this.props.onGestureCanceled();
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this.props.onGestureFinish && this.props.onGestureFinish();
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this.props.onGestureFinish && this.props.onGestureFinish();
this._goBack(immediateIndex, goBackDuration);
} else {
this.props.onGestureCanceled && this.props.onGestureCanceled();
this._reset(immediateIndex, resetDuration);
}
});
},
});
_onFloatingHeaderLayout = e => {
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
};
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
const { scene } = this.props.transitionProps;
floatingHeader = (
<NavigationProvider value={scene.descriptor.navigation}>
<View pointerEvents="box-none" onLayout={this._onFloatingHeaderLayout}>
{this._renderHeader(scene, headerMode)}
</NavigationProvider>
</View>
);
}
const {
transitionProps: { navigation, position, layout, scene, scenes },
transitionProps: { scene, scenes },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const responder = !gesturesEnabled
? null
: PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation((value: number) => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const { options } = scene.descriptor;
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
return false;
}
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
const isOnFirstCard = immediateIndex === 0;
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
onPanResponderMove: (event, gesture) => {
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
onPanResponderRelease: (event, gesture) => {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const movementDirection = gestureDirectionInverted ? -1 : 1;
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
const defaultVelocity = axisDistance / ANIMATION_DURATION;
const velocity = Math.max(
Math.abs(gestureVelocity),
defaultVelocity
);
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run stopAnimation:
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const responder = !gesturesEnabled ? null : this._panResponder;
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
@@ -435,13 +521,14 @@ class StackViewLayout extends React.Component {
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
this.props.transitionProps,
this.props.prevTransitionProps,
this.props.lastTransitionProps,
isModal
);
};
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
@@ -453,8 +540,8 @@ class StackViewLayout extends React.Component {
const hasHeader = options.header !== null;
const headerMode = this._getHeaderMode();
let marginTop = 0;
if (!hasHeader && headerMode === 'float' && this._headerLayout) {
marginTop = -this._headerLayout.height;
if (!hasHeader && headerMode === 'float') {
marginTop = -this.state.floatingHeaderHeight;
}
return (
@@ -484,4 +571,4 @@ const styles = StyleSheet.create({
},
});
export default StackViewLayout;
export default withOrientation(StackViewLayout);

View File

@@ -1,9 +1,9 @@
import { Animated, Easing, Platform } from 'react-native';
import StyleInterpolator from './StackViewStyleInterpolator';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
let IOSTransitionSpec;
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
if (supportsImprovedSpringAnimation()) {
// These are the exact values from UINavigationController's animation configuration
IOSTransitionSpec = {
timing: Animated.spring,

View File

@@ -63,6 +63,12 @@ class Transitioner extends React.Component {
nextScenes = filterStale(nextScenes);
}
// Update nextScenes when we change screenProps
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
if (nextProps.screenProps !== this.props.screenProps) {
this.setState({ nextScenes });
}
if (nextScenes === this.state.scenes) {
return;
}
@@ -90,6 +96,24 @@ class Transitioner extends React.Component {
this._prevTransitionProps = this._transitionProps;
this._transitionProps = buildTransitionProps(nextProps, nextState);
const toValue = nextProps.navigation.state.index;
if (!this._transitionProps.navigation.state.isTransitioning) {
this.setState(nextState, async () => {
const result = nextProps.onTransitionStart(
this._transitionProps,
this._prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
progress.setValue(1);
position.setValue(toValue);
this._onTransitionEnd();
});
return;
}
// get the transition spec.
const transitionUserSpec = nextProps.configureTransition
? nextProps.configureTransition(
@@ -106,7 +130,6 @@ class Transitioner extends React.Component {
const { timing } = transitionSpec;
delete transitionSpec.timing;
const toValue = nextProps.navigation.state.index;
const positionHasChanged = position.__getValue() !== toValue;
// if swiped back, indexHasChanged == true && positionHasChanged == false

View File

@@ -0,0 +1,49 @@
/* eslint react/display-name:0 */
import React from 'react';
import renderer from 'react-test-renderer';
import Transitioner from '../Transitioner';
describe('Transitioner', () => {
it('should not trigger onTransitionStart and onTransitionEnd when route params are changed', () => {
const onTransitionStartCallback = jest.fn();
const onTransitionEndCallback = jest.fn();
const transitionerProps = {
configureTransition: (transitionProps, prevTransitionProps) => ({}),
navigation: {
state: {
index: 0,
routes: [
{ key: '1', routeName: 'Foo' },
{ key: '2', routeName: 'Bar' },
],
},
goBack: () => false,
dispatch: () => false,
setParams: () => false,
navigate: () => false,
},
render: () => <div />,
onTransitionStart: onTransitionStartCallback,
onTransitionEnd: onTransitionEndCallback,
};
const nextTransitionerProps = {
...transitionerProps,
navigation: {
...transitionerProps.navigation,
state: {
index: 0,
routes: [
{ key: '1', routeName: 'Foo', params: { name: 'Zoom' } },
{ key: '2', routeName: 'Bar' },
],
},
},
};
const component = renderer.create(<Transitioner {...transitionerProps} />);
component.update(<Transitioner {...nextTransitionerProps} />);
expect(onTransitionStartCallback).not.toBeCalled();
expect(onTransitionEndCallback).not.toBeCalled();
});
});

View File

@@ -2,12 +2,7 @@
exports[`TabBarBottom renders successfully 1`] = `
<View
loaded={
Array [
0,
]
}
onLayout={[Function]}
collapsable={false}
style={
Array [
Object {
@@ -20,73 +15,67 @@ exports[`TabBarBottom renders successfully 1`] = `
]
}
>
<RCTScrollView
DEPRECATED_sendUpdatedChildFrames={false}
alwaysBounceHorizontal={false}
alwaysBounceVertical={false}
automaticallyAdjustContentInsets={false}
bounces={false}
contentContainerStyle={
<View
onLayout={[Function]}
style={
Object {
"flex": 1,
}
}
contentOffset={
Object {
"x": 0,
"y": 0,
}
}
directionalLockEnabled={true}
horizontal={true}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
onContentSizeChange={null}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onResponderGrant={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={undefined}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
overScrollMode="never"
pagingEnabled={true}
scrollEnabled={undefined}
scrollEventThrottle={1}
scrollsToTop={false}
sendMomentumEvents={true}
showsHorizontalScrollIndicator={false}
style={
Array [
Object {
"flexDirection": "row",
"flexGrow": 1,
"flexShrink": 1,
"overflow": "scroll",
},
>
<RCTScrollView
DEPRECATED_sendUpdatedChildFrames={false}
alwaysBounceHorizontal={false}
alwaysBounceVertical={false}
automaticallyAdjustContentInsets={false}
bounces={false}
contentContainerStyle={
Object {
"flex": 1,
},
]
}
>
<RCTScrollContentView
collapsable={false}
removeClippedSubviews={undefined}
}
}
contentOffset={
Object {
"x": 0,
"y": 0,
}
}
directionalLockEnabled={true}
horizontal={true}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
onContentSizeChange={null}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onResponderGrant={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={undefined}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
overScrollMode="never"
pagingEnabled={true}
scrollEnabled={undefined}
scrollEventThrottle={1}
scrollsToTop={false}
sendMomentumEvents={true}
showsHorizontalScrollIndicator={false}
style={
Array [
Object {
"flexDirection": "row",
"flexGrow": 1,
"flexShrink": 1,
"overflow": "scroll",
},
Object {
"flex": 1,
@@ -94,47 +83,62 @@ exports[`TabBarBottom renders successfully 1`] = `
]
}
>
<View
<RCTScrollContentView
collapsable={false}
removeClippedSubviews={undefined}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
Array [
Object {
"flexDirection": "row",
},
Object {
"flex": 1,
},
]
}
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
>
<View
navigation={
style={
Object {
"state": Object {
"key": "s1",
"routeName": "s1",
},
"flex": 1,
}
}
screenProps={undefined}
/>
>
<View
navigation={
Object {
"state": Object {
"key": "s1",
"routeName": "s1",
},
}
}
screenProps={undefined}
/>
</View>
</View>
</View>
</View>
</RCTScrollContentView>
</RCTScrollView>
</RCTScrollContentView>
</RCTScrollView>
</View>
</View>
`;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 B

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 761 B

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