Compare commits

...

105 Commits

Author SHA1 Message Date
Brent Vatne
27eb73cc14 Release 2.12.1 2018-08-23 16:29:12 -07:00
Brent Vatne
f01b4896e6 Release 2.12.0 2018-08-22 17:17:54 -07:00
Brent Vatne
556c31626e Update react-navigation-stack, export HeaderStyleInterpolator, and add example of using it to playground 2018-08-22 17:16:39 -07:00
Brent Vatne
b6bca3ed2e Fix drawer config in Drawer + Tabs example 2018-08-22 16:04:11 -07:00
Nicolas Charpentier
0c56b21b46 Remove duplicate private key (#4819)
A package can't be more private than that, so let's remove duplicates.
2018-08-21 16:13:22 -07:00
Kevin Coleman
912c7ca076 Bump react-native-safe-area-view dep to 0.9.0 (#4810)
https://github.com/react-navigation/react-navigation/issues/3992
2018-08-07 10:23:51 -07:00
Brent Vatne
73c76f1e4b Extract stack to react-navigation-stack (#4809)
* Extract stack to react-navigation-stack

* Update CHANGELOG

* Fix tests
2018-08-03 18:47:46 -07:00
Brent Vatne
d746a587b0 Release 2.11.2 2018-08-03 15:45:27 -07:00
Brent Vatne
dee03c839a Undo pathUtils rename 2018-08-03 15:33:38 -07:00
Brent Vatne
2104bf1a04 Release 2.11.1 2018-08-03 14:48:49 -07:00
Brent Vatne
4e2a409dca Fix a couple of exports and export a few more modules 2018-08-03 14:47:43 -07:00
Brent Vatne
51bfe8dd19 Release 2.11.0 2018-08-03 14:17:24 -07:00
Brent Vatne
04a4512c1b Export modules useful for moving stack navigator outside of core 2018-08-03 14:16:57 -07:00
Brent Vatne
4a5da86ce0 Release 2.10.0 2018-08-02 13:03:34 -07:00
Brent Vatne
a118122aed Update react-navigation-[tabs&drawer] 2018-08-02 13:02:01 -07:00
Justin Parker
a94f89ffe1 Fixes bug where null doesn't work in routerOptions paths object for deeplinking (#4791)
* Add test for handling null path set on router

* Allow null paths on the router config

Previously if you specified `null` in the router `paths`, the logic would actually fall back to the `path` specified on the route, which is especially bad if the latter is undefined, because then the path would be set as the default of the routeName.
2018-08-01 01:12:49 -07:00
Brent Vatne
9d77fd6d54 Add NavigationTestUtils and release 2.9.3 2018-07-26 15:49:34 -07:00
Brent Vatne
13cf4497ee Release 2.9.2 2018-07-25 11:52:39 -07:00
Tim Wang
9175118383 Export StackViewTransitionConfigs (#4761)
* export StackViewTransitionConfigs

* Add changelogs
2018-07-25 11:50:07 -07:00
Simone D'Avico
6fc21250ec Fix typo in header transition preset check (uitkit -> uikit) (#4757) 2018-07-25 11:49:24 -07:00
Yevhen
714d5eab6b Error when building with haul: ref to pathToRegexp.compile (#4658)
Error in source code that cause problem on react native using haul https://puu.sh/AT1uZ/158623d5a4.png
2018-07-25 20:28:05 +02:00
Brent Vatne
67233dc9ef Release 2.9.1 2018-07-24 10:49:56 -07:00
Brent Vatne
b0443c1861 Move more logs behind debug flag in stack playground 2018-07-24 10:47:59 -07:00
Brent Vatne
c0b637df52 Fix title offfset calculation 2018-07-24 10:47:02 -07:00
Brent Vatne
9a82706fba Fix snapshots 2018-07-20 14:44:26 -07:00
Brent Vatne
d973a26edb Release 2.9.0 2018-07-20 14:33:27 -07:00
Brent Vatne
852e7e1974 Respect custom background color in header wrapper 2018-07-20 14:30:38 -07:00
Brent Vatne
cd3707d64b Add headerLayoutPreset, add config for back button title visibility and make it have reasonable defaults, better back button ripple on Android (#4588) 2018-07-20 14:12:39 -07:00
Brent Vatne
3c36db455f Release 2.8.0 2018-07-19 15:48:09 -07:00
Brent Vatne
ec52c884c5 Update NavigationPlayground to Expo SDK 28 2018-07-19 15:46:24 -07:00
Brent Vatne
c4b3f25a0f Cleanup unused descriptors and handle the case where we might expect to have a descriptor but do not (#4723) 2018-07-19 13:16:38 -07:00
Eric Vicenti
93642e16e7 Fix createNavigator leak of old descriptors 2018-07-19 12:41:14 -07:00
Eric Vicenti
1a76556290 Fix leak in createNavigator
Previous descriptors had been retained because this binding caused `this.prevState` to remain referenced. This binds the component getter to null instead.
2018-07-19 12:21:22 -07:00
Reza Ghorbani
12b21f052e added header container styles to be customized (#4331) 2018-07-18 15:14:08 -07:00
Brent Vatne
c1f07dc167 Release 2.7.0 2018-07-17 15:50:18 -07:00
Brent Vatne
bc04b31d01 Add border for transparent header in example 2018-07-17 15:43:22 -07:00
Eric Vicenti
35307c70be Improve empty path and param handling (#4671)
* Overhaul Path handling

* Another test for deep link
2018-07-17 13:51:20 -07:00
Brent Vatne
7e3f4f3bec Fix tests 2018-07-17 13:49:12 -07:00
Brent Vatne
cbd0958e6f Remove unnecessary style array 2018-07-17 13:36:16 -07:00
Brent Vatne
cab4d71a5e Fix edge case where route was in nav state but never actually graduated to scene 2018-07-17 13:36:16 -07:00
Kenza Iraki
108ac0e2a9 Set borderBottom to transparent and of size 0 if headerTransparent is true (#4701) 2018-07-16 16:32:14 -07:00
Brent Vatne
fa4fdb9c57 Fix onTransitionStart config not being invoked with keyboard aware navigator, and use prop over config when available 2018-07-16 15:13:25 -07:00
Brent Vatne
ebdd2da79f Pull onTransitionStart from navigationConfig, fixes #4100 2018-07-16 14:55:34 -07:00
Matteo Codogno
1fe11c100e Fix #4608 - remove header left component only when a headerLeft optio… (#4679)
* Fix #4608 - remove header left component only when a headerLeft option is not specified

* Update CHANGELOG.md
2018-07-12 11:42:56 -07:00
Eric Vicenti
c4b84f1d66 Fix container referene to startup state data 2018-07-12 11:01:11 -07:00
Michael Lefkowitz
69f394be5b Feat/allow keyless replace (#4636)
* allow key to be undefined on StackNavigation.replace method

* added tests for replace action w/out key

* fix typo

* updated changelog

* updated teests for clarity

* added length check on routes to safely fallthrough to search
2018-07-10 23:41:42 -07:00
Eric Vicenti
316e4991ac Add enableURLHandling to navigation container 2018-07-10 14:47:57 -07:00
Ashoat Tevosyan
805064cb5e [flow] Make NavigationRoute types exact (#4667)
React Native 0.56 introduces Flow 0.75, which makes it impossible to refine `NavigationRoute` based on the presence of `index` or `routes` properties.

This PR turns `NavigationStateRoute` and `NavigationLeafRoute` into exact types, which addresses this issue.
2018-07-10 13:59:09 -07:00
Dariusz Łuksza
8f199980cb Fix changelog (#4651)
Fixes 2.6.1 header link
2018-07-06 20:21:00 -07:00
Brent Vatne
37ca6a92ca Release 2.6.2 2018-07-06 10:44:30 -07:00
Brent Vatne
980e0409dc Temporarily remove warnings on vertical padding in header 2018-07-06 10:42:32 -07:00
Brent Vatne
a00ba5918a Default to 0 elevation on transparent header 2018-07-05 15:17:08 -07:00
Brent Vatne
ad6b25cff9 Fix 2.6.1 changelog 2018-07-05 15:07:17 -07:00
Brent Vatne
a69b67d6d2 Release 2.6.1 2018-07-05 15:03:14 -07:00
Brent Vatne
dc436e4d01 Warn for more invalid header styles 2018-07-05 15:02:48 -07:00
Brent Vatne
fe95bdeee6 Fix regression for shadow in header on Android 2018-07-05 14:53:41 -07:00
Brent Vatne
525528e38f Release 2.6.0 2018-07-04 13:04:00 -07:00
liuqiang1357
9f5f3d994c Fix bug: params not be passed to navigator inside SwitchNavigator (#4306) 2018-07-04 12:28:01 -07:00
Eric Vicenti
e8c1833053 Fix stack router child router delegation priority (#4635)
Stack router had some aggressive logic for deferring to inactive child routers. The child router behavior should come after all of the appropriate stack actions, with the exception of the active child router.

This was causing issues such as https://github.com/react-navigation/react-navigation/issues/4623 , where inactive tab navigators would handle the back action, and cause the stack to attempt to  pop back to it.
2018-07-03 21:18:25 -07:00
Eric Vicenti
0921889f7a Avoid crash when calling isFocused on old route (#4634) 2018-07-03 12:03:54 -07:00
Sébastien Lorber
1951a3ac46 Add <NavigationEvents/> component (#4188)
* add NavigationEvents

* expose TabsWithNavigationEvents in lib entrypoints

* Add NavigationEvents example in playground

* Add NavigationEvents example in playground

* Add NavigationEvents tests

* Add NavigationEvents Flow declarations

* remove useless NavigationEvents constructor

* NavigationEvents => make tests more readable by avoiding beforeEach callback

* fix flow test error by adding <any, any> to React.Component
2018-06-29 07:34:11 -07:00
Eric Vicenti
4e384f8057 Routers: Deep Linking Overhaul (#4590)
* deep linking overhaul

* clean up PlatformHelpers

this had previously been required for old versions of react native and react-native-web
2018-06-29 07:27:12 -07:00
Eric Vicenti
3d06d19d6a clean up PlatformHelpers (#4586)
this had previously been required for old versions of react native and react-native-web
2018-06-28 10:02:52 -07:00
Brent Vatne
30ef5ef72b Release 2.5.5 2018-06-27 18:12:59 -07:00
Brent Vatne
c7fff52408 Delegate to child routers for more than just the top screen in the stack (#4587)
* Delegate to child routers for more than just the top screen in the stack. Fixes #4185

* Add CHANGELOG entry
2018-06-27 17:37:30 -07:00
Brent Vatne
bc01a4cd57 Throw error in development mode when title is not a string 2018-06-27 17:35:29 -07:00
Brent Vatne
cad3d70aed Update react-navigation-drawer to 0.4.3 2018-06-27 17:27:58 -07:00
Brent Vatne
bb5719f438 Throw an error when header is invalid 2018-06-27 17:22:00 -07:00
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
78 changed files with 5557 additions and 9215 deletions

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.

178
CHANGELOG.md Normal file
View File

@@ -0,0 +1,178 @@
# 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.12.1] - [2018-08-23](https://github.com/react-navigation/react-navigation/releases/tag/2.12.1)
### Fixed
- Fix crash on react-native@>=0.56 described in https://github.com/react-navigation/react-navigation/issues/4886
## [2.12.0] - [2018-08-22](https://github.com/react-navigation/react-navigation/releases/tag/2.12.0)
### Changed
- Move stack specific view code to react-navigation-stack
- Add accessibility props for inactive screens in stack (https://github.com/react-navigation/react-navigation-stack/commit/4e04428e26df9076413b57b3346a7ce357de1a77)
- Updated header title to match iOS 11/12 style correctly (https://github.com/react-navigation/react-navigation-stack/pull/1)
- Add support for animating the header background on screen transitions and add interpolator to animate it along with the rest of the screen, but this is still opt-in behavior (https://github.com/react-navigation/react-navigation-stack/pull/3)
- Updated react-native-safe-area-view to 0.2.0
## [2.11.2] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.2)
### Changed
- Revert rename of pathUtils
## [2.11.1] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.1)
### Changed
- Fix some exports related to the 2.11.0 changes to move stack navigator out of core
## [2.11.0] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.0)
### Added
- Export some modules that are useful for moving stack navigator outside of core
## [2.10.0] - [2018-08-02](https://github.com/react-navigation/react-navigation/releases/tag/2.10.0)
### Added
- `lazy` and `optimizationsEnabled` options to `createMaterialTopTabNavigator` (react-navigation-tabs@0.6.0)
### Fixed
- Android back button in stack with drawer closes drawer properly if open (react-navigation-drawer@0.5.0)
- Fixes bug where `null` doesn't work in routerOptions `paths` object for deeplinking ([#4791](https://github.com/react-navigation/react-navigation/pull/4791))
## [2.9.3] - [2018-07-26](https://github.com/react-navigation/react-navigation/releases/tag/2.9.3)
### Added
- Add `NavigationTestUtils` which can be imported by path to be used with jest snapshot testing.
## [2.9.2] - [2018-07-25](https://github.com/react-navigation/react-navigation/releases/tag/2.9.2)
### Added
- Export `StackViewTransitionConfigs` to allow you to extend default config in custom transition configs. [#4761](https://github.com/react-navigation/react-navigation/pull/4761)
### Fixed
- Error when building with haul: ref to pathToRegexp.compile(#4658).
- Error when building with haul: ref to pathToRegexp.compile. [#4658](https://github.com/react-navigation/react-navigation/pull/4658).
## [2.9.1] - [2018-07-24](https://github.com/react-navigation/react-navigation/releases/tag/2.9.1)
### Fixed
- Incorrect parameters passed to title offset calculation led to bug in header layout when no right component (https://github.com/react-navigation/react-navigation/issues/4754)
### Fixed
- Typo in Header transition preset check.
## [2.9.0] - [2018-07-20](https://github.com/react-navigation/react-navigation/releases/tag/2.9.0)
### Added
- `headerLayoutPreset: 'center' | 'left'` to provide an easy solution for [questions like this](https://github.com/react-navigation/react-navigation/issues/4615).
- `headerBackTitleEnabled` - this configuration option for stack navigator allows you to force back button titles to either be rendered or not (if you disagree with defaults for your platform and layout preset).
### Fixed
- Android back button ripple is now appropriately sized (fixes [#3955](https://github.com/react-navigation/react-navigation/issues/3955)).
- Respect header background color on container (fixes edge case where user depended on displaying content that was rendered behind the navigator, this particular behavior should not be depended on and may break in the future, but this change is still useful regardless).
## [2.8.0] - [2018-07-19](https://github.com/react-navigation/react-navigation/releases/tag/2.8.0)
### Added
- `headerLeftContainerStyle`, `headerTitleContainerStyle`, and `headerRightContainerStyle` are exposed on `navigationOptions`. These properties allow you to customize the style of the container of `headerLeft`, `headerTitle` and `headerRight` components.
### Fixed
- Fixed memory leaks in `createNavigator`: [closure scope leak](https://github.com/react-navigation/react-navigation/commit/1a765562905e93bbae0262dd20c2688221c999e8), and [clean up old descriptors](https://github.com/react-navigation/react-navigation/commit/93642e16e7ff029586b68ee732ec790504ee4862).
## [2.7.0] - [2018-07-17](https://github.com/react-navigation/react-navigation/releases/tag/2.7.0)
### Added
- The enableURLHandling prop on the top level navigator component allows you to disable deep linking handling. Currently it is always enabled. To disable it, `<RootNavigator enableURLHandling={false} />`
### Changed
- StackNavigator.replace method no longer requires a key param. If the key is left undefined, the last screen in the stack will be replaced.
### Fixed
- Support headerLeft component for the first screen in a stack (#4608).
- Removed bottomBorder when `headerTransparent` is set to true.
- Improve empty path and param handling in deep linking (#4671). This fixes issues with deep linking and fully tests the differences between path: '' and path: null. Empty string matches empty paths, and null path will let the child router handle paths at the same level. Also it makes sure that params are not duplicated between path and query when they are serialized with getPathAndParamsForState.
- Fix onTransitionStart not being invoked when provided in navigator config.(#4100)
- Rare case when users navigated back and forth quickly with exactly the right timing would cause a crash due to a scene being queued to transition, then clobbered, then attempted to render as a stale scene but without a descriptor. ([commit](https://github.com/react-navigation/react-navigation/commit/cab4d71a5e09188df3f4a294c98779eecb860a78))
## [2.6.2] - [2018-07-06](https://github.com/react-navigation/react-navigation/releases/tag/2.6.2)
### Changed
- Relax vertical padding warnings on header.
## [2.6.1] - [2018-07-05](https://github.com/react-navigation/react-navigation/releases/tag/2.6.1)
### Added
- Warn for more invalid headerStyle properties (padding, top/right/bottom/left, position).
### Fixed
- Fixed missing header shadow on Android.
## [2.6.0] - [2018-07-04](https://github.com/react-navigation/react-navigation/releases/tag/2.6.0)
### Added
- [NavigationEvents](https://github.com/react-navigation/react-navigation/pull/4188) component as a declarative interface for subscribing to navigation focus events.
### Fixed
- Fix stack router child router delegation priority (https://github.com/react-navigation/react-navigation/commit/e8c1833053e37d28f0ce505ff323565abf23b6a2)
- Avoid crash when calling isFocused on old route (https://github.com/react-navigation/react-navigation/commit/0921889f7a3acfc6d6bcc4909d209eeeee985ba7)
- Stack router no longer attempts to parse query params within path handling
- Switch router now has exact same param treatment for URLs as stack router does
### Changed
- Internally we no longer need to special case PlatformHelpers by platform as react-native-web handles the APIs we mocked out with it now.
## [2.5.5] - [2018-06-27](https://github.com/react-navigation/react-navigation/releases/tag/2.5.5)
### Added
- Throw error in development mode when header navigation option is set to a string - a common mistake that would otherwise result in a cryptic error message.
- Throw error in development mode when title is not a string.
### Fixed
- Delegate to child routers for more than just the top screen in the stack.
- Update react-navigation-drawer to 0.4.3 to fix `initialRouteParams` option
## [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.12.1...HEAD
[2.12.1]: https://github.com/react-navigation/react-navigation/compare/2.12.0...2.12.1
[2.12.0]: https://github.com/react-navigation/react-navigation/compare/2.11.2...2.12.0
[2.11.2]: https://github.com/react-navigation/react-navigation/compare/2.11.1...2.11.2
[2.11.1]: https://github.com/react-navigation/react-navigation/compare/2.11.0...2.11.1
[2.11.0]: https://github.com/react-navigation/react-navigation/compare/2.10.0...2.11.0
[2.10.0]: https://github.com/react-navigation/react-navigation/compare/2.9.3...2.10.0
[2.9.3]: https://github.com/react-navigation/react-navigation/compare/2.9.2...2.9.3
[2.9.2]: https://github.com/react-navigation/react-navigation/compare/2.9.1...2.9.2
[2.9.1]: https://github.com/react-navigation/react-navigation/compare/2.9.0...2.9.1
[2.9.0]: https://github.com/react-navigation/react-navigation/compare/2.8.0...2.9.0
[2.8.0]: https://github.com/react-navigation/react-navigation/compare/2.7.0...2.8.0
[2.7.0]: https://github.com/react-navigation/react-navigation/compare/2.6.2...2.7.0
[2.6.2]: https://github.com/react-navigation/react-navigation/compare/2.6.1...2.6.2
[2.6.1]: https://github.com/react-navigation/react-navigation/compare/2.6.0...2.6.1
[2.6.0]: https://github.com/react-navigation/react-navigation/compare/2.5.5...2.6.0
[2.5.5]: https://github.com/react-navigation/react-navigation/compare/2.5.4...2.5.5
[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

7
NavigationTestUtils.js Normal file
View File

@@ -0,0 +1,7 @@
import { _TESTING_ONLY_reset_container_count } from './src/createNavigationContainer';
export default {
resetInternalState: () => {
_TESTING_ONLY_reset_container_count();
},
};

View File

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

View File

@@ -26,6 +26,7 @@ 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';
@@ -35,6 +36,7 @@ import StackWithTranslucentHeader from './StackWithTranslucentHeader';
import SimpleTabs from './SimpleTabs';
import SwitchWithStacks from './SwitchWithStacks';
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
import TabsWithNavigationEvents from './TabsWithNavigationEvents';
import KeyboardHandlingExample from './KeyboardHandlingExample';
const ExampleInfo = {
@@ -105,6 +107,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',
@@ -121,6 +127,11 @@ const ExampleInfo = {
name: 'withNavigationFocus',
description: 'Receive the focus prop to know when a screen is focused',
},
TabsWithNavigationEvents: {
name: 'NavigationEvents',
description:
'Declarative NavigationEvents component to subscribe to navigation events',
},
KeyboardHandlingExample: {
name: 'Keyboard Handling Example',
description:
@@ -137,7 +148,12 @@ const ExampleRoutes = {
// screen: MultipleDrawer,
// },
StackWithCustomHeaderBackImage: StackWithCustomHeaderBackImage,
StackWithHeaderPreset: StackWithHeaderPreset,
...Platform.select({
ios: {
StackWithHeaderPreset: StackWithHeaderPreset,
},
android: {},
}),
StackWithTranslucentHeader: StackWithTranslucentHeader,
TabsInDrawer: TabsInDrawer,
CustomTabs: CustomTabs,
@@ -146,6 +162,7 @@ const ExampleRoutes = {
StacksWithKeys: StacksWithKeys,
StacksInTabs: StacksInTabs,
StacksOverTabs: StacksOverTabs,
StacksOverTopTabs: StacksOverTopTabs,
LinkStack: {
screen: SimpleStack,
path: 'people/Jordan',
@@ -155,6 +172,7 @@ const ExampleRoutes = {
path: 'settings',
},
TabsWithNavigationFocus,
TabsWithNavigationEvents,
KeyboardHandlingExample,
// This is commented out because it's rarely useful
// InactiveStack,

View File

@@ -10,7 +10,7 @@ import type {
} from 'react-navigation';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { Platform, ScrollView, StatusBar } from 'react-native';
import {
createStackNavigator,
SafeAreaView,
@@ -24,6 +24,8 @@ import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
const DEBUG = false;
type MyNavScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
banner: React.Node,
@@ -133,16 +135,16 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus HomeScreen', a);
DEBUG && console.log('_willFocus HomeScreen', a);
};
_onDF = a => {
console.log('_didFocus HomeScreen', a);
DEBUG && console.log('_didFocus HomeScreen', a);
};
_onWB = a => {
console.log('_willBlur HomeScreen', a);
DEBUG && console.log('_willBlur HomeScreen', a);
};
_onDB = a => {
console.log('_didBlur HomeScreen', a);
DEBUG && console.log('_didBlur HomeScreen', a);
};
render() {
@@ -177,16 +179,16 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus PhotosScreen', a);
DEBUG && console.log('_willFocus PhotosScreen', a);
};
_onDF = a => {
console.log('_didFocus PhotosScreen', a);
DEBUG && console.log('_didFocus PhotosScreen', a);
};
_onWB = a => {
console.log('_willBlur PhotosScreen', a);
DEBUG && console.log('_willBlur PhotosScreen', a);
};
_onDB = a => {
console.log('_didBlur PhotosScreen', a);
DEBUG && console.log('_didBlur PhotosScreen', a);
};
render() {
@@ -231,18 +233,23 @@ MyProfileScreen.navigationOptions = props => {
};
};
const SimpleStack = createStackNavigator({
Home: {
screen: MyHomeScreen,
const SimpleStack = createStackNavigator(
{
Home: {
screen: MyHomeScreen,
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
});
{
// headerLayoutPreset: 'center',
}
);
export default SimpleStack;

View File

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

View File

@@ -16,9 +16,14 @@ import {
Platform,
ScrollView,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { Header, createStackNavigator } from 'react-navigation';
import {
Header,
HeaderStyleInterpolator,
createStackNavigator,
} from 'react-navigation';
import invariant from 'invariant';
import SampleText from './SampleText';
@@ -229,8 +234,20 @@ const StackWithTranslucentHeader = createStackNavigator(
},
{
headerTransitionPreset: 'uikit',
// You can leave this out if you don't want the card shadow to
// be visible through the header
transitionConfig: () => ({
headerBackgroundInterpolator:
HeaderStyleInterpolator.forBackgroundWithTranslation,
}),
navigationOptions: {
headerTransparent: true,
headerStyle: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#A7A7AA',
},
headerBackground: Platform.select({
ios: <BlurView style={{ flex: 1 }} intensity={98} />,
android: (

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

@@ -13,31 +13,19 @@ const TabsInDrawer = createDrawerNavigator({
SimpleTabs: {
screen: SimpleTabs,
navigationOptions: {
drawer: () => ({
label: 'Simple Tabs',
icon: ({ tintColor }) => (
<MaterialIcons
name="filter-1"
size={24}
style={{ color: tintColor }}
/>
),
}),
drawerLabel: 'Simple tabs',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="filter-1" size={24} style={{ color: tintColor }} />
),
},
},
StacksOverTabs: {
screen: StacksOverTabs,
navigationOptions: {
drawer: () => ({
label: 'Stacks Over Tabs',
icon: ({ tintColor }) => (
<MaterialIcons
name="filter-2"
size={24}
style={{ color: tintColor }}
/>
),
}),
drawerLabel: 'Stacks Over Tabs',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="filter-2" size={24} style={{ color: tintColor }} />
),
},
},
});

View File

@@ -0,0 +1,127 @@
/**
* @flow
*/
import React from 'react';
import { FlatList, SafeAreaView, StatusBar, Text, View } from 'react-native';
import { NavigationEvents } from 'react-navigation';
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
const Event = ({ event }) => (
<View
style={{
borderColor: 'grey',
borderWidth: 1,
borderRadius: 3,
padding: 5,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Text>{event.type}</Text>
<Text>
{event.action.type.replace('Navigation/', '')}
{event.action.routeName ? '=>' + event.action.routeName : ''}
</Text>
</View>
);
const createTabScreen = (name, icon, focusedIcon) => {
class TabScreen extends React.Component<any, any> {
static navigationOptions = {
tabBarLabel: name,
tabBarIcon: ({ tintColor, focused }) => (
<MaterialCommunityIcons
name={focused ? focusedIcon : icon}
size={26}
style={{ color: focused ? tintColor : '#ccc' }}
/>
),
};
state = { eventLog: [] };
append = navigationEvent => {
this.setState(({ eventLog }) => ({
eventLog: eventLog.concat(navigationEvent),
}));
};
render() {
return (
<SafeAreaView
forceInset={{ horizontal: 'always', top: 'always' }}
style={{
flex: 1,
}}
>
<Text
style={{
margin: 10,
marginTop: 30,
fontSize: 30,
fontWeight: 'bold',
}}
>
Events for tab {name}
</Text>
<View style={{ flex: 1, width: '100%', marginTop: 10 }}>
<FlatList
data={this.state.eventLog}
keyExtractor={item => `${this.state.eventLog.indexOf(item)}`}
renderItem={({ item }) => (
<View
style={{
marginVertical: 5,
marginHorizontal: 10,
backgroundColor: '#e4e4e4',
}}
>
<Event event={item} />
</View>
)}
/>
</View>
<NavigationEvents
onWillFocus={this.append}
onDidFocus={this.append}
onWillBlur={this.append}
onDidBlur={this.append}
/>
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
return TabScreen;
};
const TabsWithNavigationEvents = 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'
),
},
},
{
shifting: false,
activeTintColor: '#F44336',
}
);
export default TabsWithNavigationEvents;

View File

@@ -2,32 +2,31 @@
"name": "NavigationPlayground",
"version": "0.1.0",
"private": true,
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "react-native-scripts start",
"eject": "react-native-scripts eject",
"android": "react-native-scripts android",
"ios": "react-native-scripts ios",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"test": "flow"
},
"dependencies": {
"expo": "^27.0.0",
"expo": "^28.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-material-bottom-tabs": "0.1.3"
"react-navigation-material-bottom-tabs": "^0.3.0",
"react-navigation-tabs": "^0.5.1"
},
"devDependencies": {
"babel-jest": "^22.4.1",
"babel-plugin-transform-remove-console": "^6.9.0",
"flow-bin": "^0.67.0",
"jest": "^22.1.3",
"jest-expo": "^26.0.0",
"react-native-scripts": "^1.5.0",
"react-test-renderer": "16.3.0-alpha.1"
"jest-expo": "^28.0.0",
"react-test-renderer": "16.3.1"
},
"jest": {
"preset": "jest-expo",

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

@@ -2,38 +2,32 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStackNavigator } from 'react-navigation';
import { initializeListeners } from 'react-navigation-redux-helpers';
import {
reduxifyNavigator,
createReactNavigationReduxMiddleware,
} from 'react-navigation-redux-helpers';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
import ProfileScreen from '../components/ProfileScreen';
import { navigationPropConstructor } from '../utils/redux';
export const AppNavigator = createStackNavigator({
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,
};
componentDidMount() {
initializeListeners('root', this.props.nav);
}
render() {
const { dispatch, nav } = this.props;
const navigation = navigationPropConstructor(dispatch, nav);
return <AppNavigator navigation={navigation} />;
}
}
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,12 +0,0 @@
import {
createReactNavigationReduxMiddleware,
createNavigationPropConstructor,
} from 'react-navigation-redux-helpers';
const middleware = createReactNavigationReduxMiddleware(
'root',
state => state.nav
);
const navigationPropConstructor = createNavigationPropConstructor('root');
export { middleware, navigationPropConstructor };

File diff suppressed because it is too large Load Diff

View File

@@ -184,7 +184,7 @@ declare module 'react-navigation' {
| NavigationLeafRoute
| NavigationStateRoute;
declare export type NavigationLeafRoute = {
declare export type NavigationLeafRoute = {|
/**
* React's key used by some navigators. No need to specify these manually,
* they will be defined by the router.
@@ -204,10 +204,12 @@ declare module 'react-navigation' {
* e.g. `{ car_id: 123 }` in a route that displays a car.
*/
params?: NavigationParams,
};
|};
declare export type NavigationStateRoute = NavigationLeafRoute &
NavigationState;
declare export type NavigationStateRoute = {|
...NavigationLeafRoute,
...$Exact<NavigationState>,
|};
/**
* Router
@@ -284,7 +286,7 @@ declare module 'react-navigation' {
| NavigationContainer<*, *, *>;
declare interface withOptionalNavigationOptions<Options> {
navigationOptions?: NavigationScreenConfig<Options>,
navigationOptions?: NavigationScreenConfig<Options>;
}
declare export type NavigationScreenComponent<
@@ -294,10 +296,11 @@ declare module 'react-navigation' {
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, Route>,
}> & withOptionalNavigationOptions<Options>;
}> &
withOptionalNavigationOptions<Options>;
declare interface withRouter<State, Options> {
router: NavigationRouter<State, Options>,
router: NavigationRouter<State, Options>;
}
declare export type NavigationNavigator<
@@ -307,7 +310,9 @@ declare module 'react-navigation' {
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, State>,
}> & withRouter<State, Options> & withOptionalNavigationOptions<Options>;
}> &
withRouter<State, Options> &
withOptionalNavigationOptions<Options>;
declare export type NavigationRouteConfig =
| NavigationComponent
@@ -449,6 +454,7 @@ declare module 'react-navigation' {
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
tabBarOnPress?: ({
navigation: NavigationScreenProp<NavigationRoute>,
defaultHandler: () => void,
}) => void,
|};
@@ -503,10 +509,6 @@ declare module 'react-navigation' {
};
declare export type NavigationScreenProp<+S> = {
...$ObjMap<
_DefaultActionCreators,
<Args>((...args: Args) => *) => (...args: Args) => boolean
>,
+state: S,
dispatch: NavigationDispatch,
addListener: (
@@ -515,6 +517,21 @@ declare module 'react-navigation' {
) => NavigationEventSubscription,
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,
@@ -542,6 +559,21 @@ declare module 'react-navigation' {
navigationOptions?: O,
}>;
/**
* NavigationEvents component
*/
declare type _NavigationEventsProps = {
navigation?: NavigationScreenProp<NavigationState>,
onWillFocus?: NavigationEventCallback,
onDidFocus?: NavigationEventCallback,
onWillBlur?: NavigationEventCallback,
onDidBlur?: NavigationEventCallback,
};
declare export var NavigationEvents: React$ComponentType<
_NavigationEventsProps
>;
/**
* Navigation container
*/
@@ -553,7 +585,9 @@ declare module 'react-navigation' {
> = React$ComponentType<{
...Props,
...NavigationContainerProps<State, Options>,
}> & withRouter<State, Options> & withOptionalNavigationOptions<Options>;
}> &
withRouter<State, Options> &
withOptionalNavigationOptions<Options>;
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
uriPrefix?: string | RegExp,
@@ -791,26 +825,6 @@ declare module 'react-navigation' {
}) => NavigationToggleDrawerAction,
};
declare type _DefaultActionCreators = {|
goBack: (routeKey?: ?string) => NavigationBackAction,
navigate: (
routeName:
| string
| {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
},
params?: NavigationParams,
action?: NavigationNavigateAction
) => NavigationNavigateAction,
setParams: (newParams: NavigationParams) => NavigationSetParamsAction,
|};
declare export function getNavigationActionCreators(
route: NavigationRoute | NavigationState
): _DefaultActionCreators;
declare type _RouterProp<S: NavigationState, O: {}> = {
router: NavigationRouter<S, O>,
};
@@ -1162,4 +1176,13 @@ declare module 'react-navigation' {
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.3.0-beta.1",
"version": "2.12.1",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -22,7 +22,8 @@
"precommit": "lint-staged"
},
"files": [
"src"
"src",
"NavigationTestUtils.js"
],
"peerDependencies": {
"react": "*",
@@ -33,12 +34,13 @@
"create-react-context": "^0.2.1",
"hoist-non-react-statics": "^2.2.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10",
"query-string": "^6.1.0",
"react-lifecycles-compat": "^3",
"react-native-safe-area-view": "^0.8.0",
"react-native-safe-area-view": "^0.9.0",
"react-navigation-deprecated-tab-navigator": "1.3.0",
"react-navigation-drawer": "0.3.0",
"react-navigation-tabs": "0.5.1"
"react-navigation-drawer": "0.5.0",
"react-navigation-stack": "0.2.3",
"react-navigation-tabs": "0.6.0"
},
"devDependencies": {
"babel-cli": "^6.24.1",
@@ -89,7 +91,7 @@
"<rootDir>/examples/"
],
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator|react-navigation-stack)"
]
},
"lint-staged": {

View File

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

View File

@@ -1,6 +0,0 @@
import React from 'react';
import { BackHandler, View } from 'react-native';
const MaskedViewIOS = () => <View>{this.props.children}</View>;
export { BackHandler, MaskedViewIOS };

View File

@@ -133,10 +133,15 @@ const StateUtils = {
* 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.
* If preserveIndex is true then replacing the route does not cause the index
* to change to the index of that route.
*/
replaceAt(state, key, route) {
replaceAt(state, key, route, preserveIndex = false) {
const index = StateUtils.indexOf(state, key);
return StateUtils.replaceAtIndex(state, index, route);
const nextIndex = preserveIndex ? state.index : index;
let nextState = StateUtils.replaceAtIndex(state, index, route);
nextState.index = nextIndex;
return nextState;
},
/**

View File

@@ -4,8 +4,12 @@ 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';
// TODO: we should create a dummy navigator here
import { createStackNavigator } from 'react-navigation-stack';
import createNavigationContainer, {
_TESTING_ONLY_reset_container_count,
} from '../createNavigationContainer';
describe('NavigationContainer', () => {
jest.useFakeTimers();
@@ -19,7 +23,7 @@ describe('NavigationContainer', () => {
const CarScreen = () => <div />;
const DogScreen = () => <div />;
const ElkScreen = () => <div />;
const NavigationContainer = createStackNavigator(
const Stack = createStackNavigator(
{
foo: {
screen: FooScreen,
@@ -44,6 +48,7 @@ describe('NavigationContainer', () => {
initialRouteName: 'foo',
}
);
const NavigationContainer = createNavigationContainer(Stack);
describe('state.nav', () => {
it("should be preloaded with the router's initial state", () => {
@@ -225,7 +230,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 />;
@@ -242,13 +247,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

@@ -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,12 +1,12 @@
import React from 'react';
import { AsyncStorage, Linking, Platform } from 'react-native';
import { AsyncStorage, Linking, Platform, BackHandler } 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';
import { urlToPathAndParams } from './routers/pathUtils';
function isStateful(props) {
return !props.navigation;
@@ -129,23 +129,12 @@ export default function createNavigationContainer(Component) {
}
}
_urlToPathAndParams(url) {
const params = {};
const delimiter = this.props.uriPrefix || '://';
let path = url.split(delimiter)[1];
if (typeof path === 'undefined') {
path = url;
} else if (path === '') {
path = '/';
}
return {
path,
params,
};
}
_handleOpenURL = ({ url }) => {
const parsedUrl = this._urlToPathAndParams(url);
const { enableURLHandling, uriPrefix } = this.props;
if (enableURLHandling === false) {
return;
}
const parsedUrl = urlToPathAndParams(url, uriPrefix);
if (parsedUrl) {
const { path, params } = parsedUrl;
const action = Component.router.getActionForPathAndParams(path, params);
@@ -214,11 +203,15 @@ export default function createNavigationContainer(Component) {
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);
const { persistenceKey, uriPrefix, enableURLHandling } = this.props;
let parsedUrl = null;
let startupStateJSON = null;
if (enableURLHandling !== false) {
startupStateJSON =
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
const url = await Linking.getInitialURL();
parsedUrl = url && urlToPathAndParams(url, uriPrefix);
}
// Initialize state. This must be done *after* any async code
// so we don't end up with a different value for this.state.nav
@@ -286,7 +279,7 @@ export default function createNavigationContainer(Component) {
);
this.dispatch(NavigationActions.init());
} else {
throw new Error(e);
throw e;
}
}

View File

@@ -1,5 +1,6 @@
import getChildEventSubscriber from './getChildEventSubscriber';
import getChildRouter from './getChildRouter';
import getNavigationActionCreators from './routers/getNavigationActionCreators';
import invariant from './utils/invariant';
const createParamGetter = route => (paramName, defaultValue) => {
@@ -18,6 +19,10 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
const childRoute = navigation.state.routes.find(r => r.key === childKey);
if (!childRoute) {
return null;
}
if (children[childKey] && children[childKey].state === childRoute) {
return children[childKey];
}
@@ -40,7 +45,9 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
...(childRouter
? childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key)
: {}),
...getNavigationActionCreators(childRoute),
};
const actionHelpers = {};
Object.keys(actionCreators).forEach(actionName => {
actionHelpers[actionName] = (...args) => {
@@ -76,12 +83,16 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
getParam: createParamGetter(childRoute),
getChildNavigation: grandChildKey =>
getChildNavigation(children[childKey], grandChildKey, () =>
getCurrentParentNavigation().getChildNavigation(childKey)
),
getChildNavigation(children[childKey], grandChildKey, () => {
const nav = getCurrentParentNavigation();
return nav && nav.getChildNavigation(childKey);
}),
isFocused: () => {
const currentNavigation = getCurrentParentNavigation();
if (!currentNavigation) {
return false;
}
const { routes, index } = currentNavigation.state;
if (!currentNavigation.isFocused()) {
return false;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import StackNavigator from '../createStackNavigator';
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

@@ -1,93 +0,0 @@
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import StackNavigator from '../createStackNavigator';
import withNavigation from '../../views/withNavigation';
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';
const styles = StyleSheet.create({
header: {
opacity: 0.5,
},
});
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: `Welcome ${
navigation.state.params ? navigation.state.params.user : 'anonymous'
}`,
gesturesEnabled: true,
headerStyle: [{ backgroundColor: 'red' }, styles.header],
});
render() {
return null;
}
}
const routeConfig = {
Home: {
screen: HomeScreen,
},
};
describe('StackNavigator', () => {
beforeEach(() => {
_TESTING_ONLY_reset_container_count();
});
it('renders successfully', () => {
const MyStackNavigator = StackNavigator(routeConfig);
const rendered = renderer.create(<MyStackNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('applies correct values when headerRight is present', () => {
const MyStackNavigator = StackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: {
headerRight: <View />,
},
},
});
const rendered = renderer.create(<MyStackNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('passes navigation to headerRight when wrapped in withNavigation', () => {
const spy = jest.fn();
class TestComponent extends React.Component {
render() {
return <View>{this.props.onPress(this.props.navigation)}</View>;
}
}
const TestComponentWithNavigation = withNavigation(TestComponent);
class A extends React.Component {
static navigationOptions = {
headerRight: <TestComponentWithNavigation onPress={spy} />,
};
render() {
return <View />;
}
}
const Nav = StackNavigator({ A: { screen: A } });
renderer.create(<Nav />);
expect(spy).toBeCalledWith(
expect.objectContaining({
navigate: expect.any(Function),
addListener: expect.any(Function),
})
);
});
});

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,359 +0,0 @@
// 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
collapsable={undefined}
style={
Object {
"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
collapsable={undefined}
style={
Object {
"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>
`;
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

@@ -1,383 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StackNavigator applies correct values when headerRight is present 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>
<View
collapsable={undefined}
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<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,
}
}
>
<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": 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>
</View>
</View>
</View>
`;
exports[`StackNavigator renders successfully 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>
<View
collapsable={undefined}
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<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,
}
}
>
<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",
}
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;

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,7 +1,7 @@
import React from 'react';
import { TextInput } from 'react-native';
export default Navigator =>
export default (Navigator, navigatorConfig) =>
class KeyboardAwareNavigator extends React.Component {
static router = Navigator.router;
_previouslyFocusedTextInput = null;
@@ -30,12 +30,12 @@ export default Navigator =>
if (this._previouslyFocusedTextInput) {
TextInput.State.focusTextInput(this._previouslyFocusedTextInput);
}
this.props.onGestureFinish && this.props.onGestureFinish();
this.props.onGestureCanceled && this.props.onGestureCanceled();
};
_handleGestureFinish = () => {
this._previouslyFocusedTextInput = null;
this.props.onGestureCanceled && this.props.onGestureCanceled();
this.props.onGestureFinish && this.props.onGestureFinish();
};
_handleTransitionStart = (transitionProps, prevTransitionProps) => {
@@ -49,7 +49,9 @@ export default Navigator =>
}
}
this.props.onTransitionStart &&
this.props.onTransitionStart(transitionProps, prevTransitionProps);
const onTransitionStart =
this.props.onTransitionStart || navigatorConfig.onTransitionStart;
onTransitionStart &&
onTransitionStart(transitionProps, prevTransitionProps);
};
};

View File

@@ -24,7 +24,7 @@ function createNavigator(NavigatorView, router, navigationConfig) {
);
}
const descriptors = { ...prevState.descriptors };
const descriptors = {};
routes.forEach(route => {
if (
@@ -36,8 +36,10 @@ function createNavigator(NavigatorView, router, navigationConfig) {
descriptors[route.key] = prevDescriptors[route.key];
return;
}
const getComponent = () =>
router.getComponentForRouteName(route.routeName);
const getComponent = router.getComponentForRouteName.bind(
null,
route.routeName
);
const childNavigation = navigation.getChildNavigation(route.key);
const options = router.getScreenOptions(childNavigation, screenProps);
descriptors[route.key] = {

View File

@@ -1,40 +0,0 @@
import 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';
function createStackNavigator(routeConfigMap, stackConfig = {}) {
const {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
disableKeyboardHandling,
getCustomActionCreators,
} = stackConfig;
const stackRouterConfig = {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
getCustomActionCreators,
};
const router = StackRouter(routeConfigMap, stackRouterConfig);
// Create a navigator with StackView as the view
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);
}
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,28 +8,40 @@ module.exports = {
get StateUtils() {
return require('./StateUtils').default;
},
get getNavigation() {
return require('./getNavigation').default;
},
// Navigators
get createNavigator() {
return require('./navigators/createNavigator').default;
},
get createKeyboardAwareNavigator() {
return require('./navigators/createKeyboardAwareNavigator').default;
},
get NavigationProvider() {
return require('./views/NavigationContext').default.NavigationProvider;
},
get NavigationConsumer() {
return require('./views/NavigationContext').default.NavigationConsumer;
},
get createStackNavigator() {
return require('./navigators/createStackNavigator').default;
return require('react-navigation-stack').createStackNavigator;
},
get StackNavigator() {
console.warn(
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
);
return require('./navigators/createStackNavigator').default;
return require('react-navigation-stack').createStackNavigator;
},
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('react-navigation-drawer').createDrawerNavigator;
@@ -71,9 +83,6 @@ module.exports = {
get DrawerActions() {
return require('react-navigation-drawer').DrawerActions;
},
get getNavigationActionCreators() {
return require('./routers/getNavigationActionCreators').default;
},
// Routers
get StackRouter() {
@@ -88,16 +97,31 @@ module.exports = {
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
get createConfigGetter() {
return require('./routers/createConfigGetter').default;
},
get getScreenForRouteName() {
return require('./routers/getScreenForRouteName').default;
},
get validateRouteConfigMap() {
return require('./routers/validateRouteConfigMap').default;
},
get pathUtils() {
return require('./routers/pathUtils').default;
},
// Views
get Transitioner() {
return require('./views/Transitioner').default;
return require('react-navigation-stack').Transitioner;
},
get StackView() {
return require('./views/StackView/StackView').default;
return require('react-navigation-stack').StackView;
},
get StackViewCard() {
return require('./views/StackView/StackViewCard').default;
return require('react-navigation-stack').StackViewCard;
},
get StackViewTransitionConfigs() {
return require('react-navigation-stack').StackViewTransitionConfigs;
},
get SafeAreaView() {
return require('react-native-safe-area-view').default;
@@ -111,13 +135,16 @@ module.exports = {
// Header
get Header() {
return require('./views/Header/Header').default;
return require('react-navigation-stack').Header;
},
get HeaderTitle() {
return require('./views/Header/HeaderTitle').default;
return require('react-navigation-stack').HeaderTitle;
},
get HeaderBackButton() {
return require('./views/Header/HeaderBackButton').default;
return require('react-navigation-stack').HeaderBackButton;
},
get HeaderStyleInterpolator() {
return require('react-navigation-stack').HeaderStyleInterpolator;
},
// DrawerView
@@ -156,6 +183,11 @@ module.exports = {
return require('./views/SwitchView/SwitchView').default;
},
// NavigationEvents
get NavigationEvents() {
return require('./views/NavigationEvents').default;
},
// HOCs
get withNavigation() {
return require('./views/withNavigation').default;

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() {
@@ -24,9 +30,6 @@ module.exports = {
get DrawerActions() {
return require('./routers/DrawerActions').default;
},
get getNavigationActionCreators() {
return require('./routers/getNavigationActionCreators').default;
},
// Routers
get StackRouter() {
@@ -39,6 +42,11 @@ module.exports = {
return require('./routers/SwitchRouter').default;
},
// NavigationEvents
get NavigationEvents() {
return require('./views/NavigationEvents').default;
},
// HOCs
get withNavigation() {
return require('./views/withNavigation').default;

View File

@@ -1,5 +1,3 @@
import pathToRegexp from 'path-to-regexp';
import NavigationActions from '../NavigationActions';
import StackActions from './StackActions';
import createConfigGetter from './createConfigGetter';
@@ -8,15 +6,7 @@ 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;
for (let key in obj) {
return false;
}
return true;
}
import { createPathParser } from './pathUtils';
function behavesLikePushAction(action) {
return (
@@ -57,8 +47,6 @@ export default (routeConfigs, stackConfig = {}) => {
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
const initialChildRouter = childRouters[initialRouteName];
const pathsByRouteNames = { ...stackConfig.paths } || {};
let paths = [];
function getInitialState(action) {
let route = {};
@@ -116,37 +104,10 @@ export default (routeConfigs, stackConfig = {}) => {
};
}
// Build paths for each route
routeNames.forEach(routeName => {
let pathPattern =
pathsByRouteNames[routeName] || routeConfigs[routeName].path;
let matchExact = !!pathPattern && !childRouters[routeName];
if (pathPattern === undefined) {
pathPattern = routeName;
}
const keys = [];
let re, toPath, priority;
if (typeof pathPattern === 'string') {
// pathPattern may be either a string or a regexp object according to path-to-regexp docs.
re = pathToRegexp(pathPattern, keys);
toPath = pathToRegexp.compile(pathPattern);
priority = 0;
} else {
// for wildcard match
re = pathToRegexp('*', keys);
toPath = () => '';
matchExact = true;
priority = -1;
}
if (!matchExact) {
const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys);
re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`);
}
pathsByRouteNames[routeName] = { re, keys, toPath, priority };
});
paths = Object.entries(pathsByRouteNames);
paths.sort((a, b) => b[1].priority - a[1].priority);
const {
getPathAndParamsForRoute,
getActionForPathAndParams,
} = createPathParser(childRouters, routeConfigs, stackConfig.paths);
return {
childRouters,
@@ -166,7 +127,6 @@ export default (routeConfigs, stackConfig = {}) => {
getActionCreators(route, navStateKey) {
return {
...getNavigationActionCreators(route),
...getCustomActionCreators(route, navStateKey),
pop: (n, params) =>
StackActions.pop({
@@ -227,29 +187,27 @@ export default (routeConfigs, stackConfig = {}) => {
return getInitialState(action);
}
// Check if the focused child scene wants to handle the action, as long as
// it is not a reset to the root stack
const activeChildRoute = state.routes[state.index];
if (
!isResetToRootStack(action) &&
action.type !== NavigationActions.NAVIGATE
) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
: -1;
const childIndex = keyIndex >= 0 ? keyIndex : state.index;
const childRoute = state.routes[childIndex];
invariant(
childRoute,
`StateUtils erroneously thought index ${childIndex} exists`
);
const childRouter = childRouters[childRoute.routeName];
if (childRouter) {
const route = childRouter.getStateForAction(action, childRoute);
if (route === null) {
return state;
}
if (route && route !== childRoute) {
return StateUtils.replaceAt(state, childRoute.key, route);
// Let the active child router handle the action
const activeChildRouter = childRouters[activeChildRoute.routeName];
if (activeChildRouter) {
const route = activeChildRouter.getStateForAction(
action,
activeChildRoute
);
if (route !== null && route !== activeChildRoute) {
return StateUtils.replaceAt(
state,
activeChildRoute.key,
route,
// the following tells replaceAt to NOT change the index to this route for the setParam action, because people don't expect param-setting actions to switch the active route
action.type === NavigationActions.SET_PARAMS
);
}
}
} else if (action.type === NavigationActions.NAVIGATE) {
@@ -401,7 +359,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,
};
}
}
}
@@ -430,7 +391,15 @@ export default (routeConfigs, stackConfig = {}) => {
// Handle replace action
if (action.type === StackActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
let routeIndex;
// If the key param is undefined, set the index to the last route in the stack
if (action.key === undefined && state.routes.length) {
routeIndex = state.routes.length - 1;
} else {
routeIndex = state.routes.findIndex(r => r.key === action.key);
}
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
@@ -545,126 +514,52 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
// By this point in the router's state handling logic, we have handled the behavior of the active route, and handled any stack actions.
// If we haven't returned by now, we should allow non-active child routers to handle this action, and switch to that index if the child state (route) does change..
const keyIndex = action.key ? StateUtils.indexOf(state, action.key) : -1;
// 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()) {
if (childRoute.key === activeChildRoute.key) {
// skip over the active child because we let it attempt to handle the action earlier
continue;
}
// If a key is provided and in routes state then let's use that
// knowledge to skip extra getStateForAction calls on other child
// routers
if (keyIndex >= 0 && childRoute.key !== action.key) {
continue;
}
let childRouter = childRouters[childRoute.routeName];
if (childRouter) {
const route = childRouter.getStateForAction(action, childRoute);
if (route === null) {
return state;
} else if (route && route !== childRoute) {
return StateUtils.replaceAt(
state,
childRoute.key,
route,
// the following tells replaceAt to NOT change the index to this route for the setParam action, because people don't expect param-setting actions to switch the active route
action.type === NavigationActions.SET_PARAMS
);
}
}
}
return state;
},
getPathAndParamsForState(state) {
const route = state.routes[state.index];
const routeName = route.routeName;
const screen = getScreenForRouteName(routeConfigs, routeName);
const subPath = pathsByRouteNames[routeName].toPath(route.params);
let path = subPath;
let params = route.params;
if (screen && screen.router) {
const stateRoute = route;
// If it has a router it's a navigator.
// If it doesn't have router it's an ordinary React component.
const child = screen.router.getPathAndParamsForState(stateRoute);
path = subPath ? `${subPath}/${child.path}` : child.path;
params = child.params ? { ...params, ...child.params } : params;
}
return {
path,
params,
};
return getPathAndParamsForRoute(route);
},
getActionForPathAndParams(pathToResolve, inputParams) {
// If the path is empty (null or empty string)
// just return the initial route action
if (!pathToResolve) {
return NavigationActions.navigate({
routeName: initialRouteName,
params: inputParams,
});
}
const [pathNameToResolve, queryString] = pathToResolve.split('?');
// Attempt to match `pathNameToResolve` with a route in this router's
// routeConfigs
let matchedRouteName;
let pathMatch;
let pathMatchKeys;
// eslint-disable-next-line no-restricted-syntax
for (const [routeName, path] of paths) {
const { re, keys } = path;
pathMatch = re.exec(pathNameToResolve);
if (pathMatch && pathMatch.length) {
pathMatchKeys = keys;
matchedRouteName = routeName;
break;
}
}
// We didn't match -- return null
if (!matchedRouteName) {
// If the path is empty (null or empty string)
// just return the initial route action
if (!pathToResolve) {
return NavigationActions.navigate({
routeName: initialRouteName,
});
}
return null;
}
// Determine nested actions:
// If our matched route for this router is a child router,
// get the action for the path AFTER the matched path for this
// router
let nestedAction;
let nestedQueryString = queryString ? '?' + queryString : '';
if (childRouters[matchedRouteName]) {
nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
pathMatch.slice(pathMatchKeys.length).join('/') + nestedQueryString
);
if (!nestedAction) {
return null;
}
}
// reduce the items of the query string. any query params may
// be overridden by path params
const queryParams = !isEmpty(inputParams)
? inputParams
: (queryString || '').split('&').reduce((result, item) => {
if (item !== '') {
const nextResult = result || {};
const [key, value] = item.split('=');
nextResult[key] = value;
return nextResult;
}
return result;
}, null);
// reduce the matched pieces of the path into the params
// of the route. `params` is null if there are no params.
const params = pathMatch.slice(1).reduce((result, matchResult, i) => {
const key = pathMatchKeys[i];
if (key.asterisk || !key) {
return result;
}
const nextResult = result || inputParams || {};
const paramName = key.name;
let decodedMatchResult;
try {
decodedMatchResult = decodeURIComponent(matchResult);
} catch (e) {
// ignore `URIError: malformed URI`
}
nextResult[paramName] = decodedMatchResult || matchResult;
return nextResult;
}, queryParams);
return NavigationActions.navigate({
routeName: matchedRouteName,
...(params ? { params } : {}),
...(nestedAction ? { action: nestedAction } : {}),
});
getActionForPathAndParams(path, params) {
return getActionForPathAndParams(path, params);
},
getScreenOptions: createConfigGetter(

View File

@@ -5,7 +5,7 @@ import createConfigGetter from './createConfigGetter';
import NavigationActions from '../NavigationActions';
import StackActions from './StackActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getNavigationActionCreators from './getNavigationActionCreators';
import { createPathParser } from './pathUtils';
const defaultActionCreators = (route, navStateKey) => ({});
@@ -22,7 +22,7 @@ export default (routeConfigs, config = {}) => {
validateRouteConfigMap(routeConfigs);
const order = config.order || Object.keys(routeConfigs);
const paths = config.paths || {};
const getCustomActionCreators =
config.getCustomActionCreators || defaultActionCreators;
@@ -37,16 +37,18 @@ export default (routeConfigs, config = {}) => {
const childRouters = {};
order.forEach(routeName => {
const routeConfig = routeConfigs[routeName];
if (!paths[routeName]) {
paths[routeName] =
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
}
childRouters[routeName] = null;
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen.router) {
childRouters[routeName] = screen.router;
}
});
const {
getPathAndParamsForRoute,
getActionForPathAndParams,
} = createPathParser(childRouters, routeConfigs, config.paths);
if (initialRouteIndex === -1) {
throw new Error(
`Invalid initialRouteName '${initialRouteName}'.` +
@@ -74,50 +76,47 @@ export default (routeConfigs, config = {}) => {
};
}
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 {
...possibleNextState,
routes: nextRoutes,
};
} else {
nextState = possibleNextState;
}
return nextState;
}
function getInitialState() {
const routes = order.map(resetChildRoute);
return {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
}
return {
childRouters,
getInitialState() {
const routes = order.map(resetChildRoute);
return {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
},
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 {
...possibleNextState,
routes: nextRoutes,
};
} else {
nextState = possibleNextState;
}
return nextState;
},
getActionCreators(route, stateKey) {
return {
...getNavigationActionCreators(route),
...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) {
@@ -154,7 +153,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,
});
@@ -191,7 +190,7 @@ export default (routeConfigs, config = {}) => {
newChildState = childRouter
? childRouter.getStateForAction(action.action, childState)
: null;
} else if (!action.action && !childRouter && action.params) {
} else if (!action.action && action.params) {
newChildState = {
...childState,
params: {
@@ -204,7 +203,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,
@@ -232,7 +231,7 @@ export default (routeConfigs, config = {}) => {
...lastRoute,
params,
};
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
routes,
});
@@ -240,7 +239,7 @@ export default (routeConfigs, config = {}) => {
}
if (activeChildIndex !== state.index) {
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
index: activeChildIndex,
});
@@ -284,7 +283,7 @@ export default (routeConfigs, config = {}) => {
}
if (index !== state.index || routes !== state.routes) {
return this.getNextState(prevState, {
return getNextState(prevState, {
...state,
index,
routes,
@@ -313,73 +312,11 @@ export default (routeConfigs, config = {}) => {
getPathAndParamsForState(state) {
const route = state.routes[state.index];
const routeName = order[state.index];
const subPath = paths[routeName];
const screen = getScreenForRouteName(routeConfigs, routeName);
let path = subPath;
let params = route.params;
if (screen && screen.router) {
const stateRoute = route;
// If it has a router it's a navigator.
// If it doesn't have router it's an ordinary React component.
const child = screen.router.getPathAndParamsForState(stateRoute);
path = subPath ? `${subPath}/${child.path}` : child.path;
params = child.params ? { ...params, ...child.params } : params;
}
return {
path,
params,
};
return getPathAndParamsForRoute(route);
},
/**
* Gets an optional action, based on a relative path and query params.
*
* This will return null if there is no action matched
*/
getActionForPathAndParams(path, params) {
if (!path) {
return NavigationActions.navigate({
routeName: initialRouteName,
params,
});
}
return (
order
.map(childId => {
const parts = path.split('/');
const pathToTest = paths[childId];
const partsInTestPath = pathToTest.split('/').length;
const pathPartsToTest = parts.slice(0, partsInTestPath).join('/');
if (pathPartsToTest === pathToTest) {
const childRouter = childRouters[childId];
const action = NavigationActions.navigate({
routeName: childId,
});
if (childRouter && childRouter.getActionForPathAndParams) {
action.action = childRouter.getActionForPathAndParams(
parts.slice(partsInTestPath).join('/'),
params
);
}
if (params) {
action.params = params;
}
return action;
}
return null;
})
.find(action => !!action) ||
order
.map(childId => {
const childRouter = childRouters[childId];
return (
childRouter && childRouter.getActionForPathAndParams(path, params)
);
})
.find(action => !!action) ||
null
);
return getActionForPathAndParams(path, params);
},
getScreenOptions: createConfigGetter(

View File

@@ -0,0 +1,596 @@
/* eslint no-shadow:0, react/no-multi-comp:0, react/display-name:0 */
import React from 'react';
import SwitchRouter from '../SwitchRouter';
import StackRouter from '../StackRouter';
import StackActions from '../StackActions';
import NavigationActions from '../../NavigationActions';
import { urlToPathAndParams } from '../pathUtils';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
_TESTING_ONLY_normalize_keys();
});
const performRouterTest = createTestRouter => {
const ListScreen = () => <div />;
const ProfileNavigator = () => <div />;
ProfileNavigator.router = StackRouter({
list: {
path: 'list/:id',
screen: ListScreen,
},
});
const MainNavigator = () => <div />;
MainNavigator.router = StackRouter({
profile: {
path: 'p/:id',
screen: ProfileNavigator,
},
});
const LoginScreen = () => <div />;
const AuthNavigator = () => <div />;
AuthNavigator.router = StackRouter({
login: {
screen: LoginScreen,
},
});
const BarScreen = () => <div />;
class FooNavigator extends React.Component {
static router = StackRouter({
bar: {
path: 'b/:barThing',
screen: BarScreen,
},
});
render() {
return <div />;
}
}
const PersonScreen = () => <div />;
const testRouter = createTestRouter({
main: {
screen: MainNavigator,
},
baz: {
path: null,
screen: FooNavigator,
},
auth: {
screen: AuthNavigator,
},
person: {
path: 'people/:id',
screen: PersonScreen,
},
foo: {
path: 'fo/:fooThing',
screen: FooNavigator,
},
});
test('Handles empty URIs with empty action', () => {
const router = createTestRouter(
{
Foo: {
screen: () => <div />,
},
Bar: {
screen: () => <div />,
},
},
{ initialRouteName: 'Bar', initialRouteParams: { foo: 42 } }
);
const action = router.getActionForPathAndParams('');
expect(action).toEqual(null);
const state = router.getStateForAction(action || NavigationActions.init());
expect(state.routes[state.index]).toEqual(
expect.objectContaining({
routeName: 'Bar',
params: { foo: 42 },
})
);
});
test('Handles paths with several params', () => {
const router = createTestRouter({
Person: {
path: 'people/:person',
screen: () => <div />,
},
Task: {
path: 'people/:person/tasks/:task',
screen: () => <div />,
},
ThingA: {
path: 'things/:good',
screen: () => <div />,
},
Thing: {
path: 'things/:good/:thing',
screen: () => <div />,
},
});
const action = router.getActionForPathAndParams(
'people/brent/tasks/everything'
);
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Task',
params: { person: 'brent', task: 'everything' },
});
const action1 = router.getActionForPathAndParams('people/lucy');
expect(action1).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Person',
params: { person: 'lucy' },
});
const action2 = router.getActionForPathAndParams('things/foo/bar');
expect(action2).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Thing',
params: { good: 'foo', thing: 'bar' },
});
const action3 = router.getActionForPathAndParams('things/foo');
expect(action3).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'ThingA',
params: { good: 'foo' },
});
});
test('Handles empty path configuration', () => {
const router = createTestRouter({
Foo: {
screen: () => <div />,
},
Bar: {
screen: () => <div />,
path: '',
},
});
const action = router.getActionForPathAndParams('');
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
params: {},
});
});
test('Handles wildcard path configuration', () => {
const router = createTestRouter({
Foo: {
screen: () => <div />,
},
Bar: {
screen: () => <div />,
path: ':something',
},
});
const action = router.getActionForPathAndParams('');
expect(action).toEqual(null);
const action1 = router.getActionForPathAndParams('Foo');
expect(action1).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
params: {},
});
const action2 = router.getActionForPathAndParams('asdf');
expect(action2).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
params: { something: 'asdf' },
});
});
test('Null path behavior', () => {
const ScreenA = () => <div />;
const router = createTestRouter({
Bar: {
screen: ScreenA,
},
Foo: {
path: null,
screen: ScreenA,
},
Baz: {
path: '',
screen: ScreenA,
},
});
const action0 = router.getActionForPathAndParams('test/random', {});
expect(action0).toBe(null);
const action1 = router.getActionForPathAndParams('', {});
expect(action1.routeName).toBe('Baz');
const state1 = router.getStateForAction(action1);
expect(state1.routes[state1.index].routeName).toBe('Baz');
});
test('Multiple null path sub routers path behavior', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
ScreenB.router = createTestRouter({
Foo: ScreenA,
});
const ScreenC = () => <div />;
ScreenC.router = createTestRouter({
Bar: {
path: 'bar/:id',
screen: ScreenA,
},
Empty: {
path: '',
screen: ScreenA,
},
});
const router = createTestRouter({
A: {
screen: ScreenA,
},
B: {
path: null,
screen: ScreenB,
},
C: {
path: null,
screen: ScreenC,
},
});
const action0 = router.getActionForPathAndParams('Foo', {});
expect(action0.routeName).toBe('B');
expect(action0.action.routeName).toBe('Foo');
const action1 = router.getActionForPathAndParams('', {});
expect(action1.routeName).toBe('C');
expect(action1.action.routeName).toBe('Empty');
const action2 = router.getActionForPathAndParams('A', {});
expect(action2.routeName).toBe('A');
const action3 = router.getActionForPathAndParams('bar/asdf', {});
expect(action3.routeName).toBe('C');
expect(action3.action.routeName).toBe('Bar');
expect(action3.action.params.id).toBe('asdf');
});
test('Null and empty string path sub routers behavior', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
ScreenB.router = createTestRouter({
Foo: ScreenA,
Baz: {
screen: ScreenA,
path: '',
},
});
const ScreenC = () => <div />;
ScreenC.router = createTestRouter({
Boo: ScreenA,
Bar: ScreenA,
Baz: {
screen: ScreenA,
path: '',
},
});
const router = createTestRouter({
B: {
path: null,
screen: ScreenB,
},
C: {
path: '',
screen: ScreenC,
},
});
const action0 = router.getActionForPathAndParams('', {});
expect(action0.routeName).toBe('C');
expect(action0.action.routeName).toBe('Baz');
const action1 = router.getActionForPathAndParams('Foo', {});
expect(action1.routeName).toBe('B');
expect(action1.action.routeName).toBe('Foo');
const action2 = router.getActionForPathAndParams('Bar', {});
expect(action2.routeName).toBe('C');
expect(action2.action.routeName).toBe('Bar');
const action3 = router.getActionForPathAndParams('unknown', {});
expect(action3).toBe(null);
});
test('Empty path acts as wildcard for nested router', () => {
const ScreenA = () => <div />;
const Foo = () => <div />;
const ScreenC = () => <div />;
ScreenC.router = createTestRouter({
Boo: ScreenA,
Bar: ScreenA,
});
Foo.router = createTestRouter({
Quo: ScreenA,
Qux: {
screen: ScreenC,
path: '',
},
});
const router = createTestRouter({
Bar: {
screen: ScreenA,
},
Foo,
});
const action0 = router.getActionForPathAndParams('Foo/Bar', {});
expect(action0.routeName).toBe('Foo');
expect(action0.action.routeName).toBe('Qux');
expect(action0.action.action.routeName).toBe('Bar');
});
test('Gets deep path with pure wildcard match', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const ScreenC = () => <div />;
ScreenA.router = createTestRouter({
Boo: { path: 'boo', screen: ScreenC },
Baz: { path: 'baz/:bazId', screen: ScreenB },
});
ScreenC.router = createTestRouter({
Boo2: { path: '', screen: ScreenB },
});
const router = createTestRouter({
Foo: {
path: null,
screen: ScreenA,
},
Bar: {
screen: ScreenB,
},
});
{
const state = {
index: 0,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
params: {
id: '123',
},
routes: [
{
index: 0,
key: 'Boo',
routeName: 'Boo',
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
},
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
],
},
{ key: 'Bar', routeName: 'Bar' },
],
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('baz/321');
expect(params.id).toEqual('123');
}
{
const state = {
index: 0,
routes: [
{
index: 0,
key: 'Foo',
routeName: 'Foo',
params: {
id: '123',
},
routes: [
{
index: 0,
key: 'Boo',
routeName: 'Boo',
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
},
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
],
},
{ key: 'Bar', routeName: 'Bar' },
],
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('boo');
expect(params).toEqual({ id: '123' });
}
});
test('URI encoded string get passed to deep link', () => {
const uri = 'people/2018%2F02%2F07';
const action = testRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
routeName: 'person',
params: {
id: '2018/02/07',
},
type: NavigationActions.NAVIGATE,
});
const malformedUri = 'people/%E0%A4%A';
const action2 = testRouter.getActionForPathAndParams(malformedUri);
expect(action2).toEqual({
routeName: 'person',
params: {
id: '%E0%A4%A',
},
type: NavigationActions.NAVIGATE,
});
});
test('URI encoded path param gets parsed and correctly printed', () => {
const router = createTestRouter({
main: {
screen: () => <div />,
},
person: {
path: 'people/:name',
screen: () => <div />,
},
});
const action = testRouter.getActionForPathAndParams('people/Henry%20L');
expect(action).toEqual({
routeName: 'person',
params: {
id: 'Henry L',
},
type: NavigationActions.NAVIGATE,
});
const s = testRouter.getStateForAction(action);
const out = testRouter.getPathAndParamsForState(s);
expect(out.path).toEqual('people/Henry%20L');
expect(out.params).toEqual({});
});
test('Querystring params get passed to nested deep link', () => {
const action = testRouter.getActionForPathAndParams(
'main/p/4/list/10259959195',
{ code: 'test', foo: 'bar' }
);
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: 'test',
foo: 'bar',
},
},
},
});
const action2 = testRouter.getActionForPathAndParams(
'main/p/4/list/10259959195',
{ code: '', foo: 'bar' }
);
expect(action2).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: '',
foo: 'bar',
},
},
},
});
});
test('paths option on router overrides path from route config', () => {
const router = createTestRouter(
{
main: {
screen: MainNavigator,
},
baz: {
path: null,
screen: FooNavigator,
},
},
{ paths: { baz: 'overridden' } }
);
const action = router.getActionForPathAndParams('overridden', {});
expect(action.type).toEqual(NavigationActions.NAVIGATE);
expect(action.routeName).toEqual('baz');
});
test('paths option set as null on router overrides path from route config', () => {
const router = createTestRouter(
{
main: {
screen: MainNavigator,
},
baz: {
path: 'bazPath',
screen: FooNavigator,
},
},
{ paths: { baz: null } }
);
const action = router.getActionForPathAndParams('b/noBaz', {});
expect(action.type).toEqual(NavigationActions.NAVIGATE);
expect(action.routeName).toEqual('baz');
});
};
describe('Path handling for stack router', () => {
performRouterTest(StackRouter);
});
describe('Path handling for switch router', () => {
performRouterTest(SwitchRouter);
});
test('Handles nested switch routers', () => {
const AScreen = () => <div />;
const DocsNavigator = () => <div />;
DocsNavigator.router = SwitchRouter({
A: AScreen,
B: AScreen,
C: AScreen,
});
DocsNavigator.path = 'docs';
const router = SwitchRouter({
Docs: DocsNavigator,
D: AScreen,
});
const action = router.getActionForPathAndParams('docs/B', {});
expect(action.type).toEqual(NavigationActions.NAVIGATE);
expect(action.routeName).toEqual('Docs');
expect(action.action.type).toEqual(NavigationActions.NAVIGATE);
expect(action.action.routeName).toEqual('B');
});

View File

@@ -208,6 +208,7 @@ describe('StackRouter', () => {
expect(AuthNavigator.router.getActionForPathAndParams('login')).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'login',
params: {},
});
});
@@ -223,7 +224,10 @@ describe('StackRouter', () => {
test('Parses paths with a query', () => {
expect(
TestStackRouter.getActionForPathAndParams('people/foo?code=test&foo=bar')
TestStackRouter.getActionForPathAndParams('people/foo', {
code: 'test',
foo: 'bar',
})
).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'person',
@@ -237,7 +241,10 @@ describe('StackRouter', () => {
test('Parses paths with an empty query value', () => {
expect(
TestStackRouter.getActionForPathAndParams('people/foo?code=&foo=bar')
TestStackRouter.getActionForPathAndParams('people/foo', {
code: '',
foo: 'bar',
})
).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'person',
@@ -255,9 +262,11 @@ describe('StackRouter', () => {
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'auth',
params: {},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'login',
params: {},
},
});
});
@@ -268,6 +277,7 @@ describe('StackRouter', () => {
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
@@ -291,6 +301,7 @@ describe('StackRouter', () => {
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'baz',
params: {},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'bar',
@@ -308,14 +319,16 @@ describe('StackRouter', () => {
});
test('Correctly returns action chain for partially matched path', () => {
const uri = 'auth/login/2';
const uri = 'auth/login';
const action = TestStackRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'auth',
params: {},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'login',
params: {},
},
});
});
@@ -692,6 +705,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);
@@ -956,6 +970,77 @@ describe('StackRouter', () => {
expect(replacedState2.routes[0].routeName).toEqual('bar');
});
test('Replace action returns most recent route if no key is provided', () => {
const GrandChildNavigator = () => <div />;
GrandChildNavigator.router = StackRouter({
Quux: { screen: () => <div /> },
Corge: { screen: () => <div /> },
Grault: { screen: () => <div /> },
});
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Woo: { screen: () => <div /> },
Qux: { screen: GrandChildNavigator },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const state3 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Qux',
},
state2
);
const state4 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Corge',
},
state3
);
const state5 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Grault',
},
state4
);
const replacedState = router.getStateForAction(
StackActions.replace({
routeName: 'Woo',
params: { meaning: 42 },
}),
state5
);
const originalCurrentScreen = state5.routes[1].routes[1].routes[2];
const replacedCurrentScreen = replacedState.routes[1].routes[1].routes[2];
expect(replacedState.routes[1].routes[1].index).toEqual(2);
expect(replacedState.routes[1].routes[1].routes.length).toEqual(3);
expect(replacedCurrentScreen.key).not.toEqual(originalCurrentScreen.key);
expect(replacedCurrentScreen.routeName).not.toEqual(
originalCurrentScreen.routeName
);
expect(replacedCurrentScreen.routeName).toEqual('Woo');
expect(replacedCurrentScreen.params.meaning).toEqual(42);
});
test('Handles push transition logic with completion action', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -988,6 +1073,43 @@ describe('StackRouter', () => {
expect(state3 && state3.isTransitioning).toEqual(false);
});
test('Back action parent is prioritized over inactive child routers', () => {
const Bar = () => <div />;
Bar.router = StackRouter({
baz: { screen: () => <div /> },
qux: { screen: () => <div /> },
});
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: Bar },
boo: { screen: () => <div /> },
});
const state = {
key: 'top',
index: 3,
routes: [
{ routeName: 'foo', key: 'f' },
{
routeName: 'bar',
key: 'b',
index: 1,
routes: [
{ routeName: 'baz', key: 'bz' },
{ routeName: 'qux', key: 'bx' },
],
},
{ routeName: 'foo', key: 'f1' },
{ routeName: 'boo', key: 'z' },
],
};
const testState = TestRouter.getStateForAction(
{ type: NavigationActions.BACK },
state
);
expect(testState.index).toBe(2);
expect(testState.routes[1].index).toBe(1);
});
test('Handle basic stack logic for components with router', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -1046,6 +1168,47 @@ describe('StackRouter', () => {
});
});
test('Gets deep path (stack behavior)', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
ScreenA.router = StackRouter({
Boo: { path: 'boo', screen: ScreenB },
Baz: { path: 'baz/:bazId', screen: ScreenB },
});
const router = StackRouter({
Foo: {
path: 'f/:id',
screen: ScreenA,
},
Bar: {
screen: ScreenB,
},
});
const state = {
index: 0,
isTransitioning: false,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
params: {
id: '123',
},
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
],
},
{ key: 'Bar', routeName: 'Bar' },
],
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('f/123/baz/321');
expect(params).toEqual({});
});
test('Handle goBack identified by key', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -1633,400 +1796,164 @@ describe('StackRouter', () => {
});
});
test('Handles empty URIs', () => {
const router = StackRouter(
{
Foo: {
screen: () => <div />,
},
Bar: {
screen: () => <div />,
},
},
{ initialRouteName: 'Bar' }
);
const action = router.getActionForPathAndParams('');
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
test('Handles deep navigate completion action', () => {
const LeafScreen = () => <div />;
const FooScreen = () => <div />;
FooScreen.router = StackRouter({
Boo: { path: 'boo', screen: LeafScreen },
Baz: { path: 'baz/:bazId', screen: LeafScreen },
});
let state = null;
if (action) {
state = router.getStateForAction(action);
}
const router = StackRouter({
Foo: {
screen: FooScreen,
},
Bar: {
screen: LeafScreen,
},
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state && state.index).toEqual(0);
expect(state && state.routes[0]).toEqual(
expect.objectContaining({
routeName: 'Bar',
})
expect(state && state.routes[0].routeName).toEqual('Foo');
const key = state && state.routes[0].key;
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
expect(state2 && state2.routes[0].index).toEqual(1);
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(!!key).toEqual(true);
const state3 = router.getStateForAction(
{
type: StackActions.COMPLETE_TRANSITION,
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
expect(state3 && state3.routes[0].index).toEqual(1);
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
});
test('Gets deep path', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
ScreenA.router = StackRouter({
Boo: { path: 'boo', screen: ScreenB },
Baz: { path: 'baz/:bazId', screen: ScreenB },
});
const router = StackRouter({
Foo: {
path: 'f/:id',
screen: ScreenA,
},
Bar: {
screen: ScreenB,
},
});
const state = {
index: 0,
isTransitioning: false,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
params: {
id: '123',
},
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
],
},
{ key: 'Bar', routeName: 'Bar' },
],
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('f/123/baz/321');
expect(params.id).toEqual('123');
expect(params.bazId).toEqual('321');
});
test('Gets deep path with pure wildcard match', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const ScreenC = () => <div />;
ScreenA.router = StackRouter({
Boo: { path: 'boo', screen: ScreenC },
Baz: { path: 'baz/:bazId', screen: ScreenB },
});
ScreenC.router = StackRouter({
Boo2: { path: '', screen: ScreenB },
});
const router = StackRouter({
Foo: {
path: null,
screen: ScreenA,
},
Bar: {
screen: ScreenB,
},
});
{
const state = {
index: 0,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
params: {
id: '123',
},
routes: [
{
index: 0,
key: 'Boo',
routeName: 'Boo',
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
},
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
],
},
{ key: 'Bar', routeName: 'Bar' },
],
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('baz/321');
expect(params.id).toEqual('123');
expect(params.bazId).toEqual('321');
}
{
const state = {
index: 0,
routes: [
{
index: 0,
key: 'Foo',
routeName: 'Foo',
params: {
id: '123',
},
routes: [
{
index: 0,
key: 'Boo',
routeName: 'Boo',
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
},
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
],
},
{ key: 'Bar', routeName: 'Bar' },
],
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('boo/');
expect(params).toEqual({ id: '123' });
}
});
test('URI encoded string get passed to deep link', () => {
const uri = 'people/2018%2F02%2F07';
const action = TestStackRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
routeName: 'person',
params: {
id: '2018/02/07',
},
type: NavigationActions.NAVIGATE,
});
const malformedUri = 'people/%E0%A4%A';
const action2 = TestStackRouter.getActionForPathAndParams(malformedUri);
expect(action2).toEqual({
routeName: 'person',
params: {
id: '%E0%A4%A',
},
type: NavigationActions.NAVIGATE,
});
});
test('Querystring params get passed to nested deep link', () => {
// uri with two non-empty query param values
const uri = 'main/p/4/list/10259959195?code=test&foo=bar';
const action = TestStackRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: 'test',
foo: 'bar',
},
},
},
});
// uri with one empty and one non-empty query param value
const uri2 = 'main/p/4/list/10259959195?code=&foo=bar';
const action2 = TestStackRouter.getActionForPathAndParams(uri2);
expect(action2).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: '',
foo: 'bar',
},
},
},
});
});
});
test('Handles deep navigate completion action', () => {
const LeafScreen = () => <div />;
const FooScreen = () => <div />;
FooScreen.router = StackRouter({
Boo: { path: 'boo', screen: LeafScreen },
Baz: { path: 'baz/:bazId', screen: LeafScreen },
});
const router = StackRouter({
Foo: {
screen: FooScreen,
},
Bar: {
screen: LeafScreen,
},
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state && state.index).toEqual(0);
expect(state && state.routes[0].routeName).toEqual('Foo');
const key = state && state.routes[0].key;
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
expect(state2 && state2.routes[0].index).toEqual(1);
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(!!key).toEqual(true);
const state3 = router.getStateForAction(
{
type: StackActions.COMPLETE_TRANSITION,
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
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,
test('order of handling navigate action is correct for nested stackrouters', () => {
const Screen = () => <div />;
const NestedStack = () => <div />;
let nestedRouter = StackRouter({
Foo: Screen,
Bar: Screen,
Baz: Screen,
},
{
initialRouteName: 'Baz',
}
);
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routes[state.index].routeName).toEqual('Baz');
NestedStack.router = nestedRouter;
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
expect(state2.routes[state2.index].routeName).toEqual('Bar');
let router = StackRouter(
{
NestedStack,
Bar: Screen,
Baz: Screen,
},
{
initialRouteName: 'Baz',
}
);
const state3 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state2
);
expect(state3.routes[state3.index].routeName).toEqual('Baz');
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state.routes[state.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 state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
expect(state2.routes[state2.index].routeName).toEqual('Bar');
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');
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

@@ -78,56 +78,6 @@ describe('SwitchRouter', () => {
expect(state3.index).toEqual(0);
});
test('paths option on SwitchRouter overrides path from route config', () => {
const router = getExampleRouter({ paths: { A: 'overridden' } });
const action = router.getActionForPathAndParams('overridden', {});
expect(action.type).toEqual(NavigationActions.NAVIGATE);
expect(action.routeName).toEqual('A');
});
test('provides correct action for getActionForPathAndParams', () => {
const router = getExampleRouter({ backBehavior: 'initialRoute' });
const action = router.getActionForPathAndParams('A1', { foo: 'bar' });
expect(action.type).toEqual(NavigationActions.NAVIGATE);
expect(action.routeName).toEqual('A1');
const action1 = router.getActionForPathAndParams('', {});
expect(action1.type).toEqual(NavigationActions.NAVIGATE);
expect(action1.routeName).toEqual('A');
const action2 = router.getActionForPathAndParams(null, {});
expect(action2.type).toEqual(NavigationActions.NAVIGATE);
expect(action2.routeName).toEqual('A');
const action3 = router.getActionForPathAndParams('great/path', {
foo: 'baz',
});
expect(action3).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'B',
params: { foo: 'baz' },
action: {
type: NavigationActions.NAVIGATE,
routeName: 'B1',
params: { foo: 'baz' },
},
});
const action4 = router.getActionForPathAndParams('great/path/B2', {
foo: 'baz',
});
expect(action4).toEqual({
type: NavigationActions.NAVIGATE,
routeName: 'B',
params: { foo: 'baz' },
action: {
type: NavigationActions.NAVIGATE,
routeName: 'B2',
params: { foo: 'baz' },
},
});
});
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?

View File

@@ -528,7 +528,7 @@ describe('TabRouter', () => {
});
});
test('Handles path configuration', () => {
test.only('Handles path configuration', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = TabRouter({
@@ -537,14 +537,17 @@ describe('TabRouter', () => {
screen: ScreenA,
},
Bar: {
path: 'b',
path: 'b/:great',
screen: ScreenB,
},
});
const params = { foo: '42' };
const action = router.getActionForPathAndParams('b/anything', params);
const expectedAction = {
params,
params: {
foo: '42',
great: 'anything',
},
routeName: 'Bar',
type: NavigationActions.NAVIGATE,
};
@@ -565,15 +568,21 @@ describe('TabRouter', () => {
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar', params },
{ key: 'Foo', routeName: 'Foo', params: undefined },
{
key: 'Bar',
routeName: 'Bar',
params: { foo: '42', great: 'anything' },
},
],
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
expect(router.getPathAndParamsForState(expectedState).path).toEqual('f');
expect(router.getPathAndParamsForState(expectedState2).path).toEqual('b');
expect(router.getPathAndParamsForState(expectedState2).path).toEqual(
'b/anything'
);
});
test('Handles default configuration', () => {

View File

@@ -0,0 +1,34 @@
import { urlToPathAndParams } from '../pathUtils';
test('urlToPathAndParams empty', () => {
const { path, params } = urlToPathAndParams('foo://');
expect(path).toBe('');
expect(params).toEqual({});
});
test('urlToPathAndParams empty params', () => {
const { path, params } = urlToPathAndParams('foo://foo/bar/b');
expect(path).toBe('foo/bar/b');
expect(params).toEqual({});
});
test('urlToPathAndParams trailing slash', () => {
const { path, params } = urlToPathAndParams('foo://foo/bar/');
expect(path).toBe('foo/bar');
expect(params).toEqual({});
});
test('urlToPathAndParams with params', () => {
const { path, params } = urlToPathAndParams('foo://foo/bar?asdf=1&dude=foo');
expect(path).toBe('foo/bar');
expect(params).toEqual({ asdf: '1', dude: 'foo' });
});
test('urlToPathAndParams with custom delimeter', () => {
const { path, params } = urlToPathAndParams(
'https://example.com/foo/bar?asdf=1',
'https://example.com/'
);
expect(path).toBe('foo/bar');
expect(params).toEqual({ asdf: '1' });
});

223
src/routers/pathUtils.js Normal file
View File

@@ -0,0 +1,223 @@
import pathToRegexp, { compile } from 'path-to-regexp';
import NavigationActions from '../NavigationActions';
import invariant from '../utils/invariant';
const queryString = require('query-string');
function isEmpty(obj) {
if (!obj) return true;
for (let key in obj) {
return false;
}
return true;
}
const getParamsFromPath = (inputParams, pathMatch, pathMatchKeys) => {
const params = pathMatch.slice(1).reduce(
// iterate over matched path params
(paramsOut, matchResult, i) => {
const key = pathMatchKeys[i];
if (!key || key.asterisk) {
return paramsOut;
}
const paramName = key.name;
let decodedMatchResult;
try {
decodedMatchResult = decodeURIComponent(matchResult);
} catch (e) {
// ignore `URIError: malformed URI`
}
paramsOut[paramName] = decodedMatchResult || matchResult;
return paramsOut;
},
{
// start with the input(query string) params, which will get overridden by path params
...inputParams,
}
);
return params;
};
const getRestOfPath = (pathMatch, pathMatchKeys) => {
const rest = pathMatch[pathMatchKeys.findIndex(k => k.asterisk) + 1];
return rest;
};
export const urlToPathAndParams = (url, uriPrefix) => {
const searchMatch = url.match(/^(.*)\?(.*)$/);
const params = searchMatch ? queryString.parse(searchMatch[2]) : {};
const urlWithoutSearch = searchMatch ? searchMatch[1] : url;
const delimiter = uriPrefix || '://';
let path = urlWithoutSearch.split(delimiter)[1];
if (path === undefined) {
path = urlWithoutSearch;
}
if (path === '/') {
path = '';
}
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
return {
path,
params,
};
};
export const createPathParser = (
childRouters,
routeConfigs,
pathConfigs = {}
) => {
const pathsByRouteNames = {};
let paths = [];
// Build pathsByRouteNames, which includes a regex to match paths for each route. Keep in mind, the regex will pass for the route and all child routes. The code that uses pathsByRouteNames will need to also verify that the child router produces an action, in the case of isPathMatchable false (a null path).
Object.keys(childRouters).forEach(routeName => {
let pathPattern;
// First check for paths on the router, then check the route config
if (pathConfigs[routeName] !== undefined) {
pathPattern = pathConfigs[routeName];
} else {
pathPattern = routeConfigs[routeName].path;
}
if (pathPattern === undefined) {
// If the user hasn't specified a path at all, then we assume the routeName is an appropriate path
pathPattern = routeName;
}
invariant(
pathPattern === null || typeof pathPattern === 'string',
`Route path for ${routeName} must be specified as a string, or null.`
);
// the path may be specified as null, which is similar to empty string because it allows child routers to handle the action, but it will not match empty paths
const isPathMatchable = pathPattern !== null;
// pathPattern is a string with inline params, such as people/:id/*foo
const exactReKeys = [];
const exactRe = isPathMatchable
? pathToRegexp(pathPattern, exactReKeys)
: null;
const extendedPathReKeys = [];
const isWildcard = pathPattern === '' || !isPathMatchable;
const extendedPathRe = pathToRegexp(
isWildcard ? '*' : `${pathPattern}/*`,
extendedPathReKeys
);
pathsByRouteNames[routeName] = {
exactRe,
exactReKeys,
extendedPathRe,
extendedPathReKeys,
isWildcard,
toPath: pathPattern === null ? () => '' : compile(pathPattern),
};
});
paths = Object.entries(pathsByRouteNames);
const getActionForPathAndParams = (pathToResolve = '', inputParams = {}) => {
// Attempt to match `pathToResolve` with a route in this router's routeConfigs, deferring to child routers
let matchedAction = null;
// eslint-disable-next-line no-restricted-syntax
for (const [routeName, path] of paths) {
const { exactRe, exactReKeys, extendedPathRe, extendedPathReKeys } = path;
const childRouter = childRouters[routeName];
const exactMatch = exactRe && exactRe.exec(pathToResolve);
if (exactMatch && exactMatch.length) {
const extendedMatch =
extendedPathRe && extendedPathRe.exec(pathToResolve);
let childAction = null;
if (extendedMatch && childRouter) {
const restOfPath = getRestOfPath(extendedMatch, extendedPathReKeys);
childAction = childRouter.getActionForPathAndParams(
restOfPath,
inputParams
);
}
return NavigationActions.navigate({
routeName,
params: getParamsFromPath(inputParams, exactMatch, exactReKeys),
action: childAction,
});
}
}
// eslint-disable-next-line no-restricted-syntax
for (const [routeName, path] of paths) {
const { extendedPathRe, extendedPathReKeys } = path;
const childRouter = childRouters[routeName];
const extendedMatch =
extendedPathRe && extendedPathRe.exec(pathToResolve);
if (extendedMatch && extendedMatch.length) {
const restOfPath = getRestOfPath(extendedMatch, extendedPathReKeys);
let childAction = null;
if (childRouter) {
childAction = childRouter.getActionForPathAndParams(
restOfPath,
inputParams
);
}
if (!childAction) {
continue;
}
return NavigationActions.navigate({
routeName,
params: getParamsFromPath(
inputParams,
extendedMatch,
extendedPathReKeys
),
action: childAction,
});
}
}
return null;
};
const getPathAndParamsForRoute = route => {
const { routeName, params } = route;
const childRouter = childRouters[routeName];
const { toPath, exactReKeys } = pathsByRouteNames[routeName];
const subPath = toPath(params);
const nonPathParams = {};
if (params) {
Object.keys(params)
.filter(paramName => !exactReKeys.find(k => k.name === paramName))
.forEach(paramName => {
nonPathParams[paramName] = params[paramName];
});
}
if (childRouter) {
// If it has a router it's a navigator.
// If it doesn't have router it's an ordinary React component.
const child = childRouter.getPathAndParamsForState(route);
return {
path: subPath ? `${subPath}/${child.path}` : child.path,
params: child.params
? { ...nonPathParams, ...child.params }
: nonPathParams,
};
}
return {
path: subPath,
params: nonPathParams,
};
};
return { getActionForPathAndParams, getPathAndParamsForRoute };
};
export default {
getParamsFromPath,
createPathParser,
};

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,602 +0,0 @@
import React from 'react';
import {
Animated,
Dimensions,
Image,
Platform,
StyleSheet,
View,
I18nManager,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import withOrientation from '../withOrientation';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad
? 32
: 44
: 56;
};
class Header extends React.PureComponent {
static defaultProps = {
layoutInterpolator: HeaderStyleInterpolator.forLayout,
leftInterpolator: HeaderStyleInterpolator.forLeft,
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
titleInterpolator: HeaderStyleInterpolator.forCenter,
rightInterpolator: HeaderStyleInterpolator.forRight,
};
static get HEIGHT() {
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
}
state = {
widths: {},
};
_getHeaderTitleString(scene) {
const options = scene.descriptor.options;
if (typeof options.headerTitle === 'string') {
return options.headerTitle;
}
return options.title;
}
_getLastScene(scene) {
return this.props.scenes.find(s => s.index === scene.index - 1);
}
_getBackButtonTitleString(scene) {
const lastScene = this._getLastScene(scene);
if (!lastScene) {
return null;
}
const { headerBackTitle } = lastScene.descriptor.options;
if (headerBackTitle || headerBackTitle === null) {
return headerBackTitle;
}
return this._getHeaderTitleString(lastScene);
}
_getTruncatedBackButtonTitle(scene) {
const lastScene = this._getLastScene(scene);
if (!lastScene) {
return null;
}
return lastScene.descriptor.options.headerTruncatedBackTitle;
}
_renderTitleComponent = props => {
const { options } = props.scene.descriptor;
const headerTitle = options.headerTitle;
if (React.isValidElement(headerTitle)) {
return headerTitle;
}
const titleString = this._getHeaderTitleString(props.scene);
const titleStyle = options.headerTitleStyle;
const color = options.headerTintColor;
const allowFontScaling = options.headerTitleAllowFontScaling;
// On iOS, width of left/right components depends on the calculated
// size of the title.
const onLayoutIOS =
Platform.OS === 'ios'
? e => {
this.setState({
widths: {
...this.state.widths,
[props.scene.key]: e.nativeEvent.layout.width,
},
});
}
: undefined;
const RenderedHeaderTitle =
headerTitle && typeof headerTitle !== 'string'
? headerTitle
: HeaderTitle;
return (
<RenderedHeaderTitle
onLayout={onLayoutIOS}
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
style={[color ? { color } : null, titleStyle]}
>
{titleString}
</RenderedHeaderTitle>
);
};
_renderLeftComponent = props => {
const { options } = props.scene.descriptor;
if (
React.isValidElement(options.headerLeft) ||
options.headerLeft === null
) {
return options.headerLeft;
}
if (props.scene.index === 0) {
return;
}
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
props.scene.descriptor.navigation.goBack(props.scene.descriptor.key);
});
};
return (
<RenderedLeftComponent
onPress={goBack}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
backImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderModularLeftComponent = (
props,
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options, navigation } = props.scene.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
navigation.goBack(props.scene.descriptor.key);
});
};
return (
<ModularHeaderBackButton
onPress={goBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
backImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderRightComponent = props => {
const { headerRight } = props.scene.descriptor.options;
return headerRight || null;
};
_renderLeft(props) {
const { options } = props.scene.descriptor;
const { transitionPreset } = this.props;
// On Android, or if we have a custom header left, or if we have a custom back image, we
// do not use the modular header (which is the one that imitates UINavigationController)
if (
transitionPreset !== 'uikit' ||
options.headerBackImage ||
options.headerLeft ||
options.headerLeft === null
) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
} else {
return this._renderModularSubView(
props,
'left',
this._renderModularLeftComponent,
this.props.leftLabelInterpolator,
this.props.leftButtonInterpolator
);
}
}
_renderTitle(props, options) {
const style = {};
const { transitionPreset } = this.props;
if (Platform.OS === 'android') {
if (!options.hasLeftComponent) {
style.left = 0;
}
if (!options.hasRightComponent) {
style.right = 0;
}
} else if (
Platform.OS === 'ios' &&
!options.hasLeftComponent &&
!options.hasRightComponent
) {
style.left = 0;
style.right = 0;
}
return this._renderSubView(
{ ...props, style },
'title',
this._renderTitleComponent,
transitionPreset === 'uikit'
? this.props.titleFromLeftInterpolator
: this.props.titleInterpolator
);
}
_renderRight(props) {
return this._renderSubView(
props,
'right',
this._renderRightComponent,
this.props.rightInterpolator
);
}
_renderModularSubView(
props,
name,
renderer,
labelStyleInterpolator,
buttonStyleInterpolator
) {
const { scene } = props;
const { index, isStale, key } = scene;
// Never render a modular back button on the first screen in a stack.
if (index === 0) {
return;
}
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const ButtonContainer = ({ children }) => (
<Animated.View
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const LabelContainer = ({ children }) => (
<Animated.View
style={[labelStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const subView = renderer(props, ButtonContainer, LabelContainer);
if (subView === null) {
return subView;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<View
key={`${name}_${key}`}
pointerEvents={pointerEvents}
style={[styles.item, styles[name], props.style]}
>
{subView}
</View>
);
}
_renderSubView(props, name, renderer, styleInterpolator) {
const { scene } = props;
const { index, isStale, key } = scene;
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const subView = renderer(props);
if (subView == null) {
return null;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<Animated.View
pointerEvents={pointerEvents}
key={`${name}_${key}`}
style={[
styles.item,
styles[name],
props.style,
styleInterpolator({
// todo: determine if we really need to splat all this.props
...this.props,
...props,
}),
]}
>
{subView}
</Animated.View>
);
}
_renderHeader(props) {
const { options } = props.scene.descriptor;
if (options.header === null) {
return null;
}
const left = this._renderLeft(props);
const right = this._renderRight(props);
const title = this._renderTitle(props, {
hasLeftComponent: !!left,
hasRightComponent: !!right,
});
const { isLandscape, transitionPreset } = this.props;
const wrapperProps = {
style: styles.header,
key: `scene_${props.scene.key}`,
};
if (
options.headerLeft ||
options.headerBackImage ||
Platform.OS !== 'ios' ||
transitionPreset !== 'uikit'
) {
return (
<View {...wrapperProps}>
{title}
{left}
{right}
</View>
);
} else {
return (
<MaskedViewIOS
{...wrapperProps}
maskElement={
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
/>
<View style={styles.iconMaskFillerRect} />
</View>
}
>
{title}
{left}
{right}
</MaskedViewIOS>
);
}
}
render() {
let appBar;
const { mode, scene, isLandscape } = this.props;
if (mode === 'float') {
const scenesByIndex = {};
this.props.scenes.forEach(scene => {
scenesByIndex[scene.index] = scene;
});
const scenesProps = Object.values(scenesByIndex).map(scene => ({
position: this.props.position,
progress: this.props.progress,
scene,
}));
appBar = scenesProps.map(this._renderHeader, this);
} else {
appBar = this._renderHeader({
position: new Animated.Value(this.props.scene.index),
progress: new Animated.Value(0),
scene: this.props.scene,
});
}
const { options } = scene.descriptor;
const { headerStyle = {} } = options;
const headerStyleObj = StyleSheet.flatten(headerStyle);
const appBarHeight = getAppBarHeight(isLandscape);
const {
alignItems,
justifyContent,
flex,
flexDirection,
flexGrow,
flexShrink,
flexBasis,
flexWrap,
...safeHeaderStyle
} = headerStyleObj;
if (__DEV__) {
warnIfHeaderStyleDefined(alignItems, 'alignItems');
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
warnIfHeaderStyleDefined(flex, 'flex');
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
}
// TODO: warn if any unsafe styles are provided
const containerStyles = [
options.headerTransparent
? styles.transparentContainer
: styles.container,
{ height: appBarHeight },
safeHeaderStyle,
];
const { headerForceInset } = options;
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
return (
<Animated.View style={this.props.layoutInterpolator(this.props)}>
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>
{options.headerBackground}
</View>
<View style={styles.flexOne}>{appBar}</View>
</SafeAreaView>
</Animated.View>
);
}
}
function warnIfHeaderStyleDefined(value, styleProp) {
if (value !== undefined) {
console.warn(
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
);
}
}
let platformContainerStyles;
if (Platform.OS === 'ios') {
platformContainerStyles = {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#A7A7AA',
};
} else {
platformContainerStyles = {
shadowColor: 'black',
shadowOpacity: 0.1,
shadowRadius: StyleSheet.hairlineWidth,
shadowOffset: {
height: StyleSheet.hairlineWidth,
},
elevation: 4,
};
}
const styles = StyleSheet.create({
container: {
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
...platformContainerStyles,
},
transparentContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
...platformContainerStyles,
},
header: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
},
item: {
backgroundColor: 'transparent',
},
iconMaskContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
iconMaskFillerRect: {
flex: 1,
backgroundColor: '#d8d8d8',
marginLeft: -3,
},
iconMask: {
// These are mostly the same as the icon in ModularHeaderBackButton
height: 21,
width: 12,
marginLeft: 9,
marginTop: -0.5, // resizes down to 20.5
alignSelf: 'center',
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
title: {
bottom: 0,
top: 0,
left: TITLE_OFFSET,
right: TITLE_OFFSET,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
},
left: {
left: 0,
bottom: 0,
top: 0,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
},
right: {
right: 0,
bottom: 0,
top: 0,
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
},
flexOne: {
flex: 1,
},
});
export default withOrientation(Header);

View File

@@ -1,152 +0,0 @@
import React from 'react';
import {
I18nManager,
Image,
Text,
View,
Platform,
StyleSheet,
} from 'react-native';
import TouchableItem from '../TouchableItem';
const defaultBackImage = require('../assets/back-icon.png');
class HeaderBackButton extends React.PureComponent {
static defaultProps = {
pressColorAndroid: 'rgba(0, 0, 0, .32)',
tintColor: Platform.select({
ios: '#037aff',
}),
truncatedTitle: 'Back',
};
state = {};
_onTextLayout = e => {
if (this.state.initialTextWidth) {
return;
}
this.setState({
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
});
};
_renderBackImage() {
const { backImage, title, tintColor } = this.props;
let BackImage;
let props;
if (React.isValidElement(backImage)) {
return backImage;
} else if (backImage) {
BackImage = backImage;
props = {
tintColor,
title,
};
} else {
BackImage = Image;
props = {
style: [
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
],
source: defaultBackImage,
};
}
return <BackImage {...props} />;
}
render() {
const {
onPress,
pressColorAndroid,
width,
title,
titleStyle,
tintColor,
truncatedTitle,
} = this.props;
const renderTruncated =
this.state.initialTextWidth && width
? this.state.initialTextWidth > width
: false;
const backButtonTitle = renderTruncated ? truncatedTitle : title;
return (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={backButtonTitle}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
pressColor={pressColorAndroid}
style={styles.container}
borderless
>
<View style={styles.container}>
{this._renderBackImage()}
{Platform.OS === 'ios' &&
typeof backButtonTitle === 'string' && (
<Text
onLayout={this._onTextLayout}
style={[
styles.title,
!!tintColor && { color: tintColor },
titleStyle,
]}
numberOfLines={1}
>
{backButtonTitle}
</Text>
)}
</View>
</TouchableItem>
);
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
backgroundColor: 'transparent',
},
title: {
fontSize: 17,
paddingRight: 10,
},
icon:
Platform.OS === 'ios'
? {
height: 21,
width: 13,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
}
: {
height: 24,
width: 24,
margin: 16,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
iconWithTitle:
Platform.OS === 'ios'
? {
marginRight: 6,
}
: {},
});
export default HeaderBackButton;

View File

@@ -1,328 +0,0 @@
import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
function hasHeader(scene) {
if (!scene) {
return true;
}
const { descriptor } = scene;
return descriptor.options.header !== null;
}
const crossFadeInterpolation = (scenes, first, index, last) => ({
inputRange: [
first,
first + 0.001,
index - 0.9,
index - 0.2,
index,
last - 0.001,
last,
],
outputRange: [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0.3 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0 : 1,
0,
],
});
/**
* Utilities that build the style for the navigation header.
*
* +-------------+-------------+-------------+
* | | | |
* | Left | Title | Right |
* | Component | Component | Component |
* | | | |
* +-------------+-------------+-------------+
*/
function isGoingBack(scenes) {
const lastSceneIndexInScenes = scenes.length - 1;
return !scenes[lastSceneIndexInScenes].isActive;
}
function forLayout(props) {
const { layout, position, scene, scenes, mode } = props;
if (mode !== 'float') {
return {};
}
const isBack = isGoingBack(scenes);
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return {};
const { first, last } = interpolate;
const index = scene.index;
const width = layout.initWidth;
// 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],
outputRange: [
rtlMult * (hasHeader(scenes[first]) ? 0 : width),
rtlMult * (hasHeader(scenes[index]) ? 0 : isBack ? width : -width),
rtlMult * (hasHeader(scenes[last]) ? 0 : -width),
],
});
return {
transform: [{ translateX }],
};
}
function forLeft(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(
crossFadeInterpolation(scenes, first, index, last)
),
};
}
function forCenter(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(
crossFadeInterpolation(scenes, first, index, last)
),
};
}
function forRight(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(
crossFadeInterpolation(scenes, first, index, last)
),
};
}
/**
* iOS UINavigationController style interpolators
*/
function forLeftButton(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
// The gist of what we're doing here is animating the left button _normally_ (fast fade)
// when both scenes in transition have headers. When the current, next, or previous scene _don't_
// have a header, we don't fade the button, and only set it's opacity to 0 at the last moment
// of the transition.
const inputRange = [
first,
first + 0.001,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last - 0.001,
last,
];
const outputRange = [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0.1 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0.1 : 1,
hasHeader(scenes[last]) ? 0 : 1,
0,
];
return {
opacity: position.interpolate({
inputRange,
outputRange,
}),
};
}
/*
* 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)
*/
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
function forLeftLabel(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const offset = LEFT_LABEL_OFFSET;
// Similarly to the animation of the left label, when animating to or from a scene without
// a header, we keep the label at full opacity and in the same position for as long as possible.
return {
// For now we fade out the label before fading in the title, so the
// differences between the label and title position can be hopefully not so
// noticable to the user
opacity: position.interpolate({
inputRange: [
first,
first + 0.001,
index - 0.35,
index,
index + 0.5,
last - 0.001,
last,
],
outputRange: [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0.5 : 1,
hasHeader(scenes[last]) ? 0 : 1,
0,
],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, first + 0.001, index, last - 0.001, last],
outputRange: I18nManager.isRTL
? [
-offset * 1.5,
hasHeader(scenes[first]) ? -offset * 1.5 : 0,
0,
hasHeader(scenes[last]) ? offset : 0,
offset,
]
: [
offset,
hasHeader(scenes[first]) ? offset : 0,
0,
hasHeader(scenes[last]) ? -offset * 1.5 : 0,
-offset * 1.5,
],
}),
},
],
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. We want the back button label to transition
* smoothly into the title text and to do this we need to understand
* where the title is positioned within the title container (since it is
* centered).
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
function forCenterFromLeft(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index - 0.5, index, index + 0.5, last];
const offset = TITLE_OFFSET_IOS;
return {
opacity: position.interpolate({
inputRange: [
first,
first + 0.001,
index - 0.5,
index,
index + 0.7,
last - 0.001,
last,
],
outputRange: [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0 : 1,
hasHeader(scenes[last]) ? 0 : 1,
0,
],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, first + 0.001, index, last - 0.001, last],
outputRange: I18nManager.isRTL
? [
-offset,
hasHeader(scenes[first]) ? -offset : 0,
0,
hasHeader(scenes[last]) ? offset : 0,
offset,
]
: [
offset,
hasHeader(scenes[first]) ? offset : 0,
0,
hasHeader(scenes[last]) ? -offset : 0,
-offset,
],
}),
},
],
};
}
export default {
forLayout,
forLeft,
forLeftButton,
forLeftLabel,
forCenterFromLeft,
forCenter,
forRight,
};

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { Text, View, Platform, StyleSheet, Animated } from 'react-native';
const AnimatedText = Animated.Text;
const HeaderTitle = ({ style, ...rest }) => (
<AnimatedText
numberOfLines={1}
{...rest}
style={[styles.title, style]}
accessibilityTraits="header"
/>
);
const styles = StyleSheet.create({
title: {
fontSize: Platform.OS === 'ios' ? 17 : 20,
fontWeight: Platform.OS === 'ios' ? '700' : '500',
color: 'rgba(0, 0, 0, .9)',
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
marginHorizontal: 16,
},
});
export default HeaderTitle;

View File

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

View File

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

View File

@@ -1,8 +1,12 @@
import React from 'react';
import propTypes from 'prop-types';
import createReactContext from 'create-react-context';
const NavigationContext = createReactContext();
export const NavigationProvider = NavigationContext.Provider;
export const NavigationConsumer = NavigationContext.Consumer;
export default {
NavigationProvider,
NavigationConsumer,
};

View File

@@ -0,0 +1,57 @@
import React from 'react';
import withNavigation from './withNavigation';
const EventNameToPropName = {
willFocus: 'onWillFocus',
didFocus: 'onDidFocus',
willBlur: 'onWillBlur',
didBlur: 'onDidBlur',
};
const EventNames = Object.keys(EventNameToPropName);
class NavigationEvents extends React.Component {
componentDidMount() {
this.subscriptions = {};
EventNames.forEach(this.addListener);
}
componentDidUpdate(prevProps) {
EventNames.forEach(eventName => {
const listenerHasChanged =
this.props[EventNameToPropName[eventName]] !==
prevProps[EventNameToPropName[eventName]];
if (listenerHasChanged) {
this.removeListener(eventName);
this.addListener(eventName);
}
});
}
componentWillUnmount() {
EventNames.forEach(this.removeListener);
}
addListener = eventName => {
const listener = this.props[EventNameToPropName[eventName]];
if (listener) {
this.subscriptions[eventName] = this.props.navigation.addListener(
eventName,
listener
);
}
};
removeListener = eventName => {
if (this.subscriptions[eventName]) {
this.subscriptions[eventName].remove();
this.subscriptions[eventName] = undefined;
}
};
render() {
return null;
}
}
export default withNavigation(NavigationEvents);

View File

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

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

@@ -1,199 +0,0 @@
import invariant from '../utils/invariant';
import shallowEqual from '../utils/shallowEqual';
const SCENE_KEY_PREFIX = 'scene_';
/**
* Helper function to compare route keys (e.g. "9", "11").
*/
function compareKey(one, two) {
const delta = one.length - two.length;
if (delta > 0) {
return 1;
}
if (delta < 0) {
return -1;
}
return one > two ? 1 : -1;
}
/**
* Helper function to sort scenes based on their index and view key.
*/
function compareScenes(one, two) {
if (one.index > two.index) {
return 1;
}
if (one.index < two.index) {
return -1;
}
return compareKey(one.key, two.key);
}
/**
* Whether two routes are the same.
*/
function areScenesShallowEqual(one, two) {
return (
one.key === two.key &&
one.index === two.index &&
one.isStale === two.isStale &&
one.isActive === two.isActive &&
areRoutesShallowEqual(one.route, two.route)
);
}
/**
* Whether two routes are the same.
*/
function areRoutesShallowEqual(one, two) {
if (!one || !two) {
return one === two;
}
if (one.key !== two.key) {
return false;
}
return shallowEqual(one, two);
}
export default function ScenesReducer(
scenes,
nextState,
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;
}
const prevScenes = new Map();
const freshScenes = new Map();
const staleScenes = new Map();
// Populate stale scenes from previous scenes marked as stale.
scenes.forEach(scene => {
const { key } = scene;
if (scene.isStale) {
staleScenes.set(key, scene);
}
prevScenes.set(key, scene);
});
const nextKeys = new Set();
nextState.routes.forEach((route, index) => {
const key = SCENE_KEY_PREFIX + route.key;
let descriptor = descriptors && descriptors[route.key];
const scene = {
index,
isActive: false,
isStale: false,
key,
route,
descriptor,
};
invariant(
!nextKeys.has(key),
`navigation.state.routes[${index}].key "${key}" conflicts with ` +
'another route!'
);
nextKeys.add(key);
if (staleScenes.has(key)) {
// A previously `stale` scene is now part of the nextState, so we
// revive it by removing it from the stale scene map.
staleScenes.delete(key);
}
freshScenes.set(key, scene);
});
if (prevState) {
// Look at the previous routes and classify any removed scenes as `stale`.
prevState.routes.forEach((route, index) => {
const key = SCENE_KEY_PREFIX + route.key;
if (freshScenes.has(key)) {
return;
}
const lastScene = scenes.find(scene => scene.route.key === route.key);
const descriptor = lastScene && lastScene.descriptor;
staleScenes.set(key, {
index,
isActive: false,
isStale: true,
key,
route,
descriptor,
});
});
}
const nextScenes = [];
const mergeScene = nextScene => {
const { key } = nextScene;
const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null;
if (prevScene && areScenesShallowEqual(prevScene, nextScene)) {
// Reuse `prevScene` as `scene` so view can avoid unnecessary re-render.
// This assumes that the scene's navigation state is immutable.
nextScenes.push(prevScene);
} else {
nextScenes.push(nextScene);
}
};
staleScenes.forEach(mergeScene);
freshScenes.forEach(mergeScene);
nextScenes.sort(compareScenes);
let activeScenesCount = 0;
nextScenes.forEach((scene, ii) => {
const isActive = !scene.isStale && scene.index === nextState.index;
if (isActive !== scene.isActive) {
nextScenes[ii] = {
...scene,
isActive,
};
}
if (isActive) {
activeScenesCount++;
}
});
invariant(
activeScenesCount === 1,
'there should always be only one scene active, not %s.',
activeScenesCount
);
if (nextScenes.length !== scenes.length) {
return nextScenes;
}
if (
nextScenes.some(
(scene, index) => !areScenesShallowEqual(scenes[index], scene)
)
) {
return nextScenes;
}
// scenes haven't changed.
return scenes;
}

View File

@@ -1,74 +0,0 @@
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';
const NativeAnimatedModule =
NativeModules && NativeModules.NativeAnimatedModule;
class StackView extends React.Component {
static defaultProps = {
navigationConfig: {
mode: 'card',
},
};
render() {
return (
<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 { navigationConfig, navigation } = this.props;
const { onTransitionEnd } = navigationConfig;
if (transition.navigation.state.isTransitioning) {
navigation.dispatch(
StackActions.completeTransition({
key: navigation.state.key,
})
);
}
onTransitionEnd && onTransitionEnd(transition, lastTransition);
}}
/>
);
}
_configureTransition = (transitionProps, prevTransitionProps) => {
return {
...TransitionConfigs.getTransitionConfig(
this.props.navigationConfig.transitionConfig,
transitionProps,
prevTransitionProps,
this.props.navigationConfig.mode === 'modal'
).transitionSpec,
useNativeDriver: !!NativeAnimatedModule,
};
};
_render = (transitionProps, lastTransitionProps) => {
const { screenProps, navigationConfig } = this.props;
return (
<StackViewLayout
{...navigationConfig}
onGestureBegin={this.props.onGestureBegin}
onGestureCanceled={this.props.onGestureCanceled}
onGestureEnd={this.props.onGestureEnd}
screenProps={screenProps}
descriptors={this.props.descriptors}
transitionProps={transitionProps}
lastTransitionProps={lastTransitionProps}
/>
);
};
}
export default StackView;

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { Animated, StyleSheet } from 'react-native';
import createPointerEventsContainer from './createPointerEventsContainer';
/**
* Component that renders the scene as card for the <StackView />.
*/
class Card extends React.Component {
render() {
const { children, pointerEvents, style } = this.props;
return (
<Animated.View
pointerEvents={pointerEvents}
ref={this.props.onComponentRef}
style={[styles.main, style]}
>
{children}
</Animated.View>
);
}
}
const styles = StyleSheet.create({
main: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#E9E9EF',
shadowColor: 'black',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.2,
shadowRadius: 5,
},
});
Card = createPointerEventsContainer(Card);
export default Card;

View File

@@ -1,565 +0,0 @@
import React from 'react';
import clamp from 'clamp';
import {
Animated,
StyleSheet,
PanResponder,
Platform,
View,
I18nManager,
Easing,
Dimensions,
} from 'react-native';
import Card from './StackViewCard';
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';
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);
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
* is always less then the full distance of the layout.
*/
const ANIMATION_DURATION = 500;
/**
* The gesture distance threshold to trigger the back behavior. For instance,
* `1/2` means that moving greater than 1/2 of the width of the screen will
* trigger a back action
*/
const POSITION_THRESHOLD = 1 / 2;
/**
* The threshold (in pixels) to start the gesture action.
*/
const RESPOND_THRESHOLD = 20;
/**
* The distance of touch start from the edge of the screen where the gesture will be recognized
*/
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const animatedSubscribeValue = animatedValue => {
if (!animatedValue.__isNative) {
return;
}
if (Object.keys(animatedValue._listeners).length === 0) {
animatedValue.addListener(emptyFunction);
}
};
class StackViewLayout extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
* be updated according to its relative position. This means that a card can effectively be
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
* corresponding location for the touch.
*/
_gestureStartValue = 0;
// tracks if a touch is currently happening
_isResponding = false;
/**
* immediateIndex is used to represent the expected index that we will be on after a
* transition. To achieve a smooth animation when swiping back, the action to go back
* doesn't actually fire until the transition completes. The immediateIndex is used during
* the transition so that gestures can be handled correctly. This is a work-around for
* cases when the user quickly swipes back several times.
*/
_immediateIndex = null;
_renderHeader(scene, headerMode) {
const { options } = scene.descriptor;
const { header } = options;
if (header === null && headerMode === 'screen') {
return null;
}
// check if it's a react element
if (React.isValidElement(header)) {
return header;
}
// Handle the case where the header option is a function, and provide the default
const renderHeader = header || (props => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
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
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.transitionProps.layout.width);
animatedSubscribeValue(props.transitionProps.layout.height);
animatedSubscribeValue(props.transitionProps.position);
}
_reset(resetToIndex, duration) {
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(this.props.transitionProps.position, {
toValue: resetToIndex,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
} else {
Animated.timing(this.props.transitionProps.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
}
}
_goBack(backFromIndex, duration) {
const { navigation, position, scenes } = this.props.transitionProps;
const toValue = Math.max(backFromIndex - 1, 0);
// set temporary index for gesture handler to respect until the action is
// dispatched at the end of the transition.
this._immediateIndex = toValue;
const onCompleteAnimation = () => {
this._immediateIndex = null;
const backFromScene = scenes.find(s => s.index === toValue + 1);
if (!this._isResponding && backFromScene) {
navigation.dispatch(
NavigationActions.back({
key: backFromScene.route.key,
immediate: true,
})
);
navigation.dispatch(StackActions.completeTransition());
}
};
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(position, {
toValue,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
} else {
Animated.timing(position, {
toValue,
duration,
easing: EaseInOut,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
}
}
_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: number) => {
this._isResponding = true;
this._gestureStartValue = value;
});
this.props.onGestureBegin && this.props.onGestureBegin();
},
onMoveShouldSetPanResponder: (event, gesture) => {
const {
transitionProps: { navigation, position, layout, scene, scenes },
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);
}
});
},
});
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
const { scene } = this.props.transitionProps;
floatingHeader = this._renderHeader(scene, headerMode);
}
const {
transitionProps: { navigation, position, layout, scene, scenes },
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;
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const responder = !gesturesEnabled ? null : this._panResponder;
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map(s => this._renderCard(s))}
</View>
{floatingHeader}
</View>
);
}
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
_renderInnerScene(scene) {
const { options, navigation, getComponent } = scene.descriptor;
const SceneComponent = getComponent();
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={styles.scenes}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(scene, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
this.props.transitionProps,
this.props.prevTransitionProps,
isModal
);
};
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
// If this screen has "header" set to `null` in it's navigation options, but
// it exists in a stack with headerMode float, add a negative margin to
// compensate for the hidden header
const { options } = scene.descriptor;
const hasHeader = options.header !== null;
const headerMode = this._getHeaderMode();
let marginTop = 0;
if (!hasHeader && headerMode === 'float') {
const { isLandscape } = this.props;
let headerHeight;
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
headerHeight = 52;
} else if (IS_IPHONE_X) {
headerHeight = 88;
} else {
headerHeight = 64;
}
} else {
headerHeight = 56;
// TODO (Android only): Need to handle translucent status bar.
}
marginTop = -headerHeight;
}
return (
<Card
{...this.props.transitionProps}
key={`card_${scene.key}`}
style={[style, { marginTop }, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(scene)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
export default withOrientation(StackViewLayout);

View File

@@ -1,167 +0,0 @@
import { I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
/**
* Utility that builds the style for the card in the cards stack.
*
* +------------+
* +-+ |
* +-+ | |
* | | | |
* | | | Focused |
* | | | Card |
* | | | |
* +-+ | |
* +-+ |
* +------------+
*/
/**
* Render the initial style when the initial layout isn't measured yet.
*/
function forInitial(props) {
const { navigation, scene } = props;
const focused = navigation.state.index === scene.index;
const opacity = focused ? 1 : 0;
// If not focused, move the scene far away.
const translate = focused ? 0 : 1000000;
return {
opacity,
transform: [{ translateX: translate }, { translateY: translate }],
};
}
/**
* Standard iOS-style slide in from the right.
*/
function forHorizontal(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = position.interpolate({
inputRange: [first, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
});
const width = layout.initWidth;
const translateX = position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-width, 0, width * 0.3]
: [width, 0, width * -0.3],
});
const translateY = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* Standard iOS-style slide in from the bottom (used for modals).
*/
function forVertical(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = position.interpolate({
inputRange: [first, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
});
const height = layout.initHeight;
const translateY = position.interpolate({
inputRange: [first, index, last],
outputRange: [height, 0, 0],
});
const translateX = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* Standard Android-style fade in from the bottom.
*/
function forFadeFromBottomAndroid(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index, last - 0.01, last];
const opacity = position.interpolate({
inputRange,
outputRange: [0, 1, 1, 0],
});
const translateY = position.interpolate({
inputRange,
outputRange: [50, 0, 0, 0],
});
const translateX = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* fadeIn and fadeOut
*/
function forFade(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = position.interpolate({
inputRange: [first, index, last],
outputRange: [0, 1, 1],
});
return {
opacity,
};
}
export default {
forHorizontal,
forVertical,
forFadeFromBottomAndroid,
forFade,
};

View File

@@ -1,110 +0,0 @@
import { Animated, Easing, Platform } from 'react-native';
import StyleInterpolator from './StackViewStyleInterpolator';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
let IOSTransitionSpec;
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
// These are the exact values from UINavigationController's animation configuration
IOSTransitionSpec = {
timing: Animated.spring,
stiffness: 1000,
damping: 500,
mass: 3,
};
} else {
// This is an approximation of the IOS spring animation using a derived bezier curve
IOSTransitionSpec = {
duration: 500,
easing: Easing.bezier(0.2833, 0.99, 0.31833, 0.99),
timing: Animated.timing,
};
}
// Standard iOS navigation transition
const SlideFromRightIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: StyleInterpolator.forHorizontal,
containerStyle: {
backgroundColor: '#000',
},
};
// Standard iOS navigation transition for modals
const ModalSlideFromBottomIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: StyleInterpolator.forVertical,
containerStyle: {
backgroundColor: '#000',
},
};
// Standard Android navigation transition when opening an Activity
const FadeInFromBottomAndroid = {
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
transitionSpec: {
duration: 350,
easing: Easing.out(Easing.poly(5)), // decelerate
timing: Animated.timing,
},
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
// Standard Android navigation transition when closing an Activity
const FadeOutToBottomAndroid = {
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
transitionSpec: {
duration: 230,
easing: Easing.in(Easing.poly(4)), // accelerate
timing: Animated.timing,
},
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
function defaultTransitionConfig(
transitionProps,
prevTransitionProps,
isModal
) {
if (Platform.OS === 'android') {
// Use the default Android animation no matter if the screen is a modal.
// Android doesn't have full-screen modals like iOS does, it has dialogs.
if (
prevTransitionProps &&
transitionProps.index < prevTransitionProps.index
) {
// Navigating back to the previous screen
return FadeOutToBottomAndroid;
}
return FadeInFromBottomAndroid;
}
// iOS and other platforms
if (isModal) {
return ModalSlideFromBottomIOS;
}
return SlideFromRightIOS;
}
function getTransitionConfig(
transitionConfigurer,
transitionProps,
prevTransitionProps,
isModal
) {
const defaultConfig = defaultTransitionConfig(
transitionProps,
prevTransitionProps,
isModal
);
if (transitionConfigurer) {
return {
...defaultConfig,
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
};
}
return defaultConfig;
}
export default {
defaultTransitionConfig,
getTransitionConfig,
};

View File

@@ -1,84 +0,0 @@
import React from 'react';
import invariant from '../../utils/invariant';
import AnimatedValueSubscription from '../AnimatedValueSubscription';
const MIN_POSITION_OFFSET = 0.01;
/**
* Create a higher-order component that automatically computes the
* `pointerEvents` property for a component whenever navigation position
* changes.
*/
export default function createPointerEventsContainer(Component) {
class Container extends React.Component {
constructor(props, context) {
super(props, context);
this._pointerEvents = this._computePointerEvents();
}
componentWillUnmount() {
this._positionListener && this._positionListener.remove();
}
render() {
this._bindPosition();
this._pointerEvents = this._computePointerEvents();
return (
<Component
{...this.props}
pointerEvents={this._pointerEvents}
onComponentRef={this._onComponentRef}
/>
);
}
_onComponentRef = component => {
this._component = component;
if (component) {
invariant(
typeof component.setNativeProps === 'function',
'component must implement method `setNativeProps`'
);
}
};
_bindPosition() {
this._positionListener && this._positionListener.remove();
this._positionListener = new AnimatedValueSubscription(
this.props.position,
this._onPositionChange
);
}
_onPositionChange = () => {
if (this._component) {
const pointerEvents = this._computePointerEvents();
if (this._pointerEvents !== pointerEvents) {
this._pointerEvents = pointerEvents;
this._component.setNativeProps({ pointerEvents });
}
}
};
_computePointerEvents() {
const { navigation, position, scene } = this.props;
if (scene.isStale || navigation.state.index !== scene.index) {
// The scene isn't focused.
return scene.index > navigation.state.index ? 'box-only' : 'none';
}
const offset = position.__getAnimatedValue() - navigation.state.index;
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
// The positon is still away from scene's index.
// Scene's children should not receive touches until the position
// is close enough to scene's index.
return 'box-only';
}
return 'auto';
}
}
return Container;
}

View File

@@ -1,286 +0,0 @@
import React from 'react';
import { Animated, Easing, StyleSheet, View } from 'react-native';
import invariant from '../utils/invariant';
import NavigationScenesReducer from './ScenesReducer';
// Used for all animations unless overriden
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
timing: Animated.timing,
};
class Transitioner extends React.Component {
constructor(props, context) {
super(props, context);
// The initial layout isn't measured. Measured layout will be only available
// when the component is mounted.
const layout = {
height: new Animated.Value(0),
initHeight: 0,
initWidth: 0,
isMeasured: false,
width: new Animated.Value(0),
};
this.state = {
layout,
position: new Animated.Value(this.props.navigation.state.index),
progress: new Animated.Value(1),
scenes: NavigationScenesReducer(
[],
this.props.navigation.state,
null,
this.props.descriptors
),
};
this._prevTransitionProps = null;
this._transitionProps = buildTransitionProps(props, this.state);
this._isMounted = false;
this._isTransitionRunning = false;
this._queuedTransition = null;
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
componentWillReceiveProps(nextProps) {
let nextScenes = NavigationScenesReducer(
this.state.scenes,
nextProps.navigation.state,
this.props.navigation.state,
nextProps.descriptors
);
if (!nextProps.navigation.state.isTransitioning) {
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;
}
const indexHasChanged =
nextProps.navigation.state.index !== this.props.navigation.state.index;
if (this._isTransitionRunning) {
this._queuedTransition = { nextProps, nextScenes, indexHasChanged };
return;
}
this._startTransition(nextProps, nextScenes, indexHasChanged);
}
_startTransition(nextProps, nextScenes, indexHasChanged) {
const nextState = {
...this.state,
scenes: nextScenes,
};
const { position, progress } = nextState;
progress.setValue(0);
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(
this._transitionProps,
this._prevTransitionProps
)
: null;
const transitionSpec = {
...DefaultTransitionSpec,
...transitionUserSpec,
};
const { timing } = transitionSpec;
delete transitionSpec.timing;
const positionHasChanged = position.__getValue() !== toValue;
// if swiped back, indexHasChanged == true && positionHasChanged == false
const animations =
indexHasChanged && positionHasChanged
? [
timing(progress, {
...transitionSpec,
toValue: 1,
}),
timing(position, {
...transitionSpec,
toValue: nextProps.navigation.state.index,
}),
]
: [];
// update scenes and play the transition
this._isTransitionRunning = true;
this.setState(nextState, async () => {
if (nextProps.onTransitionStart) {
const result = nextProps.onTransitionStart(
this._transitionProps,
this._prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
}
Animated.parallel(animations).start(this._onTransitionEnd);
});
}
render() {
return (
<View onLayout={this._onLayout} style={[styles.main]}>
{this.props.render(this._transitionProps, this._prevTransitionProps)}
</View>
);
}
_onLayout = event => {
const { height, width } = event.nativeEvent.layout;
if (
this.state.layout.initWidth === width &&
this.state.layout.initHeight === height
) {
return;
}
const layout = {
...this.state.layout,
initHeight: height,
initWidth: width,
isMeasured: true,
};
layout.height.setValue(height);
layout.width.setValue(width);
const nextState = {
...this.state,
layout,
};
this._transitionProps = buildTransitionProps(this.props, nextState);
this.setState(nextState);
};
_onTransitionEnd = () => {
if (!this._isMounted) {
return;
}
const prevTransitionProps = this._prevTransitionProps;
this._prevTransitionProps = null;
const scenes = filterStale(this.state.scenes);
const nextState = {
...this.state,
scenes,
};
this._transitionProps = buildTransitionProps(this.props, nextState);
this.setState(nextState, async () => {
if (this.props.onTransitionEnd) {
const result = this.props.onTransitionEnd(
this._transitionProps,
prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
}
if (this._queuedTransition) {
this._startTransition(
this._queuedTransition.nextProps,
this._queuedTransition.nextScenes,
this._queuedTransition.indexHasChanged
);
this._queuedTransition = null;
} else {
this._isTransitionRunning = false;
}
});
};
}
function buildTransitionProps(props, state) {
const { navigation } = props;
const { layout, position, progress, scenes } = state;
const scene = scenes.find(isSceneActive);
invariant(scene, 'Could not find active scene');
return {
layout,
navigation,
position,
progress,
scenes,
scene,
index: scene.index,
};
}
function isSceneNotStale(scene) {
return !scene.isStale;
}
function filterStale(scenes) {
const filtered = scenes.filter(isSceneNotStale);
if (filtered.length === scenes.length) {
return scenes;
}
return filtered;
}
function isSceneActive(scene) {
return scene.isActive;
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
export default Transitioner;

View File

@@ -0,0 +1,241 @@
import React from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import NavigationEvents from '../NavigationEvents';
import { NavigationProvider } from '../NavigationContext';
const createListener = () => payload => {};
// An easy way to create the 4 listeners prop
const createEventListenersProp = () => ({
onWillFocus: createListener(),
onDidFocus: createListener(),
onWillBlur: createListener(),
onDidBlur: createListener(),
});
const createNavigationAndHelpers = () => {
// A little API to spy on subscription remove calls that are performed during the tests
const removeCallsAPI = (() => {
let removeCalls = [];
return {
reset: () => {
removeCalls = [];
},
add: (name, handler) => {
removeCalls.push({ name, handler });
},
checkRemoveCalled: count => {
expect(removeCalls.length).toBe(count);
},
checkRemoveCalledWith: (name, handler) => {
expect(removeCalls).toContainEqual({ name, handler });
},
};
})();
const navigation = {
addListener: jest.fn((name, handler) => {
return {
remove: () => removeCallsAPI.add(name, handler),
};
}),
};
const checkAddListenerCalled = count => {
expect(navigation.addListener).toHaveBeenCalledTimes(count);
};
const checkAddListenerCalledWith = (eventName, handler) => {
expect(navigation.addListener).toHaveBeenCalledWith(eventName, handler);
};
const checkRemoveCalled = count => {
removeCallsAPI.checkRemoveCalled(count);
};
const checkRemoveCalledWith = (eventName, handler) => {
removeCallsAPI.checkRemoveCalledWith(eventName, handler);
};
return {
navigation,
removeCallsAPI,
checkAddListenerCalled,
checkAddListenerCalledWith,
checkRemoveCalled,
checkRemoveCalledWith,
};
};
// We test 2 distinct ways to provide the navigation to the NavigationEvents (prop/context)
const NavigationEventsTestComp = ({
withContext = true,
navigation,
...props
}) => {
if (withContext) {
return (
<NavigationProvider value={navigation}>
<NavigationEvents {...props} />
</NavigationProvider>
);
} else {
return <NavigationEvents navigation={navigation} {...props} />;
}
};
describe('NavigationEvents', () => {
it('add all listeners with navigation prop', () => {
const {
navigation,
checkAddListenerCalled,
checkAddListenerCalledWith,
} = createNavigationAndHelpers();
const eventListenerProps = createEventListenersProp();
const component = renderer.create(
<NavigationEventsTestComp
withContext={false}
navigation={navigation}
{...eventListenerProps}
/>
);
checkAddListenerCalled(4);
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
});
it('add all listeners with navigation context', () => {
const {
navigation,
checkAddListenerCalled,
checkAddListenerCalledWith,
} = createNavigationAndHelpers();
const eventListenerProps = createEventListenersProp();
const component = renderer.create(
<NavigationEventsTestComp
withContext={true}
navigation={navigation}
{...eventListenerProps}
/>
);
checkAddListenerCalled(4);
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
});
it('remove all listeners on unmount', () => {
const {
navigation,
checkRemoveCalled,
checkRemoveCalledWith,
} = createNavigationAndHelpers();
const eventListenerProps = createEventListenersProp();
const component = renderer.create(
<NavigationEventsTestComp
navigation={navigation}
{...eventListenerProps}
/>
);
checkRemoveCalled(0);
component.unmount();
checkRemoveCalled(4);
checkRemoveCalledWith('willBlur', eventListenerProps.onWillBlur);
checkRemoveCalledWith('willFocus', eventListenerProps.onWillFocus);
checkRemoveCalledWith('didBlur', eventListenerProps.onDidBlur);
checkRemoveCalledWith('didFocus', eventListenerProps.onDidFocus);
});
it('add a single listener', () => {
const {
navigation,
checkAddListenerCalled,
checkAddListenerCalledWith,
} = createNavigationAndHelpers();
const listener = createListener();
const component = renderer.create(
<NavigationEventsTestComp navigation={navigation} onDidFocus={listener} />
);
checkAddListenerCalled(1);
checkAddListenerCalledWith('didFocus', listener);
});
it('do not attempt to add/remove stable listeners on update', () => {
const {
navigation,
checkAddListenerCalled,
checkAddListenerCalledWith,
} = createNavigationAndHelpers();
const eventListenerProps = createEventListenersProp();
const component = renderer.create(
<NavigationEventsTestComp
navigation={navigation}
{...eventListenerProps}
/>
);
component.update(
<NavigationEventsTestComp
navigation={navigation}
{...eventListenerProps}
/>
);
component.update(
<NavigationEventsTestComp
navigation={navigation}
{...eventListenerProps}
/>
);
checkAddListenerCalled(4);
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
});
it('add, remove and replace (remove+add) listeners on complex updates', () => {
const {
navigation,
checkAddListenerCalled,
checkAddListenerCalledWith,
checkRemoveCalled,
checkRemoveCalledWith,
} = createNavigationAndHelpers();
const eventListenerProps = createEventListenersProp();
const component = renderer.create(
<NavigationEventsTestComp
navigation={navigation}
{...eventListenerProps}
/>
);
checkAddListenerCalled(4);
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
checkRemoveCalled(0);
const onWillFocus2 = createListener();
const onDidFocus2 = createListener();
component.update(
<NavigationEventsTestComp
navigation={navigation}
onWillBlur={eventListenerProps.onWillBlur}
onDidBlur={undefined}
onWillFocus={onWillFocus2}
onDidFocus={onDidFocus2}
/>
);
checkAddListenerCalled(6);
checkAddListenerCalledWith('willFocus', onWillFocus2);
checkAddListenerCalledWith('didFocus', onDidFocus2);
checkRemoveCalled(3);
checkRemoveCalledWith('didBlur', eventListenerProps.onDidBlur);
checkRemoveCalledWith('willFocus', eventListenerProps.onWillFocus);
checkRemoveCalledWith('didFocus', eventListenerProps.onDidFocus);
});
});

View File

@@ -1,303 +0,0 @@
import ScenesReducer from '../ScenesReducer';
/**
* Simulate scenes transtion with changes of navigation states.
*/
function testTransition(states) {
const routes = states.map(keys => ({
index: 0,
routes: keys.map(key => ({ key, routeName: '' })),
isTransitioning: false,
}));
let scenes = [];
let prevState = null;
routes.forEach(nextState => {
scenes = ScenesReducer(scenes, nextState, prevState);
prevState = nextState;
});
return scenes;
}
describe('ScenesReducer', () => {
it('gets initial scenes', () => {
const scenes = testTransition([['1', '2']]);
expect(scenes).toEqual([
{
index: 0,
isActive: true,
isStale: false,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: false,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
]);
});
it('pushes new scenes', () => {
// Transition from ['1', '2'] to ['1', '2', '3'].
const scenes = testTransition([['1', '2'], ['1', '2', '3']]);
expect(scenes).toEqual([
{
index: 0,
isActive: true,
isStale: false,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: false,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
{
index: 2,
isActive: false,
isStale: false,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
]);
});
it('gets active scene when index changes', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 1,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
const route = scenes2.find(scene => scene.isActive).route;
expect(route).toEqual({ key: '2', routeName: '' });
});
it('gets same scenes', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
expect(scenes1).toBe(scenes2);
});
it('gets different scenes when keys are different', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [{ key: '2', routeName: '' }, { key: '1', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
expect(scenes1).not.toBe(scenes2);
});
it('gets different scenes when routes are different', () => {
const state1 = {
index: 0,
routes: [
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [
{ key: '1', x: 3, routeName: '' },
{ key: '2', x: 4, routeName: '' },
],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
expect(scenes1).not.toBe(scenes2);
});
it('gets different scenes when state index changes', () => {
const state1 = {
index: 0,
routes: [
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const state2 = {
index: 1,
routes: [
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
expect(scenes1).not.toBe(scenes2);
});
it('pops scenes', () => {
// Transition from ['1', '2', '3'] to ['1', '2'].
const scenes = testTransition([['1', '2', '3'], ['1', '2']]);
expect(scenes).toEqual([
{
index: 0,
isActive: true,
isStale: false,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: false,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
{
index: 2,
isActive: false,
isStale: true,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
]);
});
it('replaces scenes', () => {
const scenes = testTransition([['1', '2'], ['3']]);
expect(scenes).toEqual([
{
index: 0,
isActive: false,
isStale: true,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 0,
isActive: true,
isStale: false,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: true,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
]);
});
it('revives scenes', () => {
const scenes = testTransition([['1', '2'], ['3'], ['2']]);
expect(scenes).toEqual([
{
index: 0,
isActive: false,
isStale: true,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 0,
isActive: true,
isStale: false,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
{
index: 0,
isActive: false,
isStale: true,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
]);
});
});

View File

@@ -1,49 +0,0 @@
/* 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();
});
});

459
yarn.lock

File diff suppressed because it is too large Load Diff