mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-19 18:38:16 +08:00
Compare commits
105 Commits
2.3.0-beta
...
2.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27eb73cc14 | ||
|
|
f01b4896e6 | ||
|
|
556c31626e | ||
|
|
b6bca3ed2e | ||
|
|
0c56b21b46 | ||
|
|
912c7ca076 | ||
|
|
73c76f1e4b | ||
|
|
d746a587b0 | ||
|
|
dee03c839a | ||
|
|
2104bf1a04 | ||
|
|
4e2a409dca | ||
|
|
51bfe8dd19 | ||
|
|
04a4512c1b | ||
|
|
4a5da86ce0 | ||
|
|
a118122aed | ||
|
|
a94f89ffe1 | ||
|
|
9d77fd6d54 | ||
|
|
13cf4497ee | ||
|
|
9175118383 | ||
|
|
6fc21250ec | ||
|
|
714d5eab6b | ||
|
|
67233dc9ef | ||
|
|
b0443c1861 | ||
|
|
c0b637df52 | ||
|
|
9a82706fba | ||
|
|
d973a26edb | ||
|
|
852e7e1974 | ||
|
|
cd3707d64b | ||
|
|
3c36db455f | ||
|
|
ec52c884c5 | ||
|
|
c4b3f25a0f | ||
|
|
93642e16e7 | ||
|
|
1a76556290 | ||
|
|
12b21f052e | ||
|
|
c1f07dc167 | ||
|
|
bc04b31d01 | ||
|
|
35307c70be | ||
|
|
7e3f4f3bec | ||
|
|
cbd0958e6f | ||
|
|
cab4d71a5e | ||
|
|
108ac0e2a9 | ||
|
|
fa4fdb9c57 | ||
|
|
ebdd2da79f | ||
|
|
1fe11c100e | ||
|
|
c4b84f1d66 | ||
|
|
69f394be5b | ||
|
|
316e4991ac | ||
|
|
805064cb5e | ||
|
|
8f199980cb | ||
|
|
37ca6a92ca | ||
|
|
980e0409dc | ||
|
|
a00ba5918a | ||
|
|
ad6b25cff9 | ||
|
|
a69b67d6d2 | ||
|
|
dc436e4d01 | ||
|
|
fe95bdeee6 | ||
|
|
525528e38f | ||
|
|
9f5f3d994c | ||
|
|
e8c1833053 | ||
|
|
0921889f7a | ||
|
|
1951a3ac46 | ||
|
|
4e384f8057 | ||
|
|
3d06d19d6a | ||
|
|
30ef5ef72b | ||
|
|
c7fff52408 | ||
|
|
bc01a4cd57 | ||
|
|
cad3d70aed | ||
|
|
bb5719f438 | ||
|
|
3dd3f5b804 | ||
|
|
3d8d5a0634 | ||
|
|
54448ed070 | ||
|
|
369ac2b568 | ||
|
|
3dc592f679 | ||
|
|
4f93200c91 | ||
|
|
665736d754 | ||
|
|
5598c3e28f | ||
|
|
cde6e845cd | ||
|
|
fb8c712ad8 | ||
|
|
350b7e0aed | ||
|
|
de112565d3 | ||
|
|
acdd515c13 | ||
|
|
452a6d2004 | ||
|
|
08c8031a71 | ||
|
|
608365266a | ||
|
|
247fba56e6 | ||
|
|
060f5dcecf | ||
|
|
fdec05c87a | ||
|
|
76da804574 | ||
|
|
dde091848a | ||
|
|
824fa32416 | ||
|
|
c518e7f36c | ||
|
|
1cfe01dbdb | ||
|
|
e62a9050fd | ||
|
|
310b909ba8 | ||
|
|
aebe8a5c23 | ||
|
|
e1df2c6c4a | ||
|
|
fa86718a24 | ||
|
|
c8e5673183 | ||
|
|
b312a5e307 | ||
|
|
ee6a6c53b1 | ||
|
|
8edec88341 | ||
|
|
b8d6d4253d | ||
|
|
0adb1ba9f1 | ||
|
|
d3ef3d1271 | ||
|
|
89a24bdc12 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -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.
|
||||
|
||||
|
||||
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
178
CHANGELOG.md
Normal 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
7
NavigationTestUtils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { _TESTING_ONLY_reset_container_count } from './src/createNavigationContainer';
|
||||
|
||||
export default {
|
||||
resetInternalState: () => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -165,7 +165,7 @@ const SimpleTabs = createBottomTabNavigator(
|
||||
},
|
||||
{
|
||||
tabBarOptions: {
|
||||
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
|
||||
activeTintColor: '#e91e63',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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: (
|
||||
|
||||
142
examples/NavigationPlayground/js/StacksOverTopTabs.js
Normal file
142
examples/NavigationPlayground/js/StacksOverTopTabs.js
Normal 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;
|
||||
@@ -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 }} />
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
127
examples/NavigationPlayground/js/TabsWithNavigationEvents.js
Normal file
127
examples/NavigationPlayground/js/TabsWithNavigationEvents.js
Normal 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;
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
89
flow/react-navigation.js
vendored
89
flow/react-navigation.js
vendored
@@ -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>;
|
||||
}
|
||||
|
||||
16
package.json
16
package.json
@@ -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": {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import {
|
||||
BackAndroid as DeprecatedBackAndroid,
|
||||
BackHandler as ModernBackHandler,
|
||||
MaskedViewIOS,
|
||||
} from 'react-native';
|
||||
|
||||
const BackHandler = ModernBackHandler || DeprecatedBackAndroid;
|
||||
|
||||
export { BackHandler, MaskedViewIOS };
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { BackHandler, View } from 'react-native';
|
||||
|
||||
const MaskedViewIOS = () => <View>{this.props.children}</View>;
|
||||
|
||||
export { BackHandler, MaskedViewIOS };
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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"`;
|
||||
@@ -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>
|
||||
`;
|
||||
9
src/navigators/createContainedSwitchNavigator.js
Normal file
9
src/navigators/createContainedSwitchNavigator.js
Normal 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;
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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] = {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
58
src/react-navigation.js
vendored
58
src/react-navigation.js
vendored
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
596
src/routers/__tests__/PathHandling-test.js
Normal file
596
src/routers/__tests__/PathHandling-test.js
Normal 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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
34
src/routers/__tests__/pathUtils-test.js
Normal file
34
src/routers/__tests__/pathUtils-test.js
Normal 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
223
src/routers/pathUtils.js
Normal 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,
|
||||
};
|
||||
@@ -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.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function docsUrl(path) {
|
||||
return `https://v2.reactnavigation.org/docs/${path}`;
|
||||
return `https://reactnavigation.org/docs/${path}`;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default (obj, key, defaultValue) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (obj.hasOwnProperty(key) && typeof obj[key] !== 'undefined') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
3
src/views/NavigationConsumer.js
Normal file
3
src/views/NavigationConsumer.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { NavigationConsumer } from './NavigationContext';
|
||||
|
||||
export default NavigationConsumer;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
57
src/views/NavigationEvents.js
Normal file
57
src/views/NavigationEvents.js
Normal 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);
|
||||
3
src/views/NavigationProvider.js
Normal file
3
src/views/NavigationProvider.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { NavigationProvider } from './NavigationContext';
|
||||
|
||||
export default NavigationProvider;
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { NavigationProvider } from './NavigationContext';
|
||||
|
||||
export default class SceneView extends React.PureComponent {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
241
src/views/__tests__/NavigationEvents-test.js
Normal file
241
src/views/__tests__/NavigationEvents-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user