mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-20 19:08:15 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d973a26edb | ||
|
|
852e7e1974 | ||
|
|
cd3707d64b | ||
|
|
3c36db455f | ||
|
|
ec52c884c5 | ||
|
|
c4b3f25a0f | ||
|
|
93642e16e7 | ||
|
|
1a76556290 | ||
|
|
12b21f052e | ||
|
|
c1f07dc167 | ||
|
|
bc04b31d01 | ||
|
|
35307c70be | ||
|
|
7e3f4f3bec | ||
|
|
cbd0958e6f | ||
|
|
cab4d71a5e | ||
|
|
108ac0e2a9 | ||
|
|
fa4fdb9c57 | ||
|
|
ebdd2da79f | ||
|
|
1fe11c100e | ||
|
|
c4b84f1d66 | ||
|
|
69f394be5b | ||
|
|
316e4991ac | ||
|
|
805064cb5e | ||
|
|
8f199980cb | ||
|
|
37ca6a92ca | ||
|
|
980e0409dc | ||
|
|
a00ba5918a | ||
|
|
ad6b25cff9 | ||
|
|
a69b67d6d2 | ||
|
|
dc436e4d01 | ||
|
|
fe95bdeee6 | ||
|
|
525528e38f | ||
|
|
9f5f3d994c | ||
|
|
e8c1833053 | ||
|
|
0921889f7a | ||
|
|
1951a3ac46 | ||
|
|
4e384f8057 | ||
|
|
3d06d19d6a | ||
|
|
30ef5ef72b | ||
|
|
c7fff52408 | ||
|
|
bc01a4cd57 | ||
|
|
cad3d70aed | ||
|
|
bb5719f438 | ||
|
|
3dd3f5b804 | ||
|
|
3d8d5a0634 | ||
|
|
54448ed070 | ||
|
|
369ac2b568 | ||
|
|
3dc592f679 | ||
|
|
4f93200c91 | ||
|
|
665736d754 | ||
|
|
5598c3e28f | ||
|
|
cde6e845cd | ||
|
|
fb8c712ad8 | ||
|
|
350b7e0aed | ||
|
|
de112565d3 | ||
|
|
acdd515c13 | ||
|
|
452a6d2004 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -25,7 +25,7 @@ Bugs with react-navigation must be reproducible *without any external libraries
|
||||
|
||||
### How to reproduce
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repoistory as that is outside of the scope of Rect Navigation.
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repoistory as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
|
||||
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -7,9 +7,92 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.9.0] - [2018-07-20](https://github.com/react-navigation/react-navigation/releases/tag/2.9.0)
|
||||
### Added
|
||||
- `headerLayoutPreset: 'center' | 'left'` to provide an easy solution for [questions like this](https://github.com/react-navigation/react-navigation/issues/4615).
|
||||
- `headerBackTitleEnabled` - this configuration option for stack navigator allows you to force back button titles to either be rendered or not (if you disagree with defaults for your platform and layout preset).
|
||||
|
||||
### Fixed
|
||||
- Android back button ripple is now appropriately sized (fixes [#3955](https://github.com/react-navigation/react-navigation/issues/3955)).
|
||||
- Respect header background color on container (fixes edge case where user depended on displaying content that was rendered behind the navigator, this particular behavior should not be depended on and may break in the future, but this change is still useful regardless).
|
||||
|
||||
|
||||
## [2.8.0] - [2018-07-19](https://github.com/react-navigation/react-navigation/releases/tag/2.8.0)
|
||||
### Added
|
||||
- `headerLeftContainerStyle`, `headerTitleContainerStyle`, and `headerRightContainerStyle` are exposed on `navigationOptions`. These properties allow you to customize the style of the container of `headerLeft`, `headerTitle` and `headerRight` components.
|
||||
|
||||
### Fixed
|
||||
- Fixed memory leaks in `createNavigator`: [closure scope leak](https://github.com/react-navigation/react-navigation/commit/1a765562905e93bbae0262dd20c2688221c999e8), and [clean up old descriptors](https://github.com/react-navigation/react-navigation/commit/93642e16e7ff029586b68ee732ec790504ee4862).
|
||||
|
||||
## [2.7.0] - [2018-07-17](https://github.com/react-navigation/react-navigation/releases/tag/2.7.0)
|
||||
### Added
|
||||
- The enableURLHandling prop on the top level navigator component allows you to disable deep linking handling. Currently it is always enabled. To disable it, `<RootNavigator enableURLHandling={false} />`
|
||||
|
||||
### Changed
|
||||
- StackNavigator.replace method no longer requires a key param. If the key is left undefined, the last screen in the stack will be replaced.
|
||||
|
||||
### Fixed
|
||||
- Support headerLeft component for the first screen in a stack (#4608).
|
||||
- Removed bottomBorder when `headerTransparent` is set to true.
|
||||
- Improve empty path and param handling in deep linking (#4671). This fixes issues with deep linking and fully tests the differences between path: '' and path: null. Empty string matches empty paths, and null path will let the child router handle paths at the same level. Also it makes sure that params are not duplicated between path and query when they are serialized with getPathAndParamsForState.
|
||||
- Fix onTransitionStart not being invoked when provided in navigator config.(#4100)
|
||||
- Rare case when users navigated back and forth quickly with exactly the right timing would cause a crash due to a scene being queued to transition, then clobbered, then attempted to render as a stale scene but without a descriptor. ([commit](https://github.com/react-navigation/react-navigation/commit/cab4d71a5e09188df3f4a294c98779eecb860a78))
|
||||
|
||||
## [2.6.2] - [2018-07-06](https://github.com/react-navigation/react-navigation/releases/tag/2.6.2)
|
||||
### Changed
|
||||
- Relax vertical padding warnings on header.
|
||||
|
||||
## [2.6.1] - [2018-07-05](https://github.com/react-navigation/react-navigation/releases/tag/2.6.1)
|
||||
### Added
|
||||
- Warn for more invalid headerStyle properties (padding, top/right/bottom/left, position).
|
||||
|
||||
### Fixed
|
||||
- Fixed missing header shadow on Android.
|
||||
|
||||
## [2.6.0] - [2018-07-04](https://github.com/react-navigation/react-navigation/releases/tag/2.6.0)
|
||||
### Added
|
||||
- [NavigationEvents](https://github.com/react-navigation/react-navigation/pull/4188) component as a declarative interface for subscribing to navigation focus events.
|
||||
|
||||
### Fixed
|
||||
- Fix stack router child router delegation priority (https://github.com/react-navigation/react-navigation/commit/e8c1833053e37d28f0ce505ff323565abf23b6a2)
|
||||
- Avoid crash when calling isFocused on old route (https://github.com/react-navigation/react-navigation/commit/0921889f7a3acfc6d6bcc4909d209eeeee985ba7)
|
||||
- Stack router no longer attempts to parse query params within path handling
|
||||
- Switch router now has exact same param treatment for URLs as stack router does
|
||||
|
||||
### Changed
|
||||
- Internally we no longer need to special case PlatformHelpers by platform as react-native-web handles the APIs we mocked out with it now.
|
||||
|
||||
## [2.5.5] - [2018-06-27](https://github.com/react-navigation/react-navigation/releases/tag/2.5.5)
|
||||
### Added
|
||||
- Throw error in development mode when header navigation option is set to a string - a common mistake that would otherwise result in a cryptic error message.
|
||||
- Throw error in development mode when title is not a string.
|
||||
|
||||
### Fixed
|
||||
- Delegate to child routers for more than just the top screen in the stack.
|
||||
- Update react-navigation-drawer to 0.4.3 to fix `initialRouteParams` option
|
||||
|
||||
## [2.5.4] - [2018-06-27](https://github.com/react-navigation/react-navigation/releases/tag/2.5.4)
|
||||
### Fixed
|
||||
- Header no longer sometimes flashes for 1 frame when using `header: null` on initial route of stack with floating header.
|
||||
- Export `createSwitchNavigator` in react-navigation.web.js
|
||||
|
||||
## [2.5.3] - [2018-06-23](https://github.com/react-navigation/react-navigation/releases/tag/2.5.3)
|
||||
### Fixed
|
||||
- `setParams` applies to the navigation object it is called on even if that is the navigation object for a navigation view (more details in https://github.com/react-navigation/react-navigation/issues/4497)
|
||||
|
||||
## [2.5.2] - [2018-06-23](https://github.com/react-navigation/react-navigation/releases/tag/2.5.2)
|
||||
### Fixed
|
||||
- Update react-navigation-drawer to fix regression in toggleDrawer
|
||||
|
||||
## [2.5.1] - [2018-06-22](https://github.com/react-navigation/react-navigation/releases/tag/2.5.1)
|
||||
### Fixed
|
||||
- `transitionConfig` in stack navigator no longer passes incorrect `fromTransitionProps` when navigating back
|
||||
|
||||
## [2.5.0] - [2018-06-22](https://github.com/react-navigation/react-navigation/releases/tag/2.5.0)
|
||||
### Changed
|
||||
- Refactor internals to make it play more nicely with web
|
||||
|
||||
### Fixed
|
||||
- `const defaultGetStateForAction = SwitchBasedNavigator.router.getStateForAction` no longer throws error.
|
||||
- Updated react-navigation-drawer to 0.4.1 which should fix issues related to automatically closing drawer when changing routes.
|
||||
|
||||
@@ -17,6 +100,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
### Changed
|
||||
- Improved examples
|
||||
|
||||
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.5.0...HEAD
|
||||
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.9.0...HEAD
|
||||
[2.9.0]: https://github.com/react-navigation/react-navigation/compare/2.8.0...2.9.0
|
||||
[2.8.0]: https://github.com/react-navigation/react-navigation/compare/2.7.0...2.8.0
|
||||
[2.7.0]: https://github.com/react-navigation/react-navigation/compare/2.6.2...2.7.0
|
||||
[2.6.2]: https://github.com/react-navigation/react-navigation/compare/2.6.1...2.6.2
|
||||
[2.6.1]: https://github.com/react-navigation/react-navigation/compare/2.6.0...2.6.1
|
||||
[2.6.0]: https://github.com/react-navigation/react-navigation/compare/2.5.5...2.6.0
|
||||
[2.5.5]: https://github.com/react-navigation/react-navigation/compare/2.5.4...2.5.5
|
||||
[2.5.4]: https://github.com/react-navigation/react-navigation/compare/2.5.3...2.5.4
|
||||
[2.5.3]: https://github.com/react-navigation/react-navigation/compare/2.5.2...2.5.3
|
||||
[2.5.2]: https://github.com/react-navigation/react-navigation/compare/2.5.1...2.5.2
|
||||
[2.5.1]: https://github.com/react-navigation/react-navigation/compare/2.5.0...2.5.1
|
||||
[2.5.0]: https://github.com/react-navigation/react-navigation/compare/2.4.1...2.5.0
|
||||
[2.4.1]: https://github.com/react-navigation/react-navigation/compare/2.4.0...2.4.1
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "27.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
"ttf",
|
||||
"mp4"
|
||||
]
|
||||
},
|
||||
"sdkVersion": "28.0.0",
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.reactnavigation.example",
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"package": "com.reactnavigation.example"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,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 = {
|
||||
@@ -126,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:
|
||||
@@ -166,6 +172,7 @@ const ExampleRoutes = {
|
||||
path: 'settings',
|
||||
},
|
||||
TabsWithNavigationFocus,
|
||||
TabsWithNavigationEvents,
|
||||
KeyboardHandlingExample,
|
||||
// This is commented out because it's rarely useful
|
||||
// InactiveStack,
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
} from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import { Platform, ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
SafeAreaView,
|
||||
@@ -24,6 +24,8 @@ import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
banner: React.Node,
|
||||
@@ -133,16 +135,16 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
this._s3.remove();
|
||||
}
|
||||
_onWF = a => {
|
||||
console.log('_willFocus HomeScreen', a);
|
||||
DEBUG && console.log('_willFocus HomeScreen', a);
|
||||
};
|
||||
_onDF = a => {
|
||||
console.log('_didFocus HomeScreen', a);
|
||||
DEBUG && console.log('_didFocus HomeScreen', a);
|
||||
};
|
||||
_onWB = a => {
|
||||
console.log('_willBlur HomeScreen', a);
|
||||
DEBUG && console.log('_willBlur HomeScreen', a);
|
||||
};
|
||||
_onDB = a => {
|
||||
console.log('_didBlur HomeScreen', a);
|
||||
DEBUG && console.log('_didBlur HomeScreen', a);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -231,18 +233,23 @@ MyProfileScreen.navigationOptions = props => {
|
||||
};
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
const SimpleStack = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
Profile: {
|
||||
path: 'people/:name',
|
||||
screen: MyProfileScreen,
|
||||
},
|
||||
Photos: {
|
||||
path: 'photos/:name',
|
||||
screen: MyPhotosScreen,
|
||||
},
|
||||
},
|
||||
Profile: {
|
||||
path: 'people/:name',
|
||||
screen: MyProfileScreen,
|
||||
},
|
||||
Photos: {
|
||||
path: 'photos/:name',
|
||||
screen: MyPhotosScreen,
|
||||
},
|
||||
});
|
||||
{
|
||||
// headerLayoutPreset: 'center',
|
||||
}
|
||||
);
|
||||
|
||||
export default SimpleStack;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Header, createStackNavigator } from 'react-navigation';
|
||||
@@ -231,6 +232,10 @@ const StackWithTranslucentHeader = createStackNavigator(
|
||||
headerTransitionPreset: 'uikit',
|
||||
navigationOptions: {
|
||||
headerTransparent: true,
|
||||
headerStyle: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#A7A7AA',
|
||||
},
|
||||
headerBackground: Platform.select({
|
||||
ios: <BlurView style={{ flex: 1 }} intensity={98} />,
|
||||
android: (
|
||||
|
||||
127
examples/NavigationPlayground/js/TabsWithNavigationEvents.js
Normal file
127
examples/NavigationPlayground/js/TabsWithNavigationEvents.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FlatList, SafeAreaView, StatusBar, Text, View } from 'react-native';
|
||||
import { NavigationEvents } from 'react-navigation';
|
||||
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
const Event = ({ event }) => (
|
||||
<View
|
||||
style={{
|
||||
borderColor: 'grey',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
padding: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Text>{event.type}</Text>
|
||||
<Text>
|
||||
{event.action.type.replace('Navigation/', '')}
|
||||
{event.action.routeName ? '=>' + event.action.routeName : ''}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon) => {
|
||||
class TabScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: name,
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? focusedIcon : icon}
|
||||
size={26}
|
||||
style={{ color: focused ? tintColor : '#ccc' }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
state = { eventLog: [] };
|
||||
|
||||
append = navigationEvent => {
|
||||
this.setState(({ eventLog }) => ({
|
||||
eventLog: eventLog.concat(navigationEvent),
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
margin: 10,
|
||||
marginTop: 30,
|
||||
fontSize: 30,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Events for tab {name}
|
||||
</Text>
|
||||
|
||||
<View style={{ flex: 1, width: '100%', marginTop: 10 }}>
|
||||
<FlatList
|
||||
data={this.state.eventLog}
|
||||
keyExtractor={item => `${this.state.eventLog.indexOf(item)}`}
|
||||
renderItem={({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 5,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: '#e4e4e4',
|
||||
}}
|
||||
>
|
||||
<Event event={item} />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<NavigationEvents
|
||||
onWillFocus={this.append}
|
||||
onDidFocus={this.append}
|
||||
onWillBlur={this.append}
|
||||
onDidBlur={this.append}
|
||||
/>
|
||||
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return TabScreen;
|
||||
};
|
||||
|
||||
const TabsWithNavigationEvents = createMaterialBottomTabNavigator(
|
||||
{
|
||||
One: {
|
||||
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
|
||||
},
|
||||
Two: {
|
||||
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
|
||||
},
|
||||
Three: {
|
||||
screen: createTabScreen(
|
||||
'Three',
|
||||
'numeric-3-box-outline',
|
||||
'numeric-3-box'
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
shifting: false,
|
||||
activeTintColor: '#F44336',
|
||||
}
|
||||
);
|
||||
|
||||
export default TabsWithNavigationEvents;
|
||||
@@ -2,23 +2,23 @@
|
||||
"name": "NavigationPlayground",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-native-scripts start",
|
||||
"eject": "react-native-scripts eject",
|
||||
"android": "react-native-scripts android",
|
||||
"ios": "react-native-scripts ios",
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"test": "flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^27.0.0",
|
||||
"expo": "^28.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-header-buttons": "^0.0.4",
|
||||
"react-navigation-material-bottom-tabs": "0.1.3",
|
||||
"react-navigation-material-bottom-tabs": "^0.3.0",
|
||||
"react-navigation-tabs": "^0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26,9 +26,8 @@
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.67.0",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
"react-test-renderer": "16.3.0-alpha.1"
|
||||
"jest-expo": "^28.0.0",
|
||||
"react-test-renderer": "16.3.1"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
25
flow/react-navigation.js
vendored
25
flow/react-navigation.js
vendored
@@ -184,7 +184,7 @@ declare module 'react-navigation' {
|
||||
| NavigationLeafRoute
|
||||
| NavigationStateRoute;
|
||||
|
||||
declare export type NavigationLeafRoute = {
|
||||
declare export type NavigationLeafRoute = {|
|
||||
/**
|
||||
* React's key used by some navigators. No need to specify these manually,
|
||||
* they will be defined by the router.
|
||||
@@ -204,10 +204,12 @@ declare module 'react-navigation' {
|
||||
* e.g. `{ car_id: 123 }` in a route that displays a car.
|
||||
*/
|
||||
params?: NavigationParams,
|
||||
};
|
||||
|};
|
||||
|
||||
declare export type NavigationStateRoute = NavigationLeafRoute &
|
||||
NavigationState;
|
||||
declare export type NavigationStateRoute = {|
|
||||
...NavigationLeafRoute,
|
||||
...$Exact<NavigationState>,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Router
|
||||
@@ -557,6 +559,21 @@ declare module 'react-navigation' {
|
||||
navigationOptions?: O,
|
||||
}>;
|
||||
|
||||
/**
|
||||
* NavigationEvents component
|
||||
*/
|
||||
|
||||
declare type _NavigationEventsProps = {
|
||||
navigation?: NavigationScreenProp<NavigationState>,
|
||||
onWillFocus?: NavigationEventCallback,
|
||||
onDidFocus?: NavigationEventCallback,
|
||||
onWillBlur?: NavigationEventCallback,
|
||||
onDidBlur?: NavigationEventCallback,
|
||||
};
|
||||
declare export var NavigationEvents: React$ComponentType<
|
||||
_NavigationEventsProps
|
||||
>;
|
||||
|
||||
/**
|
||||
* Navigation container
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "2.5.0",
|
||||
"version": "2.9.0",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
@@ -33,10 +33,11 @@
|
||||
"create-react-context": "^0.2.1",
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"query-string": "^6.1.0",
|
||||
"react-lifecycles-compat": "^3",
|
||||
"react-native-safe-area-view": "^0.8.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||
"react-navigation-drawer": "0.4.1",
|
||||
"react-navigation-drawer": "0.4.3",
|
||||
"react-navigation-tabs": "0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { AsyncStorage, Linking, Platform } from 'react-native';
|
||||
import { AsyncStorage, Linking, Platform, BackHandler } from 'react-native';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import { BackHandler } from './PlatformHelpers';
|
||||
import NavigationActions from './NavigationActions';
|
||||
import getNavigation from './getNavigation';
|
||||
import invariant from './utils/invariant';
|
||||
import docsUrl from './utils/docsUrl';
|
||||
import { urlToPathAndParams } from './routers/pathUtils';
|
||||
|
||||
function isStateful(props) {
|
||||
return !props.navigation;
|
||||
@@ -129,23 +129,12 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
}
|
||||
|
||||
_urlToPathAndParams(url) {
|
||||
const params = {};
|
||||
const delimiter = this.props.uriPrefix || '://';
|
||||
let path = url.split(delimiter)[1];
|
||||
if (typeof path === 'undefined') {
|
||||
path = url;
|
||||
} else if (path === '') {
|
||||
path = '/';
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
_handleOpenURL = ({ url }) => {
|
||||
const parsedUrl = this._urlToPathAndParams(url);
|
||||
const { enableURLHandling, uriPrefix } = this.props;
|
||||
if (enableURLHandling === false) {
|
||||
return;
|
||||
}
|
||||
const parsedUrl = urlToPathAndParams(url, uriPrefix);
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const action = Component.router.getActionForPathAndParams(path, params);
|
||||
@@ -214,11 +203,15 @@ export default function createNavigationContainer(Component) {
|
||||
Linking.addEventListener('url', this._handleOpenURL);
|
||||
|
||||
// Pull out anything that can impact state
|
||||
const { persistenceKey } = this.props;
|
||||
const startupStateJSON =
|
||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
||||
const url = await Linking.getInitialURL();
|
||||
const parsedUrl = url && this._urlToPathAndParams(url);
|
||||
const { persistenceKey, uriPrefix, enableURLHandling } = this.props;
|
||||
let parsedUrl = null;
|
||||
let startupStateJSON = null;
|
||||
if (enableURLHandling !== false) {
|
||||
startupStateJSON =
|
||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
||||
const url = await Linking.getInitialURL();
|
||||
parsedUrl = url && urlToPathAndParams(url, uriPrefix);
|
||||
}
|
||||
|
||||
// Initialize state. This must be done *after* any async code
|
||||
// so we don't end up with a different value for this.state.nav
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import getChildEventSubscriber from './getChildEventSubscriber';
|
||||
import getChildRouter from './getChildRouter';
|
||||
import getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import invariant from './utils/invariant';
|
||||
|
||||
const createParamGetter = route => (paramName, defaultValue) => {
|
||||
@@ -18,6 +19,10 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
||||
|
||||
const childRoute = navigation.state.routes.find(r => r.key === childKey);
|
||||
|
||||
if (!childRoute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children[childKey] && children[childKey].state === childRoute) {
|
||||
return children[childKey];
|
||||
}
|
||||
@@ -40,7 +45,9 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
||||
...(childRouter
|
||||
? childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key)
|
||||
: {}),
|
||||
...getNavigationActionCreators(childRoute),
|
||||
};
|
||||
|
||||
const actionHelpers = {};
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
actionHelpers[actionName] = (...args) => {
|
||||
@@ -76,12 +83,16 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
||||
getParam: createParamGetter(childRoute),
|
||||
|
||||
getChildNavigation: grandChildKey =>
|
||||
getChildNavigation(children[childKey], grandChildKey, () =>
|
||||
getCurrentParentNavigation().getChildNavigation(childKey)
|
||||
),
|
||||
getChildNavigation(children[childKey], grandChildKey, () => {
|
||||
const nav = getCurrentParentNavigation();
|
||||
return nav && nav.getChildNavigation(childKey);
|
||||
}),
|
||||
|
||||
isFocused: () => {
|
||||
const currentNavigation = getCurrentParentNavigation();
|
||||
if (!currentNavigation) {
|
||||
return false;
|
||||
}
|
||||
const { routes, index } = currentNavigation.state;
|
||||
if (!currentNavigation.isFocused()) {
|
||||
return false;
|
||||
|
||||
@@ -4,11 +4,9 @@ exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -77,11 +75,9 @@ exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -156,6 +152,7 @@ exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
@@ -265,6 +262,7 @@ exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
|
||||
@@ -4,11 +4,9 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -83,6 +81,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
@@ -209,11 +208,9 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -288,6 +285,7 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
export default Navigator =>
|
||||
export default (Navigator, navigatorConfig) =>
|
||||
class KeyboardAwareNavigator extends React.Component {
|
||||
static router = Navigator.router;
|
||||
_previouslyFocusedTextInput = null;
|
||||
@@ -49,7 +49,9 @@ export default Navigator =>
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onTransitionStart &&
|
||||
this.props.onTransitionStart(transitionProps, prevTransitionProps);
|
||||
const onTransitionStart =
|
||||
this.props.onTransitionStart || navigatorConfig.onTransitionStart;
|
||||
onTransitionStart &&
|
||||
onTransitionStart(transitionProps, prevTransitionProps);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
const descriptors = { ...prevState.descriptors };
|
||||
const descriptors = {};
|
||||
|
||||
routes.forEach(route => {
|
||||
if (
|
||||
@@ -36,8 +36,10 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
descriptors[route.key] = prevDescriptors[route.key];
|
||||
return;
|
||||
}
|
||||
const getComponent = () =>
|
||||
router.getComponentForRouteName(route.routeName);
|
||||
const getComponent = router.getComponentForRouteName.bind(
|
||||
null,
|
||||
route.routeName
|
||||
);
|
||||
const childNavigation = navigation.getChildNavigation(route.key);
|
||||
const options = router.getScreenOptions(childNavigation, screenProps);
|
||||
descriptors[route.key] = {
|
||||
|
||||
@@ -28,7 +28,7 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
// Create a navigator with StackView as the view
|
||||
let Navigator = createNavigator(StackView, router, stackConfig);
|
||||
if (!disableKeyboardHandling) {
|
||||
Navigator = createKeyboardAwareNavigator(Navigator);
|
||||
Navigator = createKeyboardAwareNavigator(Navigator, stackConfig);
|
||||
}
|
||||
|
||||
return Navigator;
|
||||
|
||||
5
src/react-navigation.js
vendored
5
src/react-navigation.js
vendored
@@ -156,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,5 +1,3 @@
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackActions from './StackActions';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
@@ -8,15 +6,7 @@ import StateUtils from '../StateUtils';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import invariant from '../utils/invariant';
|
||||
import { generateKey } from './KeyGenerator';
|
||||
import getNavigationActionCreators from './getNavigationActionCreators';
|
||||
|
||||
function isEmpty(obj) {
|
||||
if (!obj) return true;
|
||||
for (let key in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
import { createPathParser } from './pathUtils';
|
||||
|
||||
function behavesLikePushAction(action) {
|
||||
return (
|
||||
@@ -57,8 +47,6 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
|
||||
|
||||
const initialChildRouter = childRouters[initialRouteName];
|
||||
const pathsByRouteNames = { ...stackConfig.paths } || {};
|
||||
let paths = [];
|
||||
|
||||
function getInitialState(action) {
|
||||
let route = {};
|
||||
@@ -116,37 +104,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Build paths for each route
|
||||
routeNames.forEach(routeName => {
|
||||
let pathPattern =
|
||||
pathsByRouteNames[routeName] || routeConfigs[routeName].path;
|
||||
let matchExact = !!pathPattern && !childRouters[routeName];
|
||||
if (pathPattern === undefined) {
|
||||
pathPattern = routeName;
|
||||
}
|
||||
const keys = [];
|
||||
let re, toPath, priority;
|
||||
if (typeof pathPattern === 'string') {
|
||||
// pathPattern may be either a string or a regexp object according to path-to-regexp docs.
|
||||
re = pathToRegexp(pathPattern, keys);
|
||||
toPath = pathToRegexp.compile(pathPattern);
|
||||
priority = 0;
|
||||
} else {
|
||||
// for wildcard match
|
||||
re = pathToRegexp('*', keys);
|
||||
toPath = () => '';
|
||||
matchExact = true;
|
||||
priority = -1;
|
||||
}
|
||||
if (!matchExact) {
|
||||
const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys);
|
||||
re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`);
|
||||
}
|
||||
pathsByRouteNames[routeName] = { re, keys, toPath, priority };
|
||||
});
|
||||
|
||||
paths = Object.entries(pathsByRouteNames);
|
||||
paths.sort((a, b) => b[1].priority - a[1].priority);
|
||||
const {
|
||||
getPathAndParamsForRoute,
|
||||
getActionForPathAndParams,
|
||||
} = createPathParser(childRouters, routeConfigs, stackConfig.paths);
|
||||
|
||||
return {
|
||||
childRouters,
|
||||
@@ -166,7 +127,6 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, navStateKey),
|
||||
pop: (n, params) =>
|
||||
StackActions.pop({
|
||||
@@ -227,29 +187,27 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
return getInitialState(action);
|
||||
}
|
||||
|
||||
// Check if the focused child scene wants to handle the action, as long as
|
||||
// it is not a reset to the root stack
|
||||
const activeChildRoute = state.routes[state.index];
|
||||
|
||||
if (
|
||||
!isResetToRootStack(action) &&
|
||||
action.type !== NavigationActions.NAVIGATE
|
||||
) {
|
||||
const keyIndex = action.key
|
||||
? StateUtils.indexOf(state, action.key)
|
||||
: -1;
|
||||
const childIndex = keyIndex >= 0 ? keyIndex : state.index;
|
||||
const childRoute = state.routes[childIndex];
|
||||
invariant(
|
||||
childRoute,
|
||||
`StateUtils erroneously thought index ${childIndex} exists`
|
||||
);
|
||||
const childRouter = childRouters[childRoute.routeName];
|
||||
if (childRouter) {
|
||||
const route = childRouter.getStateForAction(action, childRoute);
|
||||
if (route === null) {
|
||||
return state;
|
||||
}
|
||||
if (route && route !== childRoute) {
|
||||
return StateUtils.replaceAt(state, childRoute.key, route);
|
||||
// Let the active child router handle the action
|
||||
const activeChildRouter = childRouters[activeChildRoute.routeName];
|
||||
if (activeChildRouter) {
|
||||
const route = activeChildRouter.getStateForAction(
|
||||
action,
|
||||
activeChildRoute
|
||||
);
|
||||
if (route !== null && route !== activeChildRoute) {
|
||||
return StateUtils.replaceAt(
|
||||
state,
|
||||
activeChildRoute.key,
|
||||
route,
|
||||
// the following tells replaceAt to NOT change the index to this route for the setParam action, because people don't expect param-setting actions to switch the active route
|
||||
action.type === NavigationActions.SET_PARAMS
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (action.type === NavigationActions.NAVIGATE) {
|
||||
@@ -433,7 +391,15 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// Handle replace action
|
||||
if (action.type === StackActions.REPLACE) {
|
||||
const routeIndex = state.routes.findIndex(r => r.key === action.key);
|
||||
let routeIndex;
|
||||
|
||||
// If the key param is undefined, set the index to the last route in the stack
|
||||
if (action.key === undefined && state.routes.length) {
|
||||
routeIndex = state.routes.length - 1;
|
||||
} else {
|
||||
routeIndex = state.routes.findIndex(r => r.key === action.key);
|
||||
}
|
||||
|
||||
// Only replace if the key matches one of our routes
|
||||
if (routeIndex !== -1) {
|
||||
const childRouter = childRouters[action.routeName];
|
||||
@@ -548,126 +514,52 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
// By this point in the router's state handling logic, we have handled the behavior of the active route, and handled any stack actions.
|
||||
// If we haven't returned by now, we should allow non-active child routers to handle this action, and switch to that index if the child state (route) does change..
|
||||
|
||||
const keyIndex = action.key ? StateUtils.indexOf(state, action.key) : -1;
|
||||
|
||||
// Traverse routes from the top of the stack to the bottom, so the
|
||||
// active route has the first opportunity, then the one before it, etc.
|
||||
for (let childRoute of state.routes.slice().reverse()) {
|
||||
if (childRoute.key === activeChildRoute.key) {
|
||||
// skip over the active child because we let it attempt to handle the action earlier
|
||||
continue;
|
||||
}
|
||||
// If a key is provided and in routes state then let's use that
|
||||
// knowledge to skip extra getStateForAction calls on other child
|
||||
// routers
|
||||
if (keyIndex >= 0 && childRoute.key !== action.key) {
|
||||
continue;
|
||||
}
|
||||
let childRouter = childRouters[childRoute.routeName];
|
||||
if (childRouter) {
|
||||
const route = childRouter.getStateForAction(action, childRoute);
|
||||
|
||||
if (route === null) {
|
||||
return state;
|
||||
} else if (route && route !== childRoute) {
|
||||
return StateUtils.replaceAt(
|
||||
state,
|
||||
childRoute.key,
|
||||
route,
|
||||
// the following tells replaceAt to NOT change the index to this route for the setParam action, because people don't expect param-setting actions to switch the active route
|
||||
action.type === NavigationActions.SET_PARAMS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = route.routeName;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
const subPath = pathsByRouteNames[routeName].toPath(route.params);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
return getPathAndParamsForRoute(route);
|
||||
},
|
||||
|
||||
getActionForPathAndParams(pathToResolve, inputParams) {
|
||||
// If the path is empty (null or empty string)
|
||||
// just return the initial route action
|
||||
if (!pathToResolve) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
params: inputParams,
|
||||
});
|
||||
}
|
||||
|
||||
const [pathNameToResolve, queryString] = pathToResolve.split('?');
|
||||
|
||||
// Attempt to match `pathNameToResolve` with a route in this router's
|
||||
// routeConfigs
|
||||
let matchedRouteName;
|
||||
let pathMatch;
|
||||
let pathMatchKeys;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [routeName, path] of paths) {
|
||||
const { re, keys } = path;
|
||||
pathMatch = re.exec(pathNameToResolve);
|
||||
if (pathMatch && pathMatch.length) {
|
||||
pathMatchKeys = keys;
|
||||
matchedRouteName = routeName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't match -- return null
|
||||
if (!matchedRouteName) {
|
||||
// If the path is empty (null or empty string)
|
||||
// just return the initial route action
|
||||
if (!pathToResolve) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine nested actions:
|
||||
// If our matched route for this router is a child router,
|
||||
// get the action for the path AFTER the matched path for this
|
||||
// router
|
||||
let nestedAction;
|
||||
let nestedQueryString = queryString ? '?' + queryString : '';
|
||||
if (childRouters[matchedRouteName]) {
|
||||
nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
|
||||
pathMatch.slice(pathMatchKeys.length).join('/') + nestedQueryString
|
||||
);
|
||||
if (!nestedAction) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// reduce the items of the query string. any query params may
|
||||
// be overridden by path params
|
||||
const queryParams = !isEmpty(inputParams)
|
||||
? inputParams
|
||||
: (queryString || '').split('&').reduce((result, item) => {
|
||||
if (item !== '') {
|
||||
const nextResult = result || {};
|
||||
const [key, value] = item.split('=');
|
||||
nextResult[key] = value;
|
||||
return nextResult;
|
||||
}
|
||||
return result;
|
||||
}, null);
|
||||
|
||||
// reduce the matched pieces of the path into the params
|
||||
// of the route. `params` is null if there are no params.
|
||||
const params = pathMatch.slice(1).reduce((result, matchResult, i) => {
|
||||
const key = pathMatchKeys[i];
|
||||
if (key.asterisk || !key) {
|
||||
return result;
|
||||
}
|
||||
const nextResult = result || inputParams || {};
|
||||
const paramName = key.name;
|
||||
|
||||
let decodedMatchResult;
|
||||
try {
|
||||
decodedMatchResult = decodeURIComponent(matchResult);
|
||||
} catch (e) {
|
||||
// ignore `URIError: malformed URI`
|
||||
}
|
||||
|
||||
nextResult[paramName] = decodedMatchResult || matchResult;
|
||||
return nextResult;
|
||||
}, queryParams);
|
||||
|
||||
return NavigationActions.navigate({
|
||||
routeName: matchedRouteName,
|
||||
...(params ? { params } : {}),
|
||||
...(nestedAction ? { action: nestedAction } : {}),
|
||||
});
|
||||
getActionForPathAndParams(path, params) {
|
||||
return getActionForPathAndParams(path, params);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
|
||||
@@ -5,7 +5,7 @@ import createConfigGetter from './createConfigGetter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackActions from './StackActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getNavigationActionCreators from './getNavigationActionCreators';
|
||||
import { createPathParser } from './pathUtils';
|
||||
|
||||
const defaultActionCreators = (route, navStateKey) => ({});
|
||||
|
||||
@@ -22,7 +22,7 @@ export default (routeConfigs, config = {}) => {
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
|
||||
const getCustomActionCreators =
|
||||
config.getCustomActionCreators || defaultActionCreators;
|
||||
|
||||
@@ -37,16 +37,18 @@ export default (routeConfigs, config = {}) => {
|
||||
const childRouters = {};
|
||||
order.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
if (!paths[routeName]) {
|
||||
paths[routeName] =
|
||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
||||
}
|
||||
childRouters[routeName] = null;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
if (screen.router) {
|
||||
childRouters[routeName] = screen.router;
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
getPathAndParamsForRoute,
|
||||
getActionForPathAndParams,
|
||||
} = createPathParser(childRouters, routeConfigs, config.paths);
|
||||
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}'.` +
|
||||
@@ -109,10 +111,7 @@ export default (routeConfigs, config = {}) => {
|
||||
childRouters,
|
||||
|
||||
getActionCreators(route, stateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, stateKey),
|
||||
};
|
||||
return getCustomActionCreators(route, stateKey);
|
||||
},
|
||||
|
||||
getStateForAction(action, inputState) {
|
||||
@@ -191,7 +190,7 @@ export default (routeConfigs, config = {}) => {
|
||||
newChildState = childRouter
|
||||
? childRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!action.action && !childRouter && action.params) {
|
||||
} else if (!action.action && action.params) {
|
||||
newChildState = {
|
||||
...childState,
|
||||
params: {
|
||||
@@ -313,73 +312,11 @@ export default (routeConfigs, config = {}) => {
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = order[state.index];
|
||||
const subPath = paths[routeName];
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
return getPathAndParamsForRoute(route);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an optional action, based on a relative path and query params.
|
||||
*
|
||||
* This will return null if there is no action matched
|
||||
*/
|
||||
getActionForPathAndParams(path, params) {
|
||||
if (!path) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
params,
|
||||
});
|
||||
}
|
||||
return (
|
||||
order
|
||||
.map(childId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[childId];
|
||||
const partsInTestPath = pathToTest.split('/').length;
|
||||
const pathPartsToTest = parts.slice(0, partsInTestPath).join('/');
|
||||
if (pathPartsToTest === pathToTest) {
|
||||
const childRouter = childRouters[childId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: childId,
|
||||
});
|
||||
if (childRouter && childRouter.getActionForPathAndParams) {
|
||||
action.action = childRouter.getActionForPathAndParams(
|
||||
parts.slice(partsInTestPath).join('/'),
|
||||
params
|
||||
);
|
||||
}
|
||||
if (params) {
|
||||
action.params = params;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
order
|
||||
.map(childId => {
|
||||
const childRouter = childRouters[childId];
|
||||
return (
|
||||
childRouter && childRouter.getActionForPathAndParams(path, params)
|
||||
);
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
null
|
||||
);
|
||||
return getActionForPathAndParams(path, params);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
|
||||
578
src/routers/__tests__/PathHandling-test.js
Normal file
578
src/routers/__tests__/PathHandling-test.js
Normal file
@@ -0,0 +1,578 @@
|
||||
/* eslint no-shadow:0, react/no-multi-comp:0, react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SwitchRouter from '../SwitchRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
import StackActions from '../StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import { urlToPathAndParams } from '../pathUtils';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_normalize_keys();
|
||||
});
|
||||
|
||||
const performRouterTest = createTestRouter => {
|
||||
const ListScreen = () => <div />;
|
||||
|
||||
const ProfileNavigator = () => <div />;
|
||||
ProfileNavigator.router = StackRouter({
|
||||
list: {
|
||||
path: 'list/:id',
|
||||
screen: ListScreen,
|
||||
},
|
||||
});
|
||||
|
||||
const MainNavigator = () => <div />;
|
||||
MainNavigator.router = StackRouter({
|
||||
profile: {
|
||||
path: 'p/:id',
|
||||
screen: ProfileNavigator,
|
||||
},
|
||||
});
|
||||
|
||||
const LoginScreen = () => <div />;
|
||||
|
||||
const AuthNavigator = () => <div />;
|
||||
AuthNavigator.router = StackRouter({
|
||||
login: {
|
||||
screen: LoginScreen,
|
||||
},
|
||||
});
|
||||
|
||||
const BarScreen = () => <div />;
|
||||
|
||||
class FooNavigator extends React.Component {
|
||||
static router = StackRouter({
|
||||
bar: {
|
||||
path: 'b/:barThing',
|
||||
screen: BarScreen,
|
||||
},
|
||||
});
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
const PersonScreen = () => <div />;
|
||||
|
||||
const testRouter = createTestRouter({
|
||||
main: {
|
||||
screen: MainNavigator,
|
||||
},
|
||||
baz: {
|
||||
path: null,
|
||||
screen: FooNavigator,
|
||||
},
|
||||
auth: {
|
||||
screen: AuthNavigator,
|
||||
},
|
||||
person: {
|
||||
path: 'people/:id',
|
||||
screen: PersonScreen,
|
||||
},
|
||||
foo: {
|
||||
path: 'fo/:fooThing',
|
||||
screen: FooNavigator,
|
||||
},
|
||||
});
|
||||
|
||||
test('Handles empty URIs with empty action', () => {
|
||||
const router = createTestRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{ initialRouteName: 'Bar', initialRouteParams: { foo: 42 } }
|
||||
);
|
||||
const action = router.getActionForPathAndParams('');
|
||||
expect(action).toEqual(null);
|
||||
const state = router.getStateForAction(action || NavigationActions.init());
|
||||
expect(state.routes[state.index]).toEqual(
|
||||
expect.objectContaining({
|
||||
routeName: 'Bar',
|
||||
params: { foo: 42 },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('Handles paths with several params', () => {
|
||||
const router = createTestRouter({
|
||||
Person: {
|
||||
path: 'people/:person',
|
||||
screen: () => <div />,
|
||||
},
|
||||
Task: {
|
||||
path: 'people/:person/tasks/:task',
|
||||
screen: () => <div />,
|
||||
},
|
||||
ThingA: {
|
||||
path: 'things/:good',
|
||||
screen: () => <div />,
|
||||
},
|
||||
Thing: {
|
||||
path: 'things/:good/:thing',
|
||||
screen: () => <div />,
|
||||
},
|
||||
});
|
||||
const action = router.getActionForPathAndParams(
|
||||
'people/brent/tasks/everything'
|
||||
);
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Task',
|
||||
params: { person: 'brent', task: 'everything' },
|
||||
});
|
||||
|
||||
const action1 = router.getActionForPathAndParams('people/lucy');
|
||||
expect(action1).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Person',
|
||||
params: { person: 'lucy' },
|
||||
});
|
||||
|
||||
const action2 = router.getActionForPathAndParams('things/foo/bar');
|
||||
expect(action2).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Thing',
|
||||
params: { good: 'foo', thing: 'bar' },
|
||||
});
|
||||
|
||||
const action3 = router.getActionForPathAndParams('things/foo');
|
||||
expect(action3).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'ThingA',
|
||||
params: { good: 'foo' },
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles empty path configuration', () => {
|
||||
const router = createTestRouter({
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
path: '',
|
||||
},
|
||||
});
|
||||
const action = router.getActionForPathAndParams('');
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles wildcard path configuration', () => {
|
||||
const router = createTestRouter({
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
path: ':something',
|
||||
},
|
||||
});
|
||||
const action = router.getActionForPathAndParams('');
|
||||
expect(action).toEqual(null);
|
||||
|
||||
const action1 = router.getActionForPathAndParams('Foo');
|
||||
expect(action1).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
params: {},
|
||||
});
|
||||
const action2 = router.getActionForPathAndParams('asdf');
|
||||
expect(action2).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
params: { something: 'asdf' },
|
||||
});
|
||||
});
|
||||
|
||||
test('Null path behavior', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const router = createTestRouter({
|
||||
Bar: {
|
||||
screen: ScreenA,
|
||||
},
|
||||
Foo: {
|
||||
path: null,
|
||||
screen: ScreenA,
|
||||
},
|
||||
Baz: {
|
||||
path: '',
|
||||
screen: ScreenA,
|
||||
},
|
||||
});
|
||||
const action0 = router.getActionForPathAndParams('test/random', {});
|
||||
expect(action0).toBe(null);
|
||||
|
||||
const action1 = router.getActionForPathAndParams('', {});
|
||||
expect(action1.routeName).toBe('Baz');
|
||||
const state1 = router.getStateForAction(action1);
|
||||
expect(state1.routes[state1.index].routeName).toBe('Baz');
|
||||
});
|
||||
|
||||
test('Multiple null path sub routers path behavior', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenB.router = createTestRouter({
|
||||
Foo: ScreenA,
|
||||
});
|
||||
const ScreenC = () => <div />;
|
||||
ScreenC.router = createTestRouter({
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
screen: ScreenA,
|
||||
},
|
||||
Empty: {
|
||||
path: '',
|
||||
screen: ScreenA,
|
||||
},
|
||||
});
|
||||
const router = createTestRouter({
|
||||
A: {
|
||||
screen: ScreenA,
|
||||
},
|
||||
B: {
|
||||
path: null,
|
||||
screen: ScreenB,
|
||||
},
|
||||
C: {
|
||||
path: null,
|
||||
screen: ScreenC,
|
||||
},
|
||||
});
|
||||
const action0 = router.getActionForPathAndParams('Foo', {});
|
||||
expect(action0.routeName).toBe('B');
|
||||
expect(action0.action.routeName).toBe('Foo');
|
||||
|
||||
const action1 = router.getActionForPathAndParams('', {});
|
||||
expect(action1.routeName).toBe('C');
|
||||
expect(action1.action.routeName).toBe('Empty');
|
||||
|
||||
const action2 = router.getActionForPathAndParams('A', {});
|
||||
expect(action2.routeName).toBe('A');
|
||||
|
||||
const action3 = router.getActionForPathAndParams('bar/asdf', {});
|
||||
expect(action3.routeName).toBe('C');
|
||||
expect(action3.action.routeName).toBe('Bar');
|
||||
expect(action3.action.params.id).toBe('asdf');
|
||||
});
|
||||
|
||||
test('Null and empty string path sub routers behavior', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenB.router = createTestRouter({
|
||||
Foo: ScreenA,
|
||||
Baz: {
|
||||
screen: ScreenA,
|
||||
path: '',
|
||||
},
|
||||
});
|
||||
const ScreenC = () => <div />;
|
||||
ScreenC.router = createTestRouter({
|
||||
Boo: ScreenA,
|
||||
Bar: ScreenA,
|
||||
Baz: {
|
||||
screen: ScreenA,
|
||||
path: '',
|
||||
},
|
||||
});
|
||||
const router = createTestRouter({
|
||||
B: {
|
||||
path: null,
|
||||
screen: ScreenB,
|
||||
},
|
||||
C: {
|
||||
path: '',
|
||||
screen: ScreenC,
|
||||
},
|
||||
});
|
||||
const action0 = router.getActionForPathAndParams('', {});
|
||||
expect(action0.routeName).toBe('C');
|
||||
expect(action0.action.routeName).toBe('Baz');
|
||||
|
||||
const action1 = router.getActionForPathAndParams('Foo', {});
|
||||
expect(action1.routeName).toBe('B');
|
||||
expect(action1.action.routeName).toBe('Foo');
|
||||
|
||||
const action2 = router.getActionForPathAndParams('Bar', {});
|
||||
expect(action2.routeName).toBe('C');
|
||||
expect(action2.action.routeName).toBe('Bar');
|
||||
|
||||
const action3 = router.getActionForPathAndParams('unknown', {});
|
||||
expect(action3).toBe(null);
|
||||
});
|
||||
|
||||
test('Empty path acts as wildcard for nested router', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const Foo = () => <div />;
|
||||
const ScreenC = () => <div />;
|
||||
ScreenC.router = createTestRouter({
|
||||
Boo: ScreenA,
|
||||
Bar: ScreenA,
|
||||
});
|
||||
Foo.router = createTestRouter({
|
||||
Quo: ScreenA,
|
||||
Qux: {
|
||||
screen: ScreenC,
|
||||
path: '',
|
||||
},
|
||||
});
|
||||
const router = createTestRouter({
|
||||
Bar: {
|
||||
screen: ScreenA,
|
||||
},
|
||||
Foo,
|
||||
});
|
||||
const action0 = router.getActionForPathAndParams('Foo/Bar', {});
|
||||
expect(action0.routeName).toBe('Foo');
|
||||
expect(action0.action.routeName).toBe('Qux');
|
||||
expect(action0.action.action.routeName).toBe('Bar');
|
||||
});
|
||||
|
||||
test('Gets deep path with pure wildcard match', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const ScreenC = () => <div />;
|
||||
ScreenA.router = createTestRouter({
|
||||
Boo: { path: 'boo', screen: ScreenC },
|
||||
Baz: { path: 'baz/:bazId', screen: ScreenB },
|
||||
});
|
||||
ScreenC.router = createTestRouter({
|
||||
Boo2: { path: '', screen: ScreenB },
|
||||
});
|
||||
const router = createTestRouter({
|
||||
Foo: {
|
||||
path: null,
|
||||
screen: ScreenA,
|
||||
},
|
||||
Bar: {
|
||||
screen: ScreenB,
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
params: {
|
||||
id: '123',
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
|
||||
},
|
||||
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||
],
|
||||
},
|
||||
{ key: 'Bar', routeName: 'Bar' },
|
||||
],
|
||||
};
|
||||
const { path, params } = router.getPathAndParamsForState(state);
|
||||
expect(path).toEqual('baz/321');
|
||||
expect(params.id).toEqual('123');
|
||||
}
|
||||
|
||||
{
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
params: {
|
||||
id: '123',
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
|
||||
},
|
||||
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||
],
|
||||
},
|
||||
{ key: 'Bar', routeName: 'Bar' },
|
||||
],
|
||||
};
|
||||
const { path, params } = router.getPathAndParamsForState(state);
|
||||
expect(path).toEqual('boo');
|
||||
expect(params).toEqual({ id: '123' });
|
||||
}
|
||||
});
|
||||
|
||||
test('URI encoded string get passed to deep link', () => {
|
||||
const uri = 'people/2018%2F02%2F07';
|
||||
const action = testRouter.getActionForPathAndParams(uri);
|
||||
expect(action).toEqual({
|
||||
routeName: 'person',
|
||||
params: {
|
||||
id: '2018/02/07',
|
||||
},
|
||||
type: NavigationActions.NAVIGATE,
|
||||
});
|
||||
|
||||
const malformedUri = 'people/%E0%A4%A';
|
||||
const action2 = testRouter.getActionForPathAndParams(malformedUri);
|
||||
expect(action2).toEqual({
|
||||
routeName: 'person',
|
||||
params: {
|
||||
id: '%E0%A4%A',
|
||||
},
|
||||
type: NavigationActions.NAVIGATE,
|
||||
});
|
||||
});
|
||||
|
||||
test('URI encoded path param gets parsed and correctly printed', () => {
|
||||
const router = createTestRouter({
|
||||
main: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
person: {
|
||||
path: 'people/:name',
|
||||
screen: () => <div />,
|
||||
},
|
||||
});
|
||||
|
||||
const action = testRouter.getActionForPathAndParams('people/Henry%20L');
|
||||
expect(action).toEqual({
|
||||
routeName: 'person',
|
||||
params: {
|
||||
id: 'Henry L',
|
||||
},
|
||||
type: NavigationActions.NAVIGATE,
|
||||
});
|
||||
const s = testRouter.getStateForAction(action);
|
||||
const out = testRouter.getPathAndParamsForState(s);
|
||||
expect(out.path).toEqual('people/Henry%20L');
|
||||
expect(out.params).toEqual({});
|
||||
});
|
||||
|
||||
test('Querystring params get passed to nested deep link', () => {
|
||||
const action = testRouter.getActionForPathAndParams(
|
||||
'main/p/4/list/10259959195',
|
||||
{ code: 'test', foo: 'bar' }
|
||||
);
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'main',
|
||||
params: {
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'profile',
|
||||
params: {
|
||||
id: '4',
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'list',
|
||||
params: {
|
||||
id: '10259959195',
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const action2 = testRouter.getActionForPathAndParams(
|
||||
'main/p/4/list/10259959195',
|
||||
{ code: '', foo: 'bar' }
|
||||
);
|
||||
expect(action2).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'main',
|
||||
params: {
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'profile',
|
||||
params: {
|
||||
id: '4',
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'list',
|
||||
params: {
|
||||
id: '10259959195',
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('paths option on router overrides path from route config', () => {
|
||||
const router = createTestRouter(
|
||||
{
|
||||
main: {
|
||||
screen: MainNavigator,
|
||||
},
|
||||
baz: {
|
||||
path: null,
|
||||
screen: FooNavigator,
|
||||
},
|
||||
},
|
||||
{ paths: { baz: 'overridden' } }
|
||||
);
|
||||
const action = router.getActionForPathAndParams('overridden', {});
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('baz');
|
||||
});
|
||||
};
|
||||
|
||||
describe('Path handling for stack router', () => {
|
||||
performRouterTest(StackRouter);
|
||||
});
|
||||
describe('Path handling for switch router', () => {
|
||||
performRouterTest(SwitchRouter);
|
||||
});
|
||||
|
||||
test('Handles nested switch routers', () => {
|
||||
const AScreen = () => <div />;
|
||||
const DocsNavigator = () => <div />;
|
||||
DocsNavigator.router = SwitchRouter({
|
||||
A: AScreen,
|
||||
B: AScreen,
|
||||
C: AScreen,
|
||||
});
|
||||
DocsNavigator.path = 'docs';
|
||||
const router = SwitchRouter({
|
||||
Docs: DocsNavigator,
|
||||
D: AScreen,
|
||||
});
|
||||
const action = router.getActionForPathAndParams('docs/B', {});
|
||||
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('Docs');
|
||||
expect(action.action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.action.routeName).toEqual('B');
|
||||
});
|
||||
@@ -208,6 +208,7 @@ describe('StackRouter', () => {
|
||||
expect(AuthNavigator.router.getActionForPathAndParams('login')).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'login',
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,7 +224,10 @@ describe('StackRouter', () => {
|
||||
|
||||
test('Parses paths with a query', () => {
|
||||
expect(
|
||||
TestStackRouter.getActionForPathAndParams('people/foo?code=test&foo=bar')
|
||||
TestStackRouter.getActionForPathAndParams('people/foo', {
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
})
|
||||
).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'person',
|
||||
@@ -237,7 +241,10 @@ describe('StackRouter', () => {
|
||||
|
||||
test('Parses paths with an empty query value', () => {
|
||||
expect(
|
||||
TestStackRouter.getActionForPathAndParams('people/foo?code=&foo=bar')
|
||||
TestStackRouter.getActionForPathAndParams('people/foo', {
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
})
|
||||
).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'person',
|
||||
@@ -255,9 +262,11 @@ describe('StackRouter', () => {
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'auth',
|
||||
params: {},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'login',
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -268,6 +277,7 @@ describe('StackRouter', () => {
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'main',
|
||||
params: {},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'profile',
|
||||
@@ -291,6 +301,7 @@ describe('StackRouter', () => {
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'baz',
|
||||
params: {},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'bar',
|
||||
@@ -308,14 +319,16 @@ describe('StackRouter', () => {
|
||||
});
|
||||
|
||||
test('Correctly returns action chain for partially matched path', () => {
|
||||
const uri = 'auth/login/2';
|
||||
const uri = 'auth/login';
|
||||
const action = TestStackRouter.getActionForPathAndParams(uri);
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'auth',
|
||||
params: {},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'login',
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -957,6 +970,77 @@ describe('StackRouter', () => {
|
||||
expect(replacedState2.routes[0].routeName).toEqual('bar');
|
||||
});
|
||||
|
||||
test('Replace action returns most recent route if no key is provided', () => {
|
||||
const GrandChildNavigator = () => <div />;
|
||||
GrandChildNavigator.router = StackRouter({
|
||||
Quux: { screen: () => <div /> },
|
||||
Corge: { screen: () => <div /> },
|
||||
Grault: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Woo: { screen: () => <div /> },
|
||||
Qux: { screen: GrandChildNavigator },
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Qux',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Corge',
|
||||
},
|
||||
state3
|
||||
);
|
||||
const state5 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Grault',
|
||||
},
|
||||
state4
|
||||
);
|
||||
|
||||
const replacedState = router.getStateForAction(
|
||||
StackActions.replace({
|
||||
routeName: 'Woo',
|
||||
params: { meaning: 42 },
|
||||
}),
|
||||
state5
|
||||
);
|
||||
|
||||
const originalCurrentScreen = state5.routes[1].routes[1].routes[2];
|
||||
const replacedCurrentScreen = replacedState.routes[1].routes[1].routes[2];
|
||||
|
||||
expect(replacedState.routes[1].routes[1].index).toEqual(2);
|
||||
expect(replacedState.routes[1].routes[1].routes.length).toEqual(3);
|
||||
expect(replacedCurrentScreen.key).not.toEqual(originalCurrentScreen.key);
|
||||
expect(replacedCurrentScreen.routeName).not.toEqual(
|
||||
originalCurrentScreen.routeName
|
||||
);
|
||||
expect(replacedCurrentScreen.routeName).toEqual('Woo');
|
||||
expect(replacedCurrentScreen.params.meaning).toEqual(42);
|
||||
});
|
||||
|
||||
test('Handles push transition logic with completion action', () => {
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
@@ -989,6 +1073,43 @@ describe('StackRouter', () => {
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
});
|
||||
|
||||
test('Back action parent is prioritized over inactive child routers', () => {
|
||||
const Bar = () => <div />;
|
||||
Bar.router = StackRouter({
|
||||
baz: { screen: () => <div /> },
|
||||
qux: { screen: () => <div /> },
|
||||
});
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: Bar },
|
||||
boo: { screen: () => <div /> },
|
||||
});
|
||||
const state = {
|
||||
key: 'top',
|
||||
index: 3,
|
||||
routes: [
|
||||
{ routeName: 'foo', key: 'f' },
|
||||
{
|
||||
routeName: 'bar',
|
||||
key: 'b',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ routeName: 'baz', key: 'bz' },
|
||||
{ routeName: 'qux', key: 'bx' },
|
||||
],
|
||||
},
|
||||
{ routeName: 'foo', key: 'f1' },
|
||||
{ routeName: 'boo', key: 'z' },
|
||||
],
|
||||
};
|
||||
const testState = TestRouter.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state
|
||||
);
|
||||
expect(testState.index).toBe(2);
|
||||
expect(testState.routes[1].index).toBe(1);
|
||||
});
|
||||
|
||||
test('Handle basic stack logic for components with router', () => {
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
@@ -1047,6 +1168,47 @@ describe('StackRouter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Gets deep path (stack behavior)', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenA.router = StackRouter({
|
||||
Boo: { path: 'boo', screen: ScreenB },
|
||||
Baz: { path: 'baz/:bazId', screen: ScreenB },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: {
|
||||
path: 'f/:id',
|
||||
screen: ScreenA,
|
||||
},
|
||||
Bar: {
|
||||
screen: ScreenB,
|
||||
},
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
params: {
|
||||
id: '123',
|
||||
},
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||
],
|
||||
},
|
||||
{ key: 'Bar', routeName: 'Bar' },
|
||||
],
|
||||
};
|
||||
const { path, params } = router.getPathAndParamsForState(state);
|
||||
expect(path).toEqual('f/123/baz/321');
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test('Handle goBack identified by key', () => {
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
@@ -1634,400 +1796,164 @@ describe('StackRouter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles empty URIs', () => {
|
||||
const router = StackRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{ initialRouteName: 'Bar' }
|
||||
);
|
||||
const action = router.getActionForPathAndParams('');
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
test('Handles deep navigate completion action', () => {
|
||||
const LeafScreen = () => <div />;
|
||||
const FooScreen = () => <div />;
|
||||
FooScreen.router = StackRouter({
|
||||
Boo: { path: 'boo', screen: LeafScreen },
|
||||
Baz: { path: 'baz/:bazId', screen: LeafScreen },
|
||||
});
|
||||
let state = null;
|
||||
if (action) {
|
||||
state = router.getStateForAction(action);
|
||||
}
|
||||
const router = StackRouter({
|
||||
Foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
Bar: {
|
||||
screen: LeafScreen,
|
||||
},
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state && state.index).toEqual(0);
|
||||
expect(state && state.routes[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
routeName: 'Bar',
|
||||
})
|
||||
expect(state && state.routes[0].routeName).toEqual('Foo');
|
||||
const key = state && state.routes[0].key;
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.isTransitioning).toEqual(false);
|
||||
expect(state2 && state2.routes[0].index).toEqual(1);
|
||||
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
|
||||
expect(!!key).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.COMPLETE_TRANSITION,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
expect(state3 && state3.routes[0].index).toEqual(1);
|
||||
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
|
||||
});
|
||||
|
||||
test('Gets deep path', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenA.router = StackRouter({
|
||||
Boo: { path: 'boo', screen: ScreenB },
|
||||
Baz: { path: 'baz/:bazId', screen: ScreenB },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: {
|
||||
path: 'f/:id',
|
||||
screen: ScreenA,
|
||||
},
|
||||
Bar: {
|
||||
screen: ScreenB,
|
||||
},
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
params: {
|
||||
id: '123',
|
||||
},
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||
],
|
||||
},
|
||||
{ key: 'Bar', routeName: 'Bar' },
|
||||
],
|
||||
};
|
||||
const { path, params } = router.getPathAndParamsForState(state);
|
||||
expect(path).toEqual('f/123/baz/321');
|
||||
expect(params.id).toEqual('123');
|
||||
expect(params.bazId).toEqual('321');
|
||||
});
|
||||
|
||||
test('Gets deep path with pure wildcard match', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const ScreenC = () => <div />;
|
||||
ScreenA.router = StackRouter({
|
||||
Boo: { path: 'boo', screen: ScreenC },
|
||||
Baz: { path: 'baz/:bazId', screen: ScreenB },
|
||||
});
|
||||
ScreenC.router = StackRouter({
|
||||
Boo2: { path: '', screen: ScreenB },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: {
|
||||
path: null,
|
||||
screen: ScreenA,
|
||||
},
|
||||
Bar: {
|
||||
screen: ScreenB,
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
params: {
|
||||
id: '123',
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
|
||||
},
|
||||
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||
],
|
||||
},
|
||||
{ key: 'Bar', routeName: 'Bar' },
|
||||
],
|
||||
};
|
||||
const { path, params } = router.getPathAndParamsForState(state);
|
||||
expect(path).toEqual('baz/321');
|
||||
expect(params.id).toEqual('123');
|
||||
expect(params.bazId).toEqual('321');
|
||||
}
|
||||
|
||||
{
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
params: {
|
||||
id: '123',
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
|
||||
},
|
||||
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||
],
|
||||
},
|
||||
{ key: 'Bar', routeName: 'Bar' },
|
||||
],
|
||||
};
|
||||
const { path, params } = router.getPathAndParamsForState(state);
|
||||
expect(path).toEqual('boo/');
|
||||
expect(params).toEqual({ id: '123' });
|
||||
}
|
||||
});
|
||||
|
||||
test('URI encoded string get passed to deep link', () => {
|
||||
const uri = 'people/2018%2F02%2F07';
|
||||
const action = TestStackRouter.getActionForPathAndParams(uri);
|
||||
expect(action).toEqual({
|
||||
routeName: 'person',
|
||||
params: {
|
||||
id: '2018/02/07',
|
||||
},
|
||||
type: NavigationActions.NAVIGATE,
|
||||
});
|
||||
|
||||
const malformedUri = 'people/%E0%A4%A';
|
||||
const action2 = TestStackRouter.getActionForPathAndParams(malformedUri);
|
||||
expect(action2).toEqual({
|
||||
routeName: 'person',
|
||||
params: {
|
||||
id: '%E0%A4%A',
|
||||
},
|
||||
type: NavigationActions.NAVIGATE,
|
||||
});
|
||||
});
|
||||
|
||||
test('Querystring params get passed to nested deep link', () => {
|
||||
// uri with two non-empty query param values
|
||||
const uri = 'main/p/4/list/10259959195?code=test&foo=bar';
|
||||
const action = TestStackRouter.getActionForPathAndParams(uri);
|
||||
expect(action).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'main',
|
||||
params: {
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'profile',
|
||||
params: {
|
||||
id: '4',
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'list',
|
||||
params: {
|
||||
id: '10259959195',
|
||||
code: 'test',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// uri with one empty and one non-empty query param value
|
||||
const uri2 = 'main/p/4/list/10259959195?code=&foo=bar';
|
||||
const action2 = TestStackRouter.getActionForPathAndParams(uri2);
|
||||
expect(action2).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'main',
|
||||
params: {
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'profile',
|
||||
params: {
|
||||
id: '4',
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
},
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'list',
|
||||
params: {
|
||||
id: '10259959195',
|
||||
code: '',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles deep navigate completion action', () => {
|
||||
const LeafScreen = () => <div />;
|
||||
const FooScreen = () => <div />;
|
||||
FooScreen.router = StackRouter({
|
||||
Boo: { path: 'boo', screen: LeafScreen },
|
||||
Baz: { path: 'baz/:bazId', screen: LeafScreen },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
Bar: {
|
||||
screen: LeafScreen,
|
||||
},
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state && state.index).toEqual(0);
|
||||
expect(state && state.routes[0].routeName).toEqual('Foo');
|
||||
const key = state && state.routes[0].key;
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.isTransitioning).toEqual(false);
|
||||
expect(state2 && state2.routes[0].index).toEqual(1);
|
||||
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
|
||||
expect(!!key).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.COMPLETE_TRANSITION,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
expect(state3 && state3.routes[0].index).toEqual(1);
|
||||
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
let nestedRouter = StackRouter({
|
||||
Foo: Screen,
|
||||
Bar: Screen,
|
||||
});
|
||||
|
||||
NestedStack.router = nestedRouter;
|
||||
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
let nestedRouter = StackRouter({
|
||||
Foo: Screen,
|
||||
Bar: Screen,
|
||||
Baz: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Baz',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('Baz');
|
||||
NestedStack.router = nestedRouter;
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
Bar: Screen,
|
||||
Baz: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Baz',
|
||||
}
|
||||
);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3.routes[state3.index].routeName).toEqual('Baz');
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('Baz');
|
||||
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state5 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state4
|
||||
);
|
||||
let activeState5 = state5.routes[state5.index];
|
||||
expect(activeState5.routeName).toEqual('NestedStack');
|
||||
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
const OtherNestedStack = () => <div />;
|
||||
|
||||
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
|
||||
let otherNestedRouter = StackRouter({ Foo: Screen });
|
||||
NestedStack.router = nestedRouter;
|
||||
OtherNestedStack.router = otherNestedRouter;
|
||||
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
OtherNestedStack,
|
||||
Bar: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'OtherNestedStack',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'NestedStack',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3.routes[state3.index].routeName).toEqual('Baz');
|
||||
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
|
||||
|
||||
const state5 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state4
|
||||
);
|
||||
let activeState5 = state5.routes[state5.index];
|
||||
expect(activeState5.routeName).toEqual('NestedStack');
|
||||
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
const OtherNestedStack = () => <div />;
|
||||
|
||||
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
|
||||
let otherNestedRouter = StackRouter({ Foo: Screen });
|
||||
NestedStack.router = nestedRouter;
|
||||
OtherNestedStack.router = otherNestedRouter;
|
||||
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
OtherNestedStack,
|
||||
Bar: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'OtherNestedStack',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'NestedStack',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,56 +78,6 @@ describe('SwitchRouter', () => {
|
||||
expect(state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('paths option on SwitchRouter overrides path from route config', () => {
|
||||
const router = getExampleRouter({ paths: { A: 'overridden' } });
|
||||
const action = router.getActionForPathAndParams('overridden', {});
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('A');
|
||||
});
|
||||
|
||||
test('provides correct action for getActionForPathAndParams', () => {
|
||||
const router = getExampleRouter({ backBehavior: 'initialRoute' });
|
||||
const action = router.getActionForPathAndParams('A1', { foo: 'bar' });
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('A1');
|
||||
|
||||
const action1 = router.getActionForPathAndParams('', {});
|
||||
expect(action1.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action1.routeName).toEqual('A');
|
||||
|
||||
const action2 = router.getActionForPathAndParams(null, {});
|
||||
expect(action2.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action2.routeName).toEqual('A');
|
||||
|
||||
const action3 = router.getActionForPathAndParams('great/path', {
|
||||
foo: 'baz',
|
||||
});
|
||||
expect(action3).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B',
|
||||
params: { foo: 'baz' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B1',
|
||||
params: { foo: 'baz' },
|
||||
},
|
||||
});
|
||||
|
||||
const action4 = router.getActionForPathAndParams('great/path/B2', {
|
||||
foo: 'baz',
|
||||
});
|
||||
expect(action4).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B',
|
||||
params: { foo: 'baz' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B2',
|
||||
params: { foo: 'baz' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested switchrouters', () => {
|
||||
// router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
|
||||
// if we are focused on Other and navigate to Bar, what should happen?
|
||||
|
||||
@@ -528,7 +528,7 @@ describe('TabRouter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles path configuration', () => {
|
||||
test.only('Handles path configuration', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = TabRouter({
|
||||
@@ -537,14 +537,17 @@ describe('TabRouter', () => {
|
||||
screen: ScreenA,
|
||||
},
|
||||
Bar: {
|
||||
path: 'b',
|
||||
path: 'b/:great',
|
||||
screen: ScreenB,
|
||||
},
|
||||
});
|
||||
const params = { foo: '42' };
|
||||
const action = router.getActionForPathAndParams('b/anything', params);
|
||||
const expectedAction = {
|
||||
params,
|
||||
params: {
|
||||
foo: '42',
|
||||
great: 'anything',
|
||||
},
|
||||
routeName: 'Bar',
|
||||
type: NavigationActions.NAVIGATE,
|
||||
};
|
||||
@@ -565,15 +568,21 @@ describe('TabRouter', () => {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo' },
|
||||
{ key: 'Bar', routeName: 'Bar', params },
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{
|
||||
key: 'Bar',
|
||||
routeName: 'Bar',
|
||||
params: { foo: '42', great: 'anything' },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(state2).toEqual(expectedState2);
|
||||
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
|
||||
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
||||
expect(router.getPathAndParamsForState(expectedState).path).toEqual('f');
|
||||
expect(router.getPathAndParamsForState(expectedState2).path).toEqual('b');
|
||||
expect(router.getPathAndParamsForState(expectedState2).path).toEqual(
|
||||
'b/anything'
|
||||
);
|
||||
});
|
||||
|
||||
test('Handles default configuration', () => {
|
||||
|
||||
34
src/routers/__tests__/pathUtils-test.js
Normal file
34
src/routers/__tests__/pathUtils-test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { urlToPathAndParams } from '../pathUtils';
|
||||
|
||||
test('urlToPathAndParams empty', () => {
|
||||
const { path, params } = urlToPathAndParams('foo://');
|
||||
expect(path).toBe('');
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test('urlToPathAndParams empty params', () => {
|
||||
const { path, params } = urlToPathAndParams('foo://foo/bar/b');
|
||||
expect(path).toBe('foo/bar/b');
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test('urlToPathAndParams trailing slash', () => {
|
||||
const { path, params } = urlToPathAndParams('foo://foo/bar/');
|
||||
expect(path).toBe('foo/bar');
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test('urlToPathAndParams with params', () => {
|
||||
const { path, params } = urlToPathAndParams('foo://foo/bar?asdf=1&dude=foo');
|
||||
expect(path).toBe('foo/bar');
|
||||
expect(params).toEqual({ asdf: '1', dude: 'foo' });
|
||||
});
|
||||
|
||||
test('urlToPathAndParams with custom delimeter', () => {
|
||||
const { path, params } = urlToPathAndParams(
|
||||
'https://example.com/foo/bar?asdf=1',
|
||||
'https://example.com/'
|
||||
);
|
||||
expect(path).toBe('foo/bar');
|
||||
expect(params).toEqual({ asdf: '1' });
|
||||
});
|
||||
212
src/routers/pathUtils.js
Normal file
212
src/routers/pathUtils.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
const queryString = require('query-string');
|
||||
|
||||
function isEmpty(obj) {
|
||||
if (!obj) return true;
|
||||
for (let key in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const getParamsFromPath = (inputParams, pathMatch, pathMatchKeys) => {
|
||||
const params = pathMatch.slice(1).reduce(
|
||||
// iterate over matched path params
|
||||
(paramsOut, matchResult, i) => {
|
||||
const key = pathMatchKeys[i];
|
||||
if (!key || key.asterisk) {
|
||||
return paramsOut;
|
||||
}
|
||||
const paramName = key.name;
|
||||
|
||||
let decodedMatchResult;
|
||||
try {
|
||||
decodedMatchResult = decodeURIComponent(matchResult);
|
||||
} catch (e) {
|
||||
// ignore `URIError: malformed URI`
|
||||
}
|
||||
|
||||
paramsOut[paramName] = decodedMatchResult || matchResult;
|
||||
return paramsOut;
|
||||
},
|
||||
{
|
||||
// start with the input(query string) params, which will get overridden by path params
|
||||
...inputParams,
|
||||
}
|
||||
);
|
||||
return params;
|
||||
};
|
||||
const getRestOfPath = (pathMatch, pathMatchKeys) => {
|
||||
const rest = pathMatch[pathMatchKeys.findIndex(k => k.asterisk) + 1];
|
||||
return rest;
|
||||
};
|
||||
export const urlToPathAndParams = (url, uriPrefix) => {
|
||||
const searchMatch = url.match(/^(.*)\?(.*)$/);
|
||||
const params = searchMatch ? queryString.parse(searchMatch[2]) : {};
|
||||
const urlWithoutSearch = searchMatch ? searchMatch[1] : url;
|
||||
const delimiter = uriPrefix || '://';
|
||||
let path = urlWithoutSearch.split(delimiter)[1];
|
||||
if (path === undefined) {
|
||||
path = urlWithoutSearch;
|
||||
}
|
||||
if (path === '/') {
|
||||
path = '';
|
||||
}
|
||||
if (path[path.length - 1] === '/') {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
};
|
||||
|
||||
export const createPathParser = (
|
||||
childRouters,
|
||||
routeConfigs,
|
||||
pathConfigs = {}
|
||||
) => {
|
||||
const pathsByRouteNames = {};
|
||||
let paths = [];
|
||||
|
||||
// Build pathsByRouteNames, which includes a regex to match paths for each route. Keep in mind, the regex will pass for the route and all child routes. The code that uses pathsByRouteNames will need to also verify that the child router produces an action, in the case of isPathMatchable false (a null path).
|
||||
Object.keys(childRouters).forEach(routeName => {
|
||||
let pathPattern = pathConfigs[routeName] || routeConfigs[routeName].path;
|
||||
|
||||
if (pathPattern === undefined) {
|
||||
// If the user hasn't specified a path at all, then we assume the routeName is an appropriate path
|
||||
pathPattern = routeName;
|
||||
}
|
||||
|
||||
invariant(
|
||||
pathPattern === null || typeof pathPattern === 'string',
|
||||
`Route path for ${routeName} must be specified as a string, or null.`
|
||||
);
|
||||
|
||||
// the path may be specified as null, which is similar to empty string because it allows child routers to handle the action, but it will not match empty paths
|
||||
const isPathMatchable = pathPattern !== null;
|
||||
// pathPattern is a string with inline params, such as people/:id/*foo
|
||||
const exactReKeys = [];
|
||||
const exactRe = isPathMatchable
|
||||
? pathToRegexp(pathPattern, exactReKeys)
|
||||
: null;
|
||||
const extendedPathReKeys = [];
|
||||
const isWildcard = pathPattern === '' || !isPathMatchable;
|
||||
const extendedPathRe = pathToRegexp(
|
||||
isWildcard ? '*' : `${pathPattern}/*`,
|
||||
extendedPathReKeys
|
||||
);
|
||||
|
||||
pathsByRouteNames[routeName] = {
|
||||
exactRe,
|
||||
exactReKeys,
|
||||
extendedPathRe,
|
||||
extendedPathReKeys,
|
||||
isWildcard,
|
||||
toPath:
|
||||
pathPattern === null ? () => '' : pathToRegexp.compile(pathPattern),
|
||||
};
|
||||
});
|
||||
|
||||
paths = Object.entries(pathsByRouteNames);
|
||||
|
||||
const getActionForPathAndParams = (pathToResolve = '', inputParams = {}) => {
|
||||
// Attempt to match `pathToResolve` with a route in this router's routeConfigs, deferring to child routers
|
||||
|
||||
let matchedAction = null;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [routeName, path] of paths) {
|
||||
const { exactRe, exactReKeys, extendedPathRe, extendedPathReKeys } = path;
|
||||
const childRouter = childRouters[routeName];
|
||||
|
||||
const exactMatch = exactRe && exactRe.exec(pathToResolve);
|
||||
|
||||
if (exactMatch && exactMatch.length) {
|
||||
const extendedMatch =
|
||||
extendedPathRe && extendedPathRe.exec(pathToResolve);
|
||||
let childAction = null;
|
||||
if (extendedMatch && childRouter) {
|
||||
const restOfPath = getRestOfPath(extendedMatch, extendedPathReKeys);
|
||||
childAction = childRouter.getActionForPathAndParams(
|
||||
restOfPath,
|
||||
inputParams
|
||||
);
|
||||
}
|
||||
|
||||
return NavigationActions.navigate({
|
||||
routeName,
|
||||
params: getParamsFromPath(inputParams, exactMatch, exactReKeys),
|
||||
action: childAction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [routeName, path] of paths) {
|
||||
const { extendedPathRe, extendedPathReKeys } = path;
|
||||
const childRouter = childRouters[routeName];
|
||||
|
||||
const extendedMatch =
|
||||
extendedPathRe && extendedPathRe.exec(pathToResolve);
|
||||
|
||||
if (extendedMatch && extendedMatch.length) {
|
||||
const restOfPath = getRestOfPath(extendedMatch, extendedPathReKeys);
|
||||
let childAction = null;
|
||||
if (childRouter) {
|
||||
childAction = childRouter.getActionForPathAndParams(
|
||||
restOfPath,
|
||||
inputParams
|
||||
);
|
||||
}
|
||||
if (!childAction) {
|
||||
continue;
|
||||
}
|
||||
return NavigationActions.navigate({
|
||||
routeName,
|
||||
params: getParamsFromPath(
|
||||
inputParams,
|
||||
extendedMatch,
|
||||
extendedPathReKeys
|
||||
),
|
||||
action: childAction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const getPathAndParamsForRoute = route => {
|
||||
const { routeName, params } = route;
|
||||
const childRouter = childRouters[routeName];
|
||||
const { toPath, exactReKeys } = pathsByRouteNames[routeName];
|
||||
const subPath = toPath(params);
|
||||
const nonPathParams = {};
|
||||
if (params) {
|
||||
Object.keys(params)
|
||||
.filter(paramName => !exactReKeys.find(k => k.name === paramName))
|
||||
.forEach(paramName => {
|
||||
nonPathParams[paramName] = params[paramName];
|
||||
});
|
||||
}
|
||||
if (childRouter) {
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = childRouter.getPathAndParamsForState(route);
|
||||
return {
|
||||
path: subPath ? `${subPath}/${child.path}` : child.path,
|
||||
params: child.params
|
||||
? { ...nonPathParams, ...child.params }
|
||||
: nonPathParams,
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: subPath,
|
||||
params: nonPathParams,
|
||||
};
|
||||
};
|
||||
return { getActionForPathAndParams, getPathAndParamsForRoute };
|
||||
};
|
||||
@@ -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';
|
||||
@@ -21,7 +21,47 @@ import withOrientation from '../withOrientation';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
|
||||
|
||||
// These can be adjusted by using headerTitleContainerStyle on navigationOptions
|
||||
const TITLE_OFFSET_CENTER_ALIGN = Platform.OS === 'ios' ? 70 : 56;
|
||||
const TITLE_OFFSET_LEFT_ALIGN = Platform.OS === 'ios' ? 20 : 56;
|
||||
|
||||
const getTitleOffsets = (
|
||||
layoutPreset,
|
||||
forceBackTitle,
|
||||
hasLeftComponent,
|
||||
hasRightComponent
|
||||
) => {
|
||||
if (layoutPreset === 'left') {
|
||||
// Maybe at some point we should do something different if the back title is
|
||||
// explicitly enabled, for now people can control it manually
|
||||
|
||||
let style = {
|
||||
left: TITLE_OFFSET_LEFT_ALIGN,
|
||||
right: TITLE_OFFSET_LEFT_ALIGN,
|
||||
};
|
||||
|
||||
if (!hasLeftComponent) {
|
||||
style.left = 0;
|
||||
}
|
||||
if (!hasRightComponent) {
|
||||
style.right = 0;
|
||||
}
|
||||
|
||||
return style;
|
||||
} else if (layoutPreset === 'center') {
|
||||
let style = {
|
||||
left: TITLE_OFFSET_CENTER_ALIGN,
|
||||
right: TITLE_OFFSET_CENTER_ALIGN,
|
||||
};
|
||||
if (!hasLeftComponent && !hasRightComponent) {
|
||||
style.left = 0;
|
||||
style.right = 0;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
};
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
return Platform.OS === 'ios'
|
||||
@@ -55,6 +95,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;
|
||||
}
|
||||
|
||||
@@ -83,6 +132,7 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
const { layoutPreset } = this.props;
|
||||
const { options } = props.scene.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
@@ -94,10 +144,10 @@ class Header extends React.PureComponent {
|
||||
const color = options.headerTintColor;
|
||||
const allowFontScaling = options.headerTitleAllowFontScaling;
|
||||
|
||||
// On iOS, width of left/right components depends on the calculated
|
||||
// size of the title.
|
||||
const onLayoutIOS =
|
||||
Platform.OS === 'ios'
|
||||
// When title is centered, the width of left/right components depends on the
|
||||
// calculated size of the title.
|
||||
const onLayout =
|
||||
layoutPreset === 'center'
|
||||
? e => {
|
||||
this.setState({
|
||||
widths: {
|
||||
@@ -108,18 +158,24 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const RenderedHeaderTitle =
|
||||
const HeaderTitleComponent =
|
||||
headerTitle && typeof headerTitle !== 'string'
|
||||
? headerTitle
|
||||
: HeaderTitle;
|
||||
return (
|
||||
<RenderedHeaderTitle
|
||||
onLayout={onLayoutIOS}
|
||||
<HeaderTitleComponent
|
||||
onLayout={onLayout}
|
||||
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
|
||||
style={[color ? { color } : null, titleStyle]}
|
||||
style={[
|
||||
color ? { color } : null,
|
||||
layoutPreset === 'center'
|
||||
? { textAlign: 'center' }
|
||||
: { textAlign: 'left' },
|
||||
titleStyle,
|
||||
]}
|
||||
>
|
||||
{titleString}
|
||||
</RenderedHeaderTitle>
|
||||
</HeaderTitleComponent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -132,7 +188,7 @@ class Header extends React.PureComponent {
|
||||
return options.headerLeft;
|
||||
}
|
||||
|
||||
if (props.scene.index === 0) {
|
||||
if (!options.headerLeft && props.scene.index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,7 +214,9 @@ class Header extends React.PureComponent {
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
backTitleVisible={this.props.backTitleVisible}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
layoutPreset={this.props.layoutPreset}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
@@ -211,6 +269,11 @@ class Header extends React.PureComponent {
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
let { style } = props;
|
||||
if (options.headerLeftContainerStyle) {
|
||||
style = [style, options.headerLeftContainerStyle];
|
||||
}
|
||||
|
||||
// On Android, or if we have a custom header left, or if we have a custom back image, we
|
||||
// do not use the modular header (which is the one that imitates UINavigationController)
|
||||
if (
|
||||
@@ -220,14 +283,14 @@ class Header extends React.PureComponent {
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
{ ...props, style },
|
||||
'left',
|
||||
this._renderLeftComponent,
|
||||
this.props.leftInterpolator
|
||||
);
|
||||
} else {
|
||||
return this._renderModularSubView(
|
||||
props,
|
||||
{ ...props, style },
|
||||
'left',
|
||||
this._renderModularLeftComponent,
|
||||
this.props.leftLabelInterpolator,
|
||||
@@ -237,24 +300,16 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
|
||||
_renderTitle(props, options) {
|
||||
const style = {};
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (!options.hasLeftComponent) {
|
||||
style.left = 0;
|
||||
}
|
||||
if (!options.hasRightComponent) {
|
||||
style.right = 0;
|
||||
}
|
||||
} else if (
|
||||
Platform.OS === 'ios' &&
|
||||
!options.hasLeftComponent &&
|
||||
!options.hasRightComponent
|
||||
) {
|
||||
style.left = 0;
|
||||
style.right = 0;
|
||||
}
|
||||
const { layoutPreset, transitionPreset } = this.props;
|
||||
let style = [
|
||||
{ justifyContent: layoutPreset === 'center' ? 'center' : 'flex-start' },
|
||||
getTitleOffsets(
|
||||
layoutPreset,
|
||||
options.hasLeftComponent,
|
||||
options.hasRightComponent
|
||||
),
|
||||
options.headerTitleContainerStyle,
|
||||
];
|
||||
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
@@ -267,8 +322,15 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
|
||||
_renderRight(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
let { style } = props;
|
||||
if (options.headerRightContainerStyle) {
|
||||
style = [style, options.headerRightContainerStyle];
|
||||
}
|
||||
|
||||
return this._renderSubView(
|
||||
props,
|
||||
{ ...props, style },
|
||||
'right',
|
||||
this._renderRightComponent,
|
||||
this.props.rightInterpolator
|
||||
@@ -362,7 +424,6 @@ class Header extends React.PureComponent {
|
||||
styles[name],
|
||||
props.style,
|
||||
styleInterpolator({
|
||||
// todo: determine if we really need to splat all this.props
|
||||
...this.props,
|
||||
...props,
|
||||
}),
|
||||
@@ -383,6 +444,7 @@ class Header extends React.PureComponent {
|
||||
const title = this._renderTitle(props, {
|
||||
hasLeftComponent: !!left,
|
||||
hasRightComponent: !!right,
|
||||
headerTitleContainerStyle: options.headerTitleContainerStyle,
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
@@ -464,6 +526,18 @@ class Header extends React.PureComponent {
|
||||
flexShrink,
|
||||
flexBasis,
|
||||
flexWrap,
|
||||
position,
|
||||
padding,
|
||||
paddingHorizontal,
|
||||
paddingRight,
|
||||
paddingLeft,
|
||||
// paddingVertical,
|
||||
// paddingTop,
|
||||
// paddingBottom,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
...safeHeaderStyle
|
||||
} = headerStyleObj;
|
||||
|
||||
@@ -476,6 +550,18 @@ class Header extends React.PureComponent {
|
||||
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
||||
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
||||
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
|
||||
warnIfHeaderStyleDefined(padding, 'padding');
|
||||
warnIfHeaderStyleDefined(position, 'position');
|
||||
warnIfHeaderStyleDefined(paddingHorizontal, 'paddingHorizontal');
|
||||
warnIfHeaderStyleDefined(paddingRight, 'paddingRight');
|
||||
warnIfHeaderStyleDefined(paddingLeft, 'paddingLeft');
|
||||
// warnIfHeaderStyleDefined(paddingVertical, 'paddingVertical');
|
||||
// warnIfHeaderStyleDefined(paddingTop, 'paddingTop');
|
||||
// warnIfHeaderStyleDefined(paddingBottom, 'paddingBottom');
|
||||
warnIfHeaderStyleDefined(top, 'top');
|
||||
warnIfHeaderStyleDefined(right, 'right');
|
||||
warnIfHeaderStyleDefined(bottom, 'bottom');
|
||||
warnIfHeaderStyleDefined(left, 'left');
|
||||
}
|
||||
|
||||
// TODO: warn if any unsafe styles are provided
|
||||
@@ -491,7 +577,17 @@ 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' && !options.headerTransparent
|
||||
? {
|
||||
backgroundColor:
|
||||
safeHeaderStyle.backgroundColor || DEFAULT_BACKGROUND_COLOR,
|
||||
}
|
||||
: null,
|
||||
]}
|
||||
>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
{options.headerBackground}
|
||||
@@ -504,7 +600,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 +629,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: {
|
||||
@@ -540,6 +642,9 @@ const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
...platformContainerStyles,
|
||||
borderBottomWidth: 0,
|
||||
borderBottomColor: 'transparent',
|
||||
elevation: 0,
|
||||
},
|
||||
header: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
@@ -571,12 +676,9 @@ const styles = StyleSheet.create({
|
||||
title: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: TITLE_OFFSET,
|
||||
right: TITLE_OFFSET,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
|
||||
},
|
||||
left: {
|
||||
left: 0,
|
||||
|
||||
@@ -62,9 +62,38 @@ class HeaderBackButton extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onPress, pressColorAndroid, layoutPreset, title } = this.props;
|
||||
|
||||
let button = (
|
||||
<TouchableItem
|
||||
accessibilityComponentType="button"
|
||||
accessibilityLabel={title}
|
||||
accessibilityTraits="button"
|
||||
testID="header-back"
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
pressColor={pressColorAndroid}
|
||||
style={styles.container}
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{this._renderBackImage()}
|
||||
{this._maybeRenderTitle()}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return <View style={styles.androidButtonWrapper}>{button}</View>;
|
||||
} else {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
_maybeRenderTitle() {
|
||||
const {
|
||||
onPress,
|
||||
pressColorAndroid,
|
||||
layoutPreset,
|
||||
backTitleVisible,
|
||||
width,
|
||||
title,
|
||||
titleStyle,
|
||||
@@ -79,41 +108,35 @@ class HeaderBackButton extends React.PureComponent {
|
||||
|
||||
const backButtonTitle = renderTruncated ? truncatedTitle : title;
|
||||
|
||||
// If the left preset is used and we aren't on Android, then we
|
||||
// default to disabling the label
|
||||
const titleDefaultsToDisabled =
|
||||
layoutPreset === 'left' ||
|
||||
Platform.OS === 'android' ||
|
||||
typeof backButtonTitle !== 'string';
|
||||
|
||||
// If the title is explicitly enabled then we respect that
|
||||
if (titleDefaultsToDisabled && !backTitleVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableItem
|
||||
accessibilityComponentType="button"
|
||||
accessibilityLabel={backButtonTitle}
|
||||
accessibilityTraits="button"
|
||||
testID="header-back"
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
pressColor={pressColorAndroid}
|
||||
style={styles.container}
|
||||
borderless
|
||||
<Text
|
||||
onLayout={this._onTextLayout}
|
||||
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{this._renderBackImage()}
|
||||
{Platform.OS === 'ios' &&
|
||||
typeof backButtonTitle === 'string' && (
|
||||
<Text
|
||||
onLayout={this._onTextLayout}
|
||||
style={[
|
||||
styles.title,
|
||||
!!tintColor && { color: tintColor },
|
||||
titleStyle,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{backButtonTitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
{backButtonTitle}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
androidButtonWrapper: {
|
||||
margin: 13,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
@@ -137,7 +160,7 @@ const styles = StyleSheet.create({
|
||||
: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
margin: 16,
|
||||
margin: 3,
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -17,7 +17,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: Platform.OS === 'ios' ? 17 : 20,
|
||||
fontWeight: Platform.OS === 'ios' ? '700' : '500',
|
||||
color: 'rgba(0, 0, 0, .9)',
|
||||
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
});
|
||||
|
||||
57
src/views/NavigationEvents.js
Normal file
57
src/views/NavigationEvents.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import withNavigation from './withNavigation';
|
||||
|
||||
const EventNameToPropName = {
|
||||
willFocus: 'onWillFocus',
|
||||
didFocus: 'onDidFocus',
|
||||
willBlur: 'onWillBlur',
|
||||
didBlur: 'onDidBlur',
|
||||
};
|
||||
|
||||
const EventNames = Object.keys(EventNameToPropName);
|
||||
|
||||
class NavigationEvents extends React.Component {
|
||||
componentDidMount() {
|
||||
this.subscriptions = {};
|
||||
EventNames.forEach(this.addListener);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
EventNames.forEach(eventName => {
|
||||
const listenerHasChanged =
|
||||
this.props[EventNameToPropName[eventName]] !==
|
||||
prevProps[EventNameToPropName[eventName]];
|
||||
if (listenerHasChanged) {
|
||||
this.removeListener(eventName);
|
||||
this.addListener(eventName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventNames.forEach(this.removeListener);
|
||||
}
|
||||
|
||||
addListener = eventName => {
|
||||
const listener = this.props[EventNameToPropName[eventName]];
|
||||
if (listener) {
|
||||
this.subscriptions[eventName] = this.props.navigation.addListener(
|
||||
eventName,
|
||||
listener
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
removeListener = eventName => {
|
||||
if (this.subscriptions[eventName]) {
|
||||
this.subscriptions[eventName].remove();
|
||||
this.subscriptions[eventName] = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default withNavigation(NavigationEvents);
|
||||
@@ -130,16 +130,26 @@ export default function ScenesReducer(
|
||||
return;
|
||||
}
|
||||
const lastScene = scenes.find(scene => scene.route.key === route.key);
|
||||
const descriptor = lastScene && lastScene.descriptor;
|
||||
|
||||
staleScenes.set(key, {
|
||||
index,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
key,
|
||||
route,
|
||||
descriptor,
|
||||
});
|
||||
// We can get into a weird place where we have a queued transition and then clobber
|
||||
// that transition without ever actually rendering the scene, in which case
|
||||
// there is no lastScene. If the descriptor is not available on the lastScene
|
||||
// or the descriptors prop then we just skip adding it to stale scenes and it's
|
||||
// not ever rendered.
|
||||
const descriptor = lastScene
|
||||
? lastScene.descriptor
|
||||
: descriptors[route.key];
|
||||
|
||||
if (descriptor) {
|
||||
staleScenes.set(key, {
|
||||
index,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
key,
|
||||
route,
|
||||
descriptor,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@ class StackView extends React.Component {
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
onTransitionStart={this.props.onTransitionStart}
|
||||
onTransitionStart={
|
||||
this.props.onTransitionStart ||
|
||||
this.props.navigationConfig.onTransitionStart
|
||||
}
|
||||
onTransitionEnd={(transition, lastTransition) => {
|
||||
const { navigationConfig, navigation } = this.props;
|
||||
const { onTransitionEnd } = navigationConfig;
|
||||
const onTransitionEnd =
|
||||
this.props.onTransitionEnd || navigationConfig.onTransitionEnd;
|
||||
if (transition.navigation.state.isTransitioning) {
|
||||
navigation.dispatch(
|
||||
StackActions.completeTransition({
|
||||
|
||||
@@ -34,6 +34,12 @@ const IS_IPHONE_X =
|
||||
|
||||
const EaseInOut = Easing.inOut(Easing.ease);
|
||||
|
||||
/**
|
||||
* Enumerate possible values for validation
|
||||
*/
|
||||
const HEADER_LAYOUT_PRESET_VALUES = ['center', 'left'];
|
||||
const HEADER_TRANSITION_PRESET_VALUES = ['uikit', 'fade-in-place'];
|
||||
|
||||
/**
|
||||
* The max duration of the card animation in milliseconds after released gesture.
|
||||
* The actual duration should be always less then that because the rest distance
|
||||
@@ -68,6 +74,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,15 +109,29 @@ class StackViewLayout extends React.Component {
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
state = {
|
||||
// Used when card's header is null and mode is float to make switch animation work correctly
|
||||
floatingHeaderHeight: 0,
|
||||
};
|
||||
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;
|
||||
}
|
||||
@@ -119,7 +153,7 @@ class StackViewLayout extends React.Component {
|
||||
const {
|
||||
mode,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
lastTransitionProps,
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
@@ -131,6 +165,8 @@ class StackViewLayout extends React.Component {
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
layoutPreset: this._getHeaderLayoutPreset(),
|
||||
backTitleVisible: this._getheaderBackTitleVisible(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
@@ -401,6 +437,7 @@ class StackViewLayout extends React.Component {
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
|
||||
if (headerMode === 'float') {
|
||||
const { scene } = this.props.transitionProps;
|
||||
floatingHeader = (
|
||||
@@ -448,6 +485,40 @@ class StackViewLayout extends React.Component {
|
||||
return 'float';
|
||||
}
|
||||
|
||||
_getHeaderLayoutPreset() {
|
||||
const { headerLayoutPreset } = this.props;
|
||||
if (headerLayoutPreset) {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
this._getHeaderTransitionPreset() === 'uitkit' &&
|
||||
headerLayoutPreset === 'left' &&
|
||||
Platform.OS === 'ios'
|
||||
) {
|
||||
console.warn(
|
||||
`headerTransitionPreset with the value 'ui-kit' is incompatible with headerLayoutPreset 'left'`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (HEADER_LAYOUT_PRESET_VALUES.includes(headerLayoutPreset)) {
|
||||
return headerLayoutPreset;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`Invalid configuration applied for headerLayoutPreset - expected one of ${HEADER_LAYOUT_PRESET_VALUES.join(
|
||||
', '
|
||||
)} but received ${JSON.stringify(headerLayoutPreset)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return 'left';
|
||||
} else {
|
||||
return 'center';
|
||||
}
|
||||
}
|
||||
|
||||
_getHeaderTransitionPreset() {
|
||||
// On Android or with header mode screen, we always just use in-place,
|
||||
// we ignore the option entirely (at least until we have other presets)
|
||||
@@ -455,12 +526,28 @@ class StackViewLayout extends React.Component {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
// TODO: validations: 'fade-in-place' or 'uikit' are valid
|
||||
if (this.props.headerTransitionPreset) {
|
||||
return this.props.headerTransitionPreset;
|
||||
} else {
|
||||
return 'fade-in-place';
|
||||
const { headerTransitionPreset } = this.props;
|
||||
if (headerTransitionPreset) {
|
||||
if (HEADER_TRANSITION_PRESET_VALUES.includes(headerTransitionPreset)) {
|
||||
return headerTransitionPreset;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`Invalid configuration applied for headerTransitionPreset - expected one of ${HEADER_TRANSITION_PRESET_VALUES.join(
|
||||
', '
|
||||
)} but received ${JSON.stringify(headerTransitionPreset)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
_getheaderBackTitleVisible() {
|
||||
const { headerBackTitleVisible } = this.props;
|
||||
|
||||
return headerBackTitleVisible;
|
||||
}
|
||||
|
||||
_renderInnerScene(scene) {
|
||||
@@ -498,13 +585,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 });
|
||||
|
||||
@@ -166,7 +166,7 @@ class Transitioner extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View onLayout={this._onLayout} style={[styles.main]}>
|
||||
<View onLayout={this._onLayout} style={styles.main}>
|
||||
{this.props.render(this._transitionProps, this._prevTransitionProps)}
|
||||
</View>
|
||||
);
|
||||
|
||||
241
src/views/__tests__/NavigationEvents-test.js
Normal file
241
src/views/__tests__/NavigationEvents-test.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
import NavigationEvents from '../NavigationEvents';
|
||||
import { NavigationProvider } from '../NavigationContext';
|
||||
|
||||
const createListener = () => payload => {};
|
||||
|
||||
// An easy way to create the 4 listeners prop
|
||||
const createEventListenersProp = () => ({
|
||||
onWillFocus: createListener(),
|
||||
onDidFocus: createListener(),
|
||||
onWillBlur: createListener(),
|
||||
onDidBlur: createListener(),
|
||||
});
|
||||
|
||||
const createNavigationAndHelpers = () => {
|
||||
// A little API to spy on subscription remove calls that are performed during the tests
|
||||
const removeCallsAPI = (() => {
|
||||
let removeCalls = [];
|
||||
return {
|
||||
reset: () => {
|
||||
removeCalls = [];
|
||||
},
|
||||
add: (name, handler) => {
|
||||
removeCalls.push({ name, handler });
|
||||
},
|
||||
checkRemoveCalled: count => {
|
||||
expect(removeCalls.length).toBe(count);
|
||||
},
|
||||
checkRemoveCalledWith: (name, handler) => {
|
||||
expect(removeCalls).toContainEqual({ name, handler });
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
const navigation = {
|
||||
addListener: jest.fn((name, handler) => {
|
||||
return {
|
||||
remove: () => removeCallsAPI.add(name, handler),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const checkAddListenerCalled = count => {
|
||||
expect(navigation.addListener).toHaveBeenCalledTimes(count);
|
||||
};
|
||||
const checkAddListenerCalledWith = (eventName, handler) => {
|
||||
expect(navigation.addListener).toHaveBeenCalledWith(eventName, handler);
|
||||
};
|
||||
const checkRemoveCalled = count => {
|
||||
removeCallsAPI.checkRemoveCalled(count);
|
||||
};
|
||||
const checkRemoveCalledWith = (eventName, handler) => {
|
||||
removeCallsAPI.checkRemoveCalledWith(eventName, handler);
|
||||
};
|
||||
|
||||
return {
|
||||
navigation,
|
||||
removeCallsAPI,
|
||||
checkAddListenerCalled,
|
||||
checkAddListenerCalledWith,
|
||||
checkRemoveCalled,
|
||||
checkRemoveCalledWith,
|
||||
};
|
||||
};
|
||||
|
||||
// We test 2 distinct ways to provide the navigation to the NavigationEvents (prop/context)
|
||||
const NavigationEventsTestComp = ({
|
||||
withContext = true,
|
||||
navigation,
|
||||
...props
|
||||
}) => {
|
||||
if (withContext) {
|
||||
return (
|
||||
<NavigationProvider value={navigation}>
|
||||
<NavigationEvents {...props} />
|
||||
</NavigationProvider>
|
||||
);
|
||||
} else {
|
||||
return <NavigationEvents navigation={navigation} {...props} />;
|
||||
}
|
||||
};
|
||||
|
||||
describe('NavigationEvents', () => {
|
||||
it('add all listeners with navigation prop', () => {
|
||||
const {
|
||||
navigation,
|
||||
checkAddListenerCalled,
|
||||
checkAddListenerCalledWith,
|
||||
} = createNavigationAndHelpers();
|
||||
const eventListenerProps = createEventListenersProp();
|
||||
const component = renderer.create(
|
||||
<NavigationEventsTestComp
|
||||
withContext={false}
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
checkAddListenerCalled(4);
|
||||
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
|
||||
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
|
||||
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
|
||||
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
|
||||
});
|
||||
|
||||
it('add all listeners with navigation context', () => {
|
||||
const {
|
||||
navigation,
|
||||
checkAddListenerCalled,
|
||||
checkAddListenerCalledWith,
|
||||
} = createNavigationAndHelpers();
|
||||
const eventListenerProps = createEventListenersProp();
|
||||
const component = renderer.create(
|
||||
<NavigationEventsTestComp
|
||||
withContext={true}
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
checkAddListenerCalled(4);
|
||||
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
|
||||
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
|
||||
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
|
||||
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
|
||||
});
|
||||
|
||||
it('remove all listeners on unmount', () => {
|
||||
const {
|
||||
navigation,
|
||||
checkRemoveCalled,
|
||||
checkRemoveCalledWith,
|
||||
} = createNavigationAndHelpers();
|
||||
const eventListenerProps = createEventListenersProp();
|
||||
|
||||
const component = renderer.create(
|
||||
<NavigationEventsTestComp
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
checkRemoveCalled(0);
|
||||
component.unmount();
|
||||
checkRemoveCalled(4);
|
||||
checkRemoveCalledWith('willBlur', eventListenerProps.onWillBlur);
|
||||
checkRemoveCalledWith('willFocus', eventListenerProps.onWillFocus);
|
||||
checkRemoveCalledWith('didBlur', eventListenerProps.onDidBlur);
|
||||
checkRemoveCalledWith('didFocus', eventListenerProps.onDidFocus);
|
||||
});
|
||||
|
||||
it('add a single listener', () => {
|
||||
const {
|
||||
navigation,
|
||||
checkAddListenerCalled,
|
||||
checkAddListenerCalledWith,
|
||||
} = createNavigationAndHelpers();
|
||||
const listener = createListener();
|
||||
const component = renderer.create(
|
||||
<NavigationEventsTestComp navigation={navigation} onDidFocus={listener} />
|
||||
);
|
||||
checkAddListenerCalled(1);
|
||||
checkAddListenerCalledWith('didFocus', listener);
|
||||
});
|
||||
|
||||
it('do not attempt to add/remove stable listeners on update', () => {
|
||||
const {
|
||||
navigation,
|
||||
checkAddListenerCalled,
|
||||
checkAddListenerCalledWith,
|
||||
} = createNavigationAndHelpers();
|
||||
const eventListenerProps = createEventListenersProp();
|
||||
const component = renderer.create(
|
||||
<NavigationEventsTestComp
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
component.update(
|
||||
<NavigationEventsTestComp
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
component.update(
|
||||
<NavigationEventsTestComp
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
checkAddListenerCalled(4);
|
||||
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
|
||||
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
|
||||
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
|
||||
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
|
||||
});
|
||||
|
||||
it('add, remove and replace (remove+add) listeners on complex updates', () => {
|
||||
const {
|
||||
navigation,
|
||||
checkAddListenerCalled,
|
||||
checkAddListenerCalledWith,
|
||||
checkRemoveCalled,
|
||||
checkRemoveCalledWith,
|
||||
} = createNavigationAndHelpers();
|
||||
const eventListenerProps = createEventListenersProp();
|
||||
|
||||
const component = renderer.create(
|
||||
<NavigationEventsTestComp
|
||||
navigation={navigation}
|
||||
{...eventListenerProps}
|
||||
/>
|
||||
);
|
||||
|
||||
checkAddListenerCalled(4);
|
||||
checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
|
||||
checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
|
||||
checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
|
||||
checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
|
||||
checkRemoveCalled(0);
|
||||
|
||||
const onWillFocus2 = createListener();
|
||||
const onDidFocus2 = createListener();
|
||||
|
||||
component.update(
|
||||
<NavigationEventsTestComp
|
||||
navigation={navigation}
|
||||
onWillBlur={eventListenerProps.onWillBlur}
|
||||
onDidBlur={undefined}
|
||||
onWillFocus={onWillFocus2}
|
||||
onDidFocus={onDidFocus2}
|
||||
/>
|
||||
);
|
||||
checkAddListenerCalled(6);
|
||||
checkAddListenerCalledWith('willFocus', onWillFocus2);
|
||||
checkAddListenerCalledWith('didFocus', onDidFocus2);
|
||||
checkRemoveCalled(3);
|
||||
checkRemoveCalledWith('didBlur', eventListenerProps.onDidBlur);
|
||||
checkRemoveCalledWith('willFocus', eventListenerProps.onWillFocus);
|
||||
checkRemoveCalledWith('didFocus', eventListenerProps.onDidFocus);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,17 @@
|
||||
import ScenesReducer from '../ScenesReducer';
|
||||
|
||||
const MOCK_DESCRIPTOR = {};
|
||||
|
||||
/**
|
||||
* Simulate scenes transtion with changes of navigation states.
|
||||
*/
|
||||
function testTransition(states) {
|
||||
let descriptors = states
|
||||
.reduce((acc, state) => acc.concat(state), [])
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = MOCK_DESCRIPTOR;
|
||||
return acc;
|
||||
}, {});
|
||||
const routes = states.map(keys => ({
|
||||
index: 0,
|
||||
routes: keys.map(key => ({ key, routeName: '' })),
|
||||
@@ -13,7 +21,7 @@ function testTransition(states) {
|
||||
let scenes = [];
|
||||
let prevState = null;
|
||||
routes.forEach(nextState => {
|
||||
scenes = ScenesReducer(scenes, nextState, prevState);
|
||||
scenes = ScenesReducer(scenes, nextState, prevState, descriptors);
|
||||
prevState = nextState;
|
||||
});
|
||||
|
||||
@@ -29,6 +37,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
@@ -39,6 +48,7 @@ describe('ScenesReducer', () => {
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
@@ -57,6 +67,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
@@ -67,6 +78,7 @@ describe('ScenesReducer', () => {
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
@@ -77,6 +89,7 @@ describe('ScenesReducer', () => {
|
||||
index: 2,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
@@ -136,8 +149,10 @@ describe('ScenesReducer', () => {
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1);
|
||||
const descriptors = { 1: jest.mock(), 2: jest.mock() };
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null, descriptors);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
|
||||
expect(scenes1).not.toBe(scenes2);
|
||||
});
|
||||
|
||||
@@ -160,8 +175,10 @@ describe('ScenesReducer', () => {
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1);
|
||||
const descriptors = { 1: MOCK_DESCRIPTOR, 2: MOCK_DESCRIPTOR };
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null, descriptors);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
|
||||
expect(scenes1).not.toBe(scenes2);
|
||||
});
|
||||
|
||||
@@ -184,8 +201,9 @@ describe('ScenesReducer', () => {
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1);
|
||||
const descriptors = { 1: MOCK_DESCRIPTOR, 2: MOCK_DESCRIPTOR };
|
||||
const scenes1 = ScenesReducer([], state1, null, descriptors);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
|
||||
expect(scenes1).not.toBe(scenes2);
|
||||
});
|
||||
|
||||
@@ -198,6 +216,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
@@ -208,6 +227,7 @@ describe('ScenesReducer', () => {
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
@@ -218,6 +238,7 @@ describe('ScenesReducer', () => {
|
||||
index: 2,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
@@ -235,6 +256,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
@@ -245,6 +267,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
@@ -255,6 +278,7 @@ describe('ScenesReducer', () => {
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
@@ -272,6 +296,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
@@ -282,6 +307,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
@@ -292,6 +318,7 @@ describe('ScenesReducer', () => {
|
||||
index: 0,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -4659,6 +4659,13 @@ qs@~6.5.1:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
||||
query-string@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.1.0.tgz#01e7d69f6a0940dac67a937d6c6325647aa4532a"
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
random-bytes@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
|
||||
@@ -4828,9 +4835,9 @@ react-navigation-deprecated-tab-navigator@1.3.0:
|
||||
dependencies:
|
||||
react-native-tab-view "^0.0.77"
|
||||
|
||||
react-navigation-drawer@0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-0.4.1.tgz#ea3c9218dabbe8626c91d388cf0df4b1f8b14cb7"
|
||||
react-navigation-drawer@0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-0.4.3.tgz#c04c94e2429b7e724801af05bd0a93a79cb27f71"
|
||||
dependencies:
|
||||
react-native-drawer-layout-polyfill "^1.3.2"
|
||||
|
||||
@@ -5565,6 +5572,10 @@ stream-to-observable@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe"
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
|
||||
Reference in New Issue
Block a user