Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
128a95b496 | ||
|
|
470eaf3b08 | ||
|
|
c91e8206a5 | ||
|
|
da283915f8 | ||
|
|
3031e7bd80 | ||
|
|
98a4f26f26 | ||
|
|
b7f5435c93 | ||
|
|
21ef4fcb82 | ||
|
|
5f64a2a9cb | ||
|
|
992d0fb267 | ||
|
|
6f41379ed1 | ||
|
|
267af01e72 | ||
|
|
b68c3a755d | ||
|
|
7345634493 | ||
|
|
6517169119 | ||
|
|
ea5d14a720 | ||
|
|
313d0726a8 | ||
|
|
b52f153747 | ||
|
|
44621005ff | ||
|
|
bf58364c3d | ||
|
|
b55053cde6 | ||
|
|
9abb2644a9 | ||
|
|
6a946d6ab7 | ||
|
|
395abe5200 | ||
|
|
ba62509ff4 | ||
|
|
45391db7d9 | ||
|
|
7f86362e86 | ||
|
|
99605737e9 | ||
|
|
842f5eb7b2 | ||
|
|
183ea82416 | ||
|
|
108a6504a7 | ||
|
|
f92d671746 | ||
|
|
e0c4a8f7d3 | ||
|
|
bc881c8aa1 | ||
|
|
118c19dcce | ||
|
|
01b43974e6 | ||
|
|
2f90899620 | ||
|
|
6cc86f66e1 | ||
|
|
4be99b6645 | ||
|
|
80016b7218 | ||
|
|
f555a9ec9a | ||
|
|
05cbd85d5c | ||
|
|
51965eac38 | ||
|
|
a3956bf3ce | ||
|
|
ce24c66b5a | ||
|
|
5467f0e22d | ||
|
|
1e7d8d55c3 | ||
|
|
1d2ce862c2 | ||
|
|
d778479e4a | ||
|
|
352dae50e1 | ||
|
|
61385cae59 | ||
|
|
aa3c13891e | ||
|
|
9696d7220d | ||
|
|
2b83b44816 | ||
|
|
ec749023ed | ||
|
|
adc9389eb3 | ||
|
|
54d143fee2 | ||
|
|
d50e74d0c7 | ||
|
|
22926c5230 | ||
|
|
f6c47a6c66 | ||
|
|
046a9f8930 | ||
|
|
72f17538c2 | ||
|
|
550001b053 |
22
.eslintrc
@@ -7,18 +7,18 @@
|
||||
"prettier/react"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["react", "prettier"],
|
||||
"env": {
|
||||
"jasmine": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": ["error", {
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}
|
||||
],
|
||||
|
||||
"no-underscore-dangle": "off",
|
||||
"no-use-before-define": "off",
|
||||
@@ -27,17 +27,15 @@
|
||||
"no-plusplus": "off",
|
||||
"no-class-assign": "off",
|
||||
"no-duplicate-imports": "off",
|
||||
|
||||
"import/extensions": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/no-unresolved": "off",
|
||||
|
||||
"react/jsx-filename-extension": [
|
||||
"off", { "extensions": [".js", ".jsx"] }
|
||||
],
|
||||
"react/jsx-filename-extension": ["off", { "extensions": [".js", ".jsx"] }],
|
||||
|
||||
"react/sort-comp": "off",
|
||||
"react/prefer-stateless-function": "off",
|
||||
"react/no-deprecated": "off",
|
||||
|
||||
"react/forbid-prop-types": "warn",
|
||||
"react/prop-types": "off",
|
||||
|
||||
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
@@ -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.
|
||||
|
||||
76
CHANGELOG.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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.6.1] - [2018-07-05)()
|
||||
### 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.6.0...HEAD
|
||||
[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
|
||||
@@ -1,6 +1,6 @@
|
||||
# React Navigation
|
||||
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-community/react-navigation) [](https://reactnavigation.org/docs/contributing.html)
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-navigation/react-navigation) [](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [](https://reactnavigation.org/docs/contributing.html)
|
||||
|
||||
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.
|
||||
|
||||
@@ -55,4 +55,4 @@ This library has adopted a Code of Conduct that we expect project participants t
|
||||
|
||||
## License
|
||||
|
||||
React-navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
|
||||
React Navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
|
||||
|
||||
@@ -55,8 +55,6 @@ module.system=haste
|
||||
|
||||
emoji=true
|
||||
|
||||
experimental.strict_type_args=true
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||
@@ -77,7 +75,5 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[version]
|
||||
^0.61.0
|
||||
^0.67.0
|
||||
|
||||
@@ -11,16 +11,17 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "26.0.0",
|
||||
"sdkVersion": "27.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
"ttf",
|
||||
"mp4"
|
||||
]
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.reactnavigation.example",
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"package": "com.reactnavigation.example"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
createStackNavigator,
|
||||
SafeAreaView,
|
||||
withNavigation,
|
||||
NavigationActions,
|
||||
StackActions,
|
||||
} from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
@@ -48,25 +52,55 @@ const MyBackButtonWithNavigation = withNavigation(MyBackButton);
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop, dismiss } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop && dismiss,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() =>
|
||||
navigation.dispatch(
|
||||
StackActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
routeName: 'Photos',
|
||||
params: { name: 'Jane' },
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
title="Reset photos"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack()} title="Go back" />
|
||||
<Button onPress={() => navigation.dismiss()} title="Dismiss" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (navigation.goBack()) {
|
||||
console.log('goBack handled');
|
||||
} else {
|
||||
console.log('goBack unhandled');
|
||||
}
|
||||
}}
|
||||
title="Go back"
|
||||
/>
|
||||
<Button onPress={() => dismiss()} title="Dismiss" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@@ -165,7 +165,7 @@ const SimpleTabs = createBottomTabNavigator(
|
||||
},
|
||||
{
|
||||
tabBarOptions: {
|
||||
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
|
||||
activeTintColor: '#e91e63',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { NavigationScreenProp } from 'react-navigation';
|
||||
import * as React from 'react';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import { createStackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
type NavScreenProps = {
|
||||
@@ -19,15 +21,14 @@ class HomeScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push } = navigation;
|
||||
invariant(push, 'missing `push` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenWithNoHeader')}
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
|
||||
@@ -44,18 +45,20 @@ class OtherScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenWithLongTitle')}
|
||||
onPress={() => push('ScreenWithLongTitle')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenWithNoHeader')}
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
@@ -70,10 +73,12 @@ class ScreenWithLongTitle extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { pop } = navigation;
|
||||
invariant(pop, 'missing `pop` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
@@ -89,14 +94,13 @@ class ScreenWithNoHeader extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Header, createStackNavigator } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
@@ -31,11 +33,16 @@ type MyNavScreenProps = {
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
@@ -43,11 +50,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</ScrollView>
|
||||
|
||||
@@ -94,6 +94,19 @@ const TabNav = createBottomTabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
TabNav.navigationOptions = ({ navigation }) => {
|
||||
let { routeName } = navigation.state.routes[navigation.state.index];
|
||||
let title;
|
||||
if (routeName === 'SettingsTab') {
|
||||
title = 'Settings';
|
||||
} else if (routeName === 'MainTab') {
|
||||
title = 'Home';
|
||||
}
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
const StacksOverTabs = createStackNavigator({
|
||||
Root: {
|
||||
screen: TabNav,
|
||||
@@ -107,9 +120,9 @@ const StacksOverTabs = createStackNavigator({
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
path: '/people/:name',
|
||||
navigationOptions: ({ navigation }) => {
|
||||
title: `${navigation.state.params.name}'s Profile!`;
|
||||
},
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
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;
|
||||
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;
|
||||
@@ -11,18 +11,20 @@
|
||||
"test": "flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^26.0.0",
|
||||
"react": "16.3.0-alpha.1",
|
||||
"react-native": "^0.54.0",
|
||||
"expo": "^27.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-header-buttons": "^0.0.4",
|
||||
"react-navigation-material-bottom-tabs": "0.1.3"
|
||||
"react-navigation-material-bottom-tabs": "0.1.3",
|
||||
"react-navigation-tabs": "^0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"flow-bin": "^0.67.0",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
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",
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { StackNavigator } from 'react-navigation';
|
||||
import { createStackNavigator } from 'react-navigation';
|
||||
import {
|
||||
reduxifyNavigator,
|
||||
createReactNavigationReduxMiddleware,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
import LoginScreen from '../components/LoginScreen';
|
||||
import MainScreen from '../components/MainScreen';
|
||||
import ProfileScreen from '../components/ProfileScreen';
|
||||
import { addListener } from '../utils/redux';
|
||||
|
||||
export const AppNavigator = StackNavigator({
|
||||
const middleware = createReactNavigationReduxMiddleware(
|
||||
'root',
|
||||
state => state.nav
|
||||
);
|
||||
|
||||
const RootNavigator = createStackNavigator({
|
||||
Login: { screen: LoginScreen },
|
||||
Main: { screen: MainScreen },
|
||||
Profile: { screen: ProfileScreen },
|
||||
});
|
||||
|
||||
class AppWithNavigationState extends React.Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
nav: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
<AppNavigator
|
||||
navigation={{
|
||||
dispatch,
|
||||
state: nav,
|
||||
addListener,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const AppWithNavigationState = reduxifyNavigator(RootNavigator, 'root');
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
nav: state.nav,
|
||||
state: state.nav,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
||||
const AppNavigator = connect(mapStateToProps)(AppWithNavigationState);
|
||||
|
||||
export { RootNavigator, AppNavigator, middleware };
|
||||
|
||||
@@ -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,15 +0,0 @@
|
||||
import {
|
||||
createReactNavigationReduxMiddleware,
|
||||
createReduxBoundAddListener,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
const middleware = createReactNavigationReduxMiddleware(
|
||||
"root",
|
||||
state => state.nav,
|
||||
);
|
||||
const addListener = createReduxBoundAddListener("root");
|
||||
|
||||
export {
|
||||
middleware,
|
||||
addListener,
|
||||
};
|
||||
284
flow/react-navigation.js
vendored
@@ -69,6 +69,14 @@ declare module 'react-navigation' {
|
||||
[key: string]: mixed,
|
||||
};
|
||||
|
||||
declare export type NavigationBackAction = {|
|
||||
type: 'Navigation/BACK',
|
||||
key?: ?string,
|
||||
|};
|
||||
declare export type NavigationInitAction = {|
|
||||
type: 'Navigation/INIT',
|
||||
params?: NavigationParams,
|
||||
|};
|
||||
declare export type NavigationNavigateAction = {|
|
||||
type: 'Navigation/NAVIGATE',
|
||||
routeName: string,
|
||||
@@ -79,12 +87,6 @@ declare module 'react-navigation' {
|
||||
|
||||
key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationBackAction = {|
|
||||
type: 'Navigation/BACK',
|
||||
key?: ?string,
|
||||
|};
|
||||
|
||||
declare export type NavigationSetParamsAction = {|
|
||||
type: 'Navigation/SET_PARAMS',
|
||||
|
||||
@@ -95,30 +97,6 @@ declare module 'react-navigation' {
|
||||
params: NavigationParams,
|
||||
|};
|
||||
|
||||
declare export type NavigationInitAction = {|
|
||||
type: 'Navigation/INIT',
|
||||
params?: NavigationParams,
|
||||
|};
|
||||
|
||||
declare export type NavigationResetAction = {|
|
||||
type: 'Navigation/RESET',
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
|};
|
||||
|
||||
declare export type NavigationUriAction = {|
|
||||
type: 'Navigation/URI',
|
||||
uri: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationReplaceAction = {|
|
||||
+type: 'Navigation/REPLACE',
|
||||
+key: string,
|
||||
+routeName: string,
|
||||
+params?: NavigationParams,
|
||||
+action?: NavigationNavigateAction,
|
||||
|};
|
||||
declare export type NavigationPopAction = {|
|
||||
+type: 'Navigation/POP',
|
||||
+n?: number,
|
||||
@@ -135,17 +113,51 @@ declare module 'react-navigation' {
|
||||
+action?: NavigationNavigateAction,
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationResetAction = {|
|
||||
type: 'Navigation/RESET',
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
|};
|
||||
declare export type NavigationReplaceAction = {|
|
||||
+type: 'Navigation/REPLACE',
|
||||
+key: string,
|
||||
+routeName: string,
|
||||
+params?: NavigationParams,
|
||||
+action?: NavigationNavigateAction,
|
||||
|};
|
||||
declare export type NavigationCompleteTransitionAction = {|
|
||||
+type: 'Navigation/COMPLETE_TRANSITION',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationOpenDrawerAction = {|
|
||||
+type: 'Navigation/OPEN_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationCloseDrawerAction = {|
|
||||
+type: 'Navigation/CLOSE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationToggleDrawerAction = {|
|
||||
+type: 'Navigation/TOGGLE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationAction =
|
||||
| NavigationBackAction
|
||||
| NavigationInitAction
|
||||
| NavigationNavigateAction
|
||||
| NavigationReplaceAction
|
||||
| NavigationSetParamsAction
|
||||
| NavigationPopAction
|
||||
| NavigationPopToTopAction
|
||||
| NavigationPushAction
|
||||
| NavigationBackAction
|
||||
| NavigationSetParamsAction
|
||||
| NavigationResetAction;
|
||||
| NavigationResetAction
|
||||
| NavigationReplaceAction
|
||||
| NavigationCompleteTransitionAction
|
||||
| NavigationOpenDrawerAction
|
||||
| NavigationCloseDrawerAction
|
||||
| NavigationToggleDrawerAction;
|
||||
|
||||
/**
|
||||
* NavigationState is a tree of routes for a single navigator, where each
|
||||
@@ -269,24 +281,36 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationComponent =
|
||||
| NavigationScreenComponent<NavigationRoute, *, *>
|
||||
| NavigationContainer<*, *, *>
|
||||
| any;
|
||||
| NavigationContainer<*, *, *>;
|
||||
|
||||
declare interface withOptionalNavigationOptions<Options> {
|
||||
navigationOptions?: NavigationScreenConfig<Options>;
|
||||
}
|
||||
|
||||
declare export type NavigationScreenComponent<
|
||||
Route: NavigationRoute,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
|
||||
({} | { navigationOptions: NavigationScreenConfig<Options> });
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, Route>,
|
||||
}> &
|
||||
withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare interface withRouter<State, Options> {
|
||||
router: NavigationRouter<State, Options>;
|
||||
}
|
||||
|
||||
declare export type NavigationNavigator<
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, State>,
|
||||
}> &
|
||||
withRouter<State, Options> &
|
||||
withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare export type NavigationRouteConfig =
|
||||
| NavigationComponent
|
||||
@@ -426,10 +450,10 @@ declare module 'react-navigation' {
|
||||
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
|
||||
tabBarVisible?: boolean,
|
||||
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
|
||||
tabBarOnPress?: (
|
||||
scene: TabScene,
|
||||
jumpToIndex: (index: number) => void
|
||||
) => void,
|
||||
tabBarOnPress?: ({
|
||||
navigation: NavigationScreenProp<NavigationRoute>,
|
||||
defaultHandler: () => void,
|
||||
}) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -485,8 +509,14 @@ declare module 'react-navigation' {
|
||||
declare export type NavigationScreenProp<+S> = {
|
||||
+state: S,
|
||||
dispatch: NavigationDispatch,
|
||||
addListener: (
|
||||
eventName: string,
|
||||
callback: NavigationEventCallback
|
||||
) => NavigationEventSubscription,
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
isFocused: () => boolean,
|
||||
// Shared action creators that exist for all routers
|
||||
goBack: (routeKey?: ?string) => boolean,
|
||||
dismiss: () => boolean,
|
||||
navigate: (
|
||||
routeName:
|
||||
| string
|
||||
@@ -500,24 +530,25 @@ declare module 'react-navigation' {
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
setParams: (newParams: NavigationParams) => boolean,
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
addListener: (
|
||||
eventName: string,
|
||||
callback: NavigationEventCallback
|
||||
) => NavigationEventSubscription,
|
||||
push: (
|
||||
// StackRouter action creators
|
||||
pop?: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop?: (params?: { immediate?: boolean }) => boolean,
|
||||
push?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
replace: (
|
||||
replace?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop: (params?: { immediate?: boolean }) => boolean,
|
||||
isFocused: () => boolean,
|
||||
reset?: (actions: NavigationAction[], index: number) => boolean,
|
||||
dismiss?: () => boolean,
|
||||
// DrawerRouter action creators
|
||||
openDrawer?: () => boolean,
|
||||
closeDrawer?: () => boolean,
|
||||
toggleDrawer?: () => boolean,
|
||||
};
|
||||
|
||||
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
|
||||
@@ -526,6 +557,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
|
||||
*/
|
||||
@@ -534,10 +580,12 @@ declare module 'react-navigation' {
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationContainerProps<State, Options>,
|
||||
}> &
|
||||
withRouter<State, Options> &
|
||||
withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
|
||||
uriPrefix?: string | RegExp,
|
||||
@@ -706,44 +754,73 @@ declare module 'react-navigation' {
|
||||
BACK: 'Navigation/BACK',
|
||||
INIT: 'Navigation/INIT',
|
||||
NAVIGATE: 'Navigation/NAVIGATE',
|
||||
RESET: 'Navigation/RESET',
|
||||
SET_PARAMS: 'Navigation/SET_PARAMS',
|
||||
URI: 'Navigation/URI',
|
||||
back: {
|
||||
(payload?: { key?: ?string }): NavigationBackAction,
|
||||
toString: () => string,
|
||||
},
|
||||
init: {
|
||||
(payload?: { params?: NavigationParams }): NavigationInitAction,
|
||||
toString: () => string,
|
||||
},
|
||||
navigate: {
|
||||
(payload: {
|
||||
routeName: string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
}): NavigationNavigateAction,
|
||||
toString: () => string,
|
||||
},
|
||||
reset: {
|
||||
(payload: {
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}): NavigationResetAction,
|
||||
toString: () => string,
|
||||
},
|
||||
setParams: {
|
||||
(payload: {
|
||||
key: string,
|
||||
params: NavigationParams,
|
||||
}): NavigationSetParamsAction,
|
||||
toString: () => string,
|
||||
},
|
||||
uri: {
|
||||
(payload: { uri: string }): NavigationUriAction,
|
||||
toString: () => string,
|
||||
},
|
||||
|
||||
back: (payload?: { key?: ?string }) => NavigationBackAction,
|
||||
init: (payload?: { params?: NavigationParams }) => NavigationInitAction,
|
||||
navigate: (payload: {
|
||||
routeName: string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
key?: string,
|
||||
}) => NavigationNavigateAction,
|
||||
setParams: (payload: {
|
||||
key: string,
|
||||
params: NavigationParams,
|
||||
}) => NavigationSetParamsAction,
|
||||
};
|
||||
|
||||
declare export var StackActions: {
|
||||
POP: 'Navigation/POP',
|
||||
POP_TO_TOP: 'Navigation/POP_TO_TOP',
|
||||
PUSH: 'Navigation/PUSH',
|
||||
RESET: 'Navigation/RESET',
|
||||
REPLACE: 'Navigation/REPLACE',
|
||||
COMPLETE_TRANSITION: 'Navigation/COMPLETE_TRANSITION',
|
||||
|
||||
pop: (payload: {
|
||||
n?: number,
|
||||
immediate?: boolean,
|
||||
}) => NavigationPopAction,
|
||||
popToTop: (payload: {
|
||||
immediate?: boolean,
|
||||
}) => NavigationPopToTopAction,
|
||||
push: (payload: {
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
key?: string,
|
||||
}) => NavigationPushAction,
|
||||
reset: (payload: {
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}) => NavigationResetAction,
|
||||
replace: (payload: {
|
||||
key: string,
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
}) => NavigationReplaceAction,
|
||||
completeTransition: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationCompleteTransitionAction,
|
||||
};
|
||||
|
||||
declare export var DrawerActions: {
|
||||
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
|
||||
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
|
||||
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
|
||||
|
||||
openDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationOpenDrawerAction,
|
||||
closeDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationCloseDrawerAction,
|
||||
toggleDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationToggleDrawerAction,
|
||||
};
|
||||
|
||||
declare type _RouterProp<S: NavigationState, O: {}> = {
|
||||
@@ -766,7 +843,7 @@ declare module 'react-navigation' {
|
||||
view: NavigationView<O, S>,
|
||||
router: NavigationRouter<S, O>,
|
||||
navigatorConfig?: NavigatorConfig
|
||||
): any;
|
||||
): NavigationNavigator<S, O, *>;
|
||||
|
||||
declare export function StackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
@@ -1097,4 +1174,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>;
|
||||
}
|
||||
|
||||
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "2.0.2",
|
||||
"version": "2.6.1",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
@@ -33,12 +33,12 @@
|
||||
"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-drawer-layout-polyfill": "^1.3.2",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.2.0",
|
||||
"react-navigation-tabs": "0.2.0"
|
||||
"react-native-safe-area-view": "^0.8.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||
"react-navigation-drawer": "0.4.3",
|
||||
"react-navigation-tabs": "0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
|
||||
@@ -4,6 +4,6 @@ set -eo pipefail
|
||||
|
||||
case $CIRCLE_NODE_INDEX in
|
||||
0) yarn test && yarn codecov ;;
|
||||
1) cd examples/NavigationPlayground && yarn && yarn test ;;
|
||||
#1) cd examples/NavigationPlayground && yarn && yarn test ;;
|
||||
#2) cd examples/ReduxExample && yarn && yarn test ;;
|
||||
esac
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,9 @@ import renderer from 'react-test-renderer';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import createStackNavigator from '../navigators/createStackNavigator';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../createNavigationContainer';
|
||||
import createNavigationContainer, {
|
||||
_TESTING_ONLY_reset_container_count,
|
||||
} from '../createNavigationContainer';
|
||||
|
||||
describe('NavigationContainer', () => {
|
||||
jest.useFakeTimers();
|
||||
@@ -19,7 +21,7 @@ describe('NavigationContainer', () => {
|
||||
const CarScreen = () => <div />;
|
||||
const DogScreen = () => <div />;
|
||||
const ElkScreen = () => <div />;
|
||||
const NavigationContainer = createStackNavigator(
|
||||
const Stack = createStackNavigator(
|
||||
{
|
||||
foo: {
|
||||
screen: FooScreen,
|
||||
@@ -44,6 +46,7 @@ describe('NavigationContainer', () => {
|
||||
initialRouteName: 'foo',
|
||||
}
|
||||
);
|
||||
const NavigationContainer = createNavigationContainer(Stack);
|
||||
|
||||
describe('state.nav', () => {
|
||||
it("should be preloaded with the router's initial state", () => {
|
||||
@@ -208,7 +211,7 @@ describe('NavigationContainer', () => {
|
||||
let spy = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spy.console = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -225,7 +228,7 @@ describe('NavigationContainer', () => {
|
||||
|
||||
let spy = spyConsole();
|
||||
|
||||
it('warns when you render more than one navigator explicitly', () => {
|
||||
it('warns when you render more than one container explicitly', () => {
|
||||
class BlankScreen extends React.Component {
|
||||
render() {
|
||||
return <View />;
|
||||
@@ -242,13 +245,17 @@ describe('NavigationContainer', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const ChildNavigator = createStackNavigator({
|
||||
Child: BlankScreen,
|
||||
});
|
||||
const ChildNavigator = createNavigationContainer(
|
||||
createStackNavigator({
|
||||
Child: BlankScreen,
|
||||
})
|
||||
);
|
||||
|
||||
const RootStack = createStackNavigator({
|
||||
Root: RootScreen,
|
||||
});
|
||||
const RootStack = createNavigationContainer(
|
||||
createStackNavigator({
|
||||
Root: RootScreen,
|
||||
})
|
||||
);
|
||||
|
||||
renderer.create(<RootStack />).toJSON();
|
||||
expect(spy).toMatchSnapshot();
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
102
src/__tests__/getNavigation-test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import getNavigation from '../getNavigation';
|
||||
|
||||
test('getNavigation provides default action helpers', () => {
|
||||
const router = {
|
||||
getActionCreators: () => ({}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
|
||||
const dispatch = jest.fn();
|
||||
|
||||
const topNav = getNavigation(
|
||||
router,
|
||||
{},
|
||||
dispatch,
|
||||
new Set(),
|
||||
() => ({}),
|
||||
() => topNav
|
||||
);
|
||||
|
||||
topNav.navigate('GreatRoute');
|
||||
|
||||
expect(dispatch.mock.calls.length).toBe(1);
|
||||
expect(dispatch.mock.calls[0][0].type).toBe('Navigation/NAVIGATE');
|
||||
expect(dispatch.mock.calls[0][0].routeName).toBe('GreatRoute');
|
||||
});
|
||||
|
||||
test('getNavigation provides router action helpers', () => {
|
||||
const router = {
|
||||
getActionCreators: () => ({
|
||||
foo: bar => ({ type: 'FooBarAction', bar }),
|
||||
}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
|
||||
const dispatch = jest.fn();
|
||||
|
||||
const topNav = getNavigation(
|
||||
router,
|
||||
{},
|
||||
dispatch,
|
||||
new Set(),
|
||||
() => ({}),
|
||||
() => topNav
|
||||
);
|
||||
|
||||
topNav.foo('Great');
|
||||
|
||||
expect(dispatch.mock.calls.length).toBe(1);
|
||||
expect(dispatch.mock.calls[0][0].type).toBe('FooBarAction');
|
||||
expect(dispatch.mock.calls[0][0].bar).toBe('Great');
|
||||
});
|
||||
|
||||
test('getNavigation get child navigation with router', () => {
|
||||
const actionSubscribers = new Set();
|
||||
let navigation = null;
|
||||
|
||||
const routerA = {
|
||||
getActionCreators: () => ({}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const router = {
|
||||
childRouters: {
|
||||
RouteA: routerA,
|
||||
},
|
||||
getActionCreators: () => ({}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
|
||||
const initState = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
key: 'a',
|
||||
routeName: 'RouteA',
|
||||
routes: [{ key: 'c', routeName: 'RouteC' }],
|
||||
index: 0,
|
||||
},
|
||||
{ key: 'b', routeName: 'RouteB' },
|
||||
],
|
||||
};
|
||||
|
||||
const topNav = getNavigation(
|
||||
router,
|
||||
initState,
|
||||
() => {},
|
||||
actionSubscribers,
|
||||
() => ({}),
|
||||
() => navigation
|
||||
);
|
||||
|
||||
const childNavA = topNav.getChildNavigation('a');
|
||||
|
||||
expect(childNavA.router).toBe(routerA);
|
||||
});
|
||||
@@ -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 getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import docsUrl from './utils/docsUrl';
|
||||
import { urlToPathAndParams } from './routers/pathUtils';
|
||||
|
||||
function isStateful(props) {
|
||||
return !props.navigation;
|
||||
@@ -129,23 +129,8 @@ 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 parsedUrl = urlToPathAndParams(url, this.props.uriPrefix);
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const action = Component.router.getActionForPathAndParams(path, params);
|
||||
@@ -214,11 +199,11 @@ export default function createNavigationContainer(Component) {
|
||||
Linking.addEventListener('url', this._handleOpenURL);
|
||||
|
||||
// Pull out anything that can impact state
|
||||
const { persistenceKey } = this.props;
|
||||
const { persistenceKey, uriPrefix } = this.props;
|
||||
const startupStateJSON =
|
||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
||||
const url = await Linking.getInitialURL();
|
||||
const parsedUrl = url && this._urlToPathAndParams(url);
|
||||
const 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
|
||||
@@ -285,6 +270,8 @@ export default function createNavigationContainer(Component) {
|
||||
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
|
||||
);
|
||||
this.dispatch(NavigationActions.init());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,34 +343,24 @@ export default function createNavigationContainer(Component) {
|
||||
return false;
|
||||
};
|
||||
|
||||
_getScreenProps = () => this.props.screenProps;
|
||||
|
||||
render() {
|
||||
let navigation = this.props.navigation;
|
||||
if (this._isStateful()) {
|
||||
const nav = this.state.nav;
|
||||
if (!nav) {
|
||||
const navState = this.state.nav;
|
||||
if (!navState) {
|
||||
return this._renderLoading();
|
||||
}
|
||||
if (!this._navigation || this._navigation.state !== nav) {
|
||||
this._navigation = {
|
||||
dispatch: this.dispatch,
|
||||
state: nav,
|
||||
addListener: (eventName, handler) => {
|
||||
if (eventName !== 'action') {
|
||||
return { remove: () => {} };
|
||||
}
|
||||
this._actionEventSubscribers.add(handler);
|
||||
return {
|
||||
remove: () => {
|
||||
this._actionEventSubscribers.delete(handler);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const actionCreators = getNavigationActionCreators(nav);
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
this._navigation[actionName] = (...args) =>
|
||||
this.dispatch(actionCreators[actionName](...args));
|
||||
});
|
||||
if (!this._navigation || this._navigation.state !== navState) {
|
||||
this._navigation = getNavigation(
|
||||
Component.router,
|
||||
navState,
|
||||
this.dispatch,
|
||||
this._actionEventSubscribers,
|
||||
this._getScreenProps,
|
||||
() => this._navigation
|
||||
);
|
||||
}
|
||||
navigation = this._navigation;
|
||||
}
|
||||
|
||||
113
src/getChildNavigation.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import getChildEventSubscriber from './getChildEventSubscriber';
|
||||
import getChildRouter from './getChildRouter';
|
||||
import getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import invariant from './utils/invariant';
|
||||
|
||||
const createParamGetter = route => (paramName, defaultValue) => {
|
||||
const params = route.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
||||
const children =
|
||||
navigation._childrenNavigation || (navigation._childrenNavigation = {});
|
||||
|
||||
const childRoute = navigation.state.routes.find(r => r.key === childKey);
|
||||
|
||||
if (!childRoute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children[childKey] && children[childKey].state === childRoute) {
|
||||
return children[childKey];
|
||||
}
|
||||
|
||||
const childRouter = getChildRouter(navigation.router, childRoute.routeName);
|
||||
|
||||
// If the route has children, we'll use this to pass in to the action creators
|
||||
// for the childRouter so that any action that depends on the active route will
|
||||
// behave as expected. We don't explicitly require that routers implement routes
|
||||
// and index properties, but if we did then we would put an invariant here to
|
||||
// ensure that a focusedGrandChildRoute exists if childRouter is defined.
|
||||
const focusedGrandChildRoute =
|
||||
childRoute.routes && typeof childRoute.index === 'number'
|
||||
? childRoute.routes[childRoute.index]
|
||||
: null;
|
||||
|
||||
const actionCreators = {
|
||||
...navigation.actions,
|
||||
...navigation.router.getActionCreators(childRoute, navigation.state.key),
|
||||
...(childRouter
|
||||
? childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key)
|
||||
: {}),
|
||||
...getNavigationActionCreators(childRoute),
|
||||
};
|
||||
|
||||
const actionHelpers = {};
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
actionHelpers[actionName] = (...args) => {
|
||||
const actionCreator = actionCreators[actionName];
|
||||
const action = actionCreator(...args);
|
||||
return navigation.dispatch(action);
|
||||
};
|
||||
});
|
||||
|
||||
if (children[childKey]) {
|
||||
children[childKey] = {
|
||||
...children[childKey],
|
||||
...actionHelpers,
|
||||
state: childRoute,
|
||||
router: childRouter,
|
||||
actions: actionCreators,
|
||||
getParam: createParamGetter(childRoute),
|
||||
};
|
||||
return children[childKey];
|
||||
}
|
||||
|
||||
const childSubscriber = getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
childKey
|
||||
);
|
||||
|
||||
children[childKey] = {
|
||||
...actionHelpers,
|
||||
|
||||
state: childRoute,
|
||||
router: childRouter,
|
||||
actions: actionCreators,
|
||||
getParam: createParamGetter(childRoute),
|
||||
|
||||
getChildNavigation: grandChildKey =>
|
||||
getChildNavigation(children[childKey], grandChildKey, () => {
|
||||
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;
|
||||
}
|
||||
if (routes[index].key === childKey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
dispatch: navigation.dispatch,
|
||||
getScreenProps: navigation.getScreenProps,
|
||||
dangerouslyGetParent: getCurrentParentNavigation,
|
||||
addListener: childSubscriber.addListener,
|
||||
};
|
||||
return children[childKey];
|
||||
}
|
||||
|
||||
export default getChildNavigation;
|
||||
9
src/getChildRouter.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function getChildRouter(router, routeName) {
|
||||
if (router.childRouters && router.childRouters[routeName]) {
|
||||
return router.childRouters[routeName];
|
||||
}
|
||||
|
||||
const Component = router.getComponentForRouteName(routeName);
|
||||
|
||||
return Component.router;
|
||||
}
|
||||
54
src/getNavigation.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import getChildNavigation from './getChildNavigation';
|
||||
|
||||
export default function getNavigation(
|
||||
router,
|
||||
state,
|
||||
dispatch,
|
||||
actionSubscribers,
|
||||
getScreenProps,
|
||||
getCurrentNavigation
|
||||
) {
|
||||
const actions = router.getActionCreators(state, null);
|
||||
|
||||
const navigation = {
|
||||
actions,
|
||||
router,
|
||||
state,
|
||||
dispatch,
|
||||
getScreenProps,
|
||||
getChildNavigation: childKey =>
|
||||
getChildNavigation(navigation, childKey, getCurrentNavigation),
|
||||
isFocused: childKey => {
|
||||
const { routes, index } = getCurrentNavigation().state;
|
||||
if (childKey == null || routes[index].key === childKey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
addListener: (eventName, handler) => {
|
||||
if (eventName !== 'action') {
|
||||
return { remove: () => {} };
|
||||
}
|
||||
actionSubscribers.add(handler);
|
||||
return {
|
||||
remove: () => {
|
||||
actionSubscribers.delete(handler);
|
||||
},
|
||||
};
|
||||
},
|
||||
dangerouslyGetParent: () => null,
|
||||
};
|
||||
|
||||
const actionCreators = {
|
||||
...getNavigationActionCreators(navigation.state),
|
||||
...actions,
|
||||
};
|
||||
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
navigation[actionName] = (...args) =>
|
||||
navigation.dispatch(actionCreators[actionName](...args));
|
||||
});
|
||||
|
||||
return navigation;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import DrawerNavigator from '../createDrawerNavigator';
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: `Welcome ${
|
||||
navigation.state.params ? navigation.state.params.user : 'anonymous'
|
||||
}`,
|
||||
gesturesEnabled: true,
|
||||
});
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const routeConfig = {
|
||||
Home: {
|
||||
screen: HomeScreen,
|
||||
},
|
||||
};
|
||||
|
||||
describe('DrawerNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MyDrawerNavigator = DrawerNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyDrawerNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
40
src/navigators/__tests__/NestedNavigator-test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import StackNavigator from '../createContainedStackNavigator';
|
||||
|
||||
const SubNavigator = StackNavigator({
|
||||
Home: {
|
||||
screen: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const NavNestedDirect = StackNavigator({
|
||||
Sub: {
|
||||
screen: SubNavigator,
|
||||
},
|
||||
});
|
||||
|
||||
const NavNestedIndirect = StackNavigator({
|
||||
Sub: {
|
||||
// eslint-disable-next-line react/display-name
|
||||
screen: props => <SubNavigator {...props} />,
|
||||
},
|
||||
});
|
||||
|
||||
/* Prevent React error boundaries from swallowing the errors */
|
||||
NavNestedIndirect.prototype.componentDidCatch = null;
|
||||
SubNavigator.prototype.componentDidCatch = null;
|
||||
|
||||
describe('Nested navigators', () => {
|
||||
it('renders succesfully as direct child', () => {
|
||||
const rendered = renderer.create(<NavNestedDirect />).toJSON();
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('throw when trying to pass navigation prop', () => {
|
||||
const tryRender = () => {
|
||||
renderer.create(<NavNestedIndirect />);
|
||||
};
|
||||
expect(tryRender).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import StackNavigator from '../createStackNavigator';
|
||||
import StackNavigator from '../createContainedStackNavigator';
|
||||
import withNavigation from '../../views/withNavigation';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';
|
||||
|
||||
|
||||
@@ -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,243 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DrawerNavigator renders successfully 1`] = `
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"zIndex": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
accessibilityComponentType={undefined}
|
||||
accessibilityLabel={undefined}
|
||||
accessibilityTraits={undefined}
|
||||
accessible={true}
|
||||
collapsable={undefined}
|
||||
hitSlop={undefined}
|
||||
nativeID={undefined}
|
||||
onLayout={undefined}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
pointerEvents="none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"zIndex": 1000,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
/>
|
||||
<View
|
||||
accessibilityViewIsModal={false}
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "white",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": null,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": -320,
|
||||
},
|
||||
],
|
||||
"width": 320,
|
||||
"zIndex": 1001,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={undefined}
|
||||
alwaysBounceVertical={false}
|
||||
onContentSizeChange={null}
|
||||
onMomentumScrollBegin={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={undefined}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onScrollShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onTouchCancel={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
scrollEventThrottle={undefined}
|
||||
sendMomentumEvents={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "column",
|
||||
"flexGrow": 1,
|
||||
"flexShrink": 1,
|
||||
"overflow": "scroll",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollContentView
|
||||
collapsable={false}
|
||||
removeClippedSubviews={undefined}
|
||||
style={
|
||||
Array [
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"paddingVertical": 4,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityComponentType={undefined}
|
||||
accessibilityLabel={undefined}
|
||||
accessibilityTraits={undefined}
|
||||
accessible={true}
|
||||
collapsable={undefined}
|
||||
hasTVPreferredFocus={undefined}
|
||||
hitSlop={undefined}
|
||||
isTVSelectable={true}
|
||||
nativeID={undefined}
|
||||
onLayout={undefined}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
tvParallaxProperties={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(0, 0, 0, .04)",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
ellipsizeMode="tail"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
"margin": 16,
|
||||
},
|
||||
Object {
|
||||
"color": "#2196f3",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollContentView>
|
||||
</RCTScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -0,0 +1,371 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`Nested navigators throw when trying to pass navigation prop 1`] = `"No \\"routes\\" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop"`;
|
||||
@@ -76,50 +76,37 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -127,70 +114,89 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -276,50 +282,37 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -327,52 +320,71 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
<View
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
exports[`TabNavigator renders successfully 1`] = `
|
||||
<View
|
||||
loaded={
|
||||
Array [
|
||||
0,
|
||||
]
|
||||
}
|
||||
onLayout={[Function]}
|
||||
collapsable={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
@@ -21,56 +16,65 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "stretch",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"alignItems": "stretch",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
9
src/navigators/createContainedStackNavigator.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createStackNavigator from './createStackNavigator';
|
||||
|
||||
const StackNavigator = (routeConfigs, config = {}) => {
|
||||
const navigator = createStackNavigator(routeConfigs, config);
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
export default StackNavigator;
|
||||
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,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dimensions, Platform, ScrollView } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import createNavigator from './createNavigator';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import DrawerRouter from '../routers/DrawerRouter';
|
||||
import DrawerScreen from '../views/Drawer/DrawerScreen';
|
||||
import DrawerView from '../views/Drawer/DrawerView';
|
||||
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
|
||||
|
||||
// A stack navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
|
||||
const defaultContentComponent = props => (
|
||||
<ScrollView alwaysBounceVertical={false}>
|
||||
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
|
||||
<DrawerItems {...props} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const DefaultDrawerConfig = {
|
||||
drawerWidth: () => {
|
||||
/*
|
||||
* Default drawer width is screen width - header height
|
||||
* with a max width of 280 on mobile and 320 on tablet
|
||||
* https://material.io/guidelines/patterns/navigation-drawer.html
|
||||
*/
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const smallerAxisSize = Math.min(height, width);
|
||||
const isLandscape = width > height;
|
||||
const isTablet = smallerAxisSize >= 600;
|
||||
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
|
||||
const maxWidth = isTablet ? 320 : 280;
|
||||
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
|
||||
},
|
||||
contentComponent: defaultContentComponent,
|
||||
drawerPosition: 'left',
|
||||
drawerBackgroundColor: 'white',
|
||||
useNativeAnimations: true,
|
||||
};
|
||||
|
||||
const DrawerNavigator = (routeConfigs, config = {}) => {
|
||||
const mergedConfig = { ...DefaultDrawerConfig, ...config };
|
||||
|
||||
const {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
...drawerConfig
|
||||
} = mergedConfig;
|
||||
|
||||
const routerConfig = {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
};
|
||||
|
||||
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
|
||||
|
||||
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
export default DrawerNavigator;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
|
||||
@@ -7,81 +8,37 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
static router = router;
|
||||
static navigationOptions = null;
|
||||
|
||||
childEventSubscribers = {};
|
||||
|
||||
// Cleanup subscriptions for routes that no longer exist
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
|
||||
Object.keys(this.childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this.childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove all subscription references
|
||||
componentWillUnmount() {
|
||||
this.childEventSubscribers = {};
|
||||
}
|
||||
|
||||
_isRouteFocused = route => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
state = {
|
||||
descriptors: {},
|
||||
screenProps: this.props.screenProps,
|
||||
};
|
||||
|
||||
_dangerouslyGetParent = () => {
|
||||
return this.props.navigation;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, screenProps } = this.props;
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const prevDescriptors = prevState.descriptors;
|
||||
const { navigation, screenProps } = nextProps;
|
||||
const { dispatch, state, addListener } = navigation;
|
||||
const { routes } = state;
|
||||
if (typeof routes === 'undefined') {
|
||||
throw new TypeError(
|
||||
'No "routes" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop'
|
||||
);
|
||||
}
|
||||
|
||||
const descriptors = { ...prevState.descriptors };
|
||||
|
||||
const descriptors = {};
|
||||
routes.forEach(route => {
|
||||
if (
|
||||
prevDescriptors &&
|
||||
prevDescriptors[route.key] &&
|
||||
route === prevDescriptors[route.key].state &&
|
||||
screenProps === prevState.screenProps
|
||||
) {
|
||||
descriptors[route.key] = prevDescriptors[route.key];
|
||||
return;
|
||||
}
|
||||
const getComponent = () =>
|
||||
router.getComponentForRouteName(route.routeName);
|
||||
|
||||
if (!this.childEventSubscribers[route.key]) {
|
||||
this.childEventSubscribers[route.key] = getChildEventSubscriber(
|
||||
addListener,
|
||||
route.key
|
||||
);
|
||||
}
|
||||
|
||||
const actionCreators = {
|
||||
...navigation.actions,
|
||||
...router.getActionCreators(route, state.key),
|
||||
};
|
||||
const actionHelpers = {};
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
actionHelpers[actionName] = (...args) => {
|
||||
const actionCreator = actionCreators[actionName];
|
||||
const action = actionCreator(...args);
|
||||
dispatch(action);
|
||||
};
|
||||
});
|
||||
const childNavigation = {
|
||||
...actionHelpers,
|
||||
actions: actionCreators,
|
||||
dispatch,
|
||||
state: route,
|
||||
isFocused: () => this._isRouteFocused(route),
|
||||
dangerouslyGetParent: this._dangerouslyGetParent,
|
||||
addListener: this.childEventSubscribers[route.key].addListener,
|
||||
getParam: (paramName, defaultValue) => {
|
||||
const params = route.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
};
|
||||
|
||||
const childNavigation = navigation.getChildNavigation(route.key);
|
||||
const options = router.getScreenOptions(childNavigation, screenProps);
|
||||
descriptors[route.key] = {
|
||||
key: route.key,
|
||||
@@ -92,18 +49,23 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
};
|
||||
});
|
||||
|
||||
return { descriptors, screenProps };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NavigatorView
|
||||
{...this.props}
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
screenProps={this.state.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
navigationConfig={navigationConfig}
|
||||
descriptors={descriptors}
|
||||
descriptors={this.state.descriptors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return Navigator;
|
||||
|
||||
return polyfill(Navigator);
|
||||
}
|
||||
|
||||
export default createNavigator;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
|
||||
import createNavigator from './createNavigator';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
@@ -13,6 +11,7 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
paths,
|
||||
navigationOptions,
|
||||
disableKeyboardHandling,
|
||||
getCustomActionCreators,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
@@ -21,6 +20,7 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
getCustomActionCreators,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
@@ -31,8 +31,7 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
Navigator = createKeyboardAwareNavigator(Navigator);
|
||||
}
|
||||
|
||||
// HOC to provide the navigation prop for the top-level navigator (when the prop is missing)
|
||||
return createNavigationContainer(Navigator);
|
||||
return Navigator;
|
||||
}
|
||||
|
||||
export default createStackNavigator;
|
||||
|
||||
@@ -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;
|
||||
|
||||
32
src/react-navigation.js
vendored
@@ -8,37 +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 createStackNavigator() {
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
return require('./navigators/createContainedStackNavigator').default;
|
||||
},
|
||||
get StackNavigator() {
|
||||
console.warn(
|
||||
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
|
||||
);
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
return require('./navigators/createContainedStackNavigator').default;
|
||||
},
|
||||
get createSwitchNavigator() {
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
return require('./navigators/createContainedSwitchNavigator').default;
|
||||
},
|
||||
get SwitchNavigator() {
|
||||
console.warn(
|
||||
'The SwitchNavigator function name is deprecated, please use createSwitchNavigator instead'
|
||||
);
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
return require('./navigators/createContainedSwitchNavigator').default;
|
||||
},
|
||||
get createDrawerNavigator() {
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
return require('react-navigation-drawer').createDrawerNavigator;
|
||||
},
|
||||
get DrawerNavigator() {
|
||||
console.warn(
|
||||
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
|
||||
);
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
return require('react-navigation-drawer').createDrawerNavigator;
|
||||
},
|
||||
get createTabNavigator() {
|
||||
console.warn(
|
||||
@@ -69,7 +72,7 @@ module.exports = {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
return require('react-navigation-drawer').DrawerActions;
|
||||
},
|
||||
|
||||
// Routers
|
||||
@@ -79,6 +82,9 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get DrawerRouter() {
|
||||
return require('react-navigation-drawer').DrawerRouter;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
@@ -116,10 +122,13 @@ module.exports = {
|
||||
|
||||
// DrawerView
|
||||
get DrawerView() {
|
||||
return require('./views/Drawer/DrawerView').default;
|
||||
return require('react-navigation-drawer').DrawerView;
|
||||
},
|
||||
get DrawerItems() {
|
||||
return require('./views/Drawer/DrawerNavigatorItems').default;
|
||||
return require('react-navigation-drawer').DrawerNavigatorItems;
|
||||
},
|
||||
get DrawerSidebar() {
|
||||
return require('react-navigation-drawer').DrawerSidebar;
|
||||
},
|
||||
|
||||
// TabView
|
||||
@@ -147,6 +156,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() {
|
||||
@@ -36,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,28 +0,0 @@
|
||||
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
|
||||
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
|
||||
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
|
||||
|
||||
const openDrawer = payload => ({
|
||||
type: OPEN_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const closeDrawer = payload => ({
|
||||
type: CLOSE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const toggleDrawer = payload => ({
|
||||
type: TOGGLE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
export default {
|
||||
OPEN_DRAWER,
|
||||
CLOSE_DRAWER,
|
||||
TOGGLE_DRAWER,
|
||||
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
toggleDrawer,
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import SwitchRouter from './SwitchRouter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
import invariant from '../utils/invariant';
|
||||
import withDefaultValue from '../utils/withDefaultValue';
|
||||
|
||||
import DrawerActions from './DrawerActions';
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
config = { ...config };
|
||||
config = withDefaultValue(config, 'resetOnBlur', false);
|
||||
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
|
||||
|
||||
const switchRouter = SwitchRouter(routeConfigs, config);
|
||||
|
||||
return {
|
||||
...switchRouter,
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
openDrawer: () => DrawerActions.openDrawer({ key: navStateKey }),
|
||||
closeDrawer: () => DrawerActions.closeDrawer({ key: navStateKey }),
|
||||
toggleDrawer: () => DrawerActions.toggleDrawer({ key: navStateKey }),
|
||||
...switchRouter.getActionCreators(route, navStateKey),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, lastState) {
|
||||
const state = lastState || {
|
||||
...switchRouter.getStateForAction(action, undefined),
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
|
||||
const isRouterTargeted = action.key == null || action.key === state.key;
|
||||
|
||||
if (isRouterTargeted) {
|
||||
// Only handle actions that are meant for this drawer, as specified by action.key.
|
||||
|
||||
if (action.type === DrawerActions.CLOSE_DRAWER && state.isDrawerOpen) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DrawerActions.OPEN_DRAWER && !state.isDrawerOpen) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DrawerActions.TOGGLE_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: !state.isDrawerOpen,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back on switch router for screen switching logic, and handling of child routers
|
||||
const switchedState = switchRouter.getStateForAction(action, state);
|
||||
|
||||
if (switchedState === null) {
|
||||
// The switch router or a child router is attempting to swallow this action. We return null to allow this.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (switchedState !== state) {
|
||||
if (switchedState.index !== state.index) {
|
||||
// If the tabs have changed, make sure to close the drawer
|
||||
return {
|
||||
...switchedState,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
// Return the state new state, as returned by the switch router.
|
||||
// The index hasn't changed, so this most likely means that a child router has returned a new state
|
||||
return switchedState;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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,39 +104,20 @@ 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,
|
||||
initialRouteName,
|
||||
initialRouteParams
|
||||
);
|
||||
|
||||
return {
|
||||
childRouters,
|
||||
|
||||
getComponentForState(state) {
|
||||
const activeChildRoute = state.routes[state.index];
|
||||
const { routeName } = activeChildRoute;
|
||||
@@ -164,7 +133,6 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, navStateKey),
|
||||
pop: (n, params) =>
|
||||
StackActions.pop({
|
||||
@@ -225,29 +193,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) {
|
||||
@@ -267,11 +233,18 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
);
|
||||
|
||||
if (nextRouteState === null || nextRouteState !== childRoute) {
|
||||
return StateUtils.replaceAndPrune(
|
||||
const newState = StateUtils.replaceAndPrune(
|
||||
state,
|
||||
nextRouteState ? nextRouteState.key : childRoute.key,
|
||||
nextRouteState ? nextRouteState : childRoute
|
||||
);
|
||||
return {
|
||||
...newState,
|
||||
isTransitioning:
|
||||
state.index !== newState.index
|
||||
? action.immediate !== true
|
||||
: state.isTransitioning,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,7 +365,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,126 +512,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,24 @@ 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,
|
||||
initialRouteName,
|
||||
initialRouteParams
|
||||
);
|
||||
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}'.` +
|
||||
@@ -74,48 +82,47 @@ export default (routeConfigs, config = {}) => {
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getInitialState() {
|
||||
const routes = order.map(resetChildRoute);
|
||||
function getNextState(prevState, possibleNextState) {
|
||||
if (!prevState) {
|
||||
return possibleNextState;
|
||||
}
|
||||
|
||||
let nextState;
|
||||
if (prevState.index !== possibleNextState.index && resetOnBlur) {
|
||||
const prevRouteName = prevState.routes[prevState.index].routeName;
|
||||
const nextRoutes = [...possibleNextState.routes];
|
||||
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
|
||||
|
||||
return {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
...possibleNextState,
|
||||
routes: nextRoutes,
|
||||
};
|
||||
},
|
||||
} else {
|
||||
nextState = possibleNextState;
|
||||
}
|
||||
|
||||
getNextState(prevState, possibleNextState) {
|
||||
if (!prevState) {
|
||||
return possibleNextState;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
let nextState;
|
||||
if (prevState.index !== possibleNextState.index && resetOnBlur) {
|
||||
const prevRouteName = prevState.routes[prevState.index].routeName;
|
||||
const nextRoutes = [...possibleNextState.routes];
|
||||
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
|
||||
function getInitialState() {
|
||||
const routes = order.map(resetChildRoute);
|
||||
return {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...possibleNextState,
|
||||
routes: nextRoutes,
|
||||
};
|
||||
} else {
|
||||
nextState = possibleNextState;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
},
|
||||
return {
|
||||
childRouters,
|
||||
|
||||
getActionCreators(route, stateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, stateKey),
|
||||
};
|
||||
return getCustomActionCreators(route, stateKey);
|
||||
},
|
||||
|
||||
getStateForAction(action, inputState) {
|
||||
let prevState = inputState ? { ...inputState } : inputState;
|
||||
let state = inputState || this.getInitialState();
|
||||
let state = inputState || getInitialState();
|
||||
let activeChildIndex = state.index;
|
||||
|
||||
if (action.type === NavigationActions.INIT) {
|
||||
@@ -152,7 +159,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,
|
||||
});
|
||||
@@ -189,7 +196,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: {
|
||||
@@ -202,7 +209,7 @@ export default (routeConfigs, config = {}) => {
|
||||
if (newChildState && newChildState !== childState) {
|
||||
const routes = [...state.routes];
|
||||
routes[activeChildIndex] = newChildState;
|
||||
return this.getNextState(prevState, {
|
||||
return getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
index: activeChildIndex,
|
||||
@@ -230,7 +237,7 @@ export default (routeConfigs, config = {}) => {
|
||||
...lastRoute,
|
||||
params,
|
||||
};
|
||||
return this.getNextState(prevState, {
|
||||
return getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
@@ -238,7 +245,7 @@ export default (routeConfigs, config = {}) => {
|
||||
}
|
||||
|
||||
if (activeChildIndex !== state.index) {
|
||||
return this.getNextState(prevState, {
|
||||
return getNextState(prevState, {
|
||||
...state,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
@@ -282,7 +289,7 @@ export default (routeConfigs, config = {}) => {
|
||||
}
|
||||
|
||||
if (index !== state.index || routes !== state.routes) {
|
||||
return this.getNextState(prevState, {
|
||||
return getNextState(prevState, {
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
@@ -311,73 +318,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(
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
/* eslint react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
import DrawerRouter from '../DrawerRouter';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
|
||||
describe('DrawerRouter', () => {
|
||||
test('Handles basic tab logic', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
],
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
expect(state).toEqual(expectedState);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
|
||||
state
|
||||
);
|
||||
const expectedState2 = {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
],
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
expect(state2).toEqual(expectedState2);
|
||||
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
|
||||
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
||||
});
|
||||
|
||||
test('Drawer opens closes and toggles', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.CLOSE_DRAWER },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(false);
|
||||
const state4 = router.getStateForAction(
|
||||
{ type: DrawerActions.TOGGLE_DRAWER },
|
||||
state3
|
||||
);
|
||||
expect(state4.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
|
||||
test('Drawer opens closes with key targeted', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(false);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Nested routers bubble up blocked actions', () => {
|
||||
const ScreenA = () => <div />;
|
||||
ScreenA.router = {
|
||||
getStateForAction(action, lastState) {
|
||||
if (action.type === 'CHILD_ACTION') return null;
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
|
||||
const state2 = router.getStateForAction({ type: 'CHILD_ACTION' }, state);
|
||||
expect(state2).toEqual(null);
|
||||
});
|
||||
|
||||
test('Drawer stays open when child routers return new state', () => {
|
||||
const ScreenA = () => <div />;
|
||||
ScreenA.router = {
|
||||
getStateForAction(action, lastState = { changed: false }) {
|
||||
if (action.type === 'CHILD_ACTION')
|
||||
return { ...lastState, changed: true };
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
});
|
||||
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
|
||||
const state3 = router.getStateForAction({ type: 'CHILD_ACTION' }, state2);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
expect(state3.routes[0].changed).toEqual(true);
|
||||
});
|
||||
299
src/routers/__tests__/PathHandling-test.js
Normal file
@@ -0,0 +1,299 @@
|
||||
/* 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 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 performRouterTest = createTestRouter => {
|
||||
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', () => {
|
||||
const router = createTestRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{ initialRouteName: 'Bar', initialRouteParams: { foo: 42 } }
|
||||
);
|
||||
const action = router.getActionForPathAndParams('');
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
params: { foo: 42 },
|
||||
});
|
||||
const state = router.getStateForAction(action);
|
||||
expect(state.routes[state.index]).toEqual(
|
||||
expect.objectContaining({
|
||||
routeName: 'Bar',
|
||||
params: { foo: 42 },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
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 = 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('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');
|
||||
});
|
||||
};
|
||||
|
||||
describe('Path handling for stack router', () => {
|
||||
performRouterTest(StackRouter);
|
||||
});
|
||||
describe('Path handling for switch router', () => {
|
||||
performRouterTest(SwitchRouter);
|
||||
});
|
||||
@@ -16,6 +16,7 @@ beforeEach(() => {
|
||||
const ROUTERS = {
|
||||
TabRouter,
|
||||
StackRouter,
|
||||
SwitchRouter,
|
||||
};
|
||||
|
||||
const dummyEventSubscriber = (name, handler) => ({
|
||||
@@ -26,7 +27,7 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
const Router = ROUTERS[routerName];
|
||||
|
||||
describe(`General router features - ${routerName}`, () => {
|
||||
test('title is configurable using navigationOptions and getScreenOptions', () => {
|
||||
test(`title is configurable using navigationOptions and getScreenOptions - ${routerName}`, () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
@@ -87,6 +88,31 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
).title
|
||||
).toEqual('Baz-123');
|
||||
});
|
||||
|
||||
test(`set params works in ${routerName}`, () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const router = Router({
|
||||
Foo: { screen: FooView },
|
||||
Bar: { screen: FooView },
|
||||
});
|
||||
|
||||
const initState = router.getStateForAction(NavigationActions.init());
|
||||
const initRoute = initState.routes[initState.index];
|
||||
expect(initRoute.params).toEqual(undefined);
|
||||
|
||||
const state0 = router.getStateForAction(
|
||||
NavigationActions.setParams({
|
||||
params: { foo: 42 },
|
||||
key: initRoute.key,
|
||||
}),
|
||||
initState
|
||||
);
|
||||
expect(state0.routes[state0.index].params.foo).toEqual(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -313,9 +324,11 @@ describe('StackRouter', () => {
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'auth',
|
||||
params: {},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'login',
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -426,12 +439,47 @@ describe('StackRouter', () => {
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('Handle navigation to nested navigator', () => {
|
||||
const state = TestStackRouter.getStateForAction({
|
||||
type: NavigationActions.INIT,
|
||||
});
|
||||
const action = TestStackRouter.getActionForPathAndParams('fo/22/b/hello');
|
||||
/* $FlowFixMe */
|
||||
const state2 = TestStackRouter.getStateForAction(action);
|
||||
expect(state2).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'id-4',
|
||||
isTransitioning: false,
|
||||
routeName: 'foo',
|
||||
params: {
|
||||
fooThing: '22',
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
routeName: 'bar',
|
||||
key: 'id-3',
|
||||
params: {
|
||||
barThing: 'hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('popToTop bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
@@ -657,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);
|
||||
@@ -953,6 +1002,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 />;
|
||||
@@ -1011,6 +1097,48 @@ 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.id).toEqual('123');
|
||||
expect(params.bazId).toEqual('321');
|
||||
});
|
||||
|
||||
test('Handle goBack identified by key', () => {
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
@@ -1450,6 +1578,34 @@ describe('StackRouter', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('Navigate action to previous nested StackRouter causes isTransitioning start', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Bar: { screen: ChildNavigator },
|
||||
Foo: { screen: () => <div /> },
|
||||
});
|
||||
const state = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
immediate: true,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
router.getStateForAction({ type: NavigationActions.INIT })
|
||||
);
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(0);
|
||||
expect(state2.isTransitioning).toEqual(true);
|
||||
});
|
||||
|
||||
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
|
||||
const state = TestStackRouter.getStateForAction({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1570,400 +1726,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
@@ -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' });
|
||||
});
|
||||
172
src/routers/pathUtils.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
const queryString = require('query-string');
|
||||
|
||||
function isEmpty(obj) {
|
||||
if (!obj) return true;
|
||||
for (let key in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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 = {},
|
||||
initialRouteName,
|
||||
initialRouteParams
|
||||
) => {
|
||||
const pathsByRouteNames = {};
|
||||
let paths = [];
|
||||
|
||||
// Build paths for each route
|
||||
Object.keys(childRouters).forEach(routeName => {
|
||||
let pathPattern = pathConfigs[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 if (pathPattern === null) {
|
||||
// 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, pathPattern };
|
||||
});
|
||||
|
||||
paths = Object.entries(pathsByRouteNames);
|
||||
paths.sort((a, b) => b[1].priority - a[1].priority);
|
||||
|
||||
const 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, ...initialRouteParams },
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt to match `pathToResolve` 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(pathToResolve);
|
||||
if (pathMatch && pathMatch.length) {
|
||||
pathMatchKeys = keys;
|
||||
matchedRouteName = routeName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't match -- return null to signify no action available
|
||||
if (!matchedRouteName) {
|
||||
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;
|
||||
if (childRouters[matchedRouteName]) {
|
||||
nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
|
||||
pathMatch.slice(pathMatchKeys.length).join('/'),
|
||||
inputParams
|
||||
);
|
||||
if (!nestedAction) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 NavigationActions.navigate({
|
||||
routeName: matchedRouteName,
|
||||
...(params ? { params } : {}),
|
||||
...(nestedAction ? { action: nestedAction } : {}),
|
||||
});
|
||||
};
|
||||
const getPathAndParamsForRoute = route => {
|
||||
const { routeName, params } = route;
|
||||
const childRouter = childRouters[routeName];
|
||||
const subPath = pathsByRouteNames[routeName].toPath(params);
|
||||
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 ? { ...params, ...child.params } : params,
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: subPath,
|
||||
params,
|
||||
};
|
||||
};
|
||||
return { getActionForPathAndParams, getPathAndParamsForRoute };
|
||||
};
|
||||
@@ -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,119 +0,0 @@
|
||||
import React from 'react';
|
||||
import { View, Text, Platform, StyleSheet } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
/**
|
||||
* Component that renders the navigation list in the drawer.
|
||||
*/
|
||||
const DrawerNavigatorItems = ({
|
||||
navigation: { state, navigate },
|
||||
items,
|
||||
activeItemKey,
|
||||
activeTintColor,
|
||||
activeBackgroundColor,
|
||||
inactiveTintColor,
|
||||
inactiveBackgroundColor,
|
||||
getLabel,
|
||||
renderIcon,
|
||||
onItemPress,
|
||||
itemsContainerStyle,
|
||||
itemStyle,
|
||||
labelStyle,
|
||||
activeLabelStyle,
|
||||
inactiveLabelStyle,
|
||||
iconContainerStyle,
|
||||
drawerPosition,
|
||||
}) => (
|
||||
<View style={[styles.container, itemsContainerStyle]}>
|
||||
{items.map((route, index) => {
|
||||
const focused = activeItemKey === route.key;
|
||||
const color = focused ? activeTintColor : inactiveTintColor;
|
||||
const backgroundColor = focused
|
||||
? activeBackgroundColor
|
||||
: inactiveBackgroundColor;
|
||||
const scene = { route, index, focused, tintColor: color };
|
||||
const icon = renderIcon(scene);
|
||||
const label = getLabel(scene);
|
||||
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
|
||||
return (
|
||||
<TouchableItem
|
||||
key={route.key}
|
||||
onPress={() => {
|
||||
onItemPress({ route, focused });
|
||||
}}
|
||||
delayPressIn={0}
|
||||
>
|
||||
<SafeAreaView
|
||||
style={{ backgroundColor }}
|
||||
forceInset={{
|
||||
[drawerPosition]: 'always',
|
||||
[drawerPosition === 'left' ? 'right' : 'left']: 'never',
|
||||
vertical: 'never',
|
||||
}}
|
||||
>
|
||||
<View style={[styles.item, itemStyle]}>
|
||||
{icon ? (
|
||||
<View
|
||||
style={[
|
||||
styles.icon,
|
||||
focused ? null : styles.inactiveIcon,
|
||||
iconContainerStyle,
|
||||
]}
|
||||
>
|
||||
{icon}
|
||||
</View>
|
||||
) : null}
|
||||
{typeof label === 'string' ? (
|
||||
<Text
|
||||
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</TouchableItem>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
/* Material design specs - https://material.io/guidelines/patterns/navigation-drawer.html#navigation-drawer-specs */
|
||||
DrawerNavigatorItems.defaultProps = {
|
||||
activeTintColor: '#2196f3',
|
||||
activeBackgroundColor: 'rgba(0, 0, 0, .04)',
|
||||
inactiveTintColor: 'rgba(0, 0, 0, .87)',
|
||||
inactiveBackgroundColor: 'transparent',
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingVertical: 4,
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
marginHorizontal: 16,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
inactiveIcon: {
|
||||
/*
|
||||
* Icons have 0.54 opacity according to guidelines
|
||||
* 100/87 * 54 ~= 62
|
||||
*/
|
||||
opacity: 0.62,
|
||||
},
|
||||
label: {
|
||||
margin: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
export default DrawerNavigatorItems;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import SceneView from '../SceneView';
|
||||
|
||||
/**
|
||||
* Component that renders the child screen of the drawer.
|
||||
*/
|
||||
class DrawerScreen extends React.PureComponent {
|
||||
render() {
|
||||
const { descriptors, navigation, screenProps } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
const descriptor = descriptors[routes[index].key];
|
||||
const Content = descriptor.getComponent();
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
component={Content}
|
||||
navigation={descriptor.navigation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerScreen;
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
/**
|
||||
* Component that renders the sidebar screen of the drawer.
|
||||
*/
|
||||
|
||||
class DrawerSidebar extends React.PureComponent {
|
||||
_getScreenOptions = routeKey => {
|
||||
const descriptor = this.props.descriptors[routeKey];
|
||||
invariant(
|
||||
descriptor.options,
|
||||
'Cannot access screen descriptor options from drawer sidebar'
|
||||
);
|
||||
return descriptor.options;
|
||||
};
|
||||
|
||||
_getLabel = ({ focused, tintColor, route }) => {
|
||||
const { drawerLabel, title } = this._getScreenOptions(route.key);
|
||||
if (drawerLabel) {
|
||||
return typeof drawerLabel === 'function'
|
||||
? drawerLabel({ tintColor, focused })
|
||||
: drawerLabel;
|
||||
}
|
||||
|
||||
if (typeof title === 'string') {
|
||||
return title;
|
||||
}
|
||||
|
||||
return route.routeName;
|
||||
};
|
||||
|
||||
_renderIcon = ({ focused, tintColor, route }) => {
|
||||
const { drawerIcon } = this._getScreenOptions(route.key);
|
||||
if (drawerIcon) {
|
||||
return typeof drawerIcon === 'function'
|
||||
? drawerIcon({ tintColor, focused })
|
||||
: drawerIcon;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
_onItemPress = ({ route, focused }) => {
|
||||
if (!focused) {
|
||||
let subAction;
|
||||
// TODO (v3): Revisit and repeal this behavior:
|
||||
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
|
||||
if (route.index != null && route.index !== 0) {
|
||||
subAction = StackActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
routeName: route.routes[0].routeName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
this.props.navigation.dispatch(
|
||||
NavigationActions.navigate({
|
||||
routeName: route.routeName,
|
||||
action: subAction,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const ContentComponent = this.props.contentComponent;
|
||||
if (!ContentComponent) {
|
||||
return null;
|
||||
}
|
||||
const { state } = this.props.navigation;
|
||||
invariant(typeof state.index === 'number', 'should be set');
|
||||
return (
|
||||
<View style={[styles.container, this.props.style]}>
|
||||
<ContentComponent
|
||||
{...this.props.contentOptions}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
items={state.routes}
|
||||
activeItemKey={
|
||||
state.routes[state.index] ? state.routes[state.index].key : null
|
||||
}
|
||||
screenProps={this.props.screenProps}
|
||||
getLabel={this._getLabel}
|
||||
renderIcon={this._renderIcon}
|
||||
onItemPress={this._onItemPress}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerSidebar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import DrawerLayout from 'react-native-drawer-layout-polyfill';
|
||||
|
||||
import DrawerSidebar from './DrawerSidebar';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
*/
|
||||
export default class DrawerView extends React.PureComponent {
|
||||
state = {
|
||||
drawerWidth:
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { isDrawerOpen } = this.props.navigation.state;
|
||||
const wasDrawerOpen = prevProps.navigation.state.isDrawerOpen;
|
||||
|
||||
if (isDrawerOpen && !wasDrawerOpen) {
|
||||
this._drawer.openDrawer();
|
||||
} else if (wasDrawerOpen && !isDrawerOpen) {
|
||||
this._drawer.closeDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
const { navigation } = this.props;
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (!isDrawerOpen) {
|
||||
navigation.dispatch({ type: DrawerActions.OPEN_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
const { navigation } = this.props;
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (isDrawerOpen) {
|
||||
navigation.dispatch({ type: DrawerActions.CLOSE_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_updateWidth = () => {
|
||||
const drawerWidth =
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth;
|
||||
|
||||
if (this.state.drawerWidth !== drawerWidth) {
|
||||
this.setState({ drawerWidth });
|
||||
}
|
||||
};
|
||||
|
||||
_renderNavigationView = () => {
|
||||
return (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
contentComponent={this.props.navigationConfig.contentComponent}
|
||||
contentOptions={this.props.navigationConfig.contentOptions}
|
||||
drawerPosition={this.props.navigationConfig.drawerPosition}
|
||||
style={this.props.navigationConfig.style}
|
||||
{...this.props.navigationConfig}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state } = this.props.navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = this.props.descriptors[activeKey];
|
||||
|
||||
const DrawerScreen = descriptor.getComponent();
|
||||
|
||||
const { drawerLockMode } = descriptor.options;
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={c => {
|
||||
this._drawer = c;
|
||||
}}
|
||||
drawerLockMode={
|
||||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
|
||||
this.props.navigationConfig.drawerLockMode
|
||||
}
|
||||
drawerBackgroundColor={
|
||||
this.props.navigationConfig.drawerBackgroundColor
|
||||
}
|
||||
drawerWidth={this.state.drawerWidth}
|
||||
onDrawerOpen={this._handleDrawerOpen}
|
||||
onDrawerClose={this._handleDrawerClose}
|
||||
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
|
||||
renderNavigationView={this._renderNavigationView}
|
||||
drawerPosition={
|
||||
this.props.navigationConfig.drawerPosition === 'right'
|
||||
? DrawerLayout.positions.Right
|
||||
: DrawerLayout.positions.Left
|
||||
}
|
||||
>
|
||||
<DrawerScreen
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={descriptor.navigation}
|
||||
/>
|
||||
</DrawerLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
View,
|
||||
I18nManager,
|
||||
ViewPropTypes,
|
||||
MaskedViewIOS,
|
||||
} from 'react-native';
|
||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
@@ -55,6 +55,15 @@ class Header extends React.PureComponent {
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
}
|
||||
|
||||
if (options.title && typeof options.title !== 'string' && __DEV__) {
|
||||
throw new Error(
|
||||
`Invalid title for route "${
|
||||
scene.route.routeName
|
||||
}" - title must be string or null, instead it was of type ${typeof options.title}`
|
||||
);
|
||||
}
|
||||
|
||||
return options.title;
|
||||
}
|
||||
|
||||
@@ -464,6 +473,18 @@ class Header extends React.PureComponent {
|
||||
flexShrink,
|
||||
flexBasis,
|
||||
flexWrap,
|
||||
position,
|
||||
padding,
|
||||
paddingHorizontal,
|
||||
paddingVertical,
|
||||
paddingTop,
|
||||
paddingRight,
|
||||
paddingBottom,
|
||||
paddingLeft,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
...safeHeaderStyle
|
||||
} = headerStyleObj;
|
||||
|
||||
@@ -476,6 +497,18 @@ class Header extends React.PureComponent {
|
||||
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
||||
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
||||
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
|
||||
warnIfHeaderStyleDefined(padding, 'padding');
|
||||
warnIfHeaderStyleDefined(position, 'position');
|
||||
warnIfHeaderStyleDefined(paddingVertical, 'paddingVertical');
|
||||
warnIfHeaderStyleDefined(paddingHorizontal, 'paddingHorizontal');
|
||||
warnIfHeaderStyleDefined(paddingTop, 'paddingTop');
|
||||
warnIfHeaderStyleDefined(paddingRight, 'paddingRight');
|
||||
warnIfHeaderStyleDefined(paddingBottom, 'paddingBottom');
|
||||
warnIfHeaderStyleDefined(paddingLeft, 'paddingLeft');
|
||||
warnIfHeaderStyleDefined(top, 'top');
|
||||
warnIfHeaderStyleDefined(right, 'right');
|
||||
warnIfHeaderStyleDefined(bottom, 'bottom');
|
||||
warnIfHeaderStyleDefined(left, 'left');
|
||||
}
|
||||
|
||||
// TODO: warn if any unsafe styles are provided
|
||||
@@ -491,7 +524,14 @@ class Header extends React.PureComponent {
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<Animated.View style={this.props.layoutInterpolator(this.props)}>
|
||||
<Animated.View
|
||||
style={[
|
||||
this.props.layoutInterpolator(this.props),
|
||||
Platform.OS === 'ios'
|
||||
? { backgroundColor: DEFAULT_BACKGROUND_COLOR }
|
||||
: null,
|
||||
]}
|
||||
>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
{options.headerBackground}
|
||||
@@ -504,7 +544,11 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
|
||||
function warnIfHeaderStyleDefined(value, styleProp) {
|
||||
if (value !== undefined) {
|
||||
if (styleProp === 'position' && value === 'absolute') {
|
||||
console.warn(
|
||||
"position: 'absolute' is not supported on headerStyle. If you would like to render content under the header, use the headerTransparent navigationOption."
|
||||
);
|
||||
} else if (value !== undefined) {
|
||||
console.warn(
|
||||
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
|
||||
);
|
||||
@@ -529,9 +573,11 @@ if (Platform.OS === 'ios') {
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
|
||||
backgroundColor: DEFAULT_BACKGROUND_COLOR,
|
||||
...platformContainerStyles,
|
||||
},
|
||||
transparentContainer: {
|
||||
|
||||
@@ -58,7 +58,15 @@ function forLayout(props) {
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const width = layout.initWidth;
|
||||
|
||||
// We really shouldn't render the scene at all until we know the width of the
|
||||
// stack. That said, in every case that I have ever seen, this has just been
|
||||
// the full width of the window. This won't continue to be true if we support
|
||||
// layouts like iPad master-detail. For now, in order to solve
|
||||
// https://github.com/react-navigation/react-navigation/issues/4264, I have
|
||||
// opted for the heuristic that we will use the window width until we have
|
||||
// measured (and they will usually be the same).
|
||||
const width = layout.initWidth || Dimensions.get('window').width;
|
||||
|
||||
// Make sure the header stays hidden when transitioning between 2 screens
|
||||
// with no header.
|
||||
|
||||
3
src/views/NavigationConsumer.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { NavigationConsumer } from './NavigationContext';
|
||||
|
||||
export default NavigationConsumer;
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import createReactContext from 'create-react-context';
|
||||
|
||||
const NavigationContext = createReactContext();
|
||||
|
||||
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
@@ -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 {
|
||||
|
||||
@@ -65,6 +65,17 @@ export default function ScenesReducer(
|
||||
prevState,
|
||||
descriptors
|
||||
) {
|
||||
// Always update the descriptors
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
// It will be resolved in a better way when we re-write Transitioner
|
||||
scenes.forEach(scene => {
|
||||
const { route } = scene;
|
||||
if (descriptors && descriptors[route.key]) {
|
||||
scene.descriptor = descriptors[route.key];
|
||||
}
|
||||
});
|
||||
|
||||
// Bail out early if we didn't update the state
|
||||
if (prevState === nextState) {
|
||||
return scenes;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import StackViewLayout from './StackViewLayout';
|
||||
import Transitioner from '../Transitioner';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
|
||||
@@ -22,11 +21,13 @@ class StackView extends React.Component {
|
||||
<Transitioner
|
||||
render={this._render}
|
||||
configureTransition={this._configureTransition}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
onTransitionStart={this.props.onTransitionStart}
|
||||
onTransitionEnd={(transition, lastTransition) => {
|
||||
const { onTransitionEnd, navigation } = this.props;
|
||||
const { navigationConfig, navigation } = this.props;
|
||||
const { onTransitionEnd } = navigationConfig;
|
||||
if (transition.navigation.state.isTransitioning) {
|
||||
navigation.dispatch(
|
||||
StackActions.completeTransition({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
@@ -21,7 +21,7 @@ import withOrientation from '../withOrientation';
|
||||
import { NavigationProvider } from '../NavigationContext';
|
||||
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
|
||||
@@ -68,6 +68,20 @@ const animatedSubscribeValue = animatedValue => {
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultHeaderHeight = isLandscape => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (isLandscape && !Platform.isPad) {
|
||||
return 32;
|
||||
} else if (IS_IPHONE_X) {
|
||||
return 88;
|
||||
} else {
|
||||
return 64;
|
||||
}
|
||||
} else {
|
||||
return 56;
|
||||
}
|
||||
};
|
||||
|
||||
class StackViewLayout extends React.Component {
|
||||
/**
|
||||
* Used to identify the starting point of the position when the gesture starts, such that it can
|
||||
@@ -89,10 +103,29 @@ class StackViewLayout extends React.Component {
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// Used when card's header is null and mode is float to make transition
|
||||
// between screens with headers and those without headers smooth.
|
||||
// This is not a great heuristic here. We don't know synchronously
|
||||
// on mount what the header height is so we have just used the most
|
||||
// common cases here.
|
||||
floatingHeaderHeight: getDefaultHeaderHeight(props.isLandscape),
|
||||
};
|
||||
}
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
const { options } = scene.descriptor;
|
||||
const { header } = options;
|
||||
|
||||
if (__DEV__ && typeof header === 'string') {
|
||||
throw new Error(
|
||||
`Invalid header value: "${header}". The header option must be a valid React component or null, not a string.`
|
||||
);
|
||||
}
|
||||
|
||||
if (header === null && headerMode === 'screen') {
|
||||
return null;
|
||||
}
|
||||
@@ -114,20 +147,24 @@ class StackViewLayout extends React.Component {
|
||||
const {
|
||||
mode,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
lastTransitionProps,
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
return renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
});
|
||||
return (
|
||||
<NavigationProvider value={scene.descriptor.navigation}>
|
||||
{renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
})}
|
||||
</NavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
@@ -145,10 +182,7 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
|
||||
_reset(resetToIndex, duration) {
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
) {
|
||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||
Animated.spring(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
stiffness: 5000,
|
||||
@@ -188,10 +222,7 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
) {
|
||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||
Animated.spring(position, {
|
||||
toValue,
|
||||
stiffness: 5000,
|
||||
@@ -211,6 +242,8 @@ class StackViewLayout extends React.Component {
|
||||
|
||||
_panResponder = PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
const { navigation } = this.props.transitionProps;
|
||||
const { index } = navigation.state;
|
||||
this._isResponding = false;
|
||||
this._reset(index, 0);
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
@@ -225,7 +258,7 @@ class StackViewLayout extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
position.stopAnimation((value: number) => {
|
||||
position.stopAnimation(value => {
|
||||
this._isResponding = true;
|
||||
this._gestureStartValue = value;
|
||||
});
|
||||
@@ -233,7 +266,7 @@ class StackViewLayout extends React.Component {
|
||||
},
|
||||
onMoveShouldSetPanResponder: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene, scenes },
|
||||
transitionProps: { navigation, layout, scene },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
@@ -389,30 +422,27 @@ class StackViewLayout extends React.Component {
|
||||
},
|
||||
});
|
||||
|
||||
_onFloatingHeaderLayout = e => {
|
||||
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
|
||||
};
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
|
||||
if (headerMode === 'float') {
|
||||
const { scene } = this.props.transitionProps;
|
||||
floatingHeader = (
|
||||
<NavigationProvider value={scene.descriptor.navigation}>
|
||||
<View pointerEvents="box-none" onLayout={this._onFloatingHeaderLayout}>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
</NavigationProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene, scenes },
|
||||
transitionProps: { scene, scenes },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
const gesturesEnabled =
|
||||
typeof options.gesturesEnabled === 'boolean'
|
||||
@@ -497,13 +527,14 @@ class StackViewLayout extends React.Component {
|
||||
return TransitionConfigs.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
this.props.transitionProps,
|
||||
this.props.prevTransitionProps,
|
||||
this.props.lastTransitionProps,
|
||||
isModal
|
||||
);
|
||||
};
|
||||
|
||||
_renderCard = scene => {
|
||||
const { screenInterpolator } = this._getTransitionConfig();
|
||||
|
||||
const style =
|
||||
screenInterpolator &&
|
||||
screenInterpolator({ ...this.props.transitionProps, scene });
|
||||
@@ -516,19 +547,7 @@ class StackViewLayout extends React.Component {
|
||||
const headerMode = this._getHeaderMode();
|
||||
let marginTop = 0;
|
||||
if (!hasHeader && headerMode === 'float') {
|
||||
const { isLandscape } = this.props;
|
||||
let headerHeight;
|
||||
if (Platform.OS === 'android') {
|
||||
// TODO: Need to handle translucent status bar.
|
||||
headerHeight = 56;
|
||||
} else if (isLandscape && !Platform.isPad) {
|
||||
headerHeight = 52;
|
||||
} else if (IS_IPHONE_X) {
|
||||
headerHeight = 88;
|
||||
} else {
|
||||
headerHeight = 64;
|
||||
}
|
||||
marginTop = -headerHeight;
|
||||
marginTop = -this.state.floatingHeaderHeight;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Animated, Easing, Platform } from 'react-native';
|
||||
import StyleInterpolator from './StackViewStyleInterpolator';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||
|
||||
let IOSTransitionSpec;
|
||||
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
|
||||
if (supportsImprovedSpringAnimation()) {
|
||||
// These are the exact values from UINavigationController's animation configuration
|
||||
IOSTransitionSpec = {
|
||||
timing: Animated.spring,
|
||||
|
||||
@@ -63,6 +63,12 @@ class Transitioner extends React.Component {
|
||||
nextScenes = filterStale(nextScenes);
|
||||
}
|
||||
|
||||
// Update nextScenes when we change screenProps
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
if (nextProps.screenProps !== this.props.screenProps) {
|
||||
this.setState({ nextScenes });
|
||||
}
|
||||
|
||||
if (nextScenes === this.state.scenes) {
|
||||
return;
|
||||
}
|
||||
@@ -90,6 +96,24 @@ class Transitioner extends React.Component {
|
||||
this._prevTransitionProps = this._transitionProps;
|
||||
this._transitionProps = buildTransitionProps(nextProps, nextState);
|
||||
|
||||
const toValue = nextProps.navigation.state.index;
|
||||
|
||||
if (!this._transitionProps.navigation.state.isTransitioning) {
|
||||
this.setState(nextState, async () => {
|
||||
const result = nextProps.onTransitionStart(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
progress.setValue(1);
|
||||
position.setValue(toValue);
|
||||
this._onTransitionEnd();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// get the transition spec.
|
||||
const transitionUserSpec = nextProps.configureTransition
|
||||
? nextProps.configureTransition(
|
||||
@@ -106,7 +130,6 @@ class Transitioner extends React.Component {
|
||||
const { timing } = transitionSpec;
|
||||
delete transitionSpec.timing;
|
||||
|
||||
const toValue = nextProps.navigation.state.index;
|
||||
const positionHasChanged = position.__getValue() !== toValue;
|
||||
|
||||
// if swiped back, indexHasChanged == true && positionHasChanged == false
|
||||
|
||||
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,5 +1,5 @@
|
||||
/* eslint react/display-name:0 */
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Transitioner from '../Transitioner';
|
||||
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
exports[`TabBarBottom renders successfully 1`] = `
|
||||
<View
|
||||
loaded={
|
||||
Array [
|
||||
0,
|
||||
]
|
||||
}
|
||||
onLayout={[Function]}
|
||||
collapsable={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
@@ -20,73 +15,67 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={false}
|
||||
alwaysBounceVertical={false}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
bounces={false}
|
||||
contentContainerStyle={
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
contentOffset={
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
}
|
||||
directionalLockEnabled={true}
|
||||
horizontal={true}
|
||||
keyboardDismissMode="on-drag"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={null}
|
||||
onMomentumScrollBegin={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={undefined}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onScrollShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onTouchCancel={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
overScrollMode="never"
|
||||
pagingEnabled={true}
|
||||
scrollEnabled={undefined}
|
||||
scrollEventThrottle={1}
|
||||
scrollsToTop={false}
|
||||
sendMomentumEvents={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"flexGrow": 1,
|
||||
"flexShrink": 1,
|
||||
"overflow": "scroll",
|
||||
},
|
||||
>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={false}
|
||||
alwaysBounceVertical={false}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
bounces={false}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollContentView
|
||||
collapsable={false}
|
||||
removeClippedSubviews={undefined}
|
||||
}
|
||||
}
|
||||
contentOffset={
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
}
|
||||
directionalLockEnabled={true}
|
||||
horizontal={true}
|
||||
keyboardDismissMode="on-drag"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={null}
|
||||
onMomentumScrollBegin={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={undefined}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onScrollShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onTouchCancel={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
overScrollMode="never"
|
||||
pagingEnabled={true}
|
||||
scrollEnabled={undefined}
|
||||
scrollEventThrottle={1}
|
||||
scrollsToTop={false}
|
||||
sendMomentumEvents={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"flexGrow": 1,
|
||||
"flexShrink": 1,
|
||||
"overflow": "scroll",
|
||||
},
|
||||
Object {
|
||||
"flex": 1,
|
||||
@@ -94,47 +83,62 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
<RCTScrollContentView
|
||||
collapsable={false}
|
||||
removeClippedSubviews={undefined}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
},
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
navigation={
|
||||
style={
|
||||
Object {
|
||||
"state": Object {
|
||||
"key": "s1",
|
||||
"routeName": "s1",
|
||||
},
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
screenProps={undefined}
|
||||
/>
|
||||
>
|
||||
<View
|
||||
navigation={
|
||||
Object {
|
||||
"state": Object {
|
||||
"key": "s1",
|
||||
"routeName": "s1",
|
||||
},
|
||||
}
|
||||
}
|
||||
screenProps={undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollContentView>
|
||||
</RCTScrollView>
|
||||
</RCTScrollContentView>
|
||||
</RCTScrollView>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 659 B |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 786 B After Width: | Height: | Size: 373 B |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 812 B |