mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-03 09:24:56 +08:00
Compare commits
20 Commits
2.5.5
...
@ericvicen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
655453aed3 | ||
|
|
c4b84f1d66 | ||
|
|
69f394be5b | ||
|
|
316e4991ac | ||
|
|
805064cb5e | ||
|
|
8f199980cb | ||
|
|
37ca6a92ca | ||
|
|
980e0409dc | ||
|
|
a00ba5918a | ||
|
|
ad6b25cff9 | ||
|
|
a69b67d6d2 | ||
|
|
dc436e4d01 | ||
|
|
fe95bdeee6 | ||
|
|
525528e38f | ||
|
|
9f5f3d994c | ||
|
|
e8c1833053 | ||
|
|
0921889f7a | ||
|
|
1951a3ac46 | ||
|
|
4e384f8057 | ||
|
|
3d06d19d6a |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
## [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)
|
## [2.5.5] - [2018-06-27](https://github.com/react-navigation/react-navigation/releases/tag/2.5.5)
|
||||||
### Added
|
### Added
|
||||||
@@ -45,7 +71,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||||||
### Changed
|
### Changed
|
||||||
- Improved examples
|
- Improved examples
|
||||||
|
|
||||||
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.5.5...HEAD
|
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.6.2...HEAD
|
||||||
|
[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.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.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.3]: https://github.com/react-navigation/react-navigation/compare/2.5.2...2.5.3
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import {
|
|||||||
StatusBar,
|
StatusBar,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
|
import Reactotron from 'reactotron-react-native';
|
||||||
|
|
||||||
import { SafeAreaView, createStackNavigator } from 'react-navigation';
|
import { SafeAreaView, createStackNavigator } from 'react-navigation';
|
||||||
|
|
||||||
import CustomTabs from './CustomTabs';
|
import CustomTabs from './CustomTabs';
|
||||||
@@ -36,8 +39,14 @@ import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
|||||||
import SimpleTabs from './SimpleTabs';
|
import SimpleTabs from './SimpleTabs';
|
||||||
import SwitchWithStacks from './SwitchWithStacks';
|
import SwitchWithStacks from './SwitchWithStacks';
|
||||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||||
|
import TabsWithNavigationEvents from './TabsWithNavigationEvents';
|
||||||
import KeyboardHandlingExample from './KeyboardHandlingExample';
|
import KeyboardHandlingExample from './KeyboardHandlingExample';
|
||||||
|
|
||||||
|
Reactotron.configure()
|
||||||
|
.useReactNative()
|
||||||
|
.connect();
|
||||||
|
console.tron = Reactotron;
|
||||||
|
|
||||||
const ExampleInfo = {
|
const ExampleInfo = {
|
||||||
SimpleStack: {
|
SimpleStack: {
|
||||||
name: 'Stack Example',
|
name: 'Stack Example',
|
||||||
@@ -126,6 +135,11 @@ const ExampleInfo = {
|
|||||||
name: 'withNavigationFocus',
|
name: 'withNavigationFocus',
|
||||||
description: 'Receive the focus prop to know when a screen is focused',
|
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: {
|
KeyboardHandlingExample: {
|
||||||
name: 'Keyboard Handling Example',
|
name: 'Keyboard Handling Example',
|
||||||
description:
|
description:
|
||||||
@@ -166,6 +180,7 @@ const ExampleRoutes = {
|
|||||||
path: 'settings',
|
path: 'settings',
|
||||||
},
|
},
|
||||||
TabsWithNavigationFocus,
|
TabsWithNavigationFocus,
|
||||||
|
TabsWithNavigationEvents,
|
||||||
KeyboardHandlingExample,
|
KeyboardHandlingExample,
|
||||||
// This is commented out because it's rarely useful
|
// This is commented out because it's rarely useful
|
||||||
// InactiveStack,
|
// InactiveStack,
|
||||||
@@ -333,7 +348,9 @@ const AppNavigator = createStackNavigator(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default AppNavigator;
|
const App = () => <AppNavigator persistenceKey="yes" />;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
item: {
|
item: {
|
||||||
|
|||||||
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;
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"react-navigation": "link:../..",
|
"react-navigation": "link:../..",
|
||||||
"react-navigation-header-buttons": "^0.0.4",
|
"react-navigation-header-buttons": "^0.0.4",
|
||||||
"react-navigation-material-bottom-tabs": "0.1.3",
|
"react-navigation-material-bottom-tabs": "0.1.3",
|
||||||
"react-navigation-tabs": "^0.5.1"
|
"react-navigation-tabs": "^0.5.1",
|
||||||
|
"reactotron-react-native": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-jest": "^22.4.1",
|
"babel-jest": "^22.4.1",
|
||||||
|
|||||||
@@ -4734,6 +4734,10 @@ minizlib@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minipass "^2.2.1"
|
minipass "^2.2.1"
|
||||||
|
|
||||||
|
mitt@^1.1.2:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.3.tgz#528c506238a05dce11cd914a741ea2cc332da9b8"
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
|
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
|
||||||
@@ -5446,6 +5450,13 @@ qs@6.5.2, qs@^6.4.0, qs@^6.5.0, qs@^6.5.1, qs@~6.5.1:
|
|||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
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"
|
||||||
|
|
||||||
querystring@0.2.0, querystring@^0.2.0:
|
querystring@0.2.0, querystring@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||||
@@ -5758,9 +5769,9 @@ react-navigation-deprecated-tab-navigator@1.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react-native-tab-view "^0.0.77"
|
react-native-tab-view "^0.0.77"
|
||||||
|
|
||||||
react-navigation-drawer@0.3.0:
|
react-navigation-drawer@0.4.3:
|
||||||
version "0.3.0"
|
version "0.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-0.3.0.tgz#641007213f0f1e1b55a0a4bb64d71df07b3e7208"
|
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-0.4.3.tgz#c04c94e2429b7e724801af05bd0a93a79cb27f71"
|
||||||
dependencies:
|
dependencies:
|
||||||
react-native-drawer-layout-polyfill "^1.3.2"
|
react-native-drawer-layout-polyfill "^1.3.2"
|
||||||
|
|
||||||
@@ -5871,6 +5882,18 @@ react@^16.0.0:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
|
reactotron-core-client@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/reactotron-core-client/-/reactotron-core-client-2.0.0.tgz#0229e7938ed17104b846c50295ae8cb40557e83c"
|
||||||
|
|
||||||
|
reactotron-react-native@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/reactotron-react-native/-/reactotron-react-native-2.0.0.tgz#54d1119c6640b7e8c6c7383e482474d4cef11016"
|
||||||
|
dependencies:
|
||||||
|
mitt "^1.1.2"
|
||||||
|
prop-types "^15.5.10"
|
||||||
|
reactotron-core-client "^2.0.0"
|
||||||
|
|
||||||
read-chunk@^2.0.0:
|
read-chunk@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655"
|
resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655"
|
||||||
@@ -6570,6 +6593,10 @@ stream-parser@~0.3.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug "2"
|
debug "2"
|
||||||
|
|
||||||
|
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:
|
string-length@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||||
|
|||||||
25
flow/react-navigation.js
vendored
25
flow/react-navigation.js
vendored
@@ -184,7 +184,7 @@ declare module 'react-navigation' {
|
|||||||
| NavigationLeafRoute
|
| NavigationLeafRoute
|
||||||
| NavigationStateRoute;
|
| NavigationStateRoute;
|
||||||
|
|
||||||
declare export type NavigationLeafRoute = {
|
declare export type NavigationLeafRoute = {|
|
||||||
/**
|
/**
|
||||||
* React's key used by some navigators. No need to specify these manually,
|
* React's key used by some navigators. No need to specify these manually,
|
||||||
* they will be defined by the router.
|
* 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.
|
* e.g. `{ car_id: 123 }` in a route that displays a car.
|
||||||
*/
|
*/
|
||||||
params?: NavigationParams,
|
params?: NavigationParams,
|
||||||
};
|
|};
|
||||||
|
|
||||||
declare export type NavigationStateRoute = NavigationLeafRoute &
|
declare export type NavigationStateRoute = {|
|
||||||
NavigationState;
|
...NavigationLeafRoute,
|
||||||
|
...$Exact<NavigationState>,
|
||||||
|
|};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router
|
* Router
|
||||||
@@ -557,6 +559,21 @@ declare module 'react-navigation' {
|
|||||||
navigationOptions?: O,
|
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
|
* Navigation container
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-navigation",
|
"name": "react-navigation",
|
||||||
"version": "2.5.5",
|
"version": "2.6.2",
|
||||||
"description": "Routing and navigation for your React Native apps",
|
"description": "Routing and navigation for your React Native apps",
|
||||||
"main": "src/react-navigation.js",
|
"main": "src/react-navigation.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"create-react-context": "^0.2.1",
|
"create-react-context": "^0.2.1",
|
||||||
"hoist-non-react-statics": "^2.2.0",
|
"hoist-non-react-statics": "^2.2.0",
|
||||||
"path-to-regexp": "^1.7.0",
|
"path-to-regexp": "^1.7.0",
|
||||||
|
"query-string": "^6.1.0",
|
||||||
"react-lifecycles-compat": "^3",
|
"react-lifecycles-compat": "^3",
|
||||||
"react-native-safe-area-view": "^0.8.0",
|
"react-native-safe-area-view": "^0.8.0",
|
||||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
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 { polyfill } from 'react-lifecycles-compat';
|
||||||
|
|
||||||
import { BackHandler } from './PlatformHelpers';
|
|
||||||
import NavigationActions from './NavigationActions';
|
import NavigationActions from './NavigationActions';
|
||||||
import getNavigation from './getNavigation';
|
import getNavigation from './getNavigation';
|
||||||
import invariant from './utils/invariant';
|
import invariant from './utils/invariant';
|
||||||
import docsUrl from './utils/docsUrl';
|
import docsUrl from './utils/docsUrl';
|
||||||
|
import { urlToPathAndParams } from './routers/pathUtils';
|
||||||
|
|
||||||
function isStateful(props) {
|
function isStateful(props) {
|
||||||
return !props.navigation;
|
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 }) => {
|
_handleOpenURL = ({ url }) => {
|
||||||
const parsedUrl = this._urlToPathAndParams(url);
|
const { enableURLHandling, uriPrefix } = this.props;
|
||||||
|
if (enableURLHandling === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsedUrl = urlToPathAndParams(url, uriPrefix);
|
||||||
if (parsedUrl) {
|
if (parsedUrl) {
|
||||||
const { path, params } = parsedUrl;
|
const { path, params } = parsedUrl;
|
||||||
const action = Component.router.getActionForPathAndParams(path, params);
|
const action = Component.router.getActionForPathAndParams(path, params);
|
||||||
@@ -214,11 +203,15 @@ export default function createNavigationContainer(Component) {
|
|||||||
Linking.addEventListener('url', this._handleOpenURL);
|
Linking.addEventListener('url', this._handleOpenURL);
|
||||||
|
|
||||||
// Pull out anything that can impact state
|
// Pull out anything that can impact state
|
||||||
const { persistenceKey } = this.props;
|
const { persistenceKey, uriPrefix, enableURLHandling } = this.props;
|
||||||
const startupStateJSON =
|
let parsedUrl = null;
|
||||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
let startupStateJSON = null;
|
||||||
const url = await Linking.getInitialURL();
|
if (enableURLHandling !== false) {
|
||||||
const parsedUrl = url && this._urlToPathAndParams(url);
|
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
|
// Initialize state. This must be done *after* any async code
|
||||||
// so we don't end up with a different value for this.state.nav
|
// so we don't end up with a different value for this.state.nav
|
||||||
@@ -272,6 +265,16 @@ export default function createNavigationContainer(Component) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.tron &&
|
||||||
|
console.tron.display({
|
||||||
|
name: 'Navigation',
|
||||||
|
preview: 'Initial State',
|
||||||
|
value: {
|
||||||
|
initialState: startupState,
|
||||||
|
initialAction: this._initialAction,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({ nav: startupState }, () => {
|
this.setState({ nav: startupState }, () => {
|
||||||
_reactNavigationIsHydratingState = false;
|
_reactNavigationIsHydratingState = false;
|
||||||
dispatchActions();
|
dispatchActions();
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
|||||||
|
|
||||||
const childRoute = navigation.state.routes.find(r => r.key === childKey);
|
const childRoute = navigation.state.routes.find(r => r.key === childKey);
|
||||||
|
|
||||||
|
if (!childRoute) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (children[childKey] && children[childKey].state === childRoute) {
|
if (children[childKey] && children[childKey].state === childRoute) {
|
||||||
return children[childKey];
|
return children[childKey];
|
||||||
}
|
}
|
||||||
@@ -79,12 +83,16 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
|||||||
getParam: createParamGetter(childRoute),
|
getParam: createParamGetter(childRoute),
|
||||||
|
|
||||||
getChildNavigation: grandChildKey =>
|
getChildNavigation: grandChildKey =>
|
||||||
getChildNavigation(children[childKey], grandChildKey, () =>
|
getChildNavigation(children[childKey], grandChildKey, () => {
|
||||||
getCurrentParentNavigation().getChildNavigation(childKey)
|
const nav = getCurrentParentNavigation();
|
||||||
),
|
return nav && nav.getChildNavigation(childKey);
|
||||||
|
}),
|
||||||
|
|
||||||
isFocused: () => {
|
isFocused: () => {
|
||||||
const currentNavigation = getCurrentParentNavigation();
|
const currentNavigation = getCurrentParentNavigation();
|
||||||
|
if (!currentNavigation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const { routes, index } = currentNavigation.state;
|
const { routes, index } = currentNavigation.state;
|
||||||
if (!currentNavigation.isFocused()) {
|
if (!currentNavigation.isFocused()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
5
src/react-navigation.js
vendored
5
src/react-navigation.js
vendored
@@ -156,6 +156,11 @@ module.exports = {
|
|||||||
return require('./views/SwitchView/SwitchView').default;
|
return require('./views/SwitchView/SwitchView').default;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// NavigationEvents
|
||||||
|
get NavigationEvents() {
|
||||||
|
return require('./views/NavigationEvents').default;
|
||||||
|
},
|
||||||
|
|
||||||
// HOCs
|
// HOCs
|
||||||
get withNavigation() {
|
get withNavigation() {
|
||||||
return require('./views/withNavigation').default;
|
return require('./views/withNavigation').default;
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ module.exports = {
|
|||||||
return require('./routers/SwitchRouter').default;
|
return require('./routers/SwitchRouter').default;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// NavigationEvents
|
||||||
|
get NavigationEvents() {
|
||||||
|
return require('./views/NavigationEvents').default;
|
||||||
|
},
|
||||||
|
|
||||||
// HOCs
|
// HOCs
|
||||||
get withNavigation() {
|
get withNavigation() {
|
||||||
return require('./views/withNavigation').default;
|
return require('./views/withNavigation').default;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import pathToRegexp from 'path-to-regexp';
|
|
||||||
|
|
||||||
import NavigationActions from '../NavigationActions';
|
import NavigationActions from '../NavigationActions';
|
||||||
import StackActions from './StackActions';
|
import StackActions from './StackActions';
|
||||||
import createConfigGetter from './createConfigGetter';
|
import createConfigGetter from './createConfigGetter';
|
||||||
@@ -8,14 +6,7 @@ import StateUtils from '../StateUtils';
|
|||||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||||
import invariant from '../utils/invariant';
|
import invariant from '../utils/invariant';
|
||||||
import { generateKey } from './KeyGenerator';
|
import { generateKey } from './KeyGenerator';
|
||||||
|
import { createPathParser } from './pathUtils';
|
||||||
function isEmpty(obj) {
|
|
||||||
if (!obj) return true;
|
|
||||||
for (let key in obj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function behavesLikePushAction(action) {
|
function behavesLikePushAction(action) {
|
||||||
return (
|
return (
|
||||||
@@ -56,8 +47,6 @@ export default (routeConfigs, stackConfig = {}) => {
|
|||||||
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
|
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
|
||||||
|
|
||||||
const initialChildRouter = childRouters[initialRouteName];
|
const initialChildRouter = childRouters[initialRouteName];
|
||||||
const pathsByRouteNames = { ...stackConfig.paths } || {};
|
|
||||||
let paths = [];
|
|
||||||
|
|
||||||
function getInitialState(action) {
|
function getInitialState(action) {
|
||||||
let route = {};
|
let route = {};
|
||||||
@@ -115,37 +104,16 @@ export default (routeConfigs, stackConfig = {}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build paths for each route
|
const {
|
||||||
routeNames.forEach(routeName => {
|
getPathAndParamsForRoute,
|
||||||
let pathPattern =
|
getActionForPathAndParams,
|
||||||
pathsByRouteNames[routeName] || routeConfigs[routeName].path;
|
} = createPathParser(
|
||||||
let matchExact = !!pathPattern && !childRouters[routeName];
|
childRouters,
|
||||||
if (pathPattern === undefined) {
|
routeConfigs,
|
||||||
pathPattern = routeName;
|
stackConfig.paths,
|
||||||
}
|
initialRouteName,
|
||||||
const keys = [];
|
initialRouteParams
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
childRouters,
|
childRouters,
|
||||||
@@ -225,37 +193,27 @@ export default (routeConfigs, stackConfig = {}) => {
|
|||||||
return getInitialState(action);
|
return getInitialState(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeChildRoute = state.routes[state.index];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isResetToRootStack(action) &&
|
!isResetToRootStack(action) &&
|
||||||
action.type !== NavigationActions.NAVIGATE
|
action.type !== NavigationActions.NAVIGATE
|
||||||
) {
|
) {
|
||||||
const keyIndex = action.key
|
// Let the active child router handle the action
|
||||||
? StateUtils.indexOf(state, action.key)
|
const activeChildRouter = childRouters[activeChildRoute.routeName];
|
||||||
: -1;
|
if (activeChildRouter) {
|
||||||
|
const route = activeChildRouter.getStateForAction(
|
||||||
// Traverse routes from the top of the stack to the bottom, so the
|
action,
|
||||||
// active route has the first opportunity, then the one before it, etc.
|
activeChildRoute
|
||||||
for (let childRoute of state.routes.slice().reverse()) {
|
);
|
||||||
// If a key is provided and in routes state then let's use that
|
if (route !== null && route !== activeChildRoute) {
|
||||||
// knowledge to skip extra getStateForAction calls on other child
|
return StateUtils.replaceAt(
|
||||||
// routers
|
state,
|
||||||
if (keyIndex >= 0 && childRoute.key !== action.key) {
|
activeChildRoute.key,
|
||||||
continue;
|
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
|
||||||
let childRouter = childRouters[childRoute.routeName];
|
action.type === NavigationActions.SET_PARAMS
|
||||||
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,
|
|
||||||
action.type === NavigationActions.SET_PARAMS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (action.type === NavigationActions.NAVIGATE) {
|
} else if (action.type === NavigationActions.NAVIGATE) {
|
||||||
@@ -439,7 +397,15 @@ export default (routeConfigs, stackConfig = {}) => {
|
|||||||
|
|
||||||
// Handle replace action
|
// Handle replace action
|
||||||
if (action.type === StackActions.REPLACE) {
|
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
|
// Only replace if the key matches one of our routes
|
||||||
if (routeIndex !== -1) {
|
if (routeIndex !== -1) {
|
||||||
const childRouter = childRouters[action.routeName];
|
const childRouter = childRouters[action.routeName];
|
||||||
@@ -554,126 +520,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;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPathAndParamsForState(state) {
|
getPathAndParamsForState(state) {
|
||||||
const route = state.routes[state.index];
|
const route = state.routes[state.index];
|
||||||
const routeName = route.routeName;
|
return getPathAndParamsForRoute(route);
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getActionForPathAndParams(pathToResolve, inputParams) {
|
getActionForPathAndParams(path, params) {
|
||||||
// If the path is empty (null or empty string)
|
return getActionForPathAndParams(path, params);
|
||||||
// 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 } : {}),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getScreenOptions: createConfigGetter(
|
getScreenOptions: createConfigGetter(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import createConfigGetter from './createConfigGetter';
|
|||||||
import NavigationActions from '../NavigationActions';
|
import NavigationActions from '../NavigationActions';
|
||||||
import StackActions from './StackActions';
|
import StackActions from './StackActions';
|
||||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||||
|
import { createPathParser } from './pathUtils';
|
||||||
|
|
||||||
const defaultActionCreators = (route, navStateKey) => ({});
|
const defaultActionCreators = (route, navStateKey) => ({});
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ export default (routeConfigs, config = {}) => {
|
|||||||
validateRouteConfigMap(routeConfigs);
|
validateRouteConfigMap(routeConfigs);
|
||||||
|
|
||||||
const order = config.order || Object.keys(routeConfigs);
|
const order = config.order || Object.keys(routeConfigs);
|
||||||
const paths = config.paths || {};
|
|
||||||
const getCustomActionCreators =
|
const getCustomActionCreators =
|
||||||
config.getCustomActionCreators || defaultActionCreators;
|
config.getCustomActionCreators || defaultActionCreators;
|
||||||
|
|
||||||
@@ -36,16 +37,24 @@ export default (routeConfigs, config = {}) => {
|
|||||||
const childRouters = {};
|
const childRouters = {};
|
||||||
order.forEach(routeName => {
|
order.forEach(routeName => {
|
||||||
const routeConfig = routeConfigs[routeName];
|
const routeConfig = routeConfigs[routeName];
|
||||||
if (!paths[routeName]) {
|
|
||||||
paths[routeName] =
|
|
||||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
|
||||||
}
|
|
||||||
childRouters[routeName] = null;
|
childRouters[routeName] = null;
|
||||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||||
if (screen.router) {
|
if (screen.router) {
|
||||||
childRouters[routeName] = screen.router;
|
childRouters[routeName] = screen.router;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
getPathAndParamsForRoute,
|
||||||
|
getActionForPathAndParams,
|
||||||
|
} = createPathParser(
|
||||||
|
childRouters,
|
||||||
|
routeConfigs,
|
||||||
|
config.paths,
|
||||||
|
initialRouteName,
|
||||||
|
initialRouteParams
|
||||||
|
);
|
||||||
|
|
||||||
if (initialRouteIndex === -1) {
|
if (initialRouteIndex === -1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid initialRouteName '${initialRouteName}'.` +
|
`Invalid initialRouteName '${initialRouteName}'.` +
|
||||||
@@ -187,7 +196,7 @@ export default (routeConfigs, config = {}) => {
|
|||||||
newChildState = childRouter
|
newChildState = childRouter
|
||||||
? childRouter.getStateForAction(action.action, childState)
|
? childRouter.getStateForAction(action.action, childState)
|
||||||
: null;
|
: null;
|
||||||
} else if (!action.action && !childRouter && action.params) {
|
} else if (!action.action && action.params) {
|
||||||
newChildState = {
|
newChildState = {
|
||||||
...childState,
|
...childState,
|
||||||
params: {
|
params: {
|
||||||
@@ -309,73 +318,11 @@ export default (routeConfigs, config = {}) => {
|
|||||||
|
|
||||||
getPathAndParamsForState(state) {
|
getPathAndParamsForState(state) {
|
||||||
const route = state.routes[state.index];
|
const route = state.routes[state.index];
|
||||||
const routeName = order[state.index];
|
return getPathAndParamsForRoute(route);
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
getActionForPathAndParams(path, params) {
|
||||||
if (!path) {
|
return getActionForPathAndParams(path, params);
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getScreenOptions: createConfigGetter(
|
getScreenOptions: createConfigGetter(
|
||||||
|
|||||||
299
src/routers/__tests__/PathHandling-test.js
Normal file
299
src/routers/__tests__/PathHandling-test.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/* eslint no-shadow:0, react/no-multi-comp:0, react/display-name:0 */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import SwitchRouter from '../SwitchRouter';
|
||||||
|
import StackRouter from '../StackRouter';
|
||||||
|
import StackActions from '../StackActions';
|
||||||
|
import NavigationActions from '../../NavigationActions';
|
||||||
|
import { urlToPathAndParams } from '../pathUtils';
|
||||||
|
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
_TESTING_ONLY_normalize_keys();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListScreen = () => <div />;
|
||||||
|
|
||||||
|
const ProfileNavigator = () => <div />;
|
||||||
|
ProfileNavigator.router = StackRouter({
|
||||||
|
list: {
|
||||||
|
path: 'list/:id',
|
||||||
|
screen: ListScreen,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MainNavigator = () => <div />;
|
||||||
|
MainNavigator.router = StackRouter({
|
||||||
|
profile: {
|
||||||
|
path: 'p/:id',
|
||||||
|
screen: ProfileNavigator,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoginScreen = () => <div />;
|
||||||
|
|
||||||
|
const AuthNavigator = () => <div />;
|
||||||
|
AuthNavigator.router = StackRouter({
|
||||||
|
login: {
|
||||||
|
screen: LoginScreen,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BarScreen = () => <div />;
|
||||||
|
|
||||||
|
class FooNavigator extends React.Component {
|
||||||
|
static router = StackRouter({
|
||||||
|
bar: {
|
||||||
|
path: 'b/:barThing',
|
||||||
|
screen: BarScreen,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
render() {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersonScreen = () => <div />;
|
||||||
|
|
||||||
|
const performRouterTest = createTestRouter => {
|
||||||
|
const testRouter = createTestRouter({
|
||||||
|
main: {
|
||||||
|
screen: MainNavigator,
|
||||||
|
},
|
||||||
|
baz: {
|
||||||
|
path: null,
|
||||||
|
screen: FooNavigator,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
screen: AuthNavigator,
|
||||||
|
},
|
||||||
|
person: {
|
||||||
|
path: 'people/:id',
|
||||||
|
screen: PersonScreen,
|
||||||
|
},
|
||||||
|
foo: {
|
||||||
|
path: 'fo/:fooThing',
|
||||||
|
screen: FooNavigator,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Handles empty URIs', () => {
|
||||||
|
const router = createTestRouter(
|
||||||
|
{
|
||||||
|
Foo: {
|
||||||
|
screen: () => <div />,
|
||||||
|
},
|
||||||
|
Bar: {
|
||||||
|
screen: () => <div />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ initialRouteName: 'Bar', initialRouteParams: { foo: 42 } }
|
||||||
|
);
|
||||||
|
const action = router.getActionForPathAndParams('');
|
||||||
|
expect(action).toEqual({
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'Bar',
|
||||||
|
params: { foo: 42 },
|
||||||
|
});
|
||||||
|
const state = router.getStateForAction(action);
|
||||||
|
expect(state.routes[state.index]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
routeName: 'Bar',
|
||||||
|
params: { foo: 42 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gets deep path with pure wildcard match', () => {
|
||||||
|
const ScreenA = () => <div />;
|
||||||
|
const ScreenB = () => <div />;
|
||||||
|
const ScreenC = () => <div />;
|
||||||
|
ScreenA.router = createTestRouter({
|
||||||
|
Boo: { path: 'boo', screen: ScreenC },
|
||||||
|
Baz: { path: 'baz/:bazId', screen: ScreenB },
|
||||||
|
});
|
||||||
|
ScreenC.router = createTestRouter({
|
||||||
|
Boo2: { path: '', screen: ScreenB },
|
||||||
|
});
|
||||||
|
const router = createTestRouter({
|
||||||
|
Foo: {
|
||||||
|
path: null,
|
||||||
|
screen: ScreenA,
|
||||||
|
},
|
||||||
|
Bar: {
|
||||||
|
screen: ScreenB,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const state = {
|
||||||
|
index: 0,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
key: 'Foo',
|
||||||
|
routeName: 'Foo',
|
||||||
|
params: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'Boo',
|
||||||
|
routeName: 'Boo',
|
||||||
|
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
|
||||||
|
},
|
||||||
|
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'Bar', routeName: 'Bar' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const { path, params } = router.getPathAndParamsForState(state);
|
||||||
|
expect(path).toEqual('baz/321');
|
||||||
|
expect(params.id).toEqual('123');
|
||||||
|
expect(params.bazId).toEqual('321');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const state = {
|
||||||
|
index: 0,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'Foo',
|
||||||
|
routeName: 'Foo',
|
||||||
|
params: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'Boo',
|
||||||
|
routeName: 'Boo',
|
||||||
|
routes: [{ key: 'Boo2', routeName: 'Boo2' }],
|
||||||
|
},
|
||||||
|
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'Bar', routeName: 'Bar' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const { path, params } = router.getPathAndParamsForState(state);
|
||||||
|
expect(path).toEqual('boo');
|
||||||
|
expect(params).toEqual({ id: '123' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('URI encoded string get passed to deep link', () => {
|
||||||
|
const uri = 'people/2018%2F02%2F07';
|
||||||
|
const action = testRouter.getActionForPathAndParams(uri);
|
||||||
|
expect(action).toEqual({
|
||||||
|
routeName: 'person',
|
||||||
|
params: {
|
||||||
|
id: '2018/02/07',
|
||||||
|
},
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const malformedUri = 'people/%E0%A4%A';
|
||||||
|
const action2 = testRouter.getActionForPathAndParams(malformedUri);
|
||||||
|
expect(action2).toEqual({
|
||||||
|
routeName: 'person',
|
||||||
|
params: {
|
||||||
|
id: '%E0%A4%A',
|
||||||
|
},
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Querystring params get passed to nested deep link', () => {
|
||||||
|
const action = testRouter.getActionForPathAndParams(
|
||||||
|
'main/p/4/list/10259959195',
|
||||||
|
{ code: 'test', foo: 'bar' }
|
||||||
|
);
|
||||||
|
expect(action).toEqual({
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'main',
|
||||||
|
params: {
|
||||||
|
code: 'test',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'profile',
|
||||||
|
params: {
|
||||||
|
id: '4',
|
||||||
|
code: 'test',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'list',
|
||||||
|
params: {
|
||||||
|
id: '10259959195',
|
||||||
|
code: 'test',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const action2 = testRouter.getActionForPathAndParams(
|
||||||
|
'main/p/4/list/10259959195',
|
||||||
|
{ code: '', foo: 'bar' }
|
||||||
|
);
|
||||||
|
expect(action2).toEqual({
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'main',
|
||||||
|
params: {
|
||||||
|
code: '',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'profile',
|
||||||
|
params: {
|
||||||
|
id: '4',
|
||||||
|
code: '',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
|
routeName: 'list',
|
||||||
|
params: {
|
||||||
|
id: '10259959195',
|
||||||
|
code: '',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('paths option on router overrides path from route config', () => {
|
||||||
|
const router = createTestRouter(
|
||||||
|
{
|
||||||
|
main: {
|
||||||
|
screen: MainNavigator,
|
||||||
|
},
|
||||||
|
baz: {
|
||||||
|
path: null,
|
||||||
|
screen: FooNavigator,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ paths: { baz: 'overridden' } }
|
||||||
|
);
|
||||||
|
const action = router.getActionForPathAndParams('overridden', {});
|
||||||
|
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||||
|
expect(action.routeName).toEqual('baz');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Path handling for stack router', () => {
|
||||||
|
performRouterTest(StackRouter);
|
||||||
|
});
|
||||||
|
describe('Path handling for switch router', () => {
|
||||||
|
performRouterTest(SwitchRouter);
|
||||||
|
});
|
||||||
@@ -208,6 +208,7 @@ describe('StackRouter', () => {
|
|||||||
expect(AuthNavigator.router.getActionForPathAndParams('login')).toEqual({
|
expect(AuthNavigator.router.getActionForPathAndParams('login')).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'login',
|
routeName: 'login',
|
||||||
|
params: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,7 +224,10 @@ describe('StackRouter', () => {
|
|||||||
|
|
||||||
test('Parses paths with a query', () => {
|
test('Parses paths with a query', () => {
|
||||||
expect(
|
expect(
|
||||||
TestStackRouter.getActionForPathAndParams('people/foo?code=test&foo=bar')
|
TestStackRouter.getActionForPathAndParams('people/foo', {
|
||||||
|
code: 'test',
|
||||||
|
foo: 'bar',
|
||||||
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'person',
|
routeName: 'person',
|
||||||
@@ -237,7 +241,10 @@ describe('StackRouter', () => {
|
|||||||
|
|
||||||
test('Parses paths with an empty query value', () => {
|
test('Parses paths with an empty query value', () => {
|
||||||
expect(
|
expect(
|
||||||
TestStackRouter.getActionForPathAndParams('people/foo?code=&foo=bar')
|
TestStackRouter.getActionForPathAndParams('people/foo', {
|
||||||
|
code: '',
|
||||||
|
foo: 'bar',
|
||||||
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'person',
|
routeName: 'person',
|
||||||
@@ -255,9 +262,11 @@ describe('StackRouter', () => {
|
|||||||
expect(action).toEqual({
|
expect(action).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'auth',
|
routeName: 'auth',
|
||||||
|
params: {},
|
||||||
action: {
|
action: {
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'login',
|
routeName: 'login',
|
||||||
|
params: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -268,6 +277,7 @@ describe('StackRouter', () => {
|
|||||||
expect(action).toEqual({
|
expect(action).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'main',
|
routeName: 'main',
|
||||||
|
params: {},
|
||||||
action: {
|
action: {
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'profile',
|
routeName: 'profile',
|
||||||
@@ -291,6 +301,7 @@ describe('StackRouter', () => {
|
|||||||
expect(action).toEqual({
|
expect(action).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'baz',
|
routeName: 'baz',
|
||||||
|
params: {},
|
||||||
action: {
|
action: {
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'bar',
|
routeName: 'bar',
|
||||||
@@ -313,9 +324,11 @@ describe('StackRouter', () => {
|
|||||||
expect(action).toEqual({
|
expect(action).toEqual({
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'auth',
|
routeName: 'auth',
|
||||||
|
params: {},
|
||||||
action: {
|
action: {
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'login',
|
routeName: 'login',
|
||||||
|
params: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -957,6 +970,77 @@ describe('StackRouter', () => {
|
|||||||
expect(replacedState2.routes[0].routeName).toEqual('bar');
|
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', () => {
|
test('Handles push transition logic with completion action', () => {
|
||||||
const FooScreen = () => <div />;
|
const FooScreen = () => <div />;
|
||||||
const BarScreen = () => <div />;
|
const BarScreen = () => <div />;
|
||||||
@@ -989,6 +1073,43 @@ describe('StackRouter', () => {
|
|||||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
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', () => {
|
test('Handle basic stack logic for components with router', () => {
|
||||||
const FooScreen = () => <div />;
|
const FooScreen = () => <div />;
|
||||||
const BarScreen = () => <div />;
|
const BarScreen = () => <div />;
|
||||||
@@ -1047,6 +1168,48 @@ describe('StackRouter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Gets deep path (stack behavior)', () => {
|
||||||
|
const ScreenA = () => <div />;
|
||||||
|
const ScreenB = () => <div />;
|
||||||
|
ScreenA.router = StackRouter({
|
||||||
|
Boo: { path: 'boo', screen: ScreenB },
|
||||||
|
Baz: { path: 'baz/:bazId', screen: ScreenB },
|
||||||
|
});
|
||||||
|
const router = StackRouter({
|
||||||
|
Foo: {
|
||||||
|
path: 'f/:id',
|
||||||
|
screen: ScreenA,
|
||||||
|
},
|
||||||
|
Bar: {
|
||||||
|
screen: ScreenB,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
index: 0,
|
||||||
|
isTransitioning: false,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
key: 'Foo',
|
||||||
|
routeName: 'Foo',
|
||||||
|
params: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{ key: 'Boo', routeName: 'Boo' },
|
||||||
|
{ key: 'Baz', routeName: 'Baz', params: { bazId: '321' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'Bar', routeName: 'Bar' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const { path, params } = router.getPathAndParamsForState(state);
|
||||||
|
expect(path).toEqual('f/123/baz/321');
|
||||||
|
expect(params.id).toEqual('123');
|
||||||
|
expect(params.bazId).toEqual('321');
|
||||||
|
});
|
||||||
|
|
||||||
test('Handle goBack identified by key', () => {
|
test('Handle goBack identified by key', () => {
|
||||||
const FooScreen = () => <div />;
|
const FooScreen = () => <div />;
|
||||||
const BarScreen = () => <div />;
|
const BarScreen = () => <div />;
|
||||||
@@ -1634,400 +1797,164 @@ describe('StackRouter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Handles empty URIs', () => {
|
test('Handles deep navigate completion action', () => {
|
||||||
const router = StackRouter(
|
const LeafScreen = () => <div />;
|
||||||
{
|
const FooScreen = () => <div />;
|
||||||
Foo: {
|
FooScreen.router = StackRouter({
|
||||||
screen: () => <div />,
|
Boo: { path: 'boo', screen: LeafScreen },
|
||||||
},
|
Baz: { path: 'baz/:bazId', screen: LeafScreen },
|
||||||
Bar: {
|
|
||||||
screen: () => <div />,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ initialRouteName: 'Bar' }
|
|
||||||
);
|
|
||||||
const action = router.getActionForPathAndParams('');
|
|
||||||
expect(action).toEqual({
|
|
||||||
type: NavigationActions.NAVIGATE,
|
|
||||||
routeName: 'Bar',
|
|
||||||
});
|
});
|
||||||
let state = null;
|
const router = StackRouter({
|
||||||
if (action) {
|
Foo: {
|
||||||
state = router.getStateForAction(action);
|
screen: FooScreen,
|
||||||
}
|
},
|
||||||
|
Bar: {
|
||||||
|
screen: LeafScreen,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||||
expect(state && state.index).toEqual(0);
|
expect(state && state.index).toEqual(0);
|
||||||
expect(state && state.routes[0]).toEqual(
|
expect(state && state.routes[0].routeName).toEqual('Foo');
|
||||||
expect.objectContaining({
|
const key = state && state.routes[0].key;
|
||||||
routeName: 'Bar',
|
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', () => {
|
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||||
const ScreenA = () => <div />;
|
const Screen = () => <div />;
|
||||||
const ScreenB = () => <div />;
|
const NestedStack = () => <div />;
|
||||||
ScreenA.router = StackRouter({
|
let nestedRouter = StackRouter({
|
||||||
Boo: { path: 'boo', screen: ScreenB },
|
Foo: Screen,
|
||||||
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,
|
|
||||||
Bar: Screen,
|
Bar: Screen,
|
||||||
Baz: Screen,
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
initialRouteName: 'Baz',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
NestedStack.router = nestedRouter;
|
||||||
expect(state.routes[state.index].routeName).toEqual('Baz');
|
|
||||||
|
|
||||||
const state2 = router.getStateForAction(
|
let router = StackRouter(
|
||||||
{
|
{
|
||||||
type: NavigationActions.NAVIGATE,
|
NestedStack,
|
||||||
routeName: 'Bar',
|
Bar: Screen,
|
||||||
},
|
Baz: Screen,
|
||||||
state
|
},
|
||||||
);
|
{
|
||||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
initialRouteName: 'Baz',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const state3 = router.getStateForAction(
|
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||||
{
|
expect(state.routes[state.index].routeName).toEqual('Baz');
|
||||||
type: NavigationActions.NAVIGATE,
|
|
||||||
routeName: 'Baz',
|
|
||||||
},
|
|
||||||
state2
|
|
||||||
);
|
|
||||||
expect(state3.routes[state3.index].routeName).toEqual('Baz');
|
|
||||||
|
|
||||||
const state4 = router.getStateForAction(
|
const state2 = router.getStateForAction(
|
||||||
{
|
{
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'Foo',
|
routeName: 'Bar',
|
||||||
},
|
},
|
||||||
state3
|
state
|
||||||
);
|
);
|
||||||
let activeState4 = state4.routes[state4.index];
|
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||||
expect(activeState4.routeName).toEqual('NestedStack');
|
|
||||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
|
|
||||||
|
|
||||||
const state5 = router.getStateForAction(
|
const state3 = router.getStateForAction(
|
||||||
{
|
{
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
routeName: 'Bar',
|
routeName: 'Baz',
|
||||||
},
|
},
|
||||||
state4
|
state2
|
||||||
);
|
);
|
||||||
let activeState5 = state5.routes[state5.index];
|
expect(state3.routes[state3.index].routeName).toEqual('Baz');
|
||||||
expect(activeState5.routeName).toEqual('NestedStack');
|
|
||||||
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
|
const state4 = router.getStateForAction(
|
||||||
});
|
{
|
||||||
|
type: NavigationActions.NAVIGATE,
|
||||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
routeName: 'Foo',
|
||||||
const Screen = () => <div />;
|
},
|
||||||
const NestedStack = () => <div />;
|
state3
|
||||||
const OtherNestedStack = () => <div />;
|
);
|
||||||
|
let activeState4 = state4.routes[state4.index];
|
||||||
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
|
expect(activeState4.routeName).toEqual('NestedStack');
|
||||||
let otherNestedRouter = StackRouter({ Foo: Screen });
|
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
|
||||||
NestedStack.router = nestedRouter;
|
|
||||||
OtherNestedStack.router = otherNestedRouter;
|
const state5 = router.getStateForAction(
|
||||||
|
{
|
||||||
let router = StackRouter(
|
type: NavigationActions.NAVIGATE,
|
||||||
{
|
routeName: 'Bar',
|
||||||
NestedStack,
|
},
|
||||||
OtherNestedStack,
|
state4
|
||||||
Bar: Screen,
|
);
|
||||||
},
|
let activeState5 = state5.routes[state5.index];
|
||||||
{
|
expect(activeState5.routeName).toEqual('NestedStack');
|
||||||
initialRouteName: 'OtherNestedStack',
|
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
const Screen = () => <div />;
|
||||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
|
const NestedStack = () => <div />;
|
||||||
|
const OtherNestedStack = () => <div />;
|
||||||
const state2 = router.getStateForAction(
|
|
||||||
{
|
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
|
||||||
type: NavigationActions.NAVIGATE,
|
let otherNestedRouter = StackRouter({ Foo: Screen });
|
||||||
routeName: 'Bar',
|
NestedStack.router = nestedRouter;
|
||||||
},
|
OtherNestedStack.router = otherNestedRouter;
|
||||||
state
|
|
||||||
);
|
let router = StackRouter(
|
||||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
{
|
||||||
|
NestedStack,
|
||||||
const state3 = router.getStateForAction(
|
OtherNestedStack,
|
||||||
{
|
Bar: Screen,
|
||||||
type: NavigationActions.NAVIGATE,
|
},
|
||||||
routeName: 'NestedStack',
|
{
|
||||||
},
|
initialRouteName: 'OtherNestedStack',
|
||||||
state2
|
}
|
||||||
);
|
);
|
||||||
const state4 = router.getStateForAction(
|
|
||||||
{
|
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||||
type: NavigationActions.NAVIGATE,
|
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
|
||||||
routeName: 'Bar',
|
|
||||||
},
|
const state2 = router.getStateForAction(
|
||||||
state3
|
{
|
||||||
);
|
type: NavigationActions.NAVIGATE,
|
||||||
let activeState4 = state4.routes[state4.index];
|
routeName: 'Bar',
|
||||||
expect(activeState4.routeName).toEqual('NestedStack');
|
},
|
||||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('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);
|
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', () => {
|
test('order of handling navigate action is correct for nested switchrouters', () => {
|
||||||
// router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
|
// router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
|
||||||
// if we are focused on Other and navigate to Bar, what should happen?
|
// 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 ScreenA = () => <div />;
|
||||||
const ScreenB = () => <div />;
|
const ScreenB = () => <div />;
|
||||||
const router = TabRouter({
|
const router = TabRouter({
|
||||||
@@ -537,14 +537,17 @@ describe('TabRouter', () => {
|
|||||||
screen: ScreenA,
|
screen: ScreenA,
|
||||||
},
|
},
|
||||||
Bar: {
|
Bar: {
|
||||||
path: 'b',
|
path: 'b/:great',
|
||||||
screen: ScreenB,
|
screen: ScreenB,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const params = { foo: '42' };
|
const params = { foo: '42' };
|
||||||
const action = router.getActionForPathAndParams('b/anything', params);
|
const action = router.getActionForPathAndParams('b/anything', params);
|
||||||
const expectedAction = {
|
const expectedAction = {
|
||||||
params,
|
params: {
|
||||||
|
foo: '42',
|
||||||
|
great: 'anything',
|
||||||
|
},
|
||||||
routeName: 'Bar',
|
routeName: 'Bar',
|
||||||
type: NavigationActions.NAVIGATE,
|
type: NavigationActions.NAVIGATE,
|
||||||
};
|
};
|
||||||
@@ -565,15 +568,21 @@ describe('TabRouter', () => {
|
|||||||
index: 1,
|
index: 1,
|
||||||
isTransitioning: false,
|
isTransitioning: false,
|
||||||
routes: [
|
routes: [
|
||||||
{ key: 'Foo', routeName: 'Foo' },
|
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||||
{ key: 'Bar', routeName: 'Bar', params },
|
{
|
||||||
|
key: 'Bar',
|
||||||
|
routeName: 'Bar',
|
||||||
|
params: { foo: '42', great: 'anything' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
expect(state2).toEqual(expectedState2);
|
expect(state2).toEqual(expectedState2);
|
||||||
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
|
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
|
||||||
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
||||||
expect(router.getPathAndParamsForState(expectedState).path).toEqual('f');
|
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', () => {
|
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' });
|
||||||
|
});
|
||||||
172
src/routers/pathUtils.js
Normal file
172
src/routers/pathUtils.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import pathToRegexp from 'path-to-regexp';
|
||||||
|
import NavigationActions from '../NavigationActions';
|
||||||
|
const queryString = require('query-string');
|
||||||
|
|
||||||
|
function isEmpty(obj) {
|
||||||
|
if (!obj) return true;
|
||||||
|
for (let key in obj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const urlToPathAndParams = (url, uriPrefix) => {
|
||||||
|
const searchMatch = url.match(/^(.*)\?(.*)$/);
|
||||||
|
const params = searchMatch ? queryString.parse(searchMatch[2]) : {};
|
||||||
|
const urlWithoutSearch = searchMatch ? searchMatch[1] : url;
|
||||||
|
const delimiter = uriPrefix || '://';
|
||||||
|
let path = urlWithoutSearch.split(delimiter)[1];
|
||||||
|
if (path === undefined) {
|
||||||
|
path = urlWithoutSearch;
|
||||||
|
}
|
||||||
|
if (path === '/') {
|
||||||
|
path = '';
|
||||||
|
}
|
||||||
|
if (path[path.length - 1] === '/') {
|
||||||
|
path = path.slice(0, -1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPathParser = (
|
||||||
|
childRouters,
|
||||||
|
routeConfigs,
|
||||||
|
pathConfigs = {},
|
||||||
|
initialRouteName,
|
||||||
|
initialRouteParams
|
||||||
|
) => {
|
||||||
|
const pathsByRouteNames = {};
|
||||||
|
let paths = [];
|
||||||
|
|
||||||
|
// Build paths for each route
|
||||||
|
Object.keys(childRouters).forEach(routeName => {
|
||||||
|
let pathPattern = pathConfigs[routeName] || routeConfigs[routeName].path;
|
||||||
|
let matchExact = !!pathPattern && !childRouters[routeName];
|
||||||
|
if (pathPattern === undefined) {
|
||||||
|
pathPattern = routeName;
|
||||||
|
}
|
||||||
|
const keys = [];
|
||||||
|
let re, toPath, priority;
|
||||||
|
if (typeof pathPattern === 'string') {
|
||||||
|
// pathPattern may be either a string or a regexp object according to path-to-regexp docs.
|
||||||
|
re = pathToRegexp(pathPattern, keys);
|
||||||
|
toPath = pathToRegexp.compile(pathPattern);
|
||||||
|
priority = 0;
|
||||||
|
} else if (pathPattern === null) {
|
||||||
|
// for wildcard match
|
||||||
|
re = pathToRegexp('*', keys);
|
||||||
|
toPath = () => '';
|
||||||
|
matchExact = true;
|
||||||
|
priority = -1;
|
||||||
|
}
|
||||||
|
if (!matchExact) {
|
||||||
|
const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys);
|
||||||
|
re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`);
|
||||||
|
}
|
||||||
|
pathsByRouteNames[routeName] = { re, keys, toPath, priority, pathPattern };
|
||||||
|
});
|
||||||
|
|
||||||
|
paths = Object.entries(pathsByRouteNames);
|
||||||
|
paths.sort((a, b) => b[1].priority - a[1].priority);
|
||||||
|
|
||||||
|
const getActionForPathAndParams = (pathToResolve, inputParams = {}) => {
|
||||||
|
// If the path is empty (null or empty string)
|
||||||
|
// just return the initial route action
|
||||||
|
if (!pathToResolve) {
|
||||||
|
return NavigationActions.navigate({
|
||||||
|
routeName: initialRouteName,
|
||||||
|
params: { ...inputParams, ...initialRouteParams },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to match `pathToResolve` with a route in this router's
|
||||||
|
// routeConfigs
|
||||||
|
let matchedRouteName;
|
||||||
|
let pathMatch;
|
||||||
|
let pathMatchKeys;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const [routeName, path] of paths) {
|
||||||
|
const { re, keys } = path;
|
||||||
|
pathMatch = re.exec(pathToResolve);
|
||||||
|
if (pathMatch && pathMatch.length) {
|
||||||
|
pathMatchKeys = keys;
|
||||||
|
matchedRouteName = routeName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We didn't match -- return null to signify no action available
|
||||||
|
if (!matchedRouteName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine nested actions:
|
||||||
|
// If our matched route for this router is a child router,
|
||||||
|
// get the action for the path AFTER the matched path for this
|
||||||
|
// router
|
||||||
|
let nestedAction;
|
||||||
|
if (childRouters[matchedRouteName]) {
|
||||||
|
nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
|
||||||
|
pathMatch.slice(pathMatchKeys.length).join('/'),
|
||||||
|
inputParams
|
||||||
|
);
|
||||||
|
if (!nestedAction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = pathMatch.slice(1).reduce(
|
||||||
|
// iterate over matched path params
|
||||||
|
(paramsOut, matchResult, i) => {
|
||||||
|
const key = pathMatchKeys[i];
|
||||||
|
if (!key || key.asterisk) {
|
||||||
|
return paramsOut;
|
||||||
|
}
|
||||||
|
const paramName = key.name;
|
||||||
|
|
||||||
|
let decodedMatchResult;
|
||||||
|
try {
|
||||||
|
decodedMatchResult = decodeURIComponent(matchResult);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore `URIError: malformed URI`
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsOut[paramName] = decodedMatchResult || matchResult;
|
||||||
|
return paramsOut;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// start with the input(query string) params, which will get overridden by path params
|
||||||
|
...inputParams,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return NavigationActions.navigate({
|
||||||
|
routeName: matchedRouteName,
|
||||||
|
...(params ? { params } : {}),
|
||||||
|
...(nestedAction ? { action: nestedAction } : {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const getPathAndParamsForRoute = route => {
|
||||||
|
const { routeName, params } = route;
|
||||||
|
const childRouter = childRouters[routeName];
|
||||||
|
const subPath = pathsByRouteNames[routeName].toPath(params);
|
||||||
|
if (childRouter) {
|
||||||
|
// If it has a router it's a navigator.
|
||||||
|
// If it doesn't have router it's an ordinary React component.
|
||||||
|
const child = childRouter.getPathAndParamsForState(route);
|
||||||
|
return {
|
||||||
|
path: subPath ? `${subPath}/${child.path}` : child.path,
|
||||||
|
params: child.params ? { ...params, ...child.params } : params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: subPath,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return { getActionForPathAndParams, getPathAndParamsForRoute };
|
||||||
|
};
|
||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
View,
|
View,
|
||||||
I18nManager,
|
I18nManager,
|
||||||
ViewPropTypes,
|
ViewPropTypes,
|
||||||
|
MaskedViewIOS,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
|
||||||
import SafeAreaView from 'react-native-safe-area-view';
|
import SafeAreaView from 'react-native-safe-area-view';
|
||||||
|
|
||||||
import HeaderTitle from './HeaderTitle';
|
import HeaderTitle from './HeaderTitle';
|
||||||
@@ -473,6 +473,18 @@ class Header extends React.PureComponent {
|
|||||||
flexShrink,
|
flexShrink,
|
||||||
flexBasis,
|
flexBasis,
|
||||||
flexWrap,
|
flexWrap,
|
||||||
|
position,
|
||||||
|
padding,
|
||||||
|
paddingHorizontal,
|
||||||
|
paddingRight,
|
||||||
|
paddingLeft,
|
||||||
|
// paddingVertical,
|
||||||
|
// paddingTop,
|
||||||
|
// paddingBottom,
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
...safeHeaderStyle
|
...safeHeaderStyle
|
||||||
} = headerStyleObj;
|
} = headerStyleObj;
|
||||||
|
|
||||||
@@ -485,6 +497,18 @@ class Header extends React.PureComponent {
|
|||||||
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
||||||
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
||||||
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
|
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
|
// TODO: warn if any unsafe styles are provided
|
||||||
@@ -503,7 +527,9 @@ class Header extends React.PureComponent {
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
this.props.layoutInterpolator(this.props),
|
this.props.layoutInterpolator(this.props),
|
||||||
{ backgroundColor: DEFAULT_BACKGROUND_COLOR },
|
Platform.OS === 'ios'
|
||||||
|
? { backgroundColor: DEFAULT_BACKGROUND_COLOR }
|
||||||
|
: null,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||||
@@ -518,7 +544,11 @@ class Header extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function warnIfHeaderStyleDefined(value, styleProp) {
|
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(
|
console.warn(
|
||||||
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
|
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
|
||||||
);
|
);
|
||||||
@@ -556,6 +586,7 @@ const styles = StyleSheet.create({
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
...platformContainerStyles,
|
...platformContainerStyles,
|
||||||
|
elevation: 0,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
|
|||||||
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);
|
||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
11
yarn.lock
11
yarn.lock
@@ -4659,6 +4659,13 @@ qs@~6.5.1:
|
|||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
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:
|
random-bytes@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
|
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
|
||||||
@@ -5565,6 +5572,10 @@ stream-to-observable@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe"
|
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:
|
string-length@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||||
|
|||||||
Reference in New Issue
Block a user