mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-20 02:48:14 +08:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f86362e86 | ||
|
|
99605737e9 | ||
|
|
842f5eb7b2 | ||
|
|
183ea82416 | ||
|
|
108a6504a7 | ||
|
|
f92d671746 | ||
|
|
e0c4a8f7d3 | ||
|
|
bc881c8aa1 | ||
|
|
118c19dcce | ||
|
|
01b43974e6 | ||
|
|
2f90899620 | ||
|
|
6cc86f66e1 | ||
|
|
4be99b6645 | ||
|
|
80016b7218 | ||
|
|
f555a9ec9a | ||
|
|
05cbd85d5c | ||
|
|
51965eac38 | ||
|
|
a3956bf3ce | ||
|
|
ce24c66b5a | ||
|
|
5467f0e22d | ||
|
|
1e7d8d55c3 | ||
|
|
1d2ce862c2 | ||
|
|
d778479e4a | ||
|
|
352dae50e1 | ||
|
|
61385cae59 | ||
|
|
aa3c13891e | ||
|
|
9696d7220d | ||
|
|
2b83b44816 | ||
|
|
ec749023ed | ||
|
|
adc9389eb3 | ||
|
|
54d143fee2 | ||
|
|
d50e74d0c7 | ||
|
|
22926c5230 | ||
|
|
f6c47a6c66 | ||
|
|
046a9f8930 | ||
|
|
72f17538c2 | ||
|
|
550001b053 | ||
|
|
d168ab26f9 | ||
|
|
99916328a1 | ||
|
|
08e1b53f2e | ||
|
|
2243528e97 | ||
|
|
931271198b | ||
|
|
1876706bad | ||
|
|
e97d6b26a8 | ||
|
|
ec1694f909 | ||
|
|
93db7d0c95 | ||
|
|
589b80b2fa | ||
|
|
364144d639 | ||
|
|
2fd7284fe2 | ||
|
|
6499f02157 | ||
|
|
39d5714ac0 | ||
|
|
1ceda5f04d | ||
|
|
4533883ba7 | ||
|
|
8ec2466fef | ||
|
|
1bd6593ede | ||
|
|
4e8d8ce12f | ||
|
|
9f95a7f10b | ||
|
|
f6bd3e4306 | ||
|
|
c1a94895f5 | ||
|
|
ad7cde9eb9 | ||
|
|
2643f690a9 | ||
|
|
8e52995ef3 | ||
|
|
8ed3817c90 | ||
|
|
eda9bfd567 | ||
|
|
723c5f2149 | ||
|
|
ab5481a290 | ||
|
|
df281cfed0 | ||
|
|
e0df3cf74a | ||
|
|
2440af66e4 | ||
|
|
47fe858d4e | ||
|
|
c641bee11b | ||
|
|
4b39e2db3c | ||
|
|
f7533a790f | ||
|
|
c56122466f | ||
|
|
7fc992dc58 | ||
|
|
32922cdd7d | ||
|
|
eda51b3b79 | ||
|
|
921ee09587 | ||
|
|
7ae4c60eb8 | ||
|
|
5fff7ef5c6 | ||
|
|
42bb1cc317 | ||
|
|
337fd89ad5 | ||
|
|
acf9b92ff7 | ||
|
|
5072130d6f | ||
|
|
20bbbd62ff | ||
|
|
0890896824 | ||
|
|
0cf14f8e1e | ||
|
|
e5e434c9e2 | ||
|
|
e5d8d2c216 | ||
|
|
abd5200739 | ||
|
|
202609d9cf | ||
|
|
7b4dd98255 | ||
|
|
70c644f522 | ||
|
|
5274d16e3b | ||
|
|
e5e2cbb86d | ||
|
|
a8caa0d93c | ||
|
|
f70a25a6a8 | ||
|
|
6cde6e2558 | ||
|
|
0794c0faaa | ||
|
|
ea28e84e5a | ||
|
|
419ee5318d | ||
|
|
fbbf00875b | ||
|
|
22e09f7186 | ||
|
|
ece6297e8e | ||
|
|
ad52caf57b | ||
|
|
11f5e6e8e5 | ||
|
|
1764b21f34 | ||
|
|
bbacabeba3 | ||
|
|
b140b70555 | ||
|
|
356646cbfa | ||
|
|
6234b5661e | ||
|
|
270c3f0754 | ||
|
|
6f04bdffa6 | ||
|
|
8415378784 | ||
|
|
9e47092d56 | ||
|
|
029d6ac4d2 | ||
|
|
d57d118fda | ||
|
|
2232e394bb | ||
|
|
670d48366b | ||
|
|
99ac5b6c08 | ||
|
|
68a2a106f3 | ||
|
|
b7c6d072a5 | ||
|
|
ecd9fd71e9 | ||
|
|
cfc9690326 | ||
|
|
828e7f2d43 | ||
|
|
9c3fffa47f | ||
|
|
be524e4224 | ||
|
|
095814230b | ||
|
|
9cf557bba0 | ||
|
|
5e4512f3eb | ||
|
|
ee1b5972ce | ||
|
|
2233d0e1d8 | ||
|
|
577d99c165 | ||
|
|
aa362ea776 | ||
|
|
864908a49c | ||
|
|
5cab55b8c9 | ||
|
|
9b9db86bde | ||
|
|
4def39c0f7 | ||
|
|
e6559f5878 | ||
|
|
a9d8f2e03e | ||
|
|
84a070b9d5 | ||
|
|
ee984943c7 | ||
|
|
9fdfec18f6 | ||
|
|
aee16b91a4 | ||
|
|
191439f79a | ||
|
|
b1ac152fec | ||
|
|
c588ab9f9d | ||
|
|
ae8cd41396 | ||
|
|
5038ee2360 | ||
|
|
5bf071e3ee | ||
|
|
fcbf78e658 | ||
|
|
fd75e9c14c | ||
|
|
7d36a3b137 | ||
|
|
175387b22f | ||
|
|
0dd7daecc0 | ||
|
|
42230634fd | ||
|
|
a9943e9b2e | ||
|
|
6475e32dba | ||
|
|
f67872d8f1 | ||
|
|
2c7187b22a | ||
|
|
160d44f58e | ||
|
|
d017ed01b3 | ||
|
|
c2e197f8d3 | ||
|
|
6b3968b601 | ||
|
|
b575200879 | ||
|
|
cd5bd8882e | ||
|
|
3f837c895e | ||
|
|
bc5d35aba3 | ||
|
|
9a6e0bbd98 | ||
|
|
052d22804c | ||
|
|
7a978b1087 | ||
|
|
b06fb7e432 | ||
|
|
a92ed83302 | ||
|
|
0c31bc44ea | ||
|
|
8e5ee4d312 | ||
|
|
4bb8987ab7 | ||
|
|
81a86fa091 | ||
|
|
47f357f332 | ||
|
|
bdda6fa5be | ||
|
|
b097136f74 | ||
|
|
c9b0219cec | ||
|
|
ac83cf804c | ||
|
|
cf63521516 | ||
|
|
7c488c8d49 | ||
|
|
9005494e64 | ||
|
|
0daab8c55b | ||
|
|
ae98089337 | ||
|
|
57e37a8783 | ||
|
|
b7994d28db | ||
|
|
f1bfdeee46 | ||
|
|
c6301abaed | ||
|
|
4569ad49f9 | ||
|
|
214eeb13fb | ||
|
|
416fe58cab | ||
|
|
2e47cbb3cb | ||
|
|
cd99dc8054 | ||
|
|
e27ad22c57 | ||
|
|
6785729fb5 | ||
|
|
d3b6e70d16 |
26
.eslintrc
26
.eslintrc
@@ -7,18 +7,18 @@
|
||||
"prettier/react"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["react", "prettier"],
|
||||
"env": {
|
||||
"jasmine": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": ["error", {
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}
|
||||
],
|
||||
|
||||
"no-underscore-dangle": "off",
|
||||
"no-use-before-define": "off",
|
||||
@@ -27,24 +27,20 @@
|
||||
"no-plusplus": "off",
|
||||
"no-class-assign": "off",
|
||||
"no-duplicate-imports": "off",
|
||||
|
||||
"import/extensions": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/no-unresolved": "off",
|
||||
|
||||
"react/jsx-filename-extension": [
|
||||
"off", { "extensions": [".js", ".jsx"] }
|
||||
],
|
||||
"react/jsx-filename-extension": ["off", { "extensions": [".js", ".jsx"] }],
|
||||
|
||||
"react/sort-comp": "off",
|
||||
"react/prefer-stateless-function": "off",
|
||||
"react/no-deprecated": "off",
|
||||
|
||||
"react/forbid-prop-types": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/no-unused-prop-types": "off",
|
||||
},
|
||||
"settings": {
|
||||
"react/no-unused-prop-types": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -9,6 +9,8 @@ If you have a question, feature request, or an idea for improving the library or
|
||||
- [Get help on Discord chat (#react-navigation on Reactiflux)](https://discord.gg/4xEK3nD) or [on StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation)
|
||||
- Search for your issue - it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue.
|
||||
|
||||
Bugs with react-navigation must be reproducible *without any external libraries that operate on it*. This means that if you are attempting to use Redux or MobX with it and you think you have found a bug, you must be able to reproduce it without Redux or MobX in this report. Redux related issues belong in [react-navigation-redux-helpers](https://github.com/react-navigation/react-navigation-redux-helpers), and we do not have any first-class integration with MobX at the moment.
|
||||
|
||||
---
|
||||
|
||||
### Current Behavior
|
||||
|
||||
13
CONTRIBUTING.md
Normal file
13
CONTRIBUTING.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Contributing to React Navigation
|
||||
|
||||
This library is a community effort: it can only be great if we all help out in one way or another! If you feel like you aren't experienced enough using React Navigation to contribute, you can still make an impact by:
|
||||
|
||||
* Responding to one of the open [issues](https://github.com/react-community/react-navigation/issues). Even if you can't resolve or fully answer a question, asking for more information or clarity on an issue is extremely beneficial for someone to come after you to resolve the issue.
|
||||
* Creating public example repositories or [Snacks](https://snack.expo.io/) of navigation problems you have solved and sharing the links in [Community Resources](https://github.com/react-navigation/react-navigation/blob/master/COMMUNITY_RESOURCES.md).
|
||||
* Answering questions on [Stack Overflow](https://stackoverflow.com/search?q=react-navigation).
|
||||
* Answering questions in our [Reactiflux](https://www.reactiflux.com/) channel.
|
||||
* Providing feedback on the open [PRs](https://github.com/react-navigation/react-navigation/pulls).
|
||||
* Providing feedback on the open [RFCs](https://github.com/react-navigation/rfcs).
|
||||
* Improving the [website](https://github.com/react-navigation/react-navigation.github.io).
|
||||
|
||||
If you would like to submit a pull request, please follow the [Contributors guide](https://reactnavigation.org/docs/contributing.html) to find out how. If you don't know where to start, check the ones with the label [`good first issue`](https://github.com/react-community/react-navigation/labels/good%20first%20issue) - even [fixing a typo in the documentation](https://github.com/react-community/react-navigation/pull/2727) is a worthy contribution!
|
||||
16
README.md
16
README.md
@@ -1,6 +1,6 @@
|
||||
# React Navigation
|
||||
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-community/react-navigation) [](https://reactnavigation.org/docs/guides/contributors)
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-navigation/react-navigation) [](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [](https://reactnavigation.org/docs/contributing.html)
|
||||
|
||||
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.
|
||||
|
||||
@@ -39,17 +39,7 @@ See [the help page](https://reactnavigation.org/en/help.html).
|
||||
|
||||
#### How can I help?
|
||||
|
||||
This library is a community effort: it can only be great if we all help out in one way or another! If you feel like you aren't experienced enough using React Navigation to contribute, you can still make an impact by:
|
||||
|
||||
* Responding to one of the open [issues](https://github.com/react-community/react-navigation/issues). Even if you can't resolve or fully answer a question, asking for more information or clarity on an issue is extremely beneficial for someone to come after you to resolve the issue.
|
||||
* Creating public example repositories or [Snacks](https://snack.expo.io/) of navigation problems you have solved and sharing the links in [Community Resources](https://github.com/react-navigation/react-navigation/blob/master/COMMUNITY_RESOURCES.md).
|
||||
* Answering questions on [Stack Overflow](https://stackoverflow.com/search?q=react-navigation).
|
||||
* Answering questions in our [Reactiflux](https://www.reactiflux.com/) channel.
|
||||
* Providing feedback on the open [PRs](https://github.com/react-navigation/react-navigation/pulls).
|
||||
* Providing feedback on the open [RFCs](https://github.com/react-navigation/rfcs).
|
||||
* Improving the [website](https://github.com/react-navigation/react-navigation.github.io).
|
||||
|
||||
If you would like to submit a pull request, please follow the [Contributors guide](https://reactnavigation.org/docs/contributing.html) to find out how. If you don't know where to start, check the ones with the label [`good first issue`](https://github.com/react-community/react-navigation/labels/good%20first%20issue) - even [fixing a typo in the documentation](https://github.com/react-community/react-navigation/pull/2727) is a worthy contribution!
|
||||
See our [Contributing Guide](CONTRIBUTING.md)!
|
||||
|
||||
#### Is this the only library available for navigation?
|
||||
|
||||
@@ -65,4 +55,4 @@ This library has adopted a Code of Conduct that we expect project participants t
|
||||
|
||||
## License
|
||||
|
||||
React-navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
|
||||
React Navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
|
||||
|
||||
12
assetsTransformer.js
Normal file
12
assetsTransformer.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file is needed to hijack asset imports so that test files don't attempt
|
||||
* to import them as JavaScript modules.
|
||||
* See https://github.com/facebook/jest/issues/2663#issuecomment-317109798
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
process(src, filename, config, options) {
|
||||
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
|
||||
},
|
||||
};
|
||||
@@ -55,8 +55,6 @@ module.system=haste
|
||||
|
||||
emoji=true
|
||||
|
||||
experimental.strict_type_args=true
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||
@@ -77,7 +75,5 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[version]
|
||||
^0.61.0
|
||||
^0.67.0
|
||||
|
||||
@@ -5,5 +5,6 @@ import renderer from 'react-test-renderer';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const rendered = renderer.create(<App />).toJSON();
|
||||
expect(rendered).toBeTruthy();
|
||||
// Will be null because the playground uses state persistence which happens asyncronously
|
||||
expect(rendered).toEqual(null);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,6 @@ A playground for experimenting with react-navigation in a pure-JS React Native a
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.
|
||||
|
||||
You can view this example application directly on your phone by visiting [our expo demo](https://exp.host/@react-navigation/NavigationPlayground).
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "25.0.0",
|
||||
"sdkVersion": "27.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ import {
|
||||
StatusBar,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, StackNavigator } from 'react-navigation';
|
||||
import { SafeAreaView, createStackNavigator } from 'react-navigation';
|
||||
|
||||
import CustomTabs from './CustomTabs';
|
||||
import CustomTransitioner from './CustomTransitioner';
|
||||
@@ -27,18 +27,34 @@ import ModalStack from './ModalStack';
|
||||
import StacksInTabs from './StacksInTabs';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
import StacksWithKeys from './StacksWithKeys';
|
||||
import InactiveStack from './InactiveStack';
|
||||
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
|
||||
import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import TabAnimations from './TabAnimations';
|
||||
import SwitchWithStacks from './SwitchWithStacks';
|
||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||
import KeyboardHandlingExample from './KeyboardHandlingExample';
|
||||
|
||||
const ExampleInfo = {
|
||||
SimpleStack: {
|
||||
name: 'Stack Example',
|
||||
description: 'A card stack',
|
||||
},
|
||||
SwitchWithStacks: {
|
||||
name: 'Switch between routes',
|
||||
description: 'Jump between routes',
|
||||
},
|
||||
InactiveStack: {
|
||||
name: 'Navigate idempotently to stacks in inactive routes',
|
||||
description:
|
||||
'An inactive route in a stack should be given the opportunity to handle actions',
|
||||
},
|
||||
StackWithCustomHeaderBackImage: {
|
||||
name: 'Custom header back image',
|
||||
description: 'Stack with custom header back image',
|
||||
},
|
||||
SimpleTabs: {
|
||||
name: 'Tabs Example',
|
||||
description: 'Tabs following platform conventions',
|
||||
@@ -51,10 +67,6 @@ const ExampleInfo = {
|
||||
name: 'UIKit-style Header Transitions',
|
||||
description: 'Masked back button and sliding header items. iOS only.',
|
||||
},
|
||||
StackWithHeaderPreset: {
|
||||
name: 'UIKit-style Header Transitions',
|
||||
description: 'Masked back button and sliding header items. iOS only.',
|
||||
},
|
||||
StackWithTranslucentHeader: {
|
||||
name: 'Translucent Header',
|
||||
description: 'Render arbitrary translucent content in header background.',
|
||||
@@ -105,23 +117,26 @@ const ExampleInfo = {
|
||||
name: 'Link to Settings Tab',
|
||||
description: 'Deep linking into a route in tab',
|
||||
},
|
||||
TabAnimations: {
|
||||
name: 'Animated Tabs Example',
|
||||
description: 'Tab transitions have custom animations',
|
||||
TabsWithNavigationFocus: {
|
||||
name: 'withNavigationFocus',
|
||||
description: 'Receive the focus prop to know when a screen is focused',
|
||||
},
|
||||
KeyboardHandlingExample: {
|
||||
name: 'Keyboard Handling Example',
|
||||
description:
|
||||
'Demo automatic handling of keyboard showing/hiding inside StackNavigator',
|
||||
},
|
||||
// TabsWithNavigationFocus: {
|
||||
// name: 'withNavigationFocus',
|
||||
// description: 'Receive the focus prop to know when a screen is focused',
|
||||
// },
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
SimpleStack: SimpleStack,
|
||||
SimpleStack,
|
||||
SwitchWithStacks,
|
||||
SimpleTabs: SimpleTabs,
|
||||
Drawer: Drawer,
|
||||
// MultipleDrawer: {
|
||||
// screen: MultipleDrawer,
|
||||
// },
|
||||
StackWithCustomHeaderBackImage: StackWithCustomHeaderBackImage,
|
||||
StackWithHeaderPreset: StackWithHeaderPreset,
|
||||
StackWithTranslucentHeader: StackWithTranslucentHeader,
|
||||
TabsInDrawer: TabsInDrawer,
|
||||
@@ -139,8 +154,10 @@ const ExampleRoutes = {
|
||||
screen: SimpleTabs,
|
||||
path: 'settings',
|
||||
},
|
||||
TabAnimations: TabAnimations,
|
||||
// TabsWithNavigationFocus: TabsWithNavigationFocus,
|
||||
TabsWithNavigationFocus,
|
||||
KeyboardHandlingExample,
|
||||
// This is commented out because it's rarely useful
|
||||
// InactiveStack,
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -151,7 +168,7 @@ class MainScreen extends React.Component<any, State> {
|
||||
scrollY: new Animated.Value(0),
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
Asset.fromModule(
|
||||
require('react-navigation/src/views/assets/back-icon-mask.png')
|
||||
).downloadAsync();
|
||||
@@ -286,7 +303,7 @@ class MainScreen extends React.Component<any, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const AppNavigator = StackNavigator(
|
||||
const AppNavigator = createStackNavigator(
|
||||
{
|
||||
...ExampleRoutes,
|
||||
Index: {
|
||||
@@ -305,7 +322,7 @@ const AppNavigator = StackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
export default () => <AppNavigator />;
|
||||
export default AppNavigator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
@@ -18,9 +17,9 @@ import {
|
||||
createNavigationContainer,
|
||||
SafeAreaView,
|
||||
TabRouter,
|
||||
addNavigationHelpers,
|
||||
} from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -66,19 +65,14 @@ const CustomTabBar = ({ navigation }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTabView = ({ router, navigation }) => {
|
||||
const CustomTabView = ({ descriptors, navigation }) => {
|
||||
const { routes, index } = navigation.state;
|
||||
const ActiveScreen = router.getComponentForRouteName(routes[index].routeName);
|
||||
const descriptor = descriptors[routes[index].key];
|
||||
const ActiveScreen = descriptor.getComponent();
|
||||
return (
|
||||
<SafeAreaView forceInset={{ top: 'always' }}>
|
||||
<CustomTabBar navigation={navigation} />
|
||||
<ActiveScreen
|
||||
navigation={addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: routes[index],
|
||||
})}
|
||||
screenProps={{}}
|
||||
/>
|
||||
<ActiveScreen navigation={descriptor.navigation} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
@@ -105,7 +99,7 @@ const CustomTabRouter = TabRouter(
|
||||
);
|
||||
|
||||
const CustomTabs = createNavigationContainer(
|
||||
createNavigator(CustomTabRouter)(CustomTabView)
|
||||
createNavigator(CustomTabView, CustomTabRouter, {})
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Button,
|
||||
Easing,
|
||||
Image,
|
||||
Platform,
|
||||
@@ -14,10 +13,10 @@ import {
|
||||
SafeAreaView,
|
||||
StackRouter,
|
||||
createNavigationContainer,
|
||||
addNavigationHelpers,
|
||||
createNavigator,
|
||||
} from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<SafeAreaView forceInset={{ top: 'always' }}>
|
||||
@@ -45,11 +44,12 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
|
||||
class CustomNavigationView extends Component {
|
||||
render() {
|
||||
const { navigation, router } = this.props;
|
||||
const { navigation, router, descriptors } = this.props;
|
||||
|
||||
return (
|
||||
<Transitioner
|
||||
configureTransition={this._configureTransition}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
render={this._render}
|
||||
/>
|
||||
@@ -86,16 +86,10 @@ class CustomNavigationView extends Component {
|
||||
transform: [{ scale: animatedValue }],
|
||||
};
|
||||
|
||||
// The prop `router` is populated when we call `createNavigator`.
|
||||
const Scene = router.getComponentForRouteName(scene.route.routeName);
|
||||
const Scene = scene.descriptor.getComponent();
|
||||
return (
|
||||
<Animated.View key={index} style={[styles.view, animation]}>
|
||||
<Scene
|
||||
navigation={addNavigationHelpers({
|
||||
...navigation,
|
||||
state: routes[index],
|
||||
})}
|
||||
/>
|
||||
<Scene navigation={scene.descriptor.navigation} />
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -107,7 +101,7 @@ const CustomRouter = StackRouter({
|
||||
});
|
||||
|
||||
const CustomTransitioner = createNavigationContainer(
|
||||
createNavigator(CustomRouter)(CustomNavigationView)
|
||||
createNavigator(CustomNavigationView, CustomRouter, {})
|
||||
);
|
||||
|
||||
export default CustomTransitioner;
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, DrawerNavigator, SafeAreaView } from 'react-navigation';
|
||||
import { Platform, ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
createDrawerNavigator,
|
||||
SafeAreaView,
|
||||
} from 'react-navigation';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
<SafeAreaView forceInset={{ top: 'always' }}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('DrawerOpen')}
|
||||
title="Open drawer"
|
||||
/>
|
||||
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Email')}
|
||||
title="Open other screen"
|
||||
@@ -30,14 +32,7 @@ const InboxScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner={'Inbox Screen'} navigation={navigation} />
|
||||
);
|
||||
InboxScreen.navigationOptions = {
|
||||
drawerLabel: 'Inbox',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons
|
||||
name="move-to-inbox"
|
||||
size={24}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
headerTitle: 'Inbox',
|
||||
};
|
||||
|
||||
const EmailScreen = ({ navigation }) => (
|
||||
@@ -48,23 +43,38 @@ const DraftsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
|
||||
);
|
||||
DraftsScreen.navigationOptions = {
|
||||
headerTitle: 'Drafts',
|
||||
};
|
||||
|
||||
const InboxStack = createStackNavigator({
|
||||
Inbox: { screen: InboxScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
InboxStack.navigationOptions = {
|
||||
drawerLabel: 'Inbox',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons
|
||||
name="move-to-inbox"
|
||||
size={24}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const DraftsStack = createStackNavigator({
|
||||
Drafts: { screen: DraftsScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
DraftsStack.navigationOptions = {
|
||||
drawerLabel: 'Drafts',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons name="drafts" size={24} style={{ color: tintColor }} />
|
||||
),
|
||||
};
|
||||
|
||||
const InboxStack = StackNavigator({
|
||||
Inbox: { screen: InboxScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
const DraftsStack = StackNavigator({
|
||||
Drafts: { screen: DraftsScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
const DrawerExample = DrawerNavigator(
|
||||
const DrawerExample = createDrawerNavigator(
|
||||
{
|
||||
Inbox: {
|
||||
path: '/',
|
||||
@@ -76,9 +86,6 @@ const DrawerExample = DrawerNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
initialRouteName: 'Drafts',
|
||||
contentOptions: {
|
||||
activeTintColor: '#e91e63',
|
||||
|
||||
96
examples/NavigationPlayground/js/InactiveStack.js
Normal file
96
examples/NavigationPlayground/js/InactiveStack.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { Button, Text, StatusBar, View, StyleSheet } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createSwitchNavigator,
|
||||
NavigationActions,
|
||||
} from 'react-navigation';
|
||||
|
||||
const runSubRoutes = navigation => {
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'Second2' }));
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
|
||||
};
|
||||
|
||||
const runSubRoutesWithIntermediate = navigation => {
|
||||
navigation.dispatch(toFirst1);
|
||||
navigation.dispatch(toSecond2);
|
||||
navigation.dispatch(toFirst);
|
||||
navigation.dispatch(toFirst2);
|
||||
};
|
||||
|
||||
const runSubAction = navigation => {
|
||||
navigation.dispatch(toFirst2);
|
||||
navigation.dispatch(toSecond2);
|
||||
navigation.dispatch(toFirstChild1);
|
||||
};
|
||||
|
||||
const DummyScreen = ({ routeName, navigation, style }) => {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
StyleSheet.absoluteFill,
|
||||
{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontWeight: '800' }}>
|
||||
{routeName}({navigation.state.key})
|
||||
</Text>
|
||||
<View>
|
||||
<Button title="back" onPress={() => navigation.goBack()} />
|
||||
<Button title="dismiss" onPress={() => navigation.dismiss()} />
|
||||
<Button
|
||||
title="between sub-routes"
|
||||
onPress={() => runSubRoutes(navigation)}
|
||||
/>
|
||||
<Button
|
||||
title="between sub-routes (with intermediate)"
|
||||
onPress={() => runSubRoutesWithIntermediate(navigation)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="with sub-action"
|
||||
onPress={() => runSubAction(navigation)}
|
||||
/>
|
||||
</View>
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const createDummyScreen = routeName => {
|
||||
const BoundDummyScreen = props => DummyScreen({ ...props, routeName });
|
||||
return BoundDummyScreen;
|
||||
};
|
||||
|
||||
const toFirst = NavigationActions.navigate({ routeName: 'First' });
|
||||
const toFirst1 = NavigationActions.navigate({ routeName: 'First1' });
|
||||
const toFirst2 = NavigationActions.navigate({ routeName: 'First2' });
|
||||
const toSecond2 = NavigationActions.navigate({ routeName: 'Second2' });
|
||||
const toFirstChild1 = NavigationActions.navigate({
|
||||
routeName: 'First',
|
||||
action: NavigationActions.navigate({ routeName: 'First1' }),
|
||||
});
|
||||
|
||||
export default createStackNavigator(
|
||||
{
|
||||
Other: createDummyScreen('Leaf'),
|
||||
First: createStackNavigator({
|
||||
First1: createDummyScreen('First1'),
|
||||
First2: createDummyScreen('First2'),
|
||||
}),
|
||||
Second: createStackNavigator({
|
||||
Second1: createDummyScreen('Second1'),
|
||||
Second2: createDummyScreen('Second2'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
headerMode: 'none',
|
||||
}
|
||||
);
|
||||
63
examples/NavigationPlayground/js/KeyboardHandlingExample.js
Normal file
63
examples/NavigationPlayground/js/KeyboardHandlingExample.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { StatusBar, View, TextInput, InteractionManager } from 'react-native';
|
||||
import { createStackNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class ScreenOne extends React.Component {
|
||||
static navigationOptions = {
|
||||
title: 'Home',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<View style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenTwo')}
|
||||
title="Push screen with focused text input"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenTwo extends React.Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: navigation.getParam('inputValue', 'Screen w/ Input'),
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this._textInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<View style={{ paddingTop: 30 }}>
|
||||
<View style={{ alignSelf: 'center', paddingVertical: 20 }}>
|
||||
<TextInput
|
||||
ref={c => (this._textInput = c)}
|
||||
onChangeText={inputValue => navigation.setParams({ inputValue })}
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
height: 24,
|
||||
width: 150,
|
||||
borderColor: '#555',
|
||||
borderWidth: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default createStackNavigator({
|
||||
ScreenOne,
|
||||
ScreenTwo: withNavigationFocus(ScreenTwo),
|
||||
});
|
||||
@@ -3,9 +3,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, StatusBar, Text } from 'react-native';
|
||||
import { SafeAreaView, StackNavigator } from 'react-navigation';
|
||||
import { ScrollView, StatusBar, Text } from 'react-native';
|
||||
import { SafeAreaView, createStackNavigator } from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -31,7 +32,8 @@ const MyNavScreen = ({ navigation, banner }) => (
|
||||
headerVisible:
|
||||
!navigation.state.params ||
|
||||
!navigation.state.params.headerVisible,
|
||||
})}
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
@@ -57,7 +59,7 @@ MyProfileScreen.navigationOptions = ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
});
|
||||
|
||||
const ProfileNavigator = StackNavigator(
|
||||
const ProfileNavigator = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
@@ -87,7 +89,7 @@ MyHeaderTestScreen.navigationOptions = ({ navigation }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const ModalStack = StackNavigator(
|
||||
const ModalStack = createStackNavigator(
|
||||
{
|
||||
ProfileNavigator: {
|
||||
screen: ProfileNavigator,
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView, StyleSheet } from 'react-native';
|
||||
import { DrawerNavigator } from 'react-navigation';
|
||||
import { Platform, ScrollView, StyleSheet } from 'react-native';
|
||||
import { createDrawerNavigator } from 'react-navigation';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView style={styles.container}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('DrawerOpen')}
|
||||
title="Open drawer"
|
||||
/>
|
||||
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
</ScrollView>
|
||||
);
|
||||
@@ -43,7 +41,7 @@ DraftsScreen.navigationOptions = {
|
||||
),
|
||||
};
|
||||
|
||||
const DrawerExample = DrawerNavigator(
|
||||
const DrawerExample = createDrawerNavigator(
|
||||
{
|
||||
Inbox: {
|
||||
path: '/',
|
||||
@@ -55,9 +53,6 @@ const DrawerExample = DrawerNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
initialRouteName: 'Drafts',
|
||||
contentOptions: {
|
||||
activeTintColor: '#e91e63',
|
||||
@@ -65,14 +60,10 @@ const DrawerExample = DrawerNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
const MainDrawerExample = DrawerNavigator({
|
||||
const MainDrawerExample = createDrawerNavigator({
|
||||
Drafts: {
|
||||
screen: DrawerExample,
|
||||
},
|
||||
}, {
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -4,22 +4,42 @@
|
||||
|
||||
import type {
|
||||
NavigationScreenProp,
|
||||
NavigationState,
|
||||
NavigationStateRoute,
|
||||
NavigationEventSubscription,
|
||||
} from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, SafeAreaView, withNavigation } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
SafeAreaView,
|
||||
withNavigation,
|
||||
NavigationActions,
|
||||
StackActions,
|
||||
} from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
banner: React.Node,
|
||||
};
|
||||
|
||||
class MyBackButton extends React.Component<any, any> {
|
||||
type BackButtonProps = {
|
||||
navigation: NavigationScreenProp<NavigationStateRoute>,
|
||||
};
|
||||
|
||||
class MyBackButton extends React.Component<BackButtonProps, any> {
|
||||
render() {
|
||||
return <Button onPress={this._navigateBack} title="Custom Back" />;
|
||||
return (
|
||||
<HeaderButtons>
|
||||
<HeaderButtons.Item title="Back" onPress={this._navigateBack} />
|
||||
</HeaderButtons>
|
||||
);
|
||||
}
|
||||
|
||||
_navigateBack = () => {
|
||||
@@ -32,24 +52,46 @@ const MyBackButtonWithNavigation = withNavigation(MyBackButton);
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop, dismiss } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop && dismiss,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() =>
|
||||
navigation.dispatch(
|
||||
StackActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
routeName: 'Photos',
|
||||
params: { name: 'Jane' },
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
title="Reset photos"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack()} title="Go back" />
|
||||
<Button onPress={() => dismiss()} title="Dismiss" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -57,7 +99,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
}
|
||||
|
||||
type MyHomeScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
|
||||
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
@@ -101,7 +143,7 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
}
|
||||
|
||||
type MyPhotosScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
static navigationOptions = {
|
||||
@@ -142,7 +184,7 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.name}'s Photos`}
|
||||
banner={`${navigation.getParam('name')}'s Photos`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
@@ -151,9 +193,9 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
|
||||
const MyProfileScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
|
||||
navigation.state.params.name
|
||||
}'s Profile`}
|
||||
banner={`${
|
||||
navigation.getParam('mode') === 'edit' ? 'Now Editing ' : ''
|
||||
}${navigation.getParam('name')}'s Profile`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
@@ -168,17 +210,19 @@ MyProfileScreen.navigationOptions = props => {
|
||||
// Render a button on the right side of the header.
|
||||
// When pressed switches the screen to edit mode.
|
||||
headerRight: (
|
||||
<Button
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
<HeaderButtons>
|
||||
<HeaderButtons.Item
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
</HeaderButtons>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const SimpleStack = StackNavigator({
|
||||
const SimpleStack = createStackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
|
||||
@@ -8,10 +8,11 @@ import type {
|
||||
} from 'react-navigation';
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView, StatusBar, View } from 'react-native';
|
||||
import { SafeAreaView, TabNavigator } from 'react-navigation';
|
||||
import { Platform, ScrollView, StatusBar, View } from 'react-native';
|
||||
import { SafeAreaView, createBottomTabNavigator } from 'react-navigation';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
@@ -143,7 +144,7 @@ MySettingsScreen.navigationOptions = {
|
||||
),
|
||||
};
|
||||
|
||||
const SimpleTabs = TabNavigator(
|
||||
const SimpleTabs = createBottomTabNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
@@ -163,8 +164,6 @@ const SimpleTabs = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
lazy: true,
|
||||
removeClippedSubviews: true,
|
||||
tabBarOptions: {
|
||||
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { NavigationScreenProp } from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, Button, StatusBar, StyleSheet } from 'react-native';
|
||||
import { createStackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
banner: React.Node,
|
||||
};
|
||||
|
||||
class MyCustomHeaderBackImage extends React.Component<any, any> {
|
||||
render() {
|
||||
const source = require('./assets/back.png');
|
||||
return (
|
||||
<Image
|
||||
source={source}
|
||||
style={[styles.myCustomHeaderBackImage, this.props.style]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type MyHomeScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
static navigationOptions = {
|
||||
title: 'Welcome',
|
||||
headerBackTitle: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="Home Screen" navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
type MyPhotosScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s photos`,
|
||||
headerBackTitle: null,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{`${navigation.state.params.name}'s Photos`}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
|
||||
title="Navigate to a profile screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type MyProfileScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyProfileScreen extends React.Component<MyProfileScreenProps> {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: 'Profile',
|
||||
headerBackImage: (
|
||||
<MyCustomHeaderBackImage style={styles.myCustomHeaderBackImageAlt} />
|
||||
),
|
||||
});
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{`${navigation.state.params.name}'s Profile`}</SampleText>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StackWithCustomHeaderBackImage = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
Photos: {
|
||||
path: 'photos/:name',
|
||||
screen: MyPhotosScreen,
|
||||
},
|
||||
Profile: {
|
||||
path: 'profile/:name',
|
||||
screen: MyProfileScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
headerBackImage: MyCustomHeaderBackImage,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default StackWithCustomHeaderBackImage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
myCustomHeaderBackImage: {
|
||||
height: 14.5,
|
||||
width: 24,
|
||||
marginLeft: 9,
|
||||
marginRight: 12,
|
||||
marginVertical: 12,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
myCustomHeaderBackImageAlt: {
|
||||
tintColor: '#f00',
|
||||
},
|
||||
});
|
||||
@@ -4,8 +4,11 @@
|
||||
import type { NavigationScreenProp } from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import { createStackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
type NavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
@@ -18,15 +21,17 @@ class HomeScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push } = navigation;
|
||||
invariant(push, 'missing `push` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
title="Push another screen"
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -40,14 +45,20 @@ class OtherScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
onPress={() => push('ScreenWithLongTitle')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
@@ -55,10 +66,54 @@ class OtherScreen extends React.Component<NavScreenProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const StackWithHeaderPreset = StackNavigator(
|
||||
class ScreenWithLongTitle extends React.Component<NavScreenProps> {
|
||||
static navigationOptions = {
|
||||
title: "Another title that's kind of long",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { pop } = navigation;
|
||||
invariant(pop, 'missing `pop` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenWithNoHeader extends React.Component<NavScreenProps> {
|
||||
static navigationOptions = {
|
||||
header: null,
|
||||
title: 'No Header',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StackWithHeaderPreset = createStackNavigator(
|
||||
{
|
||||
Home: HomeScreen,
|
||||
Other: OtherScreen,
|
||||
ScreenWithNoHeader: ScreenWithNoHeader,
|
||||
ScreenWithLongTitle: ScreenWithLongTitle,
|
||||
},
|
||||
{
|
||||
headerTransitionPreset: 'uikit',
|
||||
|
||||
@@ -12,15 +12,18 @@ import { isIphoneX } from 'react-native-iphone-x-helper';
|
||||
import * as React from 'react';
|
||||
import { BlurView, Constants } from 'expo';
|
||||
import {
|
||||
Button,
|
||||
Dimensions,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Header, StackNavigator } from 'react-navigation';
|
||||
import { Header, createStackNavigator } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
@@ -30,11 +33,16 @@ type MyNavScreenProps = {
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
@@ -42,11 +50,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</ScrollView>
|
||||
@@ -193,17 +201,19 @@ MyProfileScreen.navigationOptions = props => {
|
||||
// Render a button on the right side of the header.
|
||||
// When pressed switches the screen to edit mode.
|
||||
headerRight: (
|
||||
<Button
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
<HeaderButtons>
|
||||
<HeaderButtons.Item
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
</HeaderButtons>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const StackWithTranslucentHeader = StackNavigator(
|
||||
const StackWithTranslucentHeader = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { SafeAreaView, StackNavigator, TabNavigator } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createBottomTabNavigator,
|
||||
} from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -51,7 +56,7 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MainTab = StackNavigator({
|
||||
const MainTab = createStackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
path: '/',
|
||||
@@ -68,7 +73,7 @@ const MainTab = StackNavigator({
|
||||
},
|
||||
});
|
||||
|
||||
const SettingsTab = StackNavigator({
|
||||
const SettingsTab = createStackNavigator({
|
||||
Settings: {
|
||||
screen: MySettingsScreen,
|
||||
path: '/',
|
||||
@@ -84,7 +89,7 @@ const SettingsTab = StackNavigator({
|
||||
},
|
||||
});
|
||||
|
||||
const StacksInTabs = TabNavigator(
|
||||
const StacksInTabs = createBottomTabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MainTab,
|
||||
@@ -116,9 +121,9 @@ const StacksInTabs = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: false,
|
||||
swipeEnabled: false,
|
||||
tabBarOptions: {
|
||||
showLabel: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { SafeAreaView, StackNavigator, TabNavigator } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createBottomTabNavigator,
|
||||
} from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -50,7 +55,7 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const TabNav = TabNavigator(
|
||||
const TabNav = createBottomTabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MyHomeScreen,
|
||||
@@ -89,7 +94,20 @@ const TabNav = TabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
const StacksOverTabs = StackNavigator({
|
||||
TabNav.navigationOptions = ({ navigation }) => {
|
||||
let { routeName } = navigation.state.routes[navigation.state.index];
|
||||
let title;
|
||||
if (routeName === 'SettingsTab') {
|
||||
title = 'Settings';
|
||||
} else if (routeName === 'MainTab') {
|
||||
title = 'Home';
|
||||
}
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
const StacksOverTabs = createStackNavigator({
|
||||
Root: {
|
||||
screen: TabNav,
|
||||
},
|
||||
@@ -102,9 +120,9 @@ const StacksOverTabs = StackNavigator({
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
path: '/people/:name',
|
||||
navigationOptions: ({ navigation }) => {
|
||||
title: `${navigation.state.params.name}'s Profile!`;
|
||||
},
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Button, StatusBar, Text, View } from 'react-native';
|
||||
import { StackNavigator } from 'react-navigation';
|
||||
import { StatusBar, Text, View } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class HomeScreen extends React.Component<any, any> {
|
||||
render() {
|
||||
@@ -81,7 +82,7 @@ class SettingsScreen extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
const Stack = StackNavigator(
|
||||
const Stack = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: HomeScreen,
|
||||
|
||||
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { createStackNavigator, createSwitchNavigator } from 'react-navigation';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class SignInScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Please sign in',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="Sign in!" onPress={this._signInAsync} />
|
||||
<Button
|
||||
title="Go back to other examples"
|
||||
onPress={() => this.props.navigation.goBack(null)}
|
||||
/>
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_signInAsync = async () => {
|
||||
await AsyncStorage.setItem('userToken', 'abc');
|
||||
this.props.navigation.navigate('Home');
|
||||
};
|
||||
}
|
||||
|
||||
class HomeScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Welcome to the app!',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="Show me more of the app" onPress={this._showMoreApp} />
|
||||
<Button title="Actually, sign me out :)" onPress={this._signOutAsync} />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_showMoreApp = () => {
|
||||
this.props.navigation.navigate('Other');
|
||||
};
|
||||
|
||||
_signOutAsync = async () => {
|
||||
await AsyncStorage.clear();
|
||||
this.props.navigation.navigate('Auth');
|
||||
};
|
||||
}
|
||||
|
||||
class OtherScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Lots of features here',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="I'm done, sign me out" onPress={this._signOutAsync} />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_signOutAsync = async () => {
|
||||
await AsyncStorage.clear();
|
||||
this.props.navigation.navigate('Auth');
|
||||
};
|
||||
}
|
||||
|
||||
class LoadingScreen extends React.Component<any, any> {
|
||||
componentDidMount() {
|
||||
this._bootstrapAsync();
|
||||
}
|
||||
|
||||
_bootstrapAsync = async () => {
|
||||
const userToken = await AsyncStorage.getItem('userToken');
|
||||
let initialRouteName = userToken ? 'App' : 'Auth';
|
||||
this.props.navigation.navigate(initialRouteName);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const AppStack = createStackNavigator({ Home: HomeScreen, Other: OtherScreen });
|
||||
const AuthStack = createStackNavigator({ SignIn: SignInScreen });
|
||||
|
||||
export default createSwitchNavigator({
|
||||
Loading: LoadingScreen,
|
||||
App: AppStack,
|
||||
Auth: AuthStack,
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Animated, Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, TabNavigator } from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Profile', { name: 'Jordan' })}
|
||||
title="Open profile screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('NotifSettings')}
|
||||
title="Open notifications screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('SettingsTab')}
|
||||
title="Go to settings tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const MyHomeScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Home Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MyProfileScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.name}s Profile`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
const MyNotificationsSettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Notifications Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MainTab = StackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
path: '/',
|
||||
navigationOptions: {
|
||||
title: 'Welcome',
|
||||
},
|
||||
},
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
path: '/people/:name',
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const SettingsTab = StackNavigator({
|
||||
Settings: {
|
||||
screen: MySettingsScreen,
|
||||
path: '/',
|
||||
navigationOptions: () => ({
|
||||
title: 'Settings',
|
||||
}),
|
||||
},
|
||||
NotifSettings: {
|
||||
screen: MyNotificationsSettingsScreen,
|
||||
navigationOptions: {
|
||||
title: 'Notifications',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const TabAnimations = TabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MainTab,
|
||||
path: '/',
|
||||
navigationOptions: {
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-home' : 'ios-home-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
SettingsTab: {
|
||||
screen: SettingsTab,
|
||||
path: '/settings',
|
||||
navigationOptions: {
|
||||
tabBarLabel: 'Settings',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-settings' : 'ios-settings-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: true,
|
||||
configureTransition: (currentTransitionProps,nextTransitionProps) => ({
|
||||
timing: Animated.spring,
|
||||
tension: 1,
|
||||
friction: 35,
|
||||
}),
|
||||
swipeEnabled: false,
|
||||
}
|
||||
);
|
||||
|
||||
export default TabAnimations;
|
||||
@@ -3,13 +3,13 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView } from 'react-native';
|
||||
import { TabNavigator, DrawerNavigator } from 'react-navigation';
|
||||
import { Platform, ScrollView } from 'react-native';
|
||||
import { createDrawerNavigator } from 'react-navigation';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
|
||||
const TabsInDrawer = DrawerNavigator({
|
||||
const TabsInDrawer = createDrawerNavigator({
|
||||
SimpleTabs: {
|
||||
screen: SimpleTabs,
|
||||
navigationOptions: {
|
||||
|
||||
@@ -3,44 +3,82 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SafeAreaView, Text } from 'react-native';
|
||||
import { TabNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import { SafeAreaView, StatusBar, Text, View } from 'react-native';
|
||||
import { withNavigationFocus } from 'react-navigation';
|
||||
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
const TabScreen = ({ isFocused }) => (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
|
||||
{'Tab ' + name.toLowerCase()}
|
||||
class Child extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<Text style={{ color: this.props.isFocused ? 'green' : 'maroon' }}>
|
||||
{this.props.isFocused
|
||||
? 'I know that my parent is focused!'
|
||||
: 'My parent is not focused! :O'}
|
||||
</Text>
|
||||
<Text>{'props.isFocused: ' + (isFocused ? ' true' : 'false')}</Text>
|
||||
</SafeAreaView>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabScreen.navigationOptions = {
|
||||
tabBarLabel: name,
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? focusedIcon : icon}
|
||||
size={26}
|
||||
style={{ color: focused ? tintColor : '#ccc' }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
const ChildWithNavigationFocus = withNavigationFocus(Child);
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
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 = { showChild: false };
|
||||
|
||||
render() {
|
||||
const { isFocused } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
|
||||
{'Tab ' + name.toLowerCase()}
|
||||
</Text>
|
||||
<Text style={{ marginBottom: 20 }}>
|
||||
{'props.isFocused: ' + (isFocused ? ' true' : 'false')}
|
||||
</Text>
|
||||
{this.state.showChild ? (
|
||||
<ChildWithNavigationFocus />
|
||||
) : (
|
||||
<Button
|
||||
title="Press me"
|
||||
onPress={() => this.setState({ showChild: true })}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={() => this.props.navigation.pop()}
|
||||
title="Back to other examples"
|
||||
/>
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
return withNavigationFocus(TabScreen);
|
||||
};
|
||||
|
||||
const TabsWithNavigationFocus = TabNavigator(
|
||||
const TabsWithNavigationFocus = createMaterialBottomTabNavigator(
|
||||
{
|
||||
One: {
|
||||
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
|
||||
@@ -57,9 +95,8 @@ const TabsWithNavigationFocus = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: true,
|
||||
swipeEnabled: true,
|
||||
shifting: false,
|
||||
activeTintColor: '#F44336',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
BIN
examples/NavigationPlayground/js/assets/back.png
Normal file
BIN
examples/NavigationPlayground/js/assets/back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,18 @@
|
||||
import { Button as RNButton, StyleSheet, View, Platform } from 'react-native';
|
||||
import React from 'react';
|
||||
|
||||
export const Button = props => (
|
||||
<View style={styles.margin}>
|
||||
<RNButton {...props} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
margin: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
margin: 10,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import DefaultHeaderButtons from 'react-navigation-header-buttons';
|
||||
import * as React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export class HeaderButtons extends React.PureComponent {
|
||||
static Item = DefaultHeaderButtons.Item;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DefaultHeaderButtons
|
||||
color={Platform.OS === 'ios' ? '#037aff' : 'black'}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,26 @@
|
||||
"eject": "react-native-scripts eject",
|
||||
"android": "react-native-scripts android",
|
||||
"ios": "react-native-scripts ios",
|
||||
"test": "node node_modules/jest/bin/jest.js && flow"
|
||||
"test": "flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^25.0.0",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"expo": "^27.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../.."
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-header-buttons": "^0.0.4",
|
||||
"react-navigation-material-bottom-tabs": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^21.0.0",
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"jest": "^21.0.1",
|
||||
"jest-expo": "^25.1.0",
|
||||
"flow-bin": "^0.67.0",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
"react-test-renderer": "16.0.0"
|
||||
"react-test-renderer": "16.3.0-alpha.1"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo",
|
||||
|
||||
@@ -11,7 +11,9 @@ module.exports = {
|
||||
return blacklist([
|
||||
/react\-navigation\/examples\/(?!NavigationPlayground).*/,
|
||||
/react\-navigation\/node_modules\/react-native\/(.*)/,
|
||||
/react\-navigation\/node_modules\/react\/(.*)/
|
||||
/react\-navigation\/node_modules\/react\/(.*)/,
|
||||
/react\-navigation\/node_modules\/react-native-paper\/(.*)/,
|
||||
/react\-navigation\/node_modules\/@expo\/vector-icons\/(.*)/,
|
||||
]);
|
||||
},
|
||||
extraNodeModules: getNodeModulesForDirectory(path.resolve('.')),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,4 +2,4 @@
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^21.0.0",
|
||||
"jest": "^21.0.1",
|
||||
"babel-jest": "^22.4.1",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^25.1.0",
|
||||
"react-native-scripts": "^1.3.1",
|
||||
"react-test-renderer": "16.0.0"
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
|
||||
import { createStackNavigator } from 'react-navigation';
|
||||
import { initializeListeners } from 'react-navigation-redux-helpers';
|
||||
|
||||
import LoginScreen from '../components/LoginScreen';
|
||||
import MainScreen from '../components/MainScreen';
|
||||
import ProfileScreen from '../components/ProfileScreen';
|
||||
import { addListener } from '../utils/redux';
|
||||
import { navigationPropConstructor } from '../utils/redux';
|
||||
|
||||
export const AppNavigator = StackNavigator({
|
||||
export const AppNavigator = createStackNavigator({
|
||||
Login: { screen: LoginScreen },
|
||||
Main: { screen: MainScreen },
|
||||
Profile: { screen: ProfileScreen },
|
||||
@@ -20,17 +21,14 @@ class AppWithNavigationState extends React.Component {
|
||||
nav: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
initializeListeners('root', this.props.nav);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
<AppNavigator
|
||||
navigation={addNavigationHelpers({
|
||||
dispatch,
|
||||
state: nav,
|
||||
addListener,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
const navigation = navigationPropConstructor(dispatch, nav);
|
||||
return <AppNavigator navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {
|
||||
createReactNavigationReduxMiddleware,
|
||||
createReduxBoundAddListener,
|
||||
createNavigationPropConstructor,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
const middleware = createReactNavigationReduxMiddleware(
|
||||
"root",
|
||||
state => state.nav,
|
||||
'root',
|
||||
state => state.nav
|
||||
);
|
||||
const addListener = createReduxBoundAddListener("root");
|
||||
const navigationPropConstructor = createNavigationPropConstructor('root');
|
||||
|
||||
export {
|
||||
middleware,
|
||||
addListener,
|
||||
};
|
||||
export { middleware, navigationPropConstructor };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
431
flow/react-navigation.js
vendored
431
flow/react-navigation.js
vendored
@@ -48,6 +48,15 @@ declare module 'react-navigation' {
|
||||
// react-native/Libraries/Animated/src/nodes/AnimatedValue.js
|
||||
declare type AnimatedValue = Object;
|
||||
|
||||
declare type HeaderForceInset = {
|
||||
horizontal?: string,
|
||||
vertical?: string,
|
||||
left?: string,
|
||||
right?: string,
|
||||
top?: string,
|
||||
bottom?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Next, all the type declarations
|
||||
*/
|
||||
@@ -60,6 +69,14 @@ declare module 'react-navigation' {
|
||||
[key: string]: mixed,
|
||||
};
|
||||
|
||||
declare export type NavigationBackAction = {|
|
||||
type: 'Navigation/BACK',
|
||||
key?: ?string,
|
||||
|};
|
||||
declare export type NavigationInitAction = {|
|
||||
type: 'Navigation/INIT',
|
||||
params?: NavigationParams,
|
||||
|};
|
||||
declare export type NavigationNavigateAction = {|
|
||||
type: 'Navigation/NAVIGATE',
|
||||
routeName: string,
|
||||
@@ -70,12 +87,6 @@ declare module 'react-navigation' {
|
||||
|
||||
key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationBackAction = {|
|
||||
type: 'Navigation/BACK',
|
||||
key?: ?string,
|
||||
|};
|
||||
|
||||
declare export type NavigationSetParamsAction = {|
|
||||
type: 'Navigation/SET_PARAMS',
|
||||
|
||||
@@ -86,30 +97,6 @@ declare module 'react-navigation' {
|
||||
params: NavigationParams,
|
||||
|};
|
||||
|
||||
declare export type NavigationInitAction = {|
|
||||
type: 'Navigation/INIT',
|
||||
params?: NavigationParams,
|
||||
|};
|
||||
|
||||
declare export type NavigationResetAction = {|
|
||||
type: 'Navigation/RESET',
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
|};
|
||||
|
||||
declare export type NavigationUriAction = {|
|
||||
type: 'Navigation/URI',
|
||||
uri: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationReplaceAction = {|
|
||||
+type: 'Navigation/REPLACE',
|
||||
+key: string,
|
||||
+routeName: string,
|
||||
+params?: NavigationParams,
|
||||
+action?: NavigationNavigateAction,
|
||||
|};
|
||||
declare export type NavigationPopAction = {|
|
||||
+type: 'Navigation/POP',
|
||||
+n?: number,
|
||||
@@ -126,17 +113,51 @@ declare module 'react-navigation' {
|
||||
+action?: NavigationNavigateAction,
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationResetAction = {|
|
||||
type: 'Navigation/RESET',
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
|};
|
||||
declare export type NavigationReplaceAction = {|
|
||||
+type: 'Navigation/REPLACE',
|
||||
+key: string,
|
||||
+routeName: string,
|
||||
+params?: NavigationParams,
|
||||
+action?: NavigationNavigateAction,
|
||||
|};
|
||||
declare export type NavigationCompleteTransitionAction = {|
|
||||
+type: 'Navigation/COMPLETE_TRANSITION',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationOpenDrawerAction = {|
|
||||
+type: 'Navigation/OPEN_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationCloseDrawerAction = {|
|
||||
+type: 'Navigation/CLOSE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationToggleDrawerAction = {|
|
||||
+type: 'Navigation/TOGGLE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationAction =
|
||||
| NavigationBackAction
|
||||
| NavigationInitAction
|
||||
| NavigationNavigateAction
|
||||
| NavigationReplaceAction
|
||||
| NavigationSetParamsAction
|
||||
| NavigationPopAction
|
||||
| NavigationPopToTopAction
|
||||
| NavigationPushAction
|
||||
| NavigationBackAction
|
||||
| NavigationSetParamsAction
|
||||
| NavigationResetAction;
|
||||
| NavigationResetAction
|
||||
| NavigationReplaceAction
|
||||
| NavigationCompleteTransitionAction
|
||||
| NavigationOpenDrawerAction
|
||||
| NavigationCloseDrawerAction
|
||||
| NavigationToggleDrawerAction;
|
||||
|
||||
/**
|
||||
* NavigationState is a tree of routes for a single navigator, where each
|
||||
@@ -260,23 +281,33 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationComponent =
|
||||
| NavigationScreenComponent<NavigationRoute, *, *>
|
||||
| NavigationContainer<NavigationStateRoute, *, *>;
|
||||
| NavigationContainer<*, *, *>;
|
||||
|
||||
declare interface withOptionalNavigationOptions<Options> {
|
||||
navigationOptions?: NavigationScreenConfig<Options>,
|
||||
}
|
||||
|
||||
declare export type NavigationScreenComponent<
|
||||
Route: NavigationRoute,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
|
||||
({} | { navigationOptions: NavigationScreenConfig<Options> });
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, Route>,
|
||||
}> & withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare interface withRouter<State, Options> {
|
||||
router: NavigationRouter<State, Options>,
|
||||
}
|
||||
|
||||
declare export type NavigationNavigator<
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, State>,
|
||||
}> & withRouter<State, Options> & withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare export type NavigationRouteConfig =
|
||||
| NavigationComponent
|
||||
@@ -286,7 +317,6 @@ declare module 'react-navigation' {
|
||||
} & NavigationScreenRouteConfig);
|
||||
|
||||
declare export type NavigationScreenRouteConfig =
|
||||
| NavigationComponent
|
||||
| {
|
||||
screen: NavigationComponent,
|
||||
}
|
||||
@@ -334,12 +364,13 @@ declare module 'react-navigation' {
|
||||
headerTintColor?: string,
|
||||
headerLeft?: React$Node | React$ElementType,
|
||||
headerBackTitle?: string,
|
||||
headerBackImage?: ImageSource,
|
||||
headerBackImage?: React$Node | React$ElementType,
|
||||
headerTruncatedBackTitle?: string,
|
||||
headerBackTitleStyle?: TextStyleProp,
|
||||
headerPressColorAndroid?: string,
|
||||
headerRight?: React$Node,
|
||||
headerStyle?: ViewStyleProp,
|
||||
headerForceInset?: HeaderForceInset,
|
||||
headerBackground?: React$Node | React$ElementType,
|
||||
gesturesEnabled?: boolean,
|
||||
gestureResponseDistance?: { vertical?: number, horizontal?: number },
|
||||
@@ -351,6 +382,7 @@ declare module 'react-navigation' {
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
initialRouteKey?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationStackViewConfig = {|
|
||||
@@ -368,6 +400,20 @@ declare module 'react-navigation' {
|
||||
...NavigationStackRouterConfig,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Switch Navigator
|
||||
*/
|
||||
|
||||
declare export type NavigationSwitchRouterConfig = {|
|
||||
initialRouteName?: string,
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
order?: Array<string>,
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults to `'none'`
|
||||
resetOnBlur?: boolean, // defaults to `true`
|
||||
|};
|
||||
|
||||
/**
|
||||
* Tab Navigator
|
||||
*/
|
||||
@@ -379,7 +425,6 @@ declare module 'react-navigation' {
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
// todo: type these as the real route names rather than 'string'
|
||||
order?: Array<string>,
|
||||
|
||||
// Does the back button cause the router to switch to the initial tab
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|
||||
|};
|
||||
@@ -402,10 +447,9 @@ declare module 'react-navigation' {
|
||||
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
|
||||
tabBarVisible?: boolean,
|
||||
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
|
||||
tabBarOnPress?: (
|
||||
scene: TabScene,
|
||||
jumpToIndex: (index: number) => void
|
||||
) => void,
|
||||
tabBarOnPress?: ({
|
||||
navigation: NavigationScreenProp<NavigationRoute>,
|
||||
}) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -447,7 +491,7 @@ declare module 'react-navigation' {
|
||||
type: EventType,
|
||||
action: NavigationAction,
|
||||
state: NavigationState,
|
||||
lastState: NavigationState,
|
||||
lastState: ?NavigationState,
|
||||
};
|
||||
|
||||
declare export type NavigationEventCallback = (
|
||||
@@ -459,31 +503,37 @@ declare module 'react-navigation' {
|
||||
};
|
||||
|
||||
declare export type NavigationScreenProp<+S> = {
|
||||
...$ObjMap<
|
||||
_DefaultActionCreators,
|
||||
<Args>((...args: Args) => *) => (...args: Args) => boolean
|
||||
>,
|
||||
+state: S,
|
||||
dispatch: NavigationDispatch,
|
||||
goBack: (routeKey?: ?string) => boolean,
|
||||
navigate: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
setParams: (newParams: NavigationParams) => boolean,
|
||||
addListener: (
|
||||
eventName: string,
|
||||
callback: NavigationEventCallback
|
||||
) => NavigationEventSubscription,
|
||||
push: (
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
isFocused: () => boolean,
|
||||
// StackRouter action creators
|
||||
pop?: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop?: (params?: { immediate?: boolean }) => boolean,
|
||||
push?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
replace: (
|
||||
replace?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop: (params?: { immediate?: boolean }) => boolean,
|
||||
reset?: (actions: NavigationAction[], index: number) => boolean,
|
||||
dismiss?: () => boolean,
|
||||
// DrawerRouter action creators
|
||||
openDrawer?: () => boolean,
|
||||
closeDrawer?: () => boolean,
|
||||
toggleDrawer?: () => boolean,
|
||||
};
|
||||
|
||||
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
|
||||
@@ -492,29 +542,6 @@ declare module 'react-navigation' {
|
||||
navigationOptions?: O,
|
||||
}>;
|
||||
|
||||
//declare export type NavigationNavigatorProps<O: {}, S: {}> =
|
||||
// | {}
|
||||
// | { navigation: NavigationScreenProp<S> }
|
||||
// | { screenProps: {} }
|
||||
// | { navigationOptions: O }
|
||||
// | {
|
||||
// navigation: NavigationScreenProp<S>,
|
||||
// screenProps: {},
|
||||
// }
|
||||
// | {
|
||||
// navigation: NavigationScreenProp<S>,
|
||||
// navigationOptions: O,
|
||||
// }
|
||||
// | {
|
||||
// screenProps: {},
|
||||
// navigationOptions: O,
|
||||
// }
|
||||
// | {
|
||||
// navigation: NavigationScreenProp<S>,
|
||||
// screenProps: {},
|
||||
// navigationOptions: O,
|
||||
// };
|
||||
|
||||
/**
|
||||
* Navigation container
|
||||
*/
|
||||
@@ -523,19 +550,21 @@ declare module 'react-navigation' {
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationContainerProps<State, Options>,
|
||||
}> & withRouter<State, Options> & withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
|
||||
uriPrefix?: string | RegExp,
|
||||
onNavigationStateChange?: (
|
||||
onNavigationStateChange?: ?(
|
||||
NavigationState,
|
||||
NavigationState,
|
||||
NavigationAction
|
||||
) => void,
|
||||
navigation?: NavigationScreenProp<S>,
|
||||
persistenceKey?: ?string,
|
||||
renderLoadingExperimental?: React$ComponentType<{}>,
|
||||
screenProps?: *,
|
||||
navigationOptions?: O,
|
||||
}>;
|
||||
@@ -689,79 +718,129 @@ declare module 'react-navigation' {
|
||||
) => NavigationState,
|
||||
};
|
||||
|
||||
declare export function addNavigationHelpers<S: {}>(
|
||||
navigation: NavigationProp<S>
|
||||
): NavigationScreenProp<S>;
|
||||
|
||||
declare export var NavigationActions: {
|
||||
BACK: 'Navigation/BACK',
|
||||
INIT: 'Navigation/INIT',
|
||||
NAVIGATE: 'Navigation/NAVIGATE',
|
||||
RESET: 'Navigation/RESET',
|
||||
SET_PARAMS: 'Navigation/SET_PARAMS',
|
||||
URI: 'Navigation/URI',
|
||||
back: {
|
||||
(payload: { key?: ?string }): NavigationBackAction,
|
||||
toString: () => string,
|
||||
},
|
||||
init: {
|
||||
(payload?: { params?: NavigationParams }): NavigationInitAction,
|
||||
toString: () => string,
|
||||
},
|
||||
navigate: {
|
||||
(payload: {
|
||||
routeName: string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
}): NavigationNavigateAction,
|
||||
toString: () => string,
|
||||
},
|
||||
reset: {
|
||||
(payload: {
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}): NavigationResetAction,
|
||||
toString: () => string,
|
||||
},
|
||||
setParams: {
|
||||
(payload: {
|
||||
key: string,
|
||||
params: NavigationParams,
|
||||
}): NavigationSetParamsAction,
|
||||
toString: () => string,
|
||||
},
|
||||
uri: {
|
||||
(payload: { uri: string }): NavigationUriAction,
|
||||
toString: () => string,
|
||||
},
|
||||
|
||||
back: (payload?: { key?: ?string }) => NavigationBackAction,
|
||||
init: (payload?: { params?: NavigationParams }) => NavigationInitAction,
|
||||
navigate: (payload: {
|
||||
routeName: string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
key?: string,
|
||||
}) => NavigationNavigateAction,
|
||||
setParams: (payload: {
|
||||
key: string,
|
||||
params: NavigationParams,
|
||||
}) => NavigationSetParamsAction,
|
||||
};
|
||||
|
||||
declare export var StackActions: {
|
||||
POP: 'Navigation/POP',
|
||||
POP_TO_TOP: 'Navigation/POP_TO_TOP',
|
||||
PUSH: 'Navigation/PUSH',
|
||||
RESET: 'Navigation/RESET',
|
||||
REPLACE: 'Navigation/REPLACE',
|
||||
COMPLETE_TRANSITION: 'Navigation/COMPLETE_TRANSITION',
|
||||
|
||||
pop: (payload: {
|
||||
n?: number,
|
||||
immediate?: boolean,
|
||||
}) => NavigationPopAction,
|
||||
popToTop: (payload: {
|
||||
immediate?: boolean,
|
||||
}) => NavigationPopToTopAction,
|
||||
push: (payload: {
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
key?: string,
|
||||
}) => NavigationPushAction,
|
||||
reset: (payload: {
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}) => NavigationResetAction,
|
||||
replace: (payload: {
|
||||
key: string,
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
}) => NavigationReplaceAction,
|
||||
completeTransition: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationCompleteTransitionAction,
|
||||
};
|
||||
|
||||
declare export var DrawerActions: {
|
||||
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
|
||||
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
|
||||
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
|
||||
|
||||
openDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationOpenDrawerAction,
|
||||
closeDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationCloseDrawerAction,
|
||||
toggleDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationToggleDrawerAction,
|
||||
};
|
||||
|
||||
declare type _DefaultActionCreators = {|
|
||||
goBack: (routeKey?: ?string) => NavigationBackAction,
|
||||
navigate: (
|
||||
routeName:
|
||||
| string
|
||||
| {
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
key?: string,
|
||||
},
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => NavigationNavigateAction,
|
||||
setParams: (newParams: NavigationParams) => NavigationSetParamsAction,
|
||||
|};
|
||||
declare export function getNavigationActionCreators(
|
||||
route: NavigationRoute | NavigationState
|
||||
): _DefaultActionCreators;
|
||||
|
||||
declare type _RouterProp<S: NavigationState, O: {}> = {
|
||||
router: NavigationRouter<S, O>,
|
||||
};
|
||||
declare type _NavigatorCreator<
|
||||
NavigationViewProps: {},
|
||||
S: NavigationState,
|
||||
O: {}
|
||||
> = (
|
||||
NavigationView: React$ComponentType<_RouterProp<S, O> & NavigationViewProps>
|
||||
) => NavigationNavigator<S, O, NavigationViewProps>;
|
||||
declare export function createNavigator<
|
||||
S: NavigationState,
|
||||
O: {},
|
||||
NavigatorConfig: {},
|
||||
NavigationViewProps: NavigationNavigatorProps<O, S>
|
||||
>(
|
||||
|
||||
declare type NavigationDescriptor = {
|
||||
key: string,
|
||||
state: NavigationLeafRoute | NavigationStateRoute,
|
||||
navigation: NavigationScreenProp<*>,
|
||||
getComponent: () => React$ComponentType<{}>,
|
||||
};
|
||||
|
||||
declare type NavigationView<O, S> = React$ComponentType<{
|
||||
descriptors: { [key: string]: NavigationDescriptor },
|
||||
navigation: NavigationScreenProp<S>,
|
||||
}>;
|
||||
|
||||
declare export function createNavigator<O: *, S: *, NavigatorConfig: *>(
|
||||
view: NavigationView<O, S>,
|
||||
router: NavigationRouter<S, O>,
|
||||
routeConfigs?: NavigationRouteConfigMap,
|
||||
navigatorConfig?: NavigatorConfig
|
||||
): _NavigatorCreator<NavigationViewProps, S, O>;
|
||||
): NavigationNavigator<S, O, *>;
|
||||
|
||||
declare export function StackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
stackConfig?: StackNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createStackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
stackConfig?: StackNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare type _TabViewConfig = {|
|
||||
tabBarComponent?: React$ElementType,
|
||||
@@ -786,14 +865,35 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
/* TODO: fix the config for each of these tab navigator types */
|
||||
declare export function createBottomTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createMaterialTopTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare type _SwitchNavigatorConfig = {|
|
||||
...NavigationSwitchRouterConfig,
|
||||
|};
|
||||
declare export function SwitchNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _SwitchNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createSwitchNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _SwitchNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare type _DrawerViewConfig = {|
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
drawerWidth?: number | (() => number),
|
||||
drawerPosition?: 'left' | 'right',
|
||||
drawerOpenRoute?: string,
|
||||
drawerCloseRoute?: string,
|
||||
drawerToggleRoute?: string,
|
||||
contentComponent?: React$ElementType,
|
||||
contentOptions?: {},
|
||||
style?: ViewStyleProp,
|
||||
@@ -810,6 +910,10 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _DrawerNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createDrawerNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _DrawerNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare export function StackRouter(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
@@ -895,12 +999,14 @@ declare module 'react-navigation' {
|
||||
vertical?: _SafeAreaViewForceInsetValue,
|
||||
horizontal?: _SafeAreaViewForceInsetValue,
|
||||
},
|
||||
children: React$Node,
|
||||
children?: React$Node,
|
||||
style?: AnimatedViewStyleProp,
|
||||
};
|
||||
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>;
|
||||
|
||||
declare export var Header: React$ComponentType<HeaderProps>;
|
||||
declare export var Header: React$ComponentType<HeaderProps> & {
|
||||
HEIGHT: number,
|
||||
};
|
||||
|
||||
declare type _HeaderTitleProps = {
|
||||
children: React$Node,
|
||||
@@ -926,9 +1032,6 @@ declare module 'react-navigation' {
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
drawerWidth: number | (() => number),
|
||||
drawerPosition: 'left' | 'right',
|
||||
drawerOpenRoute: string,
|
||||
drawerCloseRoute: string,
|
||||
drawerToggleRoute: string,
|
||||
contentComponent: React$ElementType,
|
||||
contentOptions?: {},
|
||||
style?: ViewStyleProp,
|
||||
@@ -965,6 +1068,8 @@ declare module 'react-navigation' {
|
||||
itemsContainerStyle?: ViewStyleProp,
|
||||
itemStyle?: ViewStyleProp,
|
||||
labelStyle?: TextStyleProp,
|
||||
activeLabelStyle?: TextStyleProp,
|
||||
inactiveLabelStyle?: TextStyleProp,
|
||||
iconContainerStyle?: ViewStyleProp,
|
||||
drawerPosition: 'left' | 'right',
|
||||
};
|
||||
@@ -1044,13 +1149,17 @@ declare module 'react-navigation' {
|
||||
};
|
||||
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
|
||||
|
||||
declare type _NavigationInjectedProps = {
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
declare export function withNavigation<T: {}>(
|
||||
Component: React$ComponentType<T & _NavigationInjectedProps>
|
||||
): React$ComponentType<T>;
|
||||
declare export function withNavigationFocus<T: {}>(
|
||||
Component: React$ComponentType<T & _NavigationInjectedProps>
|
||||
): React$ComponentType<T>;
|
||||
declare export function withNavigation<Props: {}>(
|
||||
Component: React$ComponentType<Props>
|
||||
): React$ComponentType<
|
||||
$Diff<
|
||||
Props,
|
||||
{
|
||||
navigation: NavigationScreenProp<NavigationStateRoute> | void,
|
||||
}
|
||||
>
|
||||
>;
|
||||
declare export function withNavigationFocus<Props: {}>(
|
||||
Component: React$ComponentType<Props>
|
||||
): React$ComponentType<$Diff<Props, { isFocused: boolean | void }>>;
|
||||
}
|
||||
|
||||
55
package.json
55
package.json
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "1.2.1",
|
||||
"version": "2.2.0",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
"url": "git@github.com:react-navigation/react-navigation.git",
|
||||
"type": "git"
|
||||
},
|
||||
"author":
|
||||
"Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>",
|
||||
"author": "Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>, Brent Vatne <brent@expo.io>",
|
||||
"license": "BSD-2-Clause",
|
||||
"scripts": {
|
||||
"start": "npm run ios",
|
||||
@@ -22,38 +21,44 @@
|
||||
"format": "eslint --fix .",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"files": ["src"],
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"clamp": "^1.0.1",
|
||||
"create-react-context": "^0.2.1",
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^3",
|
||||
"react-native-drawer-layout-polyfill": "^1.3.2",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"react-native-tab-view": "^0.0.74"
|
||||
"react-native-safe-area-view": "^0.8.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||
"react-navigation-tabs": "0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-jest": "^20.0.3",
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-preset-react-native": "^2.1.0",
|
||||
"codecov": "^2.2.0",
|
||||
"eslint": "^4.2.0",
|
||||
"eslint-config-prettier": "^2.3.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.2",
|
||||
"eslint-plugin-prettier": "^2.1.2",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-react": "^7.1.0",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^25.1.0",
|
||||
"lint-staged": "^4.2.1",
|
||||
"prettier": "^1.5.3",
|
||||
"prettier-eslint": "^6.4.2",
|
||||
"prettier": "^1.12.1",
|
||||
"prettier-eslint": "^8.8.1",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"react-native-vector-icons": "^4.2.0",
|
||||
@@ -62,14 +67,30 @@
|
||||
"jest": {
|
||||
"notify": true,
|
||||
"preset": "react-native",
|
||||
"testRegex": "./src/.*\\-test\\.js$",
|
||||
"setupFiles": ["<rootDir>/jest-setup.js"],
|
||||
"testRegex": "/__tests__/[^/]+-test\\.js$",
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
],
|
||||
"coverageDirectory": "./coverage/",
|
||||
"collectCoverage": true,
|
||||
"coverageReporters": ["lcov"],
|
||||
"collectCoverageFrom": ["src/**/*.js"],
|
||||
"coveragePathIgnorePatterns": ["jest-setup.js"],
|
||||
"modulePathIgnorePatterns": ["examples"]
|
||||
"coverageReporters": [
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"jest-setup.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.png$": "<rootDir>/assetsTransformer.js"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/examples/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
|
||||
@@ -4,6 +4,6 @@ set -eo pipefail
|
||||
|
||||
case $CIRCLE_NODE_INDEX in
|
||||
0) yarn test && yarn codecov ;;
|
||||
1) yarn link && cd examples/NavigationPlayground && yarn && yarn link react-navigation && yarn test ;;
|
||||
#1) cd examples/NavigationPlayground && yarn && yarn test ;;
|
||||
#2) cd examples/ReduxExample && yarn && yarn test ;;
|
||||
esac
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
const BACK = 'Navigation/BACK';
|
||||
const INIT = 'Navigation/INIT';
|
||||
const NAVIGATE = 'Navigation/NAVIGATE';
|
||||
const POP = 'Navigation/POP';
|
||||
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
|
||||
const PUSH = 'Navigation/PUSH';
|
||||
const RESET = 'Navigation/RESET';
|
||||
const REPLACE = 'Navigation/REPLACE';
|
||||
const SET_PARAMS = 'Navigation/SET_PARAMS';
|
||||
const URI = 'Navigation/URI';
|
||||
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
|
||||
|
||||
const createAction = (type, fn) => {
|
||||
fn.toString = () => type;
|
||||
return fn;
|
||||
};
|
||||
|
||||
const back = createAction(BACK, (payload = {}) => ({
|
||||
const back = (payload = {}) => ({
|
||||
type: BACK,
|
||||
key: payload.key,
|
||||
immediate: payload.immediate,
|
||||
}));
|
||||
});
|
||||
|
||||
const init = createAction(INIT, (payload = {}) => {
|
||||
const init = (payload = {}) => {
|
||||
const action = {
|
||||
type: INIT,
|
||||
};
|
||||
@@ -29,9 +17,9 @@ const init = createAction(INIT, (payload = {}) => {
|
||||
action.params = payload.params;
|
||||
}
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
const navigate = createAction(NAVIGATE, payload => {
|
||||
const navigate = payload => {
|
||||
const action = {
|
||||
type: NAVIGATE,
|
||||
routeName: payload.routeName,
|
||||
@@ -46,91 +34,24 @@ const navigate = createAction(NAVIGATE, payload => {
|
||||
action.key = payload.key;
|
||||
}
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
const pop = createAction(POP, payload => ({
|
||||
type: POP,
|
||||
n: payload && payload.n,
|
||||
immediate: payload && payload.immediate,
|
||||
}));
|
||||
|
||||
const popToTop = createAction(POP_TO_TOP, payload => ({
|
||||
type: POP_TO_TOP,
|
||||
immediate: payload && payload.immediate,
|
||||
key: payload && payload.key,
|
||||
}));
|
||||
|
||||
const push = createAction(PUSH, payload => {
|
||||
const action = {
|
||||
type: PUSH,
|
||||
routeName: payload.routeName,
|
||||
};
|
||||
if (payload.params) {
|
||||
action.params = payload.params;
|
||||
}
|
||||
if (payload.action) {
|
||||
action.action = payload.action;
|
||||
}
|
||||
return action;
|
||||
});
|
||||
|
||||
const reset = createAction(RESET, payload => ({
|
||||
type: RESET,
|
||||
index: payload.index,
|
||||
key: payload.key,
|
||||
actions: payload.actions,
|
||||
}));
|
||||
|
||||
const replace = createAction(REPLACE, payload => ({
|
||||
type: REPLACE,
|
||||
key: payload.key,
|
||||
newKey: payload.newKey,
|
||||
params: payload.params,
|
||||
action: payload.action,
|
||||
routeName: payload.routeName,
|
||||
immediate: payload.immediate,
|
||||
}));
|
||||
|
||||
const setParams = createAction(SET_PARAMS, payload => ({
|
||||
const setParams = payload => ({
|
||||
type: SET_PARAMS,
|
||||
key: payload.key,
|
||||
params: payload.params,
|
||||
}));
|
||||
|
||||
const uri = createAction(URI, payload => ({
|
||||
type: URI,
|
||||
uri: payload.uri,
|
||||
}));
|
||||
|
||||
const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
|
||||
type: COMPLETE_TRANSITION,
|
||||
key: payload && payload.key,
|
||||
}));
|
||||
});
|
||||
|
||||
export default {
|
||||
// Action constants
|
||||
BACK,
|
||||
INIT,
|
||||
NAVIGATE,
|
||||
POP,
|
||||
POP_TO_TOP,
|
||||
PUSH,
|
||||
RESET,
|
||||
REPLACE,
|
||||
SET_PARAMS,
|
||||
URI,
|
||||
COMPLETE_TRANSITION,
|
||||
|
||||
// Action creators
|
||||
back,
|
||||
init,
|
||||
navigate,
|
||||
pop,
|
||||
popToTop,
|
||||
push,
|
||||
reset,
|
||||
replace,
|
||||
setParams,
|
||||
uri,
|
||||
completeTransition,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ const StateUtils = {
|
||||
* routes of the navigation state, or -1 if it is not present.
|
||||
*/
|
||||
indexOf(state, key) {
|
||||
return state.routes.map(route => route.key).indexOf(key);
|
||||
return state.routes.findIndex(route => route.key === key);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -116,8 +116,23 @@ const StateUtils = {
|
||||
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the positon to where the new route in the
|
||||
* stack is at.
|
||||
* Note that this moves the index to the position to where the new route in the
|
||||
* stack is at and updates the routes array accordingly.
|
||||
*/
|
||||
replaceAndPrune(state, key, route) {
|
||||
const index = StateUtils.indexOf(state, key);
|
||||
const replaced = StateUtils.replaceAtIndex(state, index, route);
|
||||
|
||||
return {
|
||||
...replaced,
|
||||
routes: replaced.routes.slice(0, index + 1),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the position to where the new route in the
|
||||
* stack is at. Does not prune the routes.
|
||||
*/
|
||||
replaceAt(state, key, route) {
|
||||
const index = StateUtils.indexOf(state, key);
|
||||
@@ -137,7 +152,7 @@ const StateUtils = {
|
||||
route.key
|
||||
);
|
||||
|
||||
if (state.routes[index] === route) {
|
||||
if (state.routes[index] === route && index === state.index) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -153,7 +168,7 @@ const StateUtils = {
|
||||
|
||||
/**
|
||||
* Resets all routes.
|
||||
* Note that this moves the index to the positon to where the last route in the
|
||||
* Note that this moves the index to the position to where the last route in the
|
||||
* stack is at if the param `index` isn't provided.
|
||||
*/
|
||||
reset(state, routes, index) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
describe('actions', () => {
|
||||
describe('generic navigation actions', () => {
|
||||
const params = { foo: 'bar' };
|
||||
const navigateAction = NavigationActions.navigate({ routeName: 'another' });
|
||||
|
||||
it('exports back action and type', () => {
|
||||
expect(NavigationActions.back.toString()).toEqual(NavigationActions.BACK);
|
||||
expect(NavigationActions.back()).toEqual({ type: NavigationActions.BACK });
|
||||
expect(NavigationActions.back({ key: 'test' })).toEqual({
|
||||
type: NavigationActions.BACK,
|
||||
@@ -14,7 +13,6 @@ describe('actions', () => {
|
||||
});
|
||||
|
||||
it('exports init action and type', () => {
|
||||
expect(NavigationActions.init.toString()).toEqual(NavigationActions.INIT);
|
||||
expect(NavigationActions.init()).toEqual({ type: NavigationActions.INIT });
|
||||
expect(NavigationActions.init({ params })).toEqual({
|
||||
type: NavigationActions.INIT,
|
||||
@@ -23,9 +21,6 @@ describe('actions', () => {
|
||||
});
|
||||
|
||||
it('exports navigate action and type', () => {
|
||||
expect(NavigationActions.navigate.toString()).toEqual(
|
||||
NavigationActions.NAVIGATE
|
||||
);
|
||||
expect(NavigationActions.navigate({ routeName: 'test' })).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'test',
|
||||
@@ -47,36 +42,7 @@ describe('actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('exports reset action and type', () => {
|
||||
expect(NavigationActions.reset.toString()).toEqual(NavigationActions.RESET);
|
||||
expect(NavigationActions.reset({ index: 0, actions: [] })).toEqual({
|
||||
type: NavigationActions.RESET,
|
||||
index: 0,
|
||||
actions: [],
|
||||
});
|
||||
expect(
|
||||
NavigationActions.reset({
|
||||
index: 0,
|
||||
key: 'test',
|
||||
actions: [navigateAction],
|
||||
})
|
||||
).toEqual({
|
||||
type: NavigationActions.RESET,
|
||||
index: 0,
|
||||
key: 'test',
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'another',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('exports setParams action and type', () => {
|
||||
expect(NavigationActions.setParams.toString()).toEqual(
|
||||
NavigationActions.SET_PARAMS
|
||||
);
|
||||
expect(
|
||||
NavigationActions.setParams({
|
||||
key: 'test',
|
||||
@@ -88,12 +54,4 @@ describe('actions', () => {
|
||||
params,
|
||||
});
|
||||
});
|
||||
|
||||
it('exports uri action and type', () => {
|
||||
expect(NavigationActions.uri.toString()).toEqual(NavigationActions.URI);
|
||||
expect(NavigationActions.uri({ uri: 'http://google.com' })).toEqual({
|
||||
type: NavigationActions.URI,
|
||||
uri: 'http://google.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import React from 'react';
|
||||
import 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackNavigator from '../navigators/StackNavigator';
|
||||
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const BazScreen = () => <div />;
|
||||
const CarScreen = () => <div />;
|
||||
const DogScreen = () => <div />;
|
||||
const ElkScreen = () => <div />;
|
||||
const NavigationContainer = StackNavigator(
|
||||
{
|
||||
foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
baz: {
|
||||
screen: BazScreen,
|
||||
},
|
||||
car: {
|
||||
screen: CarScreen,
|
||||
},
|
||||
dog: {
|
||||
screen: DogScreen,
|
||||
},
|
||||
elk: {
|
||||
screen: ElkScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'foo',
|
||||
}
|
||||
);
|
||||
|
||||
jest.useFakeTimers();
|
||||
import createStackNavigator from '../navigators/createStackNavigator';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../createNavigationContainer';
|
||||
|
||||
describe('NavigationContainer', () => {
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const BazScreen = () => <div />;
|
||||
const CarScreen = () => <div />;
|
||||
const DogScreen = () => <div />;
|
||||
const ElkScreen = () => <div />;
|
||||
const NavigationContainer = createStackNavigator(
|
||||
{
|
||||
foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
baz: {
|
||||
screen: BazScreen,
|
||||
},
|
||||
car: {
|
||||
screen: CarScreen,
|
||||
},
|
||||
dog: {
|
||||
screen: DogScreen,
|
||||
},
|
||||
elk: {
|
||||
screen: ElkScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'foo',
|
||||
}
|
||||
);
|
||||
|
||||
describe('state.nav', () => {
|
||||
it("should be preloaded with the router's initial state", () => {
|
||||
const navigationContainer = renderer
|
||||
@@ -198,4 +202,57 @@ describe('NavigationContainer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('warnings', () => {
|
||||
function spyConsole() {
|
||||
let spy = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
spy.console.mockRestore();
|
||||
});
|
||||
|
||||
return spy;
|
||||
}
|
||||
|
||||
describe('detached navigators', () => {
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
let spy = spyConsole();
|
||||
|
||||
it('warns when you render more than one navigator explicitly', () => {
|
||||
class BlankScreen extends React.Component {
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
class RootScreen extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<ChildNavigator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ChildNavigator = createStackNavigator({
|
||||
Child: BlankScreen,
|
||||
});
|
||||
|
||||
const RootStack = createStackNavigator({
|
||||
Root: RootScreen,
|
||||
});
|
||||
|
||||
renderer.create(<RootStack />).toJSON();
|
||||
expect(spy).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('StateUtils', () => {
|
||||
).toEqual(newState);
|
||||
});
|
||||
|
||||
it('Returns the state if index matches the route', () => {
|
||||
it('Returns the state with updated index if route is unchanged but index changes', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
@@ -210,7 +210,7 @@ describe('StateUtils', () => {
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
||||
).toEqual(state);
|
||||
).toEqual({ ...state, index: 1 });
|
||||
});
|
||||
|
||||
// Reset
|
||||
|
||||
13
src/__tests__/__snapshots__/NavigationContainer-test.js.snap
Normal file
13
src/__tests__/__snapshots__/NavigationContainer-test.js.snap
Normal file
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NavigationContainer warnings detached navigators warns when you render more than one navigator explicitly 1`] = `
|
||||
Object {
|
||||
"console": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://v2.reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -1,118 +0,0 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
|
||||
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
|
||||
remove: () => {},
|
||||
});
|
||||
|
||||
describe('addNavigationHelpers', () => {
|
||||
it('handles Back action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'A', routeName: 'Home' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).goBack('A')
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.BACK,
|
||||
key: 'A',
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles Back action when the key is not defined', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { routeName: 'Home' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).goBack()
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({ type: NavigationActions.BACK });
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles Navigate action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { routeName: 'Home' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).navigate('Profile', { name: 'Matt' })
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
params: { name: 'Matt' },
|
||||
routeName: 'Profile',
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles SetParams action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).setParams({ notificationsEnabled: 'yes' })
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
key: 'B',
|
||||
params: { notificationsEnabled: 'yes' },
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles GetParams action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings', params: { name: 'Peter' } },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).getParam('name', 'Brent')
|
||||
).toEqual('Peter');
|
||||
});
|
||||
|
||||
it('handles GetParams action with default param', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).getParam('name', 'Brent')
|
||||
).toEqual('Brent');
|
||||
});
|
||||
|
||||
it('handles GetParams action with param value as null', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings', params: { name: null } },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).getParam('name')
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -11,10 +11,8 @@ test('child action events only flow when focused', () => {
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const testState = {
|
||||
key: 'foo',
|
||||
routeName: 'FooRoute',
|
||||
@@ -66,11 +64,9 @@ test('grandchildren subscription', () => {
|
||||
const parentSubscriber = getChildEventSubscriber(
|
||||
grandParentSubscriber,
|
||||
'parent'
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
).addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const parentBlurState = {
|
||||
key: 'foo',
|
||||
routeName: 'FooRoute',
|
||||
@@ -135,11 +131,9 @@ test('grandchildren transitions', () => {
|
||||
const parentSubscriber = getChildEventSubscriber(
|
||||
grandParentSubscriber,
|
||||
'parent'
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
).addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const makeFakeState = (childIndex, childIsTransitioning) => ({
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
@@ -230,11 +224,9 @@ test('grandchildren pass through transitions', () => {
|
||||
const parentSubscriber = getChildEventSubscriber(
|
||||
grandParentSubscriber,
|
||||
'parent'
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
).addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const makeFakeState = (childIndex, childIsTransitioning) => ({
|
||||
index: childIndex,
|
||||
isTransitioning: childIsTransitioning,
|
||||
@@ -322,10 +314,8 @@ test('child focus with transition', () => {
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const randomAction = { type: 'FooAction' };
|
||||
const testState = {
|
||||
key: 'foo',
|
||||
@@ -417,10 +407,8 @@ test('child focus with immediate transition', () => {
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const randomAction = { type: 'FooAction' };
|
||||
const testState = {
|
||||
key: 'foo',
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/* Helpers for navigation */
|
||||
|
||||
import NavigationActions from './NavigationActions';
|
||||
import invariant from './utils/invariant';
|
||||
|
||||
export default function(navigation) {
|
||||
return {
|
||||
...navigation,
|
||||
goBack: key => {
|
||||
let actualizedKey = key;
|
||||
if (key === undefined && navigation.state.key) {
|
||||
invariant(
|
||||
typeof navigation.state.key === 'string',
|
||||
'key should be a string'
|
||||
);
|
||||
actualizedKey = navigation.state.key;
|
||||
}
|
||||
return navigation.dispatch(
|
||||
NavigationActions.back({ key: actualizedKey })
|
||||
);
|
||||
},
|
||||
navigate: (navigateTo, params, action) => {
|
||||
if (typeof navigateTo === 'string') {
|
||||
return navigation.dispatch(
|
||||
NavigationActions.navigate({ routeName: navigateTo, params, action })
|
||||
);
|
||||
}
|
||||
invariant(
|
||||
typeof navigateTo === 'object',
|
||||
'Must navigateTo an object or a string'
|
||||
);
|
||||
invariant(
|
||||
params == null,
|
||||
'Params must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
action == null,
|
||||
'Child action must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
return navigation.dispatch(NavigationActions.navigate(navigateTo));
|
||||
},
|
||||
pop: (n, params) =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.pop({ n, immediate: params && params.immediate })
|
||||
),
|
||||
popToTop: params =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.popToTop({ immediate: params && params.immediate })
|
||||
),
|
||||
/**
|
||||
* For updating current route params. For example the nav bar title and
|
||||
* buttons are based on the route params.
|
||||
* This means `setParams` can be used to update nav bar for example.
|
||||
*/
|
||||
setParams: params => {
|
||||
invariant(
|
||||
navigation.state.key && typeof navigation.state.key === 'string',
|
||||
'setParams cannot be called by root navigator'
|
||||
);
|
||||
const key = navigation.state.key;
|
||||
return navigation.dispatch(NavigationActions.setParams({ params, key }));
|
||||
},
|
||||
|
||||
getParam: (paramName, defaultValue) => {
|
||||
const params = navigation.state.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
push: (routeName, params, action) =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.push({ routeName, params, action })
|
||||
),
|
||||
|
||||
replace: (routeName, params, action) =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.replace({
|
||||
routeName,
|
||||
params,
|
||||
action,
|
||||
key: navigation.state.key,
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Linking } from 'react-native';
|
||||
import { AsyncStorage, Linking, Platform } from 'react-native';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import { BackHandler } from './PlatformHelpers';
|
||||
import NavigationActions from './NavigationActions';
|
||||
import addNavigationHelpers from './addNavigationHelpers';
|
||||
import invariant from './utils/invariant';
|
||||
import getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import docsUrl from './utils/docsUrl';
|
||||
|
||||
function isStateful(props) {
|
||||
return !props.navigation;
|
||||
}
|
||||
|
||||
function validateProps(props) {
|
||||
if (isStateful(props)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { navigation, screenProps, ...containerProps } = props;
|
||||
|
||||
const keys = Object.keys(containerProps);
|
||||
|
||||
if (keys.length !== 0) {
|
||||
throw new Error(
|
||||
'This navigator has both navigation and container props, so it is ' +
|
||||
`unclear if it should own its own state. Remove props: "${keys.join(
|
||||
', '
|
||||
)}" ` +
|
||||
'if the navigator should get its state from the navigation prop. If the ' +
|
||||
'navigator should maintain its own state, do not pass a navigation prop.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Track the number of stateful container instances. Warn if >0 and not using the
|
||||
// detached prop to explicitly acknowledge the behavior. We should deprecated implicit
|
||||
// stateful navigation containers in a future release and require a provider style pattern
|
||||
// instead in order to eliminate confusion entirely.
|
||||
let _statefulContainerCount = 0;
|
||||
export function _TESTING_ONLY_reset_container_count() {
|
||||
_statefulContainerCount = 0;
|
||||
}
|
||||
|
||||
// We keep a global flag to catch errors during the state persistence hydrating scenario.
|
||||
// The innermost navigator who catches the error will dispatch a new init action.
|
||||
let _reactNavigationIsHydratingState = false;
|
||||
// Unfortunate to use global state here, but it seems necessesary for the time
|
||||
// being. There seems to be some problems with cascading componentDidCatch
|
||||
// handlers. Ideally the inner non-stateful navigator catches the error and
|
||||
// re-throws it, to be caught by the top-level stateful navigator.
|
||||
|
||||
/**
|
||||
* Create an HOC that injects the navigation and manages the navigation state
|
||||
@@ -18,12 +63,17 @@ export default function createNavigationContainer(Component) {
|
||||
static router = Component.router;
|
||||
static navigationOptions = null;
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
validateProps(nextProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
_actionEventSubscribers = new Set();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._validateProps(props);
|
||||
validateProps(props);
|
||||
|
||||
this._initialAction = NavigationActions.init();
|
||||
|
||||
@@ -41,14 +91,21 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
nav: this._isStateful()
|
||||
? Component.router.getStateForAction(this._initialAction)
|
||||
: null,
|
||||
nav:
|
||||
this._isStateful() && !props.persistenceKey
|
||||
? Component.router.getStateForAction(this._initialAction)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
_renderLoading() {
|
||||
return this.props.renderLoadingExperimental
|
||||
? this.props.renderLoadingExperimental()
|
||||
: null;
|
||||
}
|
||||
|
||||
_isStateful() {
|
||||
return !this.props.navigation;
|
||||
return isStateful(this.props);
|
||||
}
|
||||
|
||||
_validateProps(props) {
|
||||
@@ -127,74 +184,177 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._validateProps(nextProps);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Clear cached _nav every tick
|
||||
if (this._nav === this.state.nav) {
|
||||
this._nav = null;
|
||||
// Clear cached _navState every tick
|
||||
if (this._navState === this.state.nav) {
|
||||
this._navState = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
this._isMounted = true;
|
||||
if (!this._isStateful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__ && !this.props.detached) {
|
||||
if (_statefulContainerCount > 0) {
|
||||
// Temporarily only show this on iOS due to this issue:
|
||||
// https://github.com/react-navigation/react-navigation/issues/4196#issuecomment-390827829
|
||||
if (Platform.OS === 'ios') {
|
||||
console.warn(
|
||||
`You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl(
|
||||
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_statefulContainerCount++;
|
||||
Linking.addEventListener('url', this._handleOpenURL);
|
||||
|
||||
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
|
||||
// Pull out anything that can impact state
|
||||
const { persistenceKey } = this.props;
|
||||
const startupStateJSON =
|
||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
||||
const url = await Linking.getInitialURL();
|
||||
const parsedUrl = url && this._urlToPathAndParams(url);
|
||||
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action: this._initialAction,
|
||||
state: this.state.nav,
|
||||
lastState: null,
|
||||
})
|
||||
);
|
||||
// Initialize state. This must be done *after* any async code
|
||||
// so we don't end up with a different value for this.state.nav
|
||||
// due to changes while async function was resolving
|
||||
let action = this._initialAction;
|
||||
let startupState = this.state.nav;
|
||||
if (!startupState) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Init new Navigation State');
|
||||
startupState = Component.router.getStateForAction(action);
|
||||
}
|
||||
|
||||
// Pull persisted state from AsyncStorage
|
||||
if (startupStateJSON) {
|
||||
try {
|
||||
startupState = JSON.parse(startupStateJSON);
|
||||
_reactNavigationIsHydratingState = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Pull state out of URL
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const urlAction = Component.router.getActionForPathAndParams(
|
||||
path,
|
||||
params
|
||||
);
|
||||
if (urlAction) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Applying Navigation Action for Initial URL:', url);
|
||||
action = urlAction;
|
||||
startupState = Component.router.getStateForAction(
|
||||
urlAction,
|
||||
startupState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dispatchActions = () =>
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action,
|
||||
state: this.state.nav,
|
||||
lastState: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (startupState === this.state.nav) {
|
||||
dispatchActions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ nav: startupState }, () => {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
dispatchActions();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidCatch(e, errorInfo) {
|
||||
if (_reactNavigationIsHydratingState) {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
console.warn(
|
||||
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
|
||||
);
|
||||
this.dispatch(NavigationActions.init());
|
||||
} else {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
_persistNavigationState = async nav => {
|
||||
const { persistenceKey } = this.props;
|
||||
if (!persistenceKey) {
|
||||
return;
|
||||
}
|
||||
await AsyncStorage.setItem(persistenceKey, JSON.stringify(nav));
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
Linking.removeEventListener('url', this._handleOpenURL);
|
||||
this.subs && this.subs.remove();
|
||||
|
||||
if (this._isStateful()) {
|
||||
_statefulContainerCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-tick temporary storage for state.nav
|
||||
|
||||
dispatch = action => {
|
||||
if (!this._isStateful()) {
|
||||
return false;
|
||||
if (this.props.navigation) {
|
||||
return this.props.navigation.dispatch(action);
|
||||
}
|
||||
this._nav = this._nav || this.state.nav;
|
||||
const oldNav = this._nav;
|
||||
invariant(oldNav, 'should be set in constructor if stateful');
|
||||
const nav = Component.router.getStateForAction(action, oldNav);
|
||||
|
||||
// navState will have the most up-to-date value, because setState sometimes behaves asyncronously
|
||||
this._navState = this._navState || this.state.nav;
|
||||
const lastNavState = this._navState;
|
||||
invariant(lastNavState, 'should be set in constructor if stateful');
|
||||
const reducedState = Component.router.getStateForAction(
|
||||
action,
|
||||
lastNavState
|
||||
);
|
||||
const navState = reducedState === null ? lastNavState : reducedState;
|
||||
|
||||
const dispatchActionEvents = () => {
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action,
|
||||
state: nav,
|
||||
lastState: oldNav,
|
||||
state: navState,
|
||||
lastState: lastNavState,
|
||||
})
|
||||
);
|
||||
};
|
||||
if (nav && nav !== oldNav) {
|
||||
|
||||
if (reducedState === null) {
|
||||
// The router will return null when action has been handled and the state hasn't changed.
|
||||
// dispatch returns true when something has been handled.
|
||||
dispatchActionEvents();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (navState !== lastNavState) {
|
||||
// Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
|
||||
this._nav = nav;
|
||||
this.setState({ nav }, () => {
|
||||
this._onNavigationStateChange(oldNav, nav, action);
|
||||
this._navState = navState;
|
||||
this.setState({ nav: navState }, () => {
|
||||
this._onNavigationStateChange(lastNavState, navState, action);
|
||||
dispatchActionEvents();
|
||||
this._persistNavigationState(navState);
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
dispatchActionEvents();
|
||||
}
|
||||
|
||||
dispatchActionEvents();
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -202,9 +362,11 @@ export default function createNavigationContainer(Component) {
|
||||
let navigation = this.props.navigation;
|
||||
if (this._isStateful()) {
|
||||
const nav = this.state.nav;
|
||||
invariant(nav, 'should be set in constructor if stateful');
|
||||
if (!nav) {
|
||||
return this._renderLoading();
|
||||
}
|
||||
if (!this._navigation || this._navigation.state !== nav) {
|
||||
this._navigation = addNavigationHelpers({
|
||||
this._navigation = {
|
||||
dispatch: this.dispatch,
|
||||
state: nav,
|
||||
addListener: (eventName, handler) => {
|
||||
@@ -218,6 +380,11 @@ export default function createNavigationContainer(Component) {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const actionCreators = getNavigationActionCreators(nav);
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
this._navigation[actionName] = (...args) =>
|
||||
this.dispatch(actionCreators[actionName](...args));
|
||||
});
|
||||
}
|
||||
navigation = this._navigation;
|
||||
@@ -227,5 +394,5 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
}
|
||||
|
||||
return NavigationContainer;
|
||||
return polyfill(NavigationContainer);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
const willBlurSubscribers = new Set();
|
||||
const didBlurSubscribers = new Set();
|
||||
|
||||
const removeAll = () => {
|
||||
[
|
||||
actionSubscribers,
|
||||
willFocusSubscribers,
|
||||
didFocusSubscribers,
|
||||
willBlurSubscribers,
|
||||
didBlurSubscribers,
|
||||
].forEach(set => set.clear());
|
||||
|
||||
upstreamSubscribers.forEach(subs => subs && subs.remove());
|
||||
};
|
||||
|
||||
const getChildSubscribers = evtName => {
|
||||
switch (evtName) {
|
||||
case 'action':
|
||||
@@ -43,10 +55,6 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
// considered blurred
|
||||
let lastEmittedEvent = 'didBlur';
|
||||
|
||||
const cleanup = () => {
|
||||
upstreamSubscribers.forEach(subs => subs && subs.remove());
|
||||
};
|
||||
|
||||
const upstreamEvents = [
|
||||
'willFocus',
|
||||
'didFocus',
|
||||
@@ -130,18 +138,24 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
emit((lastEmittedEvent = 'didBlur'), childPayload);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEmittedEvent === 'didBlur' && !newRoute) {
|
||||
removeAll();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return (eventName, eventHandler) => {
|
||||
const subscribers = getChildSubscribers(eventName);
|
||||
if (!subscribers) {
|
||||
throw new Error(`Invalid event name "${eventName}"`);
|
||||
}
|
||||
subscribers.add(eventHandler);
|
||||
const remove = () => {
|
||||
subscribers.delete(eventHandler);
|
||||
};
|
||||
return { remove };
|
||||
return {
|
||||
addListener(eventName, eventHandler) {
|
||||
const subscribers = getChildSubscribers(eventName);
|
||||
if (!subscribers) {
|
||||
throw new Error(`Invalid event name "${eventName}"`);
|
||||
}
|
||||
subscribers.add(eventHandler);
|
||||
const remove = () => {
|
||||
subscribers.delete(eventHandler);
|
||||
};
|
||||
return { remove };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from './createNavigator';
|
||||
import CardStackTransitioner from '../views/CardStack/CardStackTransitioner';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
// A stack navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
|
||||
export default (routeConfigMap, stackConfig = {}) => {
|
||||
const {
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
headerMode,
|
||||
headerTransitionPreset,
|
||||
mode,
|
||||
cardStyle,
|
||||
transitionConfig,
|
||||
onTransitionStart,
|
||||
onTransitionEnd,
|
||||
navigationOptions,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
|
||||
// Create a navigator with CardStackTransitioner as the view
|
||||
const navigator = createNavigator(router, routeConfigMap, stackConfig)(
|
||||
props => (
|
||||
<CardStackTransitioner
|
||||
{...props}
|
||||
headerMode={headerMode}
|
||||
headerTransitionPreset={headerTransitionPreset}
|
||||
mode={mode}
|
||||
cardStyle={cardStyle}
|
||||
transitionConfig={transitionConfig}
|
||||
onTransitionStart={onTransitionStart}
|
||||
onTransitionEnd={(lastTransition, transition) => {
|
||||
const { state, dispatch } = props.navigation;
|
||||
dispatch(NavigationActions.completeTransition({ key: state.key }));
|
||||
onTransitionEnd && onTransitionEnd();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import createNavigator from './createNavigator';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import TabRouter from '../routers/TabRouter';
|
||||
import TabView from '../views/TabView/TabView';
|
||||
import TabBarTop from '../views/TabView/TabBarTop';
|
||||
import TabBarBottom from '../views/TabView/TabBarBottom';
|
||||
|
||||
// A tab navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
|
||||
const TabNavigator = (routeConfigs, config = {}) => {
|
||||
// Use the look native to the platform by default
|
||||
const mergedConfig = { ...TabNavigator.Presets.Default, ...config };
|
||||
const {
|
||||
tabBarComponent,
|
||||
tabBarPosition,
|
||||
tabBarOptions,
|
||||
lazy,
|
||||
removeClippedSubviews,
|
||||
swipeEnabled,
|
||||
animationEnabled,
|
||||
configureTransition,
|
||||
initialLayout,
|
||||
...tabsConfig
|
||||
} = mergedConfig;
|
||||
|
||||
const router = TabRouter(routeConfigs, tabsConfig);
|
||||
|
||||
const navigator = createNavigator(router, routeConfigs, config)(props => (
|
||||
<TabView
|
||||
{...props}
|
||||
lazy={lazy}
|
||||
removeClippedSubviews={removeClippedSubviews}
|
||||
tabBarComponent={tabBarComponent}
|
||||
tabBarPosition={tabBarPosition}
|
||||
tabBarOptions={tabBarOptions}
|
||||
swipeEnabled={swipeEnabled}
|
||||
animationEnabled={animationEnabled}
|
||||
configureTransition={configureTransition}
|
||||
initialLayout={initialLayout}
|
||||
/>
|
||||
));
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
const Presets = {
|
||||
iOSBottomTabs: {
|
||||
tabBarComponent: TabBarBottom,
|
||||
tabBarPosition: 'bottom',
|
||||
swipeEnabled: false,
|
||||
animationEnabled: false,
|
||||
initialLayout: undefined,
|
||||
},
|
||||
AndroidTopTabs: {
|
||||
tabBarComponent: TabBarTop,
|
||||
tabBarPosition: 'top',
|
||||
swipeEnabled: true,
|
||||
animationEnabled: true,
|
||||
initialLayout: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Use these to get Android-style top tabs even on iOS or vice versa.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const HomeScreenTabNavigator = TabNavigator({
|
||||
* Chat: {
|
||||
* screen: ChatScreen,
|
||||
* },
|
||||
* ...
|
||||
* }, {
|
||||
* ...TabNavigator.Presets.AndroidTopTabs,
|
||||
* tabBarOptions: {
|
||||
* ...
|
||||
* },
|
||||
* });
|
||||
*```
|
||||
*/
|
||||
TabNavigator.Presets = {
|
||||
iOSBottomTabs: Presets.iOSBottomTabs,
|
||||
AndroidTopTabs: Presets.AndroidTopTabs,
|
||||
Default:
|
||||
Platform.OS === 'ios' ? Presets.iOSBottomTabs : Presets.AndroidTopTabs,
|
||||
};
|
||||
|
||||
export default TabNavigator;
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import DrawerNavigator from '../DrawerNavigator';
|
||||
import DrawerNavigator from '../createDrawerNavigator';
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
|
||||
@@ -2,7 +2,9 @@ import React, { Component } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import StackNavigator from '../StackNavigator';
|
||||
import StackNavigator from '../createStackNavigator';
|
||||
import withNavigation from '../../views/withNavigation';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
@@ -31,6 +33,10 @@ const routeConfig = {
|
||||
};
|
||||
|
||||
describe('StackNavigator', () => {
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
it('renders successfully', () => {
|
||||
const MyStackNavigator = StackNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyStackNavigator />).toJSON();
|
||||
@@ -51,4 +57,37 @@ describe('StackNavigator', () => {
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('passes navigation to headerRight when wrapped in withNavigation', () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
class TestComponent extends React.Component {
|
||||
render() {
|
||||
return <View>{this.props.onPress(this.props.navigation)}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
const TestComponentWithNavigation = withNavigation(TestComponent);
|
||||
|
||||
class A extends React.Component {
|
||||
static navigationOptions = {
|
||||
headerRight: <TestComponentWithNavigation onPress={spy} />,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
const Nav = StackNavigator({ A: { screen: A } });
|
||||
|
||||
renderer.create(<Nav />);
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
navigate: expect.any(Function),
|
||||
addListener: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import SwitchNavigator from '../createSwitchNavigator';
|
||||
|
||||
const A = () => <View />;
|
||||
const B = () => <View />;
|
||||
const routeConfig = { A, B };
|
||||
|
||||
describe('SwitchNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MySwitchNavigator = SwitchNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MySwitchNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,9 @@ import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import TabNavigator from '../TabNavigator';
|
||||
const {
|
||||
createTabNavigator,
|
||||
} = require('react-navigation-deprecated-tab-navigator');
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
@@ -25,7 +27,7 @@ const routeConfig = {
|
||||
|
||||
describe('TabNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MyTabNavigator = TabNavigator(routeConfig);
|
||||
const MyTabNavigator = createTabNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyTabNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
|
||||
@@ -224,6 +224,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
|
||||
"color": "#2196f3",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
@@ -48,9 +48,10 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#EFEFF4",
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
@@ -76,36 +77,31 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -113,70 +109,89 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -233,9 +248,10 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#EFEFF4",
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
@@ -261,36 +277,31 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -298,52 +309,71 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
<View
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwitchNavigator renders successfully 1`] = `<View />`;
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
exports[`TabNavigator renders successfully 1`] = `
|
||||
<View
|
||||
loaded={
|
||||
Array [
|
||||
0,
|
||||
]
|
||||
}
|
||||
onLayout={[Function]}
|
||||
collapsable={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
@@ -21,56 +16,65 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "stretch",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"alignItems": "stretch",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -137,9 +141,15 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"height": 29,
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -147,13 +157,13 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"minWidth": 30,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -162,13 +172,13 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"minWidth": 30,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -4,8 +4,7 @@ import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import createNavigator from './createNavigator';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import TabRouter from '../routers/TabRouter';
|
||||
import DrawerScreen from '../views/Drawer/DrawerScreen';
|
||||
import DrawerRouter from '../routers/DrawerRouter';
|
||||
import DrawerView from '../views/Drawer/DrawerView';
|
||||
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
|
||||
|
||||
@@ -38,9 +37,6 @@ const DefaultDrawerConfig = {
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
|
||||
},
|
||||
contentComponent: defaultContentComponent,
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
drawerPosition: 'left',
|
||||
drawerBackgroundColor: 'white',
|
||||
useNativeAnimations: true,
|
||||
@@ -48,58 +44,27 @@ const DefaultDrawerConfig = {
|
||||
|
||||
const DrawerNavigator = (routeConfigs, config = {}) => {
|
||||
const mergedConfig = { ...DefaultDrawerConfig, ...config };
|
||||
|
||||
const {
|
||||
containerConfig,
|
||||
drawerWidth,
|
||||
drawerLockMode,
|
||||
contentComponent,
|
||||
contentOptions,
|
||||
drawerPosition,
|
||||
useNativeAnimations,
|
||||
drawerBackgroundColor,
|
||||
drawerOpenRoute,
|
||||
drawerCloseRoute,
|
||||
drawerToggleRoute,
|
||||
...tabsConfig
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
getCustomActionCreators,
|
||||
...drawerConfig
|
||||
} = mergedConfig;
|
||||
|
||||
const contentRouter = TabRouter(routeConfigs, tabsConfig);
|
||||
const drawerRouter = TabRouter(
|
||||
{
|
||||
[drawerCloseRoute]: {
|
||||
screen: createNavigator(contentRouter, routeConfigs, config)(props => (
|
||||
<DrawerScreen {...props} />
|
||||
)),
|
||||
},
|
||||
[drawerOpenRoute]: {
|
||||
screen: () => null,
|
||||
},
|
||||
[drawerToggleRoute]: {
|
||||
screen: () => null,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: drawerCloseRoute,
|
||||
}
|
||||
);
|
||||
const routerConfig = {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
getCustomActionCreators,
|
||||
};
|
||||
|
||||
const navigator = createNavigator(drawerRouter, routeConfigs, config)(
|
||||
props => (
|
||||
<DrawerView
|
||||
{...props}
|
||||
drawerBackgroundColor={drawerBackgroundColor}
|
||||
drawerLockMode={drawerLockMode}
|
||||
useNativeAnimations={useNativeAnimations}
|
||||
drawerWidth={drawerWidth}
|
||||
contentComponent={contentComponent}
|
||||
contentOptions={contentOptions}
|
||||
drawerPosition={drawerPosition}
|
||||
drawerOpenRoute={drawerOpenRoute}
|
||||
drawerCloseRoute={drawerCloseRoute}
|
||||
drawerToggleRoute={drawerToggleRoute}
|
||||
/>
|
||||
)
|
||||
);
|
||||
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
|
||||
|
||||
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
55
src/navigators/createKeyboardAwareNavigator.js
Normal file
55
src/navigators/createKeyboardAwareNavigator.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
export default Navigator =>
|
||||
class KeyboardAwareNavigator extends React.Component {
|
||||
static router = Navigator.router;
|
||||
_previouslyFocusedTextInput = null;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Navigator
|
||||
{...this.props}
|
||||
onGestureBegin={this._handleGestureBegin}
|
||||
onGestureCanceled={this._handleGestureCanceled}
|
||||
onGestureFinish={this._handleGestureFinish}
|
||||
onTransitionStart={this._handleTransitionStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_handleGestureBegin = () => {
|
||||
this._previouslyFocusedTextInput = TextInput.State.currentlyFocusedField();
|
||||
if (this._previouslyFocusedTextInput) {
|
||||
TextInput.State.blurTextInput(this._previouslyFocusedTextInput);
|
||||
}
|
||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
||||
};
|
||||
|
||||
_handleGestureCanceled = () => {
|
||||
if (this._previouslyFocusedTextInput) {
|
||||
TextInput.State.focusTextInput(this._previouslyFocusedTextInput);
|
||||
}
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
};
|
||||
|
||||
_handleGestureFinish = () => {
|
||||
this._previouslyFocusedTextInput = null;
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
};
|
||||
|
||||
_handleTransitionStart = (transitionProps, prevTransitionProps) => {
|
||||
// TODO: We should not even have received the transition start event
|
||||
// in the case where the index did not change, I believe. We
|
||||
// should revisit this after 2.0 release.
|
||||
if (transitionProps.index !== prevTransitionProps.index) {
|
||||
const currentField = TextInput.State.currentlyFocusedField();
|
||||
if (currentField) {
|
||||
TextInput.State.blurTextInput(currentField);
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onTransitionStart &&
|
||||
this.props.onTransitionStart(transitionProps, prevTransitionProps);
|
||||
};
|
||||
};
|
||||
@@ -1,19 +1,137 @@
|
||||
import React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
/**
|
||||
* Creates a navigator based on a router and a view that renders the screens.
|
||||
*/
|
||||
export default function createNavigator(router, routeConfigs, navigatorConfig) {
|
||||
return NavigationView => {
|
||||
class Navigator extends React.Component {
|
||||
static router = router;
|
||||
static navigationOptions = null;
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
|
||||
render() {
|
||||
return <NavigationView {...this.props} router={router} />;
|
||||
function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
class Navigator extends React.Component {
|
||||
static router = router;
|
||||
static navigationOptions = null;
|
||||
|
||||
state = {
|
||||
descriptors: {},
|
||||
childEventSubscribers: {},
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const { navigation, screenProps } = nextProps;
|
||||
const { dispatch, state, addListener } = navigation;
|
||||
const { routes } = state;
|
||||
|
||||
const descriptors = { ...prevState.descriptors };
|
||||
const childEventSubscribers = { ...prevState.childEventSubscribers };
|
||||
routes.forEach(route => {
|
||||
if (!descriptors[route.key] || descriptors[route.key].state !== route) {
|
||||
const getComponent = () =>
|
||||
router.getComponentForRouteName(route.routeName);
|
||||
|
||||
if (!childEventSubscribers[route.key]) {
|
||||
childEventSubscribers[route.key] = getChildEventSubscriber(
|
||||
addListener,
|
||||
route.key
|
||||
);
|
||||
}
|
||||
|
||||
const actionCreators = {
|
||||
...navigation.actions,
|
||||
...router.getActionCreators(route, state.key),
|
||||
};
|
||||
const actionHelpers = {};
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
actionHelpers[actionName] = (...args) => {
|
||||
const actionCreator = actionCreators[actionName];
|
||||
const action = actionCreator(...args);
|
||||
dispatch(action);
|
||||
};
|
||||
});
|
||||
const childNavigation = {
|
||||
...actionHelpers,
|
||||
actions: actionCreators,
|
||||
dispatch,
|
||||
state: route,
|
||||
addListener: childEventSubscribers[route.key].addListener,
|
||||
getParam: (paramName, defaultValue) => {
|
||||
const params = route.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
};
|
||||
|
||||
const options = router.getScreenOptions(childNavigation, screenProps);
|
||||
descriptors[route.key] = {
|
||||
key: route.key,
|
||||
getComponent,
|
||||
options,
|
||||
state: route,
|
||||
navigation: childNavigation,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
descriptors,
|
||||
childEventSubscribers,
|
||||
};
|
||||
}
|
||||
|
||||
// Cleanup subscriptions for routes that no longer exist
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
|
||||
let childEventSubscribers = { ...this.state.childEventSubscribers };
|
||||
Object.keys(childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
if (
|
||||
childEventSubscribers.length !== this.state.childEventSubscribers.length
|
||||
) {
|
||||
this.setState({ childEventSubscribers });
|
||||
}
|
||||
}
|
||||
|
||||
return Navigator;
|
||||
};
|
||||
_isRouteFocused = route => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_dangerouslyGetParent = () => {
|
||||
return this.props.navigation;
|
||||
};
|
||||
|
||||
render() {
|
||||
// Mutation in render 😩
|
||||
// The problem:
|
||||
// - We don't want to re-render each screen every time the parent navigator changes
|
||||
// - But we need to be able to access the parent navigator from callbacks
|
||||
// - These functions should only be used within callbacks, but they are passed in props,
|
||||
// which is what makes this awkward. What's a good way to pass in stuff that we don't
|
||||
// want people to depend on in render?
|
||||
let descriptors = { ...this.state.descriptors };
|
||||
Object.values(descriptors).forEach(descriptor => {
|
||||
descriptor.navigation.isFocused = () =>
|
||||
this._isRouteFocused(descriptor.state);
|
||||
descriptor.navigation.dangerouslyGetParent = this._dangerouslyGetParent;
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigatorView
|
||||
{...this.props}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
navigationConfig={navigationConfig}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return polyfill(Navigator);
|
||||
}
|
||||
|
||||
export default createNavigator;
|
||||
|
||||
40
src/navigators/createStackNavigator.js
Normal file
40
src/navigators/createStackNavigator.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
|
||||
import createNavigator from './createNavigator';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
|
||||
function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
const {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
disableKeyboardHandling,
|
||||
getCustomActionCreators,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
getCustomActionCreators,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
|
||||
// Create a navigator with StackView as the view
|
||||
let Navigator = createNavigator(StackView, router, stackConfig);
|
||||
if (!disableKeyboardHandling) {
|
||||
Navigator = createKeyboardAwareNavigator(Navigator);
|
||||
}
|
||||
|
||||
// HOC to provide the navigation prop for the top-level navigator (when the prop is missing)
|
||||
return createNavigationContainer(Navigator);
|
||||
}
|
||||
|
||||
export default createStackNavigator;
|
||||
13
src/navigators/createSwitchNavigator.js
Normal file
13
src/navigators/createSwitchNavigator.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from '../navigators/createNavigator';
|
||||
import SwitchRouter from '../routers/SwitchRouter';
|
||||
import SwitchView from '../views/SwitchView/SwitchView';
|
||||
|
||||
function createSwitchNavigator(routeConfigMap, switchConfig = {}) {
|
||||
const router = SwitchRouter(routeConfigMap, switchConfig);
|
||||
const Navigator = createNavigator(SwitchView, router, switchConfig);
|
||||
return createNavigationContainer(Navigator);
|
||||
}
|
||||
|
||||
export default createSwitchNavigator;
|
||||
117
src/react-navigation.js
vendored
117
src/react-navigation.js
vendored
@@ -8,25 +8,71 @@ module.exports = {
|
||||
get StateUtils() {
|
||||
return require('./StateUtils').default;
|
||||
},
|
||||
get addNavigationHelpers() {
|
||||
return require('./addNavigationHelpers').default;
|
||||
},
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
|
||||
// Navigators
|
||||
get createNavigator() {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
get StackNavigator() {
|
||||
return require('./navigators/StackNavigator').default;
|
||||
get createStackNavigator() {
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
},
|
||||
get TabNavigator() {
|
||||
return require('./navigators/TabNavigator').default;
|
||||
get StackNavigator() {
|
||||
console.warn(
|
||||
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
|
||||
);
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
},
|
||||
get createSwitchNavigator() {
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
},
|
||||
get SwitchNavigator() {
|
||||
console.warn(
|
||||
'The SwitchNavigator function name is deprecated, please use createSwitchNavigator instead'
|
||||
);
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
},
|
||||
get createDrawerNavigator() {
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
},
|
||||
get DrawerNavigator() {
|
||||
return require('./navigators/DrawerNavigator').default;
|
||||
console.warn(
|
||||
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
|
||||
);
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
},
|
||||
get createTabNavigator() {
|
||||
console.warn(
|
||||
'createTabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator')
|
||||
.createTabNavigator;
|
||||
},
|
||||
get TabNavigator() {
|
||||
console.warn(
|
||||
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator')
|
||||
.createTabNavigator;
|
||||
},
|
||||
get createBottomTabNavigator() {
|
||||
return require('react-navigation-tabs').createBottomTabNavigator;
|
||||
},
|
||||
get createMaterialTopTabNavigator() {
|
||||
return require('react-navigation-tabs').createMaterialTopTabNavigator;
|
||||
},
|
||||
|
||||
// Actions
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
get StackActions() {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
},
|
||||
get getNavigationActionCreators() {
|
||||
return require('./routers/getNavigationActionCreators').default;
|
||||
},
|
||||
|
||||
// Routers
|
||||
@@ -36,23 +82,32 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get DrawerRouter() {
|
||||
return require('./routers/DrawerRouter').default;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
|
||||
// Views
|
||||
get Transitioner() {
|
||||
return require('./views/Transitioner').default;
|
||||
},
|
||||
get CardStackTransitioner() {
|
||||
return require('./views/CardStack/CardStackTransitioner').default;
|
||||
get StackView() {
|
||||
return require('./views/StackView/StackView').default;
|
||||
},
|
||||
get CardStack() {
|
||||
return require('./views/CardStack/CardStack').default;
|
||||
},
|
||||
get Card() {
|
||||
return require('./views/CardStack/Card').default;
|
||||
get StackViewCard() {
|
||||
return require('./views/StackView/StackViewCard').default;
|
||||
},
|
||||
get SafeAreaView() {
|
||||
return require('react-native-safe-area-view').default;
|
||||
},
|
||||
get SceneView() {
|
||||
return require('./views/SceneView').default;
|
||||
},
|
||||
get ResourceSavingSceneView() {
|
||||
return require('./views/ResourceSavingSceneView').default;
|
||||
},
|
||||
|
||||
// Header
|
||||
get Header() {
|
||||
@@ -72,16 +127,33 @@ module.exports = {
|
||||
get DrawerItems() {
|
||||
return require('./views/Drawer/DrawerNavigatorItems').default;
|
||||
},
|
||||
get DrawerSidebar() {
|
||||
return require('./views/Drawer/DrawerSidebar').default;
|
||||
},
|
||||
|
||||
// TabView
|
||||
get TabView() {
|
||||
return require('./views/TabView/TabView').default;
|
||||
console.warn(
|
||||
'TabView is deprecated. Please use the react-navigation-tabs package instead: https://github.com/react-navigation/react-navigation-tabs'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').TabView;
|
||||
},
|
||||
get TabBarTop() {
|
||||
return require('./views/TabView/TabBarTop').default;
|
||||
console.warn(
|
||||
'TabBarTop is deprecated. Please use the react-navigation-tabs package instead: https://github.com/react-navigation/react-navigation-tabs'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').TabBarTop;
|
||||
},
|
||||
get TabBarBottom() {
|
||||
return require('./views/TabView/TabBarBottom').default;
|
||||
console.warn(
|
||||
'TabBarBottom is deprecated. Please use the react-navigation-tabs package instead: https://github.com/react-navigation/react-navigation-tabs'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').TabBarBottom;
|
||||
},
|
||||
|
||||
// SwitchView
|
||||
get SwitchView() {
|
||||
return require('./views/SwitchView/SwitchView').default;
|
||||
},
|
||||
|
||||
// HOCs
|
||||
@@ -91,4 +163,7 @@ module.exports = {
|
||||
get withNavigationFocus() {
|
||||
return require('./views/withNavigationFocus').default;
|
||||
},
|
||||
get withOrientation() {
|
||||
return require('./views/withOrientation').default;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,18 +8,26 @@ module.exports = {
|
||||
get StateUtils() {
|
||||
return require('./StateUtils').default;
|
||||
},
|
||||
get addNavigationHelpers() {
|
||||
return require('./addNavigationHelpers').default;
|
||||
},
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
|
||||
// Navigators
|
||||
get createNavigator() {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
|
||||
// Actions
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
get StackActions() {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
},
|
||||
get getNavigationActionCreators() {
|
||||
return require('./routers/getNavigationActionCreators').default;
|
||||
},
|
||||
|
||||
// Routers
|
||||
get StackRouter() {
|
||||
return require('./routers/StackRouter').default;
|
||||
@@ -27,6 +35,9 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
|
||||
// HOCs
|
||||
get withNavigation() {
|
||||
|
||||
28
src/routers/DrawerActions.js
Normal file
28
src/routers/DrawerActions.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
|
||||
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
|
||||
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
|
||||
|
||||
const openDrawer = payload => ({
|
||||
type: OPEN_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const closeDrawer = payload => ({
|
||||
type: CLOSE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const toggleDrawer = payload => ({
|
||||
type: TOGGLE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
export default {
|
||||
OPEN_DRAWER,
|
||||
CLOSE_DRAWER,
|
||||
TOGGLE_DRAWER,
|
||||
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
toggleDrawer,
|
||||
};
|
||||
98
src/routers/DrawerRouter.js
Normal file
98
src/routers/DrawerRouter.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import SwitchRouter from './SwitchRouter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
import invariant from '../utils/invariant';
|
||||
import withDefaultValue from '../utils/withDefaultValue';
|
||||
|
||||
import DrawerActions from './DrawerActions';
|
||||
|
||||
const getActiveRouteKey = route => {
|
||||
if (route.routes && route.routes[route.index]) {
|
||||
return getActiveRouteKey(route.routes[route.index]);
|
||||
}
|
||||
return route.key;
|
||||
};
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
config = { ...config };
|
||||
config = withDefaultValue(config, 'resetOnBlur', false);
|
||||
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
|
||||
|
||||
const switchRouter = SwitchRouter(routeConfigs, config);
|
||||
|
||||
return {
|
||||
...switchRouter,
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
openDrawer: () => DrawerActions.openDrawer({ key: navStateKey }),
|
||||
closeDrawer: () => DrawerActions.closeDrawer({ key: navStateKey }),
|
||||
toggleDrawer: () => DrawerActions.toggleDrawer({ key: navStateKey }),
|
||||
...switchRouter.getActionCreators(route, navStateKey),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, state) {
|
||||
// Set up the initial state if needed
|
||||
if (!state) {
|
||||
return {
|
||||
...switchRouter.getStateForAction(action, undefined),
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
const isRouterTargeted = action.key == null || action.key === state.key;
|
||||
|
||||
if (isRouterTargeted) {
|
||||
// Only handle actions that are meant for this drawer, as specified by action.key.
|
||||
|
||||
if (action.type === DrawerActions.CLOSE_DRAWER && state.isDrawerOpen) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DrawerActions.OPEN_DRAWER && !state.isDrawerOpen) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DrawerActions.TOGGLE_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: !state.isDrawerOpen,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back on switch router for screen switching logic, and handling of child routers
|
||||
const switchedState = switchRouter.getStateForAction(action, state);
|
||||
|
||||
if (switchedState === null) {
|
||||
// The switch router or a child router is attempting to swallow this action. We return null to allow this.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Has the switch router changed the state?
|
||||
if (switchedState !== state) {
|
||||
if (getActiveRouteKey(switchedState) !== getActiveRouteKey(state)) {
|
||||
// If any navigation has happened, make sure to close the drawer
|
||||
return {
|
||||
...switchedState,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
// At this point, return the state as defined by the switch router.
|
||||
// The active route key hasn't changed, so this most likely means that a child router has returned
|
||||
// a new state like a param change, but the same key is still active and the drawer will remain open
|
||||
return switchedState;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
};
|
||||
};
|
||||
52
src/routers/StackActions.js
Normal file
52
src/routers/StackActions.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const POP = 'Navigation/POP';
|
||||
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
|
||||
const PUSH = 'Navigation/PUSH';
|
||||
const RESET = 'Navigation/RESET';
|
||||
const REPLACE = 'Navigation/REPLACE';
|
||||
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
|
||||
|
||||
const pop = payload => ({
|
||||
type: POP,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const popToTop = payload => ({
|
||||
type: POP_TO_TOP,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const push = payload => ({
|
||||
type: PUSH,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const reset = payload => ({
|
||||
type: RESET,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const replace = payload => ({
|
||||
type: REPLACE,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const completeTransition = payload => ({
|
||||
type: COMPLETE_TRANSITION,
|
||||
...payload,
|
||||
});
|
||||
|
||||
export default {
|
||||
POP,
|
||||
POP_TO_TOP,
|
||||
PUSH,
|
||||
RESET,
|
||||
REPLACE,
|
||||
COMPLETE_TRANSITION,
|
||||
|
||||
pop,
|
||||
popToTop,
|
||||
push,
|
||||
reset,
|
||||
replace,
|
||||
completeTransition,
|
||||
};
|
||||
@@ -1,13 +1,14 @@
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackActions from './StackActions';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import StateUtils from '../StateUtils';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
|
||||
import invariant from '../utils/invariant';
|
||||
import { generateKey } from './KeyGenerator';
|
||||
import getNavigationActionCreators from './getNavigationActionCreators';
|
||||
|
||||
function isEmpty(obj) {
|
||||
if (!obj) return true;
|
||||
@@ -20,10 +21,16 @@ function isEmpty(obj) {
|
||||
function behavesLikePushAction(action) {
|
||||
return (
|
||||
action.type === NavigationActions.NAVIGATE ||
|
||||
action.type === NavigationActions.PUSH
|
||||
action.type === StackActions.PUSH
|
||||
);
|
||||
}
|
||||
|
||||
const defaultActionCreators = (route, navStateKey) => ({});
|
||||
|
||||
function isResetToRootStack(action) {
|
||||
return action.type === StackActions.RESET && action.key === null;
|
||||
}
|
||||
|
||||
export default (routeConfigs, stackConfig = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
@@ -44,6 +51,8 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
});
|
||||
|
||||
const { initialRouteParams } = stackConfig;
|
||||
const getCustomActionCreators =
|
||||
stackConfig.getCustomActionCreators || defaultActionCreators;
|
||||
|
||||
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
|
||||
|
||||
@@ -92,11 +101,12 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
...(action.params || {}),
|
||||
...(initialRouteParams || {}),
|
||||
};
|
||||
const { initialRouteKey } = stackConfig;
|
||||
route = {
|
||||
...route,
|
||||
...(params ? { params } : {}),
|
||||
routeName: initialRouteName,
|
||||
key: action.key || generateKey(),
|
||||
key: action.key || (initialRouteKey || generateKey()),
|
||||
};
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
@@ -136,7 +146,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
});
|
||||
|
||||
paths = Object.entries(pathsByRouteNames);
|
||||
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
|
||||
paths.sort((a, b) => b[1].priority - a[1].priority);
|
||||
|
||||
return {
|
||||
getComponentForState(state) {
|
||||
@@ -152,6 +162,63 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, navStateKey),
|
||||
pop: (n, params) =>
|
||||
StackActions.pop({
|
||||
n,
|
||||
...params,
|
||||
}),
|
||||
popToTop: params => StackActions.popToTop(params),
|
||||
push: (routeName, params, action) =>
|
||||
StackActions.push({
|
||||
routeName,
|
||||
params,
|
||||
action,
|
||||
}),
|
||||
replace: (replaceWith, params, action, newKey) => {
|
||||
if (typeof replaceWith === 'string') {
|
||||
return StackActions.replace({
|
||||
routeName: replaceWith,
|
||||
params,
|
||||
action,
|
||||
key: route.key,
|
||||
newKey,
|
||||
});
|
||||
}
|
||||
invariant(
|
||||
typeof replaceWith === 'object',
|
||||
'Must replaceWith an object or a string'
|
||||
);
|
||||
invariant(
|
||||
params == null,
|
||||
'Params must not be provided to .replace() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
action == null,
|
||||
'Child action must not be provided to .replace() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
newKey == null,
|
||||
'Child action must not be provided to .replace() when specifying an object'
|
||||
);
|
||||
return StackActions.replace(replaceWith);
|
||||
},
|
||||
reset: (actions, index) =>
|
||||
StackActions.reset({
|
||||
actions,
|
||||
index: index == null ? actions.length - 1 : index,
|
||||
key: navStateKey,
|
||||
}),
|
||||
dismiss: () =>
|
||||
NavigationActions.back({
|
||||
key: navStateKey,
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, state) {
|
||||
// Set up the initial state if needed
|
||||
if (!state) {
|
||||
@@ -160,7 +227,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// Check if the focused child scene wants to handle the action, as long as
|
||||
// it is not a reset to the root stack
|
||||
if (action.type !== NavigationActions.RESET || action.key !== null) {
|
||||
if (
|
||||
!isResetToRootStack(action) &&
|
||||
action.type !== NavigationActions.NAVIGATE
|
||||
) {
|
||||
const keyIndex = action.key
|
||||
? StateUtils.indexOf(state, action.key)
|
||||
: -1;
|
||||
@@ -180,6 +250,38 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
return StateUtils.replaceAt(state, childRoute.key, route);
|
||||
}
|
||||
}
|
||||
} else if (action.type === NavigationActions.NAVIGATE) {
|
||||
// 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()) {
|
||||
let childRouter = childRouters[childRoute.routeName];
|
||||
let childAction =
|
||||
action.routeName === childRoute.routeName && action.action
|
||||
? action.action
|
||||
: action;
|
||||
|
||||
if (childRouter) {
|
||||
const nextRouteState = childRouter.getStateForAction(
|
||||
childAction,
|
||||
childRoute
|
||||
);
|
||||
|
||||
if (nextRouteState === null || nextRouteState !== childRoute) {
|
||||
const newState = StateUtils.replaceAndPrune(
|
||||
state,
|
||||
nextRouteState ? nextRouteState.key : childRoute.key,
|
||||
nextRouteState ? nextRouteState : childRoute
|
||||
);
|
||||
return {
|
||||
...newState,
|
||||
isTransitioning:
|
||||
state.index !== newState.index
|
||||
? action.immediate !== true
|
||||
: state.isTransitioning,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle explicit push navigation action. This must happen after the
|
||||
@@ -192,46 +294,50 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
let route;
|
||||
|
||||
invariant(
|
||||
action.type !== NavigationActions.PUSH || action.key == null,
|
||||
action.type !== StackActions.PUSH || action.key == null,
|
||||
'StackRouter does not support key on the push action'
|
||||
);
|
||||
|
||||
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
|
||||
if (action.key) {
|
||||
const lastRouteIndex = state.routes.findIndex(
|
||||
r => r.key === action.key
|
||||
);
|
||||
if (lastRouteIndex !== -1) {
|
||||
// If index is unchanged and params are not being set, leave state identity intact
|
||||
if (state.index === lastRouteIndex && !action.params) {
|
||||
return state;
|
||||
}
|
||||
// Before pushing a new route we first try to find one in the existing route stack
|
||||
// More information on this: https://github.com/react-navigation/rfcs/blob/master/text/0004-less-pushy-navigate.md
|
||||
const lastRouteIndex = state.routes.findIndex(r => {
|
||||
if (action.key) {
|
||||
return r.key === action.key;
|
||||
} else {
|
||||
return r.routeName === action.routeName;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the now unused routes at the tail of the routes array
|
||||
const routes = state.routes.slice(0, lastRouteIndex + 1);
|
||||
if (action.type !== StackActions.PUSH && lastRouteIndex !== -1) {
|
||||
// If index is unchanged and params are not being set, leave state identity intact
|
||||
if (state.index === lastRouteIndex && !action.params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply params if provided, otherwise leave route identity intact
|
||||
if (action.params) {
|
||||
const route = state.routes.find(r => r.key === action.key);
|
||||
routes[lastRouteIndex] = {
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return state with new index. Change isTransitioning only if index has changed
|
||||
return {
|
||||
...state,
|
||||
isTransitioning:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true
|
||||
: undefined,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
// Remove the now unused routes at the tail of the routes array
|
||||
const routes = state.routes.slice(0, lastRouteIndex + 1);
|
||||
|
||||
// Apply params if provided, otherwise leave route identity intact
|
||||
if (action.params) {
|
||||
const route = state.routes[lastRouteIndex];
|
||||
routes[lastRouteIndex] = {
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return state with new index. Change isTransitioning only if index has changed
|
||||
return {
|
||||
...state,
|
||||
isTransitioning:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true
|
||||
: state.isTransitioning,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
if (childRouter) {
|
||||
@@ -256,15 +362,11 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
isTransitioning: action.immediate !== true,
|
||||
};
|
||||
} else if (
|
||||
action.type === NavigationActions.PUSH &&
|
||||
action.type === StackActions.PUSH &&
|
||||
childRouters[action.routeName] === undefined
|
||||
) {
|
||||
// If we've made it this far with a push action, we return the
|
||||
// state with a new identity to prevent the action from bubbling
|
||||
// back up.
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
// Return the state identity to bubble the action up
|
||||
return state;
|
||||
}
|
||||
|
||||
// Handle navigation to other child routers that are not yet pushed
|
||||
@@ -304,7 +406,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
|
||||
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
|
||||
if (action.type === NavigationActions.POP_TO_TOP) {
|
||||
if (action.type === StackActions.POP_TO_TOP) {
|
||||
// Refuse to handle pop to top if a key is given that doesn't correspond
|
||||
// to this router
|
||||
if (action.key && state.key !== action.key) {
|
||||
@@ -313,11 +415,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// If we're already at the top, then we return the state with a new
|
||||
// identity so that the action is handled by this router.
|
||||
if (state.index === 0) {
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
} else {
|
||||
if (state.index > 0) {
|
||||
return {
|
||||
...state,
|
||||
isTransitioning: action.immediate !== true,
|
||||
@@ -329,7 +427,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
|
||||
// Handle replace action
|
||||
if (action.type === NavigationActions.REPLACE) {
|
||||
if (action.type === StackActions.REPLACE) {
|
||||
const routeIndex = state.routes.findIndex(r => r.key === action.key);
|
||||
// Only replace if the key matches one of our routes
|
||||
if (routeIndex !== -1) {
|
||||
@@ -355,7 +453,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// Update transitioning state
|
||||
if (
|
||||
action.type === NavigationActions.COMPLETE_TRANSITION &&
|
||||
action.type === StackActions.COMPLETE_TRANSITION &&
|
||||
(action.key == null || action.key === state.key) &&
|
||||
state.isTransitioning
|
||||
) {
|
||||
@@ -385,7 +483,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.RESET) {
|
||||
if (action.type === StackActions.RESET) {
|
||||
// Only handle reset actions that are unspecified or match this state key
|
||||
if (action.key != null && action.key != state.key) {
|
||||
// Deliberately use != instead of !== so we can match null with
|
||||
@@ -422,11 +520,11 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
if (
|
||||
action.type === NavigationActions.BACK ||
|
||||
action.type === NavigationActions.POP
|
||||
action.type === StackActions.POP
|
||||
) {
|
||||
const { key, n, immediate } = action;
|
||||
let backRouteIndex = state.index;
|
||||
if (action.type === NavigationActions.POP && n != null) {
|
||||
if (action.type === StackActions.POP && n != null) {
|
||||
// determine the index to go back *from*. In this case, n=1 means to go
|
||||
// back from state.index, as if it were a normal "BACK" action
|
||||
backRouteIndex = Math.max(1, state.index - n + 1);
|
||||
@@ -442,15 +540,9 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
index: backRouteIndex - 1,
|
||||
isTransitioning: immediate !== true,
|
||||
};
|
||||
} else if (
|
||||
backRouteIndex === 0 &&
|
||||
action.type === NavigationActions.POP
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
@@ -481,6 +573,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (!pathToResolve) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
params: inputParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -551,7 +644,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (key.asterisk || !key) {
|
||||
return result;
|
||||
}
|
||||
const nextResult = result || {};
|
||||
const nextResult = result || inputParams || {};
|
||||
const paramName = key.name;
|
||||
|
||||
let decodedMatchResult;
|
||||
@@ -576,7 +669,5 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
routeConfigs,
|
||||
stackConfig.navigationOptions
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
};
|
||||
|
||||
388
src/routers/SwitchRouter.js
Normal file
388
src/routers/SwitchRouter.js
Normal file
@@ -0,0 +1,388 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackActions from './StackActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getNavigationActionCreators from './getNavigationActionCreators';
|
||||
|
||||
const defaultActionCreators = (route, navStateKey) => ({});
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
NavigationActions.SET_PARAMS,
|
||||
// Todo: make SwitchRouter not depend on StackActions..
|
||||
StackActions.COMPLETE_TRANSITION,
|
||||
].includes(actionType);
|
||||
}
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
const getCustomActionCreators =
|
||||
config.getCustomActionCreators || defaultActionCreators;
|
||||
|
||||
const initialRouteParams = config.initialRouteParams;
|
||||
const initialRouteName = config.initialRouteName || order[0];
|
||||
const backBehavior = config.backBehavior || 'none';
|
||||
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
|
||||
const resetOnBlur = config.hasOwnProperty('resetOnBlur')
|
||||
? config.resetOnBlur
|
||||
: true;
|
||||
const initialRouteIndex = order.indexOf(initialRouteName);
|
||||
const childRouters = {};
|
||||
order.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
if (!paths[routeName]) {
|
||||
paths[routeName] =
|
||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
||||
}
|
||||
childRouters[routeName] = null;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
if (screen.router) {
|
||||
childRouters[routeName] = screen.router;
|
||||
}
|
||||
});
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}'.` +
|
||||
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
function resetChildRoute(routeName) {
|
||||
const params =
|
||||
routeName === initialRouteName ? initialRouteParams : undefined;
|
||||
const childRouter = childRouters[routeName];
|
||||
if (childRouter) {
|
||||
const childAction = NavigationActions.init();
|
||||
return {
|
||||
...childRouter.getStateForAction(childAction),
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getInitialState() {
|
||||
const routes = order.map(resetChildRoute);
|
||||
return {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
};
|
||||
},
|
||||
|
||||
getNextState(prevState, possibleNextState) {
|
||||
if (!prevState) {
|
||||
return possibleNextState;
|
||||
}
|
||||
|
||||
let nextState;
|
||||
if (prevState.index !== possibleNextState.index && resetOnBlur) {
|
||||
const prevRouteName = prevState.routes[prevState.index].routeName;
|
||||
const nextRoutes = [...possibleNextState.routes];
|
||||
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
|
||||
|
||||
return {
|
||||
...possibleNextState,
|
||||
routes: nextRoutes,
|
||||
};
|
||||
} else {
|
||||
nextState = possibleNextState;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
},
|
||||
|
||||
getActionCreators(route, stateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, stateKey),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, inputState) {
|
||||
let prevState = inputState ? { ...inputState } : inputState;
|
||||
let state = inputState || this.getInitialState();
|
||||
let activeChildIndex = state.index;
|
||||
|
||||
if (action.type === NavigationActions.INIT) {
|
||||
// NOTE(brentvatne): this seems weird... why are we merging these
|
||||
// params into child routes?
|
||||
// ---------------------------------------------------------------
|
||||
// Merge any params from the action into all the child routes
|
||||
const { params } = action;
|
||||
if (params) {
|
||||
state.routes = state.routes.map(route => ({
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...params,
|
||||
...(route.routeName === initialRouteName
|
||||
? initialRouteParams
|
||||
: null),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Let the current child handle it
|
||||
const activeChildLastState = state.routes[state.index];
|
||||
const activeChildRouter = childRouters[order[state.index]];
|
||||
if (activeChildRouter) {
|
||||
const activeChildState = activeChildRouter.getStateForAction(
|
||||
action,
|
||||
activeChildLastState
|
||||
);
|
||||
if (!activeChildState && inputState) {
|
||||
return null;
|
||||
}
|
||||
if (activeChildState && activeChildState !== activeChildLastState) {
|
||||
const routes = [...state.routes];
|
||||
routes[state.index] = activeChildState;
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tab changing. Do this after letting the current tab try to
|
||||
// handle the action, to allow inner children to change first
|
||||
const isBackEligible =
|
||||
action.key == null || action.key === activeChildLastState.key;
|
||||
if (action.type === NavigationActions.BACK) {
|
||||
if (isBackEligible && shouldBackNavigateToInitialRoute) {
|
||||
activeChildIndex = initialRouteIndex;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
let didNavigate = false;
|
||||
if (action.type === NavigationActions.NAVIGATE) {
|
||||
didNavigate = !!order.find((childId, i) => {
|
||||
if (childId === action.routeName) {
|
||||
activeChildIndex = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (didNavigate) {
|
||||
const childState = state.routes[activeChildIndex];
|
||||
const childRouter = childRouters[action.routeName];
|
||||
let newChildState;
|
||||
|
||||
if (action.action) {
|
||||
newChildState = childRouter
|
||||
? childRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!action.action && !childRouter && action.params) {
|
||||
newChildState = {
|
||||
...childState,
|
||||
params: {
|
||||
...(childState.params || {}),
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (newChildState && newChildState !== childState) {
|
||||
const routes = [...state.routes];
|
||||
routes[activeChildIndex] = newChildState;
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
} else if (
|
||||
!newChildState &&
|
||||
state.index === activeChildIndex &&
|
||||
prevState
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.SET_PARAMS) {
|
||||
const key = action.key;
|
||||
const lastRoute = state.routes.find(route => route.key === key);
|
||||
if (lastRoute) {
|
||||
const params = {
|
||||
...lastRoute.params,
|
||||
...action.params,
|
||||
};
|
||||
const routes = [...state.routes];
|
||||
routes[state.routes.indexOf(lastRoute)] = {
|
||||
...lastRoute,
|
||||
params,
|
||||
};
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (activeChildIndex !== state.index) {
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
} else if (didNavigate && !inputState) {
|
||||
return state;
|
||||
} else if (didNavigate) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
// Let other children handle it and switch to the first child that returns a new state
|
||||
let index = state.index;
|
||||
let routes = state.routes;
|
||||
order.find((childId, i) => {
|
||||
const childRouter = childRouters[childId];
|
||||
if (i === index) {
|
||||
return false;
|
||||
}
|
||||
let childState = routes[i];
|
||||
if (childRouter) {
|
||||
childState = childRouter.getStateForAction(action, childState);
|
||||
}
|
||||
if (!childState) {
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
if (childState !== routes[i]) {
|
||||
routes = [...routes];
|
||||
routes[i] = childState;
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Nested routers can be updated after switching children with actions such as SET_PARAMS
|
||||
// and COMPLETE_TRANSITION.
|
||||
// NOTE: This may be problematic with custom routers because we whitelist the actions
|
||||
// that can be handled by child routers without automatically changing index.
|
||||
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
|
||||
index = state.index;
|
||||
}
|
||||
|
||||
if (index !== state.index || routes !== state.routes) {
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
getComponentForState(state) {
|
||||
const routeName = state.routes[state.index].routeName;
|
||||
invariant(
|
||||
routeName,
|
||||
`There is no route defined for index ${state.index}. Check that
|
||||
that you passed in a navigation state with a valid tab/screen index.`
|
||||
);
|
||||
const childRouter = childRouters[routeName];
|
||||
if (childRouter) {
|
||||
return childRouter.getComponentForState(state.routes[state.index]);
|
||||
}
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getComponentForRouteName(routeName) {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = order[state.index];
|
||||
const subPath = paths[routeName];
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an optional action, based on a relative path and query params.
|
||||
*
|
||||
* This will return null if there is no action matched
|
||||
*/
|
||||
getActionForPathAndParams(path, params) {
|
||||
if (!path) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
params,
|
||||
});
|
||||
}
|
||||
return (
|
||||
order
|
||||
.map(childId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[childId];
|
||||
const partsInTestPath = pathToTest.split('/').length;
|
||||
const pathPartsToTest = parts.slice(0, partsInTestPath).join('/');
|
||||
if (pathPartsToTest === pathToTest) {
|
||||
const childRouter = childRouters[childId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: childId,
|
||||
});
|
||||
if (childRouter && childRouter.getActionForPathAndParams) {
|
||||
action.action = childRouter.getActionForPathAndParams(
|
||||
parts.slice(partsInTestPath).join('/'),
|
||||
params
|
||||
);
|
||||
}
|
||||
if (params) {
|
||||
action.params = params;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
order
|
||||
.map(childId => {
|
||||
const childRouter = childRouters[childId];
|
||||
return (
|
||||
childRouter && childRouter.getActionForPathAndParams(path, params)
|
||||
);
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
routeConfigs,
|
||||
config.navigationOptions
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,329 +1,11 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
NavigationActions.SET_PARAMS,
|
||||
NavigationActions.COMPLETE_TRANSITION,
|
||||
].includes(actionType);
|
||||
}
|
||||
import SwitchRouter from './SwitchRouter';
|
||||
import withDefaultValue from '../utils/withDefaultValue';
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
config = { ...config };
|
||||
config = withDefaultValue(config, 'resetOnBlur', false);
|
||||
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
const initialRouteParams = config.initialRouteParams;
|
||||
const initialRouteName = config.initialRouteName || order[0];
|
||||
const initialRouteIndex = order.indexOf(initialRouteName);
|
||||
const backBehavior = config.backBehavior || 'initialRoute';
|
||||
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
|
||||
const tabRouters = {};
|
||||
order.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
paths[routeName] =
|
||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
||||
tabRouters[routeName] = null;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
if (screen.router) {
|
||||
tabRouters[routeName] = screen.router;
|
||||
}
|
||||
});
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}' for TabRouter. ` +
|
||||
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
getStateForAction(action, inputState) {
|
||||
// Establish a default state
|
||||
let state = inputState;
|
||||
if (!state) {
|
||||
const routes = order.map(routeName => {
|
||||
const params =
|
||||
routeName === initialRouteName ? initialRouteParams : undefined;
|
||||
const tabRouter = tabRouters[routeName];
|
||||
if (tabRouter) {
|
||||
const childAction = NavigationActions.init();
|
||||
return {
|
||||
...tabRouter.getStateForAction(childAction),
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
});
|
||||
state = {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
};
|
||||
// console.log(`${order.join('-')}: Initial state`, {state});
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.INIT) {
|
||||
// Merge any params from the action into all the child routes
|
||||
const { params } = action;
|
||||
if (params) {
|
||||
state.routes = state.routes.map(route => ({
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...params,
|
||||
...(route.routeName === initialRouteName
|
||||
? initialRouteParams
|
||||
: null),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Let the current tab handle it
|
||||
const activeTabLastState = state.routes[state.index];
|
||||
const activeTabRouter = tabRouters[order[state.index]];
|
||||
if (activeTabRouter) {
|
||||
const activeTabState = activeTabRouter.getStateForAction(
|
||||
action,
|
||||
activeTabLastState
|
||||
);
|
||||
if (!activeTabState && inputState) {
|
||||
return null;
|
||||
}
|
||||
if (activeTabState && activeTabState !== activeTabLastState) {
|
||||
const routes = [...state.routes];
|
||||
routes[state.index] = activeTabState;
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tab changing. Do this after letting the current tab try to
|
||||
// handle the action, to allow inner tabs to change first
|
||||
let activeTabIndex = state.index;
|
||||
const isBackEligible =
|
||||
action.key == null || action.key === activeTabLastState.key;
|
||||
if (action.type === NavigationActions.BACK) {
|
||||
if (isBackEligible && shouldBackNavigateToInitialRoute) {
|
||||
activeTabIndex = initialRouteIndex;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
let didNavigate = false;
|
||||
if (action.type === NavigationActions.NAVIGATE) {
|
||||
const navigateAction = action;
|
||||
didNavigate = !!order.find((tabId, i) => {
|
||||
if (tabId === navigateAction.routeName) {
|
||||
activeTabIndex = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (didNavigate) {
|
||||
const childState = state.routes[activeTabIndex];
|
||||
let newChildState;
|
||||
|
||||
const tabRouter = tabRouters[action.routeName];
|
||||
|
||||
if (action.action) {
|
||||
newChildState = tabRouter
|
||||
? tabRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!tabRouter && action.params) {
|
||||
newChildState = {
|
||||
...childState,
|
||||
params: {
|
||||
...(childState.params || {}),
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (newChildState && newChildState !== childState) {
|
||||
const routes = [...state.routes];
|
||||
routes[activeTabIndex] = newChildState;
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
index: activeTabIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.type === NavigationActions.SET_PARAMS) {
|
||||
const key = action.key;
|
||||
const lastRoute = state.routes.find(route => route.key === key);
|
||||
if (lastRoute) {
|
||||
const params = {
|
||||
...lastRoute.params,
|
||||
...action.params,
|
||||
};
|
||||
const routes = [...state.routes];
|
||||
routes[state.routes.indexOf(lastRoute)] = {
|
||||
...lastRoute,
|
||||
params,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (activeTabIndex !== state.index) {
|
||||
return {
|
||||
...state,
|
||||
index: activeTabIndex,
|
||||
};
|
||||
} else if (didNavigate && !inputState) {
|
||||
return state;
|
||||
} else if (didNavigate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Let other tabs handle it and switch to the first tab that returns a new state
|
||||
let index = state.index;
|
||||
let routes = state.routes;
|
||||
order.find((tabId, i) => {
|
||||
const tabRouter = tabRouters[tabId];
|
||||
if (i === index) {
|
||||
return false;
|
||||
}
|
||||
let tabState = routes[i];
|
||||
if (tabRouter) {
|
||||
// console.log(`${order.join('-')}: Processing child router:`, {action, tabState});
|
||||
tabState = tabRouter.getStateForAction(action, tabState);
|
||||
}
|
||||
if (!tabState) {
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
if (tabState !== routes[i]) {
|
||||
routes = [...routes];
|
||||
routes[i] = tabState;
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// console.log(`${order.join('-')}: Processed other tabs:`, {lastIndex: state.index, index});
|
||||
|
||||
// Nested routers can be updated after switching tabs with actions such as SET_PARAMS
|
||||
// and COMPLETE_TRANSITION.
|
||||
// NOTE: This may be problematic with custom routers because we whitelist the actions
|
||||
// that can be handled by child routers without automatically changing index.
|
||||
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
|
||||
index = state.index;
|
||||
}
|
||||
|
||||
if (index !== state.index || routes !== state.routes) {
|
||||
return {
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
getComponentForState(state) {
|
||||
const routeName = state.routes[state.index].routeName;
|
||||
invariant(
|
||||
routeName,
|
||||
`There is no route defined for index ${state.index}. Check that
|
||||
that you passed in a navigation state with a valid tab/screen index.`
|
||||
);
|
||||
const childRouter = tabRouters[routeName];
|
||||
if (childRouter) {
|
||||
return childRouter.getComponentForState(state.routes[state.index]);
|
||||
}
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getComponentForRouteName(routeName) {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = order[state.index];
|
||||
const subPath = paths[routeName];
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return (
|
||||
order
|
||||
.map(tabId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[tabId];
|
||||
if (parts[0] === pathToTest) {
|
||||
const tabRouter = tabRouters[tabId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: tabId,
|
||||
});
|
||||
if (tabRouter && tabRouter.getActionForPathAndParams) {
|
||||
action.action = tabRouter.getActionForPathAndParams(
|
||||
parts.slice(1).join('/'),
|
||||
params
|
||||
);
|
||||
} else if (params) {
|
||||
action.params = params;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
order
|
||||
.map(tabId => {
|
||||
const tabRouter = tabRouters[tabId];
|
||||
return (
|
||||
tabRouter && tabRouter.getActionForPathAndParams(path, params)
|
||||
);
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
routeConfigs,
|
||||
config.navigationOptions
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
const switchRouter = SwitchRouter(routeConfigs, config);
|
||||
return switchRouter;
|
||||
};
|
||||
|
||||
176
src/routers/__tests__/DrawerRouter-test.js
Normal file
176
src/routers/__tests__/DrawerRouter-test.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/* eslint react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
import DrawerRouter from '../DrawerRouter';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
|
||||
describe('DrawerRouter', () => {
|
||||
test('Handles basic tab logic', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
],
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
expect(state).toEqual(expectedState);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
|
||||
state
|
||||
);
|
||||
const expectedState2 = {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
],
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
expect(state2).toEqual(expectedState2);
|
||||
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
|
||||
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
||||
});
|
||||
|
||||
test('Handles initial route navigation', () => {
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const router = DrawerRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
Bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
},
|
||||
{ initialRouteName: 'Bar' }
|
||||
);
|
||||
const state = router.getStateForAction({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
});
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isDrawerOpen: false,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{
|
||||
key: 'Foo',
|
||||
params: undefined,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
{
|
||||
key: 'Bar',
|
||||
params: undefined,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Drawer opens closes and toggles', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.CLOSE_DRAWER },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(false);
|
||||
const state4 = router.getStateForAction(
|
||||
{ type: DrawerActions.TOGGLE_DRAWER },
|
||||
state3
|
||||
);
|
||||
expect(state4.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
|
||||
test('Drawer opens closes with key targeted', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(false);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Nested routers bubble up blocked actions', () => {
|
||||
const ScreenA = () => <div />;
|
||||
ScreenA.router = {
|
||||
getStateForAction(action, lastState) {
|
||||
if (action.type === 'CHILD_ACTION') return null;
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
|
||||
const state2 = router.getStateForAction({ type: 'CHILD_ACTION' }, state);
|
||||
expect(state2).toEqual(null);
|
||||
});
|
||||
|
||||
test('Drawer stays open when child routers return new state', () => {
|
||||
const ScreenA = () => <div />;
|
||||
ScreenA.router = {
|
||||
getStateForAction(action, lastState = { changed: false }) {
|
||||
if (action.type === 'CHILD_ACTION')
|
||||
return { ...lastState, changed: true };
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
});
|
||||
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
|
||||
const state3 = router.getStateForAction({ type: 'CHILD_ACTION' }, state2);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
expect(state3.routes[0].changed).toEqual(true);
|
||||
});
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint react/no-multi-comp:0 */
|
||||
/* eslint react/no-multi-comp:0, react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import StackRouter from '../StackRouter';
|
||||
import TabRouter from '../TabRouter';
|
||||
import SwitchRouter from '../SwitchRouter';
|
||||
import DrawerRouter from '../DrawerRouter';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import DrawerActions from '../DrawerActions';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -16,6 +18,8 @@ beforeEach(() => {
|
||||
const ROUTERS = {
|
||||
TabRouter,
|
||||
StackRouter,
|
||||
DrawerRouter,
|
||||
SwitchRouter,
|
||||
};
|
||||
|
||||
const dummyEventSubscriber = (name, handler) => ({
|
||||
@@ -26,7 +30,7 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
const Router = ROUTERS[routerName];
|
||||
|
||||
describe(`General router features - ${routerName}`, () => {
|
||||
test('title is configurable using navigationOptions and getScreenOptions', () => {
|
||||
test(`title is configurable using navigationOptions and getScreenOptions - ${routerName}`, () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
@@ -58,38 +62,132 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
];
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[0],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[1],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('BarTitle');
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[2],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('Baz-123');
|
||||
});
|
||||
|
||||
test(`set params works in ${routerName}`, () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const router = Router({
|
||||
Foo: { screen: FooView },
|
||||
Bar: { screen: FooView },
|
||||
});
|
||||
|
||||
const initState = router.getStateForAction(NavigationActions.init());
|
||||
const initRoute = initState.routes[initState.index];
|
||||
expect(initRoute.params).toEqual(undefined);
|
||||
|
||||
const state0 = router.getStateForAction(
|
||||
NavigationActions.setParams({
|
||||
params: { foo: 42 },
|
||||
key: initRoute.key,
|
||||
}),
|
||||
initState
|
||||
);
|
||||
expect(state0.routes[state0.index].params.foo).toEqual(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Nested navigate behavior test', () => {
|
||||
const Leaf = () => <div />;
|
||||
|
||||
const First = () => <div />;
|
||||
First.router = StackRouter({
|
||||
First1: Leaf,
|
||||
First2: Leaf,
|
||||
});
|
||||
|
||||
const Second = () => <div />;
|
||||
Second.router = StackRouter({
|
||||
Second1: Leaf,
|
||||
Second2: Leaf,
|
||||
});
|
||||
|
||||
const Main = () => <div />;
|
||||
Main.router = StackRouter({
|
||||
First,
|
||||
Second,
|
||||
});
|
||||
const TestRouter = SwitchRouter({
|
||||
Login: Leaf,
|
||||
Main,
|
||||
});
|
||||
|
||||
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
|
||||
const state2 = TestRouter.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'First' },
|
||||
state1
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
expect(state2.routes[1].index).toEqual(0);
|
||||
expect(state2.routes[1].routes[0].index).toEqual(0);
|
||||
|
||||
const state3 = TestRouter.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'Second2' },
|
||||
state2
|
||||
);
|
||||
expect(state3.index).toEqual(1);
|
||||
expect(state3.routes[1].index).toEqual(1); // second
|
||||
expect(state3.routes[1].routes[1].index).toEqual(1); //second.second2
|
||||
|
||||
const state4 = TestRouter.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'First',
|
||||
action: { type: NavigationActions.NAVIGATE, routeName: 'First2' },
|
||||
},
|
||||
state3,
|
||||
true
|
||||
);
|
||||
expect(state4.index).toEqual(1); // main
|
||||
expect(state4.routes[1].index).toEqual(0); // first
|
||||
expect(state4.routes[1].routes[0].index).toEqual(1); // first2
|
||||
|
||||
const state5 = TestRouter.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'First',
|
||||
action: { type: NavigationActions.NAVIGATE, routeName: 'First1' },
|
||||
},
|
||||
state3 // second.second2 is active on state3
|
||||
);
|
||||
expect(state5.index).toEqual(1); // main
|
||||
expect(state5.routes[1].index).toEqual(0); // first
|
||||
expect(state5.routes[1].routes[0].index).toEqual(0); // first.first1
|
||||
});
|
||||
|
||||
test('Handles no-op actions with tabs within stack router', () => {
|
||||
const BarView = () => <div />;
|
||||
const FooTabNavigator = () => <div />;
|
||||
@@ -158,6 +256,70 @@ test('Handles deep action', () => {
|
||||
expect(state2 && state2.routes[1].index).toEqual(1);
|
||||
});
|
||||
|
||||
test('Handles the navigate action with params', () => {
|
||||
const FooTabNavigator = () => <div />;
|
||||
FooTabNavigator.router = TabRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Boo: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const TestRouter = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: FooTabNavigator },
|
||||
});
|
||||
const state = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
immediate: true,
|
||||
routeName: 'Bar',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.routes[1].params).toEqual({ foo: '42' });
|
||||
expect(state2 && state2.routes[1].routes).toEqual([
|
||||
{
|
||||
key: 'Baz',
|
||||
routeName: 'Baz',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
{
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Handles the setParams action', () => {
|
||||
const FooTabNavigator = () => <div />;
|
||||
FooTabNavigator.router = TabRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
});
|
||||
const TestRouter = StackRouter({
|
||||
Foo: { screen: FooTabNavigator },
|
||||
Bar: { screen: () => <div /> },
|
||||
});
|
||||
const state = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
params: { name: 'foobar' },
|
||||
key: 'Baz',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.routes[0].routes).toEqual([
|
||||
{
|
||||
key: 'Baz',
|
||||
routeName: 'Baz',
|
||||
params: { name: 'foobar' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Supports lazily-evaluated getScreen', () => {
|
||||
const BarView = () => <div />;
|
||||
const FooTabNavigator = () => <div />;
|
||||
@@ -250,3 +412,112 @@ test('Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION
|
||||
expect(stateAfterCompleteTransition.index).toEqual(1);
|
||||
expect(stateAfterSetParams.index).toEqual(1);
|
||||
});
|
||||
|
||||
test('Inner actions are only unpacked if the current tab matches', () => {
|
||||
const PlainScreen = () => <div />;
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenB.router = StackRouter({
|
||||
Baz: { screen: PlainScreen },
|
||||
Zoo: { screen: PlainScreen },
|
||||
});
|
||||
ScreenA.router = StackRouter({
|
||||
Bar: { screen: PlainScreen },
|
||||
Boo: { screen: ScreenB },
|
||||
});
|
||||
const TestRouter = TabRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
});
|
||||
const screenApreState = {
|
||||
index: 0,
|
||||
key: 'Init',
|
||||
isTransitioning: false,
|
||||
routeName: 'Foo',
|
||||
routes: [{ key: 'Init', routeName: 'Bar' }],
|
||||
};
|
||||
const preState = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
routes: [screenApreState],
|
||||
};
|
||||
|
||||
const comparable = state => {
|
||||
let result = {};
|
||||
if (typeof state.routeName === 'string') {
|
||||
result = { ...result, routeName: state.routeName };
|
||||
}
|
||||
if (state.routes instanceof Array) {
|
||||
result = {
|
||||
...result,
|
||||
routes: state.routes.map(comparable),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: 'Boo',
|
||||
action: NavigationActions.navigate({ routeName: 'Zoo' }),
|
||||
});
|
||||
|
||||
const expectedState = ScreenA.router.getStateForAction(
|
||||
action,
|
||||
screenApreState
|
||||
);
|
||||
const state = TestRouter.getStateForAction(action, preState);
|
||||
const innerState = state ? state.routes[0] : state;
|
||||
|
||||
expect(expectedState && comparable(expectedState)).toEqual(
|
||||
innerState && comparable(innerState)
|
||||
);
|
||||
});
|
||||
|
||||
test('DrawerRouter will close drawer on child navigaton, not on child param changes', () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const BarRouter = SwitchRouter({
|
||||
Qux: FooView,
|
||||
Quo: FooView,
|
||||
});
|
||||
class BarView extends React.Component {
|
||||
static router = BarRouter;
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const router = DrawerRouter({
|
||||
Bar: BarView,
|
||||
Foo: FooView,
|
||||
});
|
||||
|
||||
const emptyState = router.getStateForAction(NavigationActions.init());
|
||||
const initState = router.getStateForAction(
|
||||
DrawerActions.openDrawer(),
|
||||
emptyState
|
||||
);
|
||||
expect(initState.isDrawerOpen).toBe(true);
|
||||
|
||||
const state0 = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'Quo' }),
|
||||
initState
|
||||
);
|
||||
expect(state0.isDrawerOpen).toBe(false);
|
||||
|
||||
const initSwitchState = initState.routes[initState.index];
|
||||
const initQuxState = initSwitchState.routes[initSwitchState.index];
|
||||
|
||||
const state1 = router.getStateForAction(
|
||||
NavigationActions.setParams({
|
||||
key: initQuxState.key,
|
||||
params: { foo: 'bar' },
|
||||
}),
|
||||
initState
|
||||
);
|
||||
expect(state1.isDrawerOpen).toBe(true);
|
||||
const state1switchState = state1.routes[state1.index];
|
||||
const state1quxState = state1switchState.routes[state1switchState.index];
|
||||
expect(state1quxState.params.foo).toEqual('bar');
|
||||
});
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import StackRouter from '../StackRouter';
|
||||
import TabRouter from '../TabRouter';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
import StackActions from '../StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_normalize_keys();
|
||||
@@ -366,7 +365,38 @@ describe('StackRouter', () => {
|
||||
expect(pushedState.routes[1].routes[1].routeName).toEqual('qux');
|
||||
});
|
||||
|
||||
test('pop does not bubble up', () => {
|
||||
test('push bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
Bad: { screen: () => <div /> },
|
||||
});
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.PUSH,
|
||||
routeName: 'Bad',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(2);
|
||||
expect(state3 && state3.routes.length).toEqual(3);
|
||||
});
|
||||
|
||||
test('pop bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
@@ -389,46 +419,14 @@ describe('StackRouter', () => {
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.POP,
|
||||
type: StackActions.POP,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.routes[1].index).toEqual(0);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('push does not bubble up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
Bad: { screen: () => <div /> },
|
||||
});
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.PUSH,
|
||||
routeName: 'Bad',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('popToTop does not bubble up', () => {
|
||||
test('popToTop bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
@@ -449,12 +447,11 @@ describe('StackRouter', () => {
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.POP_TO_TOP,
|
||||
type: StackActions.POP_TO_TOP,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.routes[1].index).toEqual(0);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('popToTop targets StackRouter by key if specified', () => {
|
||||
@@ -478,7 +475,7 @@ describe('StackRouter', () => {
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.POP_TO_TOP,
|
||||
type: StackActions.POP_TO_TOP,
|
||||
key: state2.key,
|
||||
},
|
||||
state2
|
||||
@@ -486,6 +483,44 @@ describe('StackRouter', () => {
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('pop action works as expected', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 3,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'A', routeName: 'foo' },
|
||||
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
|
||||
{ key: 'C', routeName: 'foo' },
|
||||
{ key: 'D', routeName: 'bar' },
|
||||
],
|
||||
};
|
||||
const poppedState = TestRouter.getStateForAction(StackActions.pop(), state);
|
||||
expect(poppedState.routes.length).toBe(3);
|
||||
expect(poppedState.index).toBe(2);
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
StackActions.pop({ n: 2, immediate: true }),
|
||||
state
|
||||
);
|
||||
expect(poppedState2.routes.length).toBe(2);
|
||||
expect(poppedState2.index).toBe(1);
|
||||
expect(poppedState2.isTransitioning).toBe(false);
|
||||
|
||||
const poppedState3 = TestRouter.getStateForAction(
|
||||
StackActions.pop({ n: 5 }),
|
||||
state
|
||||
);
|
||||
expect(poppedState3.routes.length).toBe(1);
|
||||
expect(poppedState3.index).toBe(0);
|
||||
expect(poppedState3.isTransitioning).toBe(true);
|
||||
});
|
||||
|
||||
test('popToTop works as expected', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
@@ -502,19 +537,19 @@ describe('StackRouter', () => {
|
||||
],
|
||||
};
|
||||
const poppedState = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop(),
|
||||
StackActions.popToTop(),
|
||||
state
|
||||
);
|
||||
expect(poppedState.routes.length).toBe(1);
|
||||
expect(poppedState.index).toBe(0);
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop(),
|
||||
StackActions.popToTop(),
|
||||
poppedState
|
||||
);
|
||||
expect(poppedState).toEqual(poppedState2);
|
||||
const poppedImmediatelyState = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop({ immediate: true }),
|
||||
StackActions.popToTop({ immediate: true }),
|
||||
state
|
||||
);
|
||||
expect(poppedImmediatelyState.routes.length).toBe(1);
|
||||
@@ -522,7 +557,59 @@ describe('StackRouter', () => {
|
||||
expect(poppedImmediatelyState.isTransitioning).toBe(false);
|
||||
});
|
||||
|
||||
test('Navigate Pushes duplicate routeName', () => {
|
||||
test('Navigate does not push duplicate routeName', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteName: 'foo' }
|
||||
);
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const barState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
initState
|
||||
);
|
||||
expect(barState.index).toEqual(1);
|
||||
expect(barState.routes[1].routeName).toEqual('bar');
|
||||
const navigateOnBarState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
barState
|
||||
);
|
||||
expect(navigateOnBarState).toEqual(null);
|
||||
});
|
||||
|
||||
test('Navigate focuses given routeName if already active in stack', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
baz: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteName: 'foo' }
|
||||
);
|
||||
const initialState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const fooBarState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
initialState
|
||||
);
|
||||
const fooBarBazState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'baz' }),
|
||||
fooBarState
|
||||
);
|
||||
expect(fooBarBazState.index).toEqual(2);
|
||||
expect(fooBarBazState.routes[2].routeName).toEqual('baz');
|
||||
|
||||
const fooState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo' }),
|
||||
fooBarBazState
|
||||
);
|
||||
expect(fooState.index).toEqual(0);
|
||||
expect(fooState.routes.length).toEqual(1);
|
||||
expect(fooState.routes[0].routeName).toEqual('foo');
|
||||
});
|
||||
|
||||
test('Navigate pushes duplicate routeName if unique key is provided', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
@@ -535,7 +622,7 @@ describe('StackRouter', () => {
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'new-unique-key!' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState.index).toEqual(2);
|
||||
@@ -576,24 +663,103 @@ describe('StackRouter', () => {
|
||||
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge');
|
||||
});
|
||||
|
||||
test('Navigate with key is idempotent', () => {
|
||||
test('Navigate to initial screen is possible', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteKey: 'foo' }
|
||||
);
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState).toEqual(null);
|
||||
});
|
||||
|
||||
test('Navigate with key and without it is idempotent', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
for (key of ['a', null]) {
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState).toEqual(null);
|
||||
}
|
||||
});
|
||||
|
||||
// https://github.com/react-navigation/react-navigation/issues/4063
|
||||
test('Navigate on inactive stackrouter is idempotent', () => {
|
||||
const FirstChildNavigator = () => <div />;
|
||||
FirstChildNavigator.router = StackRouter({
|
||||
First1: () => <div />,
|
||||
First2: () => <div />,
|
||||
});
|
||||
|
||||
const SecondChildNavigator = () => <div />;
|
||||
SecondChildNavigator.router = StackRouter({
|
||||
Second1: () => <div />,
|
||||
Second2: () => <div />,
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Leaf: () => <div />,
|
||||
First: FirstChildNavigator,
|
||||
Second: SecondChildNavigator,
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
|
||||
const first = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'First2' }),
|
||||
state
|
||||
);
|
||||
|
||||
const second = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'Second2' }),
|
||||
first
|
||||
);
|
||||
|
||||
const firstAgain = router.getStateForAction(
|
||||
NavigationActions.navigate({
|
||||
routeName: 'First2',
|
||||
params: { debug: true },
|
||||
}),
|
||||
second
|
||||
);
|
||||
|
||||
expect(first.routes.length).toEqual(2);
|
||||
expect(first.index).toEqual(1);
|
||||
expect(second.routes.length).toEqual(3);
|
||||
expect(second.index).toEqual(2);
|
||||
|
||||
expect(firstAgain.index).toEqual(1);
|
||||
expect(firstAgain.routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('Navigate to current routeName returns null to indicate handled action', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const navigatedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState.index).toEqual(1);
|
||||
expect(pushedTwiceState.routes[1].routeName).toEqual('bar');
|
||||
expect(navigatedState).toBe(null);
|
||||
});
|
||||
|
||||
test('Push behaves like navigate, except for key', () => {
|
||||
@@ -603,19 +769,39 @@ describe('StackRouter', () => {
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.push({ routeName: 'bar' }),
|
||||
StackActions.push({ routeName: 'bar' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
expect(() => {
|
||||
TestRouter.getStateForAction(
|
||||
{ type: NavigationActions.PUSH, routeName: 'bar', key: 'a' },
|
||||
{ type: StackActions.PUSH, routeName: 'bar', key: 'a' },
|
||||
pushedState
|
||||
);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test('Push adds new routes every time', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
StackActions.push({ routeName: 'bar' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const secondPushedState = TestRouter.getStateForAction(
|
||||
StackActions.push({ routeName: 'bar' }),
|
||||
pushedState
|
||||
);
|
||||
expect(secondPushedState.index).toEqual(2);
|
||||
expect(secondPushedState.routes[2].routeName).toEqual('bar');
|
||||
});
|
||||
|
||||
test('Navigate backwards with key removes leading routes', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
@@ -709,7 +895,7 @@ describe('StackRouter', () => {
|
||||
NavigationActions.navigate({ routeName: 'foo' })
|
||||
);
|
||||
const replacedState = TestRouter.getStateForAction(
|
||||
NavigationActions.replace({
|
||||
StackActions.replace({
|
||||
routeName: 'bar',
|
||||
params: { meaning: 42 },
|
||||
key: initState.routes[0].key,
|
||||
@@ -722,7 +908,7 @@ describe('StackRouter', () => {
|
||||
expect(replacedState.routes[0].routeName).toEqual('bar');
|
||||
expect(replacedState.routes[0].params.meaning).toEqual(42);
|
||||
const replacedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.replace({
|
||||
StackActions.replace({
|
||||
routeName: 'bar',
|
||||
key: initState.routes[0].key,
|
||||
newKey: 'wow',
|
||||
@@ -759,7 +945,7 @@ describe('StackRouter', () => {
|
||||
expect(state2 && state2.isTransitioning).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.COMPLETE_TRANSITION,
|
||||
type: StackActions.COMPLETE_TRANSITION,
|
||||
},
|
||||
state2
|
||||
);
|
||||
@@ -981,15 +1167,51 @@ describe('StackRouter', () => {
|
||||
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
|
||||
});
|
||||
|
||||
test('Handles the SetParams action for inactive routes', () => {
|
||||
const router = StackRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Bar',
|
||||
initialRouteParams: { name: 'Zoo' },
|
||||
}
|
||||
);
|
||||
const initialState = {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
key: 'RouteA',
|
||||
routeName: 'Foo',
|
||||
params: { name: 'InitialParam', other: 'Unchanged' },
|
||||
},
|
||||
{ key: 'RouteB', routeName: 'Bar', params: {} },
|
||||
],
|
||||
};
|
||||
const state = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
params: { name: 'NewParam' },
|
||||
key: 'RouteA',
|
||||
},
|
||||
initialState
|
||||
);
|
||||
expect(state.index).toEqual(1);
|
||||
expect(state.routes[0].params).toEqual({
|
||||
name: 'NewParam',
|
||||
other: 'Unchanged',
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles the setParams action with nested routers', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
const GrandChildNavigator = () => <div />;
|
||||
GrandChildNavigator.router = StackRouter({
|
||||
Quux: { screen: () => <div /> },
|
||||
Corge: { screen: () => <div /> },
|
||||
});
|
||||
ChildNavigator.router = TabRouter({
|
||||
Baz: { screen: GrandChildNavigator },
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
@@ -1006,10 +1228,10 @@ describe('StackRouter', () => {
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
|
||||
expect(state2 && state2.routes[0].routes).toEqual([
|
||||
{
|
||||
key: 'id-0',
|
||||
routeName: 'Quux',
|
||||
routeName: 'Baz',
|
||||
params: { name: 'foobar' },
|
||||
},
|
||||
]);
|
||||
@@ -1027,7 +1249,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1062,7 +1284,7 @@ describe('StackRouter', () => {
|
||||
});
|
||||
const state1 = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const resetAction = {
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: 'Bad Key',
|
||||
actions: [
|
||||
{
|
||||
@@ -1095,7 +1317,7 @@ describe('StackRouter', () => {
|
||||
});
|
||||
|
||||
test('Handles the reset action with nested Router', () => {
|
||||
const ChildRouter = TabRouter({
|
||||
const ChildRouter = StackRouter({
|
||||
baz: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
@@ -1115,7 +1337,8 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: null,
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1167,7 +1390,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: 'Init',
|
||||
actions: [
|
||||
{
|
||||
@@ -1182,7 +1405,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: null,
|
||||
actions: [
|
||||
{
|
||||
@@ -1227,6 +1450,34 @@ describe('StackRouter', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('Navigate action to previous nested StackRouter causes isTransitioning start', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Bar: { screen: ChildNavigator },
|
||||
Foo: { screen: () => <div /> },
|
||||
});
|
||||
const state = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
immediate: true,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
router.getStateForAction({ type: NavigationActions.INIT })
|
||||
);
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(0);
|
||||
expect(state2.isTransitioning).toEqual(true);
|
||||
});
|
||||
|
||||
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
|
||||
const state = TestStackRouter.getStateForAction({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1347,42 +1598,6 @@ describe('StackRouter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles the navigate action with params and nested TabRouter', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = TabRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Boo: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
});
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
immediate: true,
|
||||
routeName: 'Bar',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.routes[1].params).toEqual({ foo: '42' });
|
||||
expect(state2 && state2.routes[1].routes).toEqual([
|
||||
{
|
||||
key: 'Baz',
|
||||
routeName: 'Baz',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
{
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Handles empty URIs', () => {
|
||||
const router = StackRouter(
|
||||
{
|
||||
@@ -1654,7 +1869,7 @@ test('Handles deep navigate completion action', () => {
|
||||
expect(!!key).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.COMPLETE_TRANSITION,
|
||||
type: StackActions.COMPLETE_TRANSITION,
|
||||
},
|
||||
state2
|
||||
);
|
||||
@@ -1663,3 +1878,120 @@ test('Handles deep navigate completion action', () => {
|
||||
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,
|
||||
Baz: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Baz',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('Baz');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3.routes[state3.index].routeName).toEqual('Baz');
|
||||
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
|
||||
|
||||
const state5 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state4
|
||||
);
|
||||
let activeState5 = state5.routes[state5.index];
|
||||
expect(activeState5.routeName).toEqual('NestedStack');
|
||||
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
const OtherNestedStack = () => <div />;
|
||||
|
||||
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
|
||||
let otherNestedRouter = StackRouter({ Foo: Screen });
|
||||
NestedStack.router = nestedRouter;
|
||||
OtherNestedStack.router = otherNestedRouter;
|
||||
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
OtherNestedStack,
|
||||
Bar: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'OtherNestedStack',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'NestedStack',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
252
src/routers/__tests__/SwitchRouter-test.js
Normal file
252
src/routers/__tests__/SwitchRouter-test.js
Normal file
@@ -0,0 +1,252 @@
|
||||
/* eslint react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
import SwitchRouter from '../SwitchRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
describe('SwitchRouter', () => {
|
||||
test('resets the route when unfocusing a tab by default', () => {
|
||||
const router = getExampleRouter();
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
|
||||
state
|
||||
);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].routes.length).toEqual(2);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.routes[0].index).toEqual(0);
|
||||
expect(state3.routes[0].routes.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('does not reset the route on unfocus if resetOnBlur is false', () => {
|
||||
const router = getExampleRouter({ resetOnBlur: false });
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
|
||||
state
|
||||
);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].routes.length).toEqual(2);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.routes[0].index).toEqual(1);
|
||||
expect(state3.routes[0].routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('ignores back by default', () => {
|
||||
const router = getExampleRouter();
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.index).toEqual(1);
|
||||
});
|
||||
|
||||
test('handles back if given a backBehavior', () => {
|
||||
const router = getExampleRouter({ backBehavior: 'initialRoute' });
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('paths option on SwitchRouter overrides path from route config', () => {
|
||||
const router = getExampleRouter({ paths: { A: 'overridden' } });
|
||||
const action = router.getActionForPathAndParams('overridden', {});
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('A');
|
||||
});
|
||||
|
||||
test('provides correct action for getActionForPathAndParams', () => {
|
||||
const router = getExampleRouter({ backBehavior: 'initialRoute' });
|
||||
const action = router.getActionForPathAndParams('A1', { foo: 'bar' });
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('A1');
|
||||
|
||||
const action1 = router.getActionForPathAndParams('', {});
|
||||
expect(action1.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action1.routeName).toEqual('A');
|
||||
|
||||
const action2 = router.getActionForPathAndParams(null, {});
|
||||
expect(action2.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action2.routeName).toEqual('A');
|
||||
|
||||
const action3 = router.getActionForPathAndParams('great/path', {
|
||||
foo: 'baz',
|
||||
});
|
||||
expect(action3).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B',
|
||||
params: { foo: 'baz' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B1',
|
||||
params: { foo: 'baz' },
|
||||
},
|
||||
});
|
||||
|
||||
const action4 = router.getActionForPathAndParams('great/path/B2', {
|
||||
foo: 'baz',
|
||||
});
|
||||
expect(action4).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B',
|
||||
params: { foo: 'baz' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B2',
|
||||
params: { foo: 'baz' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested switchrouters', () => {
|
||||
// router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
|
||||
// if we are focused on Other and navigate to Bar, what should happen?
|
||||
|
||||
const Screen = () => <div />;
|
||||
const NestedSwitch = () => <div />;
|
||||
const OtherNestedSwitch = () => <div />;
|
||||
|
||||
let nestedRouter = SwitchRouter({ Foo: Screen, Bar: Screen });
|
||||
let otherNestedRouter = SwitchRouter({ Foo: Screen });
|
||||
NestedSwitch.router = nestedRouter;
|
||||
OtherNestedSwitch.router = otherNestedRouter;
|
||||
|
||||
let router = SwitchRouter(
|
||||
{
|
||||
NestedSwitch,
|
||||
OtherNestedSwitch,
|
||||
Bar: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'OtherNestedSwitch',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedSwitch');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'NestedSwitch',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedSwitch');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
// https://github.com/react-navigation/react-navigation.github.io/issues/117#issuecomment-385597628
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const MainStack = () => <div />;
|
||||
const LoginStack = () => <div />;
|
||||
MainStack.router = StackRouter({ Home: Screen, Profile: Screen });
|
||||
LoginStack.router = StackRouter({ Form: Screen, ForgotPassword: Screen });
|
||||
|
||||
let router = SwitchRouter(
|
||||
{
|
||||
Home: Screen,
|
||||
Login: LoginStack,
|
||||
Main: MainStack,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Login',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('Login');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Home',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Home');
|
||||
});
|
||||
});
|
||||
|
||||
const getExampleRouter = (config = {}) => {
|
||||
const PlainScreen = () => <div />;
|
||||
const StackA = () => <div />;
|
||||
const StackB = () => <div />;
|
||||
|
||||
StackA.router = StackRouter({
|
||||
A1: PlainScreen,
|
||||
A2: PlainScreen,
|
||||
});
|
||||
|
||||
StackB.router = StackRouter({
|
||||
B1: PlainScreen,
|
||||
B2: PlainScreen,
|
||||
});
|
||||
|
||||
const router = SwitchRouter(
|
||||
{
|
||||
A: {
|
||||
screen: StackA,
|
||||
path: '',
|
||||
},
|
||||
B: {
|
||||
screen: StackB,
|
||||
path: 'great/path',
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'A',
|
||||
...config,
|
||||
}
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import TabRouter from '../TabRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
@@ -140,6 +140,46 @@ describe('TabRouter', () => {
|
||||
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
|
||||
});
|
||||
|
||||
test('Handles the SetParams action for inactive routes', () => {
|
||||
const router = TabRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Bar',
|
||||
}
|
||||
);
|
||||
const initialState = {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
key: 'RouteA',
|
||||
routeName: 'Foo',
|
||||
params: { name: 'InitialParam', other: 'Unchanged' },
|
||||
},
|
||||
{ key: 'RouteB', routeName: 'Bar', params: {} },
|
||||
],
|
||||
};
|
||||
const state = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
params: { name: 'NewParam' },
|
||||
key: 'RouteA',
|
||||
},
|
||||
initialState
|
||||
);
|
||||
expect(state.index).toEqual(1);
|
||||
expect(state.routes[0].params).toEqual({
|
||||
name: 'NewParam',
|
||||
other: 'Unchanged',
|
||||
});
|
||||
});
|
||||
|
||||
test('getStateForAction returns null when navigating to same tab', () => {
|
||||
const router = TabRouter(
|
||||
{ Foo: BareLeafRouteConfig, Bar: BareLeafRouteConfig },
|
||||
@@ -181,6 +221,7 @@ describe('TabRouter', () => {
|
||||
const navAction = {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
params: { foo: '42' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
@@ -351,7 +392,7 @@ describe('TabRouter', () => {
|
||||
});
|
||||
const MidNavigator = () => <div />;
|
||||
MidNavigator.router = TabRouter({
|
||||
Foo: { screen: ChildNavigator0 },
|
||||
Fee: { screen: ChildNavigator0 },
|
||||
Bar: { screen: ChildNavigator1 },
|
||||
});
|
||||
const router = TabRouter({
|
||||
@@ -371,8 +412,8 @@ describe('TabRouter', () => {
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
key: 'Fee',
|
||||
routeName: 'Fee',
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
@@ -410,8 +451,8 @@ describe('TabRouter', () => {
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
key: 'Fee',
|
||||
routeName: 'Fee',
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
@@ -444,7 +485,10 @@ describe('TabRouter', () => {
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
action: { type: NavigationActions.NAVIGATE, routeName: 'Zap' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Zap',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(state4).toEqual({
|
||||
@@ -459,8 +503,8 @@ describe('TabRouter', () => {
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
key: 'Fee',
|
||||
routeName: 'Fee',
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
@@ -690,56 +734,15 @@ describe('TabRouter', () => {
|
||||
expect(state2).toEqual(state0);
|
||||
});
|
||||
|
||||
test('pop action works as expected', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 3,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'A', routeName: 'foo' },
|
||||
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
|
||||
{ key: 'C', routeName: 'foo' },
|
||||
{ key: 'D', routeName: 'bar' },
|
||||
],
|
||||
};
|
||||
const poppedState = TestRouter.getStateForAction(
|
||||
NavigationActions.pop(),
|
||||
state
|
||||
);
|
||||
expect(poppedState.routes.length).toBe(3);
|
||||
expect(poppedState.index).toBe(2);
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.pop({ n: 2, immediate: true }),
|
||||
state
|
||||
);
|
||||
expect(poppedState2.routes.length).toBe(2);
|
||||
expect(poppedState2.index).toBe(1);
|
||||
expect(poppedState2.isTransitioning).toBe(false);
|
||||
|
||||
const poppedState3 = TestRouter.getStateForAction(
|
||||
NavigationActions.pop({ n: 5 }),
|
||||
state
|
||||
);
|
||||
expect(poppedState3.routes.length).toBe(1);
|
||||
expect(poppedState3.index).toBe(0);
|
||||
expect(poppedState3.isTransitioning).toBe(true);
|
||||
});
|
||||
|
||||
test('Inner actions are only unpacked if the current tab matches', () => {
|
||||
const PlainScreen = () => <div />;
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenB.router = StackRouter({
|
||||
ScreenB.router = TabRouter({
|
||||
Baz: { screen: PlainScreen },
|
||||
Zoo: { screen: PlainScreen },
|
||||
});
|
||||
ScreenA.router = StackRouter({
|
||||
ScreenA.router = TabRouter({
|
||||
Bar: { screen: PlainScreen },
|
||||
Boo: { screen: ScreenB },
|
||||
});
|
||||
@@ -748,10 +751,10 @@ describe('TabRouter', () => {
|
||||
});
|
||||
const screenApreState = {
|
||||
index: 0,
|
||||
key: 'Init',
|
||||
key: 'Foo',
|
||||
isTransitioning: false,
|
||||
routeName: 'Foo',
|
||||
routes: [{ key: 'Init', routeName: 'Bar' }],
|
||||
routes: [{ key: 'Bar', routeName: 'Bar' }],
|
||||
};
|
||||
const preState = {
|
||||
index: 0,
|
||||
@@ -777,7 +780,6 @@ describe('TabRouter', () => {
|
||||
routeName: 'Boo',
|
||||
action: NavigationActions.navigate({ routeName: 'Zoo' }),
|
||||
});
|
||||
|
||||
const expectedState = ScreenA.router.getStateForAction(
|
||||
action,
|
||||
screenApreState
|
||||
@@ -785,8 +787,25 @@ describe('TabRouter', () => {
|
||||
const state = router.getStateForAction(action, preState);
|
||||
const innerState = state ? state.routes[0] : state;
|
||||
|
||||
expect(innerState.routes[1].index).toEqual(1);
|
||||
expect(expectedState && comparable(expectedState)).toEqual(
|
||||
innerState && comparable(innerState)
|
||||
);
|
||||
|
||||
const noMatchAction = NavigationActions.navigate({
|
||||
routeName: 'Qux',
|
||||
action: NavigationActions.navigate({ routeName: 'Zoo' }),
|
||||
});
|
||||
const expectedState2 = ScreenA.router.getStateForAction(
|
||||
noMatchAction,
|
||||
screenApreState
|
||||
);
|
||||
const state2 = router.getStateForAction(noMatchAction, preState);
|
||||
const innerState2 = state2 ? state2.routes[0] : state2;
|
||||
|
||||
expect(innerState2.routes[1].index).toEqual(0);
|
||||
expect(expectedState2 && comparable(expectedState2)).toEqual(
|
||||
innerState2 && comparable(innerState2)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`validateRouteConfigMap Fails if both screen and getScreen are defined 1`] = `"Route 'Home' should declare a screen or a getScreen, not both."`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on bad object 1`] = `
|
||||
"The component for route 'Home' must be a React component. For example:
|
||||
|
||||
import MyScreen from './MyScreen';
|
||||
...
|
||||
Home: MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
import MyNavigator from './MyNavigator';
|
||||
...
|
||||
Home: MyNavigator,
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on empty bare screen 1`] = `
|
||||
"The component for route 'Home' must be a React component. For example:
|
||||
|
||||
import MyScreen from './MyScreen';
|
||||
...
|
||||
Home: MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
import MyNavigator from './MyNavigator';
|
||||
...
|
||||
Home: MyNavigator,
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on empty config 1`] = `"Please specify at least one route when configuring a navigator."`;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component } from 'react';
|
||||
import createConfigGetter from '../createConfigGetter';
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
|
||||
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
|
||||
remove: () => {},
|
||||
@@ -67,81 +66,81 @@ test('should get config for screen', () => {
|
||||
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[0],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('Welcome anonymous');
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[1],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('Welcome jane');
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[0],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).gesturesEnabled
|
||||
).toEqual(true);
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[2],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('Settings!!!');
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[2],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).gesturesEnabled
|
||||
).toEqual(false);
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[3],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('10 new notifications');
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[3],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).gesturesEnabled
|
||||
).toEqual(true);
|
||||
expect(
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[4],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).gesturesEnabled
|
||||
).toEqual(false);
|
||||
@@ -164,11 +163,11 @@ test('should throw if the route does not exist', () => {
|
||||
|
||||
expect(() =>
|
||||
getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[0],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
)
|
||||
).toThrowError(
|
||||
|
||||
@@ -13,9 +13,19 @@ ProfileNavigator.router = StackRouter({
|
||||
});
|
||||
|
||||
describe('validateRouteConfigMap', () => {
|
||||
test('Fails on empty bare screen', () => {
|
||||
const invalidMap = {
|
||||
Home: undefined,
|
||||
};
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails on empty config', () => {
|
||||
const invalidMap = {};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails on bad object', () => {
|
||||
const invalidMap = {
|
||||
@@ -23,7 +33,9 @@ describe('validateRouteConfigMap', () => {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails if both screen and getScreen are defined', () => {
|
||||
const invalidMap = {
|
||||
@@ -32,15 +44,17 @@ describe('validateRouteConfigMap', () => {
|
||||
getScreen: () => ListScreen,
|
||||
},
|
||||
};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Succeeds on a valid config', () => {
|
||||
const invalidMap = {
|
||||
const validMap = {
|
||||
Home: {
|
||||
screen: ProfileNavigator,
|
||||
},
|
||||
Chat: ListScreen,
|
||||
};
|
||||
validateRouteConfigMap(invalidMap);
|
||||
validateRouteConfigMap(validMap);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
import validateScreenOptions from './validateScreenOptions';
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
|
||||
function applyConfig(configurer, navigationOptions, configProps) {
|
||||
if (typeof configurer === 'function') {
|
||||
@@ -38,40 +36,15 @@ export default (routeConfigs, navigatorScreenConfig) => (
|
||||
|
||||
const Component = getScreenForRouteName(routeConfigs, route.routeName);
|
||||
|
||||
let outputConfig = {};
|
||||
|
||||
const router = Component.router;
|
||||
if (router) {
|
||||
const { routes, index } = route;
|
||||
if (!route || !routes || index == null) {
|
||||
throw new Error(
|
||||
`Expect nav state to have routes and index, ${JSON.stringify(route)}`
|
||||
);
|
||||
}
|
||||
const childRoute = routes[index];
|
||||
const childNavigation = addNavigationHelpers({
|
||||
state: childRoute,
|
||||
dispatch,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
childRoute.key
|
||||
),
|
||||
});
|
||||
outputConfig = router.getScreenOptions(childNavigation, screenProps);
|
||||
}
|
||||
|
||||
const routeConfig = routeConfigs[route.routeName];
|
||||
|
||||
const routeScreenConfig = routeConfig.navigationOptions;
|
||||
const routeScreenConfig =
|
||||
routeConfig === Component ? null : routeConfig.navigationOptions;
|
||||
const componentScreenConfig = Component.navigationOptions;
|
||||
|
||||
const configOptions = { navigation, screenProps: screenProps || {} };
|
||||
|
||||
outputConfig = applyConfig(
|
||||
navigatorScreenConfig,
|
||||
outputConfig,
|
||||
configOptions
|
||||
);
|
||||
let outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions);
|
||||
outputConfig = applyConfig(
|
||||
componentScreenConfig,
|
||||
outputConfig,
|
||||
|
||||
46
src/routers/getNavigationActionCreators.js
Normal file
46
src/routers/getNavigationActionCreators.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
const getNavigationActionCreators = route => {
|
||||
return {
|
||||
goBack: key => {
|
||||
let actualizedKey = key;
|
||||
if (key === undefined && route.key) {
|
||||
invariant(typeof route.key === 'string', 'key should be a string');
|
||||
actualizedKey = route.key;
|
||||
}
|
||||
return NavigationActions.back({ key: actualizedKey });
|
||||
},
|
||||
navigate: (navigateTo, params, action) => {
|
||||
if (typeof navigateTo === 'string') {
|
||||
return NavigationActions.navigate({
|
||||
routeName: navigateTo,
|
||||
params,
|
||||
action,
|
||||
});
|
||||
}
|
||||
invariant(
|
||||
typeof navigateTo === 'object',
|
||||
'Must navigateTo an object or a string'
|
||||
);
|
||||
invariant(
|
||||
params == null,
|
||||
'Params must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
action == null,
|
||||
'Child action must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
return NavigationActions.navigate(navigateTo);
|
||||
},
|
||||
setParams: params => {
|
||||
invariant(
|
||||
route.key && typeof route.key === 'string',
|
||||
'setParams cannot be called by root navigator'
|
||||
);
|
||||
return NavigationActions.setParams({ params, key: route.key });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getNavigationActionCreators;
|
||||
@@ -1,7 +0,0 @@
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
export default () =>
|
||||
invariant(
|
||||
false,
|
||||
'`getScreenConfig` has been replaced with `getScreenOptions`'
|
||||
);
|
||||
@@ -13,16 +13,13 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
|
||||
routeNames.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
|
||||
const screenComponent = routeConfig.screen
|
||||
? routeConfig.screen
|
||||
: routeConfig;
|
||||
const screenComponent = getScreenComponent(routeConfig);
|
||||
|
||||
if (
|
||||
screenComponent &&
|
||||
typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen
|
||||
!screenComponent ||
|
||||
(typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen)
|
||||
) {
|
||||
throw new Error(
|
||||
`The component for route '${routeName}' must be a ` +
|
||||
@@ -48,4 +45,12 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
});
|
||||
}
|
||||
|
||||
function getScreenComponent(routeConfig) {
|
||||
if (!routeConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return routeConfig.screen ? routeConfig.screen : routeConfig;
|
||||
}
|
||||
|
||||
export default validateRouteConfigMap;
|
||||
|
||||
3
src/utils/docsUrl.js
Normal file
3
src/utils/docsUrl.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function docsUrl(path) {
|
||||
return `https://v2.reactnavigation.org/docs/${path}`;
|
||||
}
|
||||
8
src/utils/withDefaultValue.js
Normal file
8
src/utils/withDefaultValue.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default (obj, key, defaultValue) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj[key] = defaultValue;
|
||||
return obj;
|
||||
};
|
||||
@@ -1,501 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
PanResponder,
|
||||
Platform,
|
||||
View,
|
||||
I18nManager,
|
||||
Easing,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './Card';
|
||||
import Header from '../Header/Header';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import getChildEventSubscriber from '../../getChildEventSubscriber';
|
||||
import SceneView from '../SceneView';
|
||||
|
||||
import TransitionConfigs from './TransitionConfigs';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
|
||||
const EaseInOut = Easing.inOut(Easing.ease);
|
||||
|
||||
/**
|
||||
* The max duration of the card animation in milliseconds after released gesture.
|
||||
* The actual duration should be always less then that because the rest distance
|
||||
* is always less then the full distance of the layout.
|
||||
*/
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
/**
|
||||
* The gesture distance threshold to trigger the back behavior. For instance,
|
||||
* `1/2` means that moving greater than 1/2 of the width of the screen will
|
||||
* trigger a back action
|
||||
*/
|
||||
const POSITION_THRESHOLD = 1 / 2;
|
||||
|
||||
/**
|
||||
* The threshold (in pixels) to start the gesture action.
|
||||
*/
|
||||
const RESPOND_THRESHOLD = 20;
|
||||
|
||||
/**
|
||||
* The distance of touch start from the edge of the screen where the gesture will be recognized
|
||||
*/
|
||||
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
|
||||
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
||||
|
||||
const animatedSubscribeValue = animatedValue => {
|
||||
if (!animatedValue.__isNative) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(animatedValue._listeners).length === 0) {
|
||||
animatedValue.addListener(emptyFunction);
|
||||
}
|
||||
};
|
||||
|
||||
class CardStack extends React.Component {
|
||||
/**
|
||||
* Used to identify the starting point of the position when the gesture starts, such that it can
|
||||
* be updated according to its relative position. This means that a card can effectively be
|
||||
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
|
||||
* corresponding location for the touch.
|
||||
*/
|
||||
_gestureStartValue = 0;
|
||||
|
||||
// tracks if a touch is currently happening
|
||||
_isResponding = false;
|
||||
|
||||
/**
|
||||
* immediateIndex is used to represent the expected index that we will be on after a
|
||||
* transition. To achieve a smooth animation when swiping back, the action to go back
|
||||
* doesn't actually fire until the transition completes. The immediateIndex is used during
|
||||
* the transition so that gestures can be handled correctly. This is a work-around for
|
||||
* cases when the user quickly swipes back several times.
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
_screenDetails = {};
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.screenProps !== this.props.screenProps) {
|
||||
this._screenDetails = {};
|
||||
}
|
||||
props.transitionProps.scenes.forEach(newScene => {
|
||||
if (
|
||||
this._screenDetails[newScene.key] &&
|
||||
this._screenDetails[newScene.key].state !== newScene.route
|
||||
) {
|
||||
this._screenDetails[newScene.key] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getScreenDetails = scene => {
|
||||
const { screenProps, transitionProps: { navigation }, router } = this.props;
|
||||
let screenDetails = this._screenDetails[scene.key];
|
||||
if (!screenDetails || screenDetails.state !== scene.route) {
|
||||
const screenNavigation = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: scene.route,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
scene.route.key
|
||||
),
|
||||
});
|
||||
screenDetails = {
|
||||
state: scene.route,
|
||||
navigation: screenNavigation,
|
||||
options: router.getScreenOptions(screenNavigation, screenProps),
|
||||
};
|
||||
this._screenDetails[scene.key] = screenDetails;
|
||||
}
|
||||
return screenDetails;
|
||||
};
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
const { header } = this._getScreenDetails(scene).options;
|
||||
|
||||
if (typeof header !== 'undefined' && typeof header !== 'function') {
|
||||
return header;
|
||||
}
|
||||
|
||||
const renderHeader = header || (props => <Header {...props} />);
|
||||
const {
|
||||
headerLeftInterpolator,
|
||||
headerTitleInterpolator,
|
||||
headerRightInterpolator,
|
||||
} = this._getTransitionConfig();
|
||||
|
||||
const {
|
||||
mode,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
return renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
getScreenDetails: this._getScreenDetails,
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_animatedSubscribe(props) {
|
||||
// Hack to make this work with native driven animations. We add a single listener
|
||||
// so the JS value of the following animated values gets updated. We rely on
|
||||
// some Animated private APIs and not doing so would require using a bunch of
|
||||
// value listeners but we'd have to remove them to not leak and I'm not sure
|
||||
// when we'd do that with the current structure we have. `stopAnimation` callback
|
||||
// is also broken with native animated values that have no listeners so if we
|
||||
// want to remove this we have to fix this too.
|
||||
animatedSubscribeValue(props.transitionProps.layout.width);
|
||||
animatedSubscribeValue(props.transitionProps.layout.height);
|
||||
animatedSubscribeValue(props.transitionProps.position);
|
||||
}
|
||||
|
||||
_reset(resetToIndex, duration) {
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
) {
|
||||
Animated.spring(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
stiffness: 5000,
|
||||
damping: 600,
|
||||
mass: 3,
|
||||
useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
}).start();
|
||||
} else {
|
||||
Animated.timing(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
duration,
|
||||
easing: EaseInOut,
|
||||
useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
_goBack(backFromIndex, duration) {
|
||||
const { navigation, position, scenes } = this.props.transitionProps;
|
||||
const toValue = Math.max(backFromIndex - 1, 0);
|
||||
|
||||
// set temporary index for gesture handler to respect until the action is
|
||||
// dispatched at the end of the transition.
|
||||
this._immediateIndex = toValue;
|
||||
|
||||
const onCompleteAnimation = () => {
|
||||
this._immediateIndex = null;
|
||||
const backFromScene = scenes.find(s => s.index === toValue + 1);
|
||||
if (!this._isResponding && backFromScene) {
|
||||
navigation.dispatch(
|
||||
NavigationActions.back({
|
||||
key: backFromScene.route.key,
|
||||
immediate: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
) {
|
||||
Animated.spring(position, {
|
||||
toValue,
|
||||
stiffness: 5000,
|
||||
damping: 600,
|
||||
mass: 3,
|
||||
useNativeDriver: position.__isNative,
|
||||
}).start(onCompleteAnimation);
|
||||
} else {
|
||||
Animated.timing(position, {
|
||||
toValue,
|
||||
duration,
|
||||
easing: EaseInOut,
|
||||
useNativeDriver: position.__isNative,
|
||||
}).start(onCompleteAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'float') {
|
||||
floatingHeader = this._renderHeader(
|
||||
this.props.transitionProps.scene,
|
||||
headerMode
|
||||
);
|
||||
}
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene, scenes },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = this._getScreenDetails(scene);
|
||||
const gestureDirectionInverted = options.gestureDirection === 'inverted';
|
||||
|
||||
const gesturesEnabled =
|
||||
typeof options.gesturesEnabled === 'boolean'
|
||||
? options.gesturesEnabled
|
||||
: Platform.OS === 'ios';
|
||||
|
||||
const responder = !gesturesEnabled
|
||||
? null
|
||||
: PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
this._isResponding = false;
|
||||
this._reset(index, 0);
|
||||
},
|
||||
onPanResponderGrant: () => {
|
||||
position.stopAnimation(value => {
|
||||
this._isResponding = true;
|
||||
this._gestureStartValue = value;
|
||||
});
|
||||
},
|
||||
onMoveShouldSetPanResponder: (event, gesture) => {
|
||||
if (index !== scene.index) {
|
||||
return false;
|
||||
}
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
|
||||
const currentDragPosition =
|
||||
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
|
||||
const axisLength = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const axisHasBeenMeasured = !!axisLength;
|
||||
|
||||
// Measure the distance from the touch to the edge of the screen
|
||||
const screenEdgeDistance = gestureDirectionInverted
|
||||
? axisLength - (currentDragPosition - currentDragDistance)
|
||||
: currentDragPosition - currentDragDistance;
|
||||
// Compare to the gesture distance relavant to card or modal
|
||||
const {
|
||||
gestureResponseDistance: userGestureResponseDistance = {},
|
||||
} = this._getScreenDetails(scene).options;
|
||||
const gestureResponseDistance = isVertical
|
||||
? userGestureResponseDistance.vertical ||
|
||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
||||
: userGestureResponseDistance.horizontal ||
|
||||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
||||
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
|
||||
if (screenEdgeDistance > gestureResponseDistance) {
|
||||
// Reject touches that started in the middle of the screen
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasDraggedEnough =
|
||||
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
|
||||
|
||||
const isOnFirstCard = immediateIndex === 0;
|
||||
const shouldSetResponder =
|
||||
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
|
||||
return shouldSetResponder;
|
||||
},
|
||||
onPanResponderMove: (event, gesture) => {
|
||||
// Handle the moving touches for our granted responder
|
||||
const startValue = this._gestureStartValue;
|
||||
const axis = isVertical ? 'dy' : 'dx';
|
||||
const axisDistance = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const currentValue =
|
||||
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
|
||||
? startValue + gesture[axis] / axisDistance
|
||||
: startValue - gesture[axis] / axisDistance;
|
||||
const value = clamp(index - 1, currentValue, index);
|
||||
position.setValue(value);
|
||||
},
|
||||
onPanResponderTerminationRequest: () =>
|
||||
// Returning false will prevent other views from becoming responder while
|
||||
// the navigation view is the responder (mid-gesture)
|
||||
false,
|
||||
onPanResponderRelease: (event, gesture) => {
|
||||
if (!this._isResponding) {
|
||||
return;
|
||||
}
|
||||
this._isResponding = false;
|
||||
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
|
||||
// Calculate animate duration according to gesture speed and moved distance
|
||||
const axisDistance = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const movementDirection = gestureDirectionInverted ? -1 : 1;
|
||||
const movedDistance =
|
||||
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
|
||||
const gestureVelocity =
|
||||
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
|
||||
const defaultVelocity = axisDistance / ANIMATION_DURATION;
|
||||
const velocity = Math.max(
|
||||
Math.abs(gestureVelocity),
|
||||
defaultVelocity
|
||||
);
|
||||
const resetDuration = gestureDirectionInverted
|
||||
? (axisDistance - movedDistance) / velocity
|
||||
: movedDistance / velocity;
|
||||
const goBackDuration = gestureDirectionInverted
|
||||
? movedDistance / velocity
|
||||
: (axisDistance - movedDistance) / velocity;
|
||||
|
||||
// To asyncronously get the current animated value, we need to run stopAnimation:
|
||||
position.stopAnimation(value => {
|
||||
// If the speed of the gesture release is significant, use that as the indication
|
||||
// of intent
|
||||
if (gestureVelocity < -0.5) {
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
return;
|
||||
}
|
||||
if (gestureVelocity > 0.5) {
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
|
||||
// and the back will happen.
|
||||
if (value <= index - POSITION_THRESHOLD) {
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
} else {
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handlers = gesturesEnabled ? responder.panHandlers : {};
|
||||
const containerStyle = [
|
||||
styles.container,
|
||||
this._getTransitionConfig().containerStyle,
|
||||
];
|
||||
|
||||
return (
|
||||
<View {...handlers} style={containerStyle}>
|
||||
<View style={styles.scenes}>
|
||||
{scenes.map(s => this._renderCard(s))}
|
||||
</View>
|
||||
{floatingHeader}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_getHeaderMode() {
|
||||
if (this.props.headerMode) {
|
||||
return this.props.headerMode;
|
||||
}
|
||||
if (Platform.OS === 'android' || this.props.mode === 'modal') {
|
||||
return 'screen';
|
||||
}
|
||||
return 'float';
|
||||
}
|
||||
|
||||
_getHeaderTransitionPreset() {
|
||||
// On Android or with header mode screen, we always just use in-place,
|
||||
// we ignore the option entirely (at least until we have other presets)
|
||||
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
// TODO: validations: 'fade-in-place' or 'uikit' are valid
|
||||
if (this.props.headerTransitionPreset) {
|
||||
return this.props.headerTransitionPreset;
|
||||
} else {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
}
|
||||
|
||||
_renderInnerScene(SceneComponent, scene) {
|
||||
const { navigation } = this._getScreenDetails(scene);
|
||||
const { screenProps } = this.props;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
</View>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getTransitionConfig = () => {
|
||||
const isModal = this.props.mode === 'modal';
|
||||
|
||||
return TransitionConfigs.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
this.props.transitionProps,
|
||||
this.props.prevTransitionProps,
|
||||
isModal
|
||||
);
|
||||
};
|
||||
|
||||
_renderCard = scene => {
|
||||
const { screenInterpolator } = this._getTransitionConfig();
|
||||
const style =
|
||||
screenInterpolator &&
|
||||
screenInterpolator({ ...this.props.transitionProps, scene });
|
||||
|
||||
const SceneComponent = this.props.router.getComponentForRouteName(
|
||||
scene.route.routeName
|
||||
);
|
||||
|
||||
const { transitionProps, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...props}
|
||||
{...transitionProps}
|
||||
key={`card_${scene.key}`}
|
||||
style={[style, this.props.cardStyle]}
|
||||
scene={scene}
|
||||
>
|
||||
{this._renderInnerScene(SceneComponent, scene)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
// Header is physically rendered after scenes so that Header won't be
|
||||
// covered by the shadows of the scenes.
|
||||
// That said, we'd have use `flexDirection: 'column-reverse'` to move
|
||||
// Header above the scenes.
|
||||
flexDirection: 'column-reverse',
|
||||
},
|
||||
scenes: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default CardStack;
|
||||
@@ -1,82 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import CardStack from './CardStack';
|
||||
import CardStackStyleInterpolator from './CardStackStyleInterpolator';
|
||||
import Transitioner from '../Transitioner';
|
||||
import TransitionConfigs from './TransitionConfigs';
|
||||
|
||||
const NativeAnimatedModule =
|
||||
NativeModules && NativeModules.NativeAnimatedModule;
|
||||
|
||||
class CardStackTransitioner extends React.Component {
|
||||
static defaultProps = {
|
||||
mode: 'card',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Transitioner
|
||||
configureTransition={this._configureTransition}
|
||||
navigation={this.props.navigation}
|
||||
render={this._render}
|
||||
onTransitionStart={this.props.onTransitionStart}
|
||||
onTransitionEnd={this.props.onTransitionEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_configureTransition = (
|
||||
// props for the new screen
|
||||
transitionProps,
|
||||
// props for the old screen
|
||||
prevTransitionProps
|
||||
) => {
|
||||
const isModal = this.props.mode === 'modal';
|
||||
// Copy the object so we can assign useNativeDriver below
|
||||
const transitionSpec = {
|
||||
...TransitionConfigs.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
).transitionSpec,
|
||||
};
|
||||
if (
|
||||
!!NativeAnimatedModule &&
|
||||
// Native animation support also depends on the transforms used:
|
||||
CardStackStyleInterpolator.canUseNativeDriver()
|
||||
) {
|
||||
// Internal undocumented prop
|
||||
transitionSpec.useNativeDriver = true;
|
||||
}
|
||||
return transitionSpec;
|
||||
};
|
||||
|
||||
_render = (props, prevProps) => {
|
||||
const {
|
||||
screenProps,
|
||||
headerMode,
|
||||
headerTransitionPreset,
|
||||
mode,
|
||||
router,
|
||||
cardStyle,
|
||||
transitionConfig,
|
||||
} = this.props;
|
||||
return (
|
||||
<CardStack
|
||||
screenProps={screenProps}
|
||||
headerMode={headerMode}
|
||||
headerTransitionPreset={headerTransitionPreset}
|
||||
mode={mode}
|
||||
router={router}
|
||||
cardStyle={cardStyle}
|
||||
transitionConfig={transitionConfig}
|
||||
transitionProps={props}
|
||||
prevTransitionProps={prevProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default CardStackTransitioner;
|
||||
@@ -21,6 +21,8 @@ const DrawerNavigatorItems = ({
|
||||
itemsContainerStyle,
|
||||
itemStyle,
|
||||
labelStyle,
|
||||
activeLabelStyle,
|
||||
inactiveLabelStyle,
|
||||
iconContainerStyle,
|
||||
drawerPosition,
|
||||
}) => (
|
||||
@@ -34,6 +36,7 @@ const DrawerNavigatorItems = ({
|
||||
const scene = { route, index, focused, tintColor: color };
|
||||
const icon = renderIcon(scene);
|
||||
const label = getLabel(scene);
|
||||
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
|
||||
return (
|
||||
<TouchableItem
|
||||
key={route.key}
|
||||
@@ -63,7 +66,9 @@ const DrawerNavigatorItems = ({
|
||||
</View>
|
||||
) : null}
|
||||
{typeof label === 'string' ? (
|
||||
<Text style={[styles.label, { color }, labelStyle]}>
|
||||
<Text
|
||||
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import SceneView from '../SceneView';
|
||||
import withCachedChildNavigation from '../../withCachedChildNavigation';
|
||||
|
||||
/**
|
||||
* Component that renders the child screen of the drawer.
|
||||
*/
|
||||
class DrawerScreen extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
router,
|
||||
navigation,
|
||||
childNavigationProps,
|
||||
screenProps,
|
||||
} = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
const childNavigation = childNavigationProps[routes[index].key];
|
||||
const Content = router.getComponentForRouteName(routes[index].routeName);
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
component={Content}
|
||||
navigation={childNavigation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withCachedChildNavigation(DrawerScreen);
|
||||
@@ -1,33 +1,22 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import withCachedChildNavigation from '../../withCachedChildNavigation';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
/**
|
||||
* Component that renders the sidebar screen of the drawer.
|
||||
*/
|
||||
|
||||
class DrawerSidebar extends React.PureComponent {
|
||||
_getScreenOptions = routeKey => {
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
'DrawerClose'
|
||||
);
|
||||
const descriptor = this.props.descriptors[routeKey];
|
||||
invariant(
|
||||
DrawerScreen.router,
|
||||
'NavigationComponent with routeName DrawerClose should be a Navigator'
|
||||
);
|
||||
const { [routeKey]: childNavigation } = this.props.childNavigationProps;
|
||||
return DrawerScreen.router.getScreenOptions(
|
||||
childNavigation.state.index !== undefined // if the child screen is a StackRouter then always show the screen options of its first screen (see #1914)
|
||||
? {
|
||||
...childNavigation,
|
||||
state: { ...childNavigation.state, index: 0 },
|
||||
}
|
||||
: childNavigation,
|
||||
this.props.screenProps
|
||||
descriptor.options,
|
||||
'Cannot access screen descriptor options from drawer sidebar'
|
||||
);
|
||||
return descriptor.options;
|
||||
};
|
||||
|
||||
_getLabel = ({ focused, tintColor, route }) => {
|
||||
@@ -56,12 +45,12 @@ class DrawerSidebar extends React.PureComponent {
|
||||
};
|
||||
|
||||
_onItemPress = ({ route, focused }) => {
|
||||
this.props.navigation.navigate('DrawerClose');
|
||||
if (!focused) {
|
||||
let subAction;
|
||||
// TODO (v3): Revisit and repeal this behavior:
|
||||
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
|
||||
if (route.index !== undefined && route.index !== 0) {
|
||||
subAction = NavigationActions.reset({
|
||||
if (route.index != null && route.index !== 0) {
|
||||
subAction = StackActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
@@ -70,7 +59,12 @@ class DrawerSidebar extends React.PureComponent {
|
||||
],
|
||||
});
|
||||
}
|
||||
this.props.navigation.navigate(route.routeName, undefined, subAction);
|
||||
this.props.navigation.dispatch(
|
||||
NavigationActions.navigate({
|
||||
routeName: route.routeName,
|
||||
action: subAction,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,6 +80,7 @@ class DrawerSidebar extends React.PureComponent {
|
||||
<ContentComponent
|
||||
{...this.props.contentOptions}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
items={state.routes}
|
||||
activeItemKey={
|
||||
state.routes[state.index] ? state.routes[state.index].key : null
|
||||
@@ -94,7 +89,6 @@ class DrawerSidebar extends React.PureComponent {
|
||||
getLabel={this._getLabel}
|
||||
renderIcon={this._renderIcon}
|
||||
onItemPress={this._onItemPress}
|
||||
router={this.props.router}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
/>
|
||||
</View>
|
||||
@@ -102,7 +96,7 @@ class DrawerSidebar extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export default withCachedChildNavigation(DrawerSidebar);
|
||||
export default DrawerSidebar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -2,24 +2,24 @@ import React from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import DrawerLayout from 'react-native-drawer-layout-polyfill';
|
||||
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import DrawerSidebar from './DrawerSidebar';
|
||||
import getChildEventSubscriber from '../../getChildEventSubscriber';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
*/
|
||||
export default class DrawerView extends React.PureComponent {
|
||||
_drawerState = 'closed';
|
||||
|
||||
state = {
|
||||
drawerWidth:
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth,
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this._updateScreenNavigation(this.props.navigation);
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
@@ -27,108 +27,71 @@ export default class DrawerView extends React.PureComponent {
|
||||
Dimensions.removeEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.navigation.state.index !== nextProps.navigation.state.index
|
||||
) {
|
||||
const {
|
||||
drawerOpenRoute,
|
||||
drawerCloseRoute,
|
||||
drawerToggleRoute,
|
||||
} = this.props;
|
||||
const { routes, index } = nextProps.navigation.state;
|
||||
if (routes[index].routeName === drawerOpenRoute) {
|
||||
this._drawer.openDrawer();
|
||||
} else if (routes[index].routeName === drawerToggleRoute) {
|
||||
if (this.props.navigation.state.index === 0) {
|
||||
this.props.navigation.navigate(drawerOpenRoute);
|
||||
} else {
|
||||
this.props.navigation.navigate(drawerCloseRoute);
|
||||
}
|
||||
} else {
|
||||
this._drawer.closeDrawer();
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { isDrawerOpen } = this.props.navigation.state;
|
||||
const wasDrawerOpen = prevProps.navigation.state.isDrawerOpen;
|
||||
|
||||
if (isDrawerOpen && !wasDrawerOpen && this._drawerState === 'closed') {
|
||||
this._drawerState = 'opening';
|
||||
this._drawer.openDrawer();
|
||||
} else if (wasDrawerOpen && !isDrawerOpen && this._drawerState === 'open') {
|
||||
this._drawerState = 'closing';
|
||||
this._drawer.closeDrawer();
|
||||
}
|
||||
this._updateScreenNavigation(nextProps.navigation);
|
||||
}
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
const { navigation, drawerOpenRoute } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
if (routes[index].routeName !== drawerOpenRoute) {
|
||||
this.props.navigation.navigate(drawerOpenRoute);
|
||||
const { navigation } = this.props;
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (!isDrawerOpen && this._drawerState === 'closed') {
|
||||
navigation.dispatch({ type: DrawerActions.OPEN_DRAWER });
|
||||
}
|
||||
this._drawerState = 'open';
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
const { navigation, drawerCloseRoute } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
if (routes[index].routeName !== drawerCloseRoute) {
|
||||
this.props.navigation.navigate(drawerCloseRoute);
|
||||
const { navigation } = this.props;
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (isDrawerOpen && this._drawerState === 'open') {
|
||||
navigation.dispatch({ type: DrawerActions.CLOSE_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_updateScreenNavigation = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
route => route.routeName === drawerCloseRoute
|
||||
);
|
||||
if (
|
||||
this._screenNavigationProp &&
|
||||
this._screenNavigationProp.state === navigationState
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._screenNavigationProp = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: navigationState,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
navigationState.key
|
||||
),
|
||||
});
|
||||
this._drawerState = 'closed';
|
||||
};
|
||||
|
||||
_updateWidth = () => {
|
||||
const drawerWidth =
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth;
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth;
|
||||
|
||||
if (this.state.drawerWidth !== drawerWidth) {
|
||||
this.setState({ drawerWidth });
|
||||
}
|
||||
};
|
||||
|
||||
_getNavigationState = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
route => route.routeName === drawerCloseRoute
|
||||
_renderNavigationView = () => {
|
||||
return (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
contentComponent={this.props.navigationConfig.contentComponent}
|
||||
contentOptions={this.props.navigationConfig.contentOptions}
|
||||
drawerPosition={this.props.navigationConfig.drawerPosition}
|
||||
style={this.props.navigationConfig.style}
|
||||
{...this.props.navigationConfig}
|
||||
/>
|
||||
);
|
||||
return navigationState;
|
||||
};
|
||||
|
||||
_renderNavigationView = () => (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this._screenNavigationProp}
|
||||
router={this.props.router}
|
||||
contentComponent={this.props.contentComponent}
|
||||
contentOptions={this.props.contentOptions}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
style={this.props.style}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
this.props.drawerCloseRoute
|
||||
);
|
||||
const { state } = this.props.navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = this.props.descriptors[activeKey];
|
||||
|
||||
const config = this.props.router.getScreenOptions(
|
||||
this._screenNavigationProp,
|
||||
this.props.screenProps
|
||||
);
|
||||
const DrawerScreen = descriptor.getComponent();
|
||||
|
||||
const { drawerLockMode } = descriptor.options;
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
@@ -136,24 +99,27 @@ export default class DrawerView extends React.PureComponent {
|
||||
this._drawer = c;
|
||||
}}
|
||||
drawerLockMode={
|
||||
drawerLockMode ||
|
||||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
|
||||
(config && config.drawerLockMode)
|
||||
this.props.navigationConfig.drawerLockMode
|
||||
}
|
||||
drawerBackgroundColor={
|
||||
this.props.navigationConfig.drawerBackgroundColor
|
||||
}
|
||||
drawerBackgroundColor={this.props.drawerBackgroundColor}
|
||||
drawerWidth={this.state.drawerWidth}
|
||||
onDrawerOpen={this._handleDrawerOpen}
|
||||
onDrawerClose={this._handleDrawerClose}
|
||||
useNativeAnimations={this.props.useNativeAnimations}
|
||||
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
|
||||
renderNavigationView={this._renderNavigationView}
|
||||
drawerPosition={
|
||||
this.props.drawerPosition === 'right'
|
||||
this.props.navigationConfig.drawerPosition === 'right'
|
||||
? DrawerLayout.positions.Right
|
||||
: DrawerLayout.positions.Left
|
||||
}
|
||||
>
|
||||
<DrawerScreen
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this._screenNavigationProp}
|
||||
navigation={descriptor.navigation}
|
||||
/>
|
||||
</DrawerLayout>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
I18nManager,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
||||
@@ -24,12 +25,15 @@ const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
return Platform.OS === 'ios'
|
||||
? isLandscape && !Platform.isPad ? 32 : 44
|
||||
? isLandscape && !Platform.isPad
|
||||
? 32
|
||||
: 44
|
||||
: 56;
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
layoutInterpolator: HeaderStyleInterpolator.forLayout,
|
||||
leftInterpolator: HeaderStyleInterpolator.forLeft,
|
||||
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
|
||||
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
|
||||
@@ -47,11 +51,11 @@ class Header extends React.PureComponent {
|
||||
};
|
||||
|
||||
_getHeaderTitleString(scene) {
|
||||
const sceneOptions = this.props.getScreenDetails(scene).options;
|
||||
if (typeof sceneOptions.headerTitle === 'string') {
|
||||
return sceneOptions.headerTitle;
|
||||
const options = scene.descriptor.options;
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
}
|
||||
return sceneOptions.title;
|
||||
return options.title;
|
||||
}
|
||||
|
||||
_getLastScene(scene) {
|
||||
@@ -63,7 +67,7 @@ class Header extends React.PureComponent {
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
const { headerBackTitle } = this.props.getScreenDetails(lastScene).options;
|
||||
const { headerBackTitle } = lastScene.descriptor.options;
|
||||
if (headerBackTitle || headerBackTitle === null) {
|
||||
return headerBackTitle;
|
||||
}
|
||||
@@ -75,27 +79,20 @@ class Header extends React.PureComponent {
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
return this.props.getScreenDetails(lastScene).options
|
||||
.headerTruncatedBackTitle;
|
||||
return lastScene.descriptor.options.headerTruncatedBackTitle;
|
||||
}
|
||||
|
||||
_navigateBack = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.props.navigation.goBack(this.props.scene.route.key);
|
||||
});
|
||||
};
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
const details = this.props.getScreenDetails(props.scene);
|
||||
const headerTitle = details.options.headerTitle;
|
||||
const { options } = props.scene.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
return headerTitle;
|
||||
}
|
||||
const titleString = this._getHeaderTitleString(props.scene);
|
||||
|
||||
const titleStyle = details.options.headerTitleStyle;
|
||||
const color = details.options.headerTintColor;
|
||||
const allowFontScaling = details.options.headerTitleAllowFontScaling;
|
||||
const titleStyle = options.headerTitleStyle;
|
||||
const color = options.headerTintColor;
|
||||
const allowFontScaling = options.headerTitleAllowFontScaling;
|
||||
|
||||
// On iOS, width of left/right components depends on the calculated
|
||||
// size of the title.
|
||||
@@ -127,8 +124,7 @@ class Header extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderLeftComponent = props => {
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
|
||||
const { options } = props.scene.descriptor;
|
||||
if (
|
||||
React.isValidElement(options.headerLeft) ||
|
||||
options.headerLeft === null
|
||||
@@ -148,12 +144,18 @@ class Header extends React.PureComponent {
|
||||
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
|
||||
: undefined;
|
||||
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
props.scene.descriptor.navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<RenderedLeftComponent
|
||||
onPress={this._navigateBack}
|
||||
onPress={goBack}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
@@ -167,7 +169,7 @@ class Header extends React.PureComponent {
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
) => {
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
const { options, navigation } = props.scene.descriptor;
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
@@ -176,14 +178,21 @@ class Header extends React.PureComponent {
|
||||
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
|
||||
: undefined;
|
||||
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModularHeaderBackButton
|
||||
onPress={this._navigateBack}
|
||||
onPress={goBack}
|
||||
ButtonContainerComponent={ButtonContainerComponent}
|
||||
LabelContainerComponent={LabelContainerComponent}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
@@ -193,13 +202,12 @@ class Header extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderRightComponent = props => {
|
||||
const details = this.props.getScreenDetails(props.scene);
|
||||
const { headerRight } = details.options;
|
||||
const { headerRight } = props.scene.descriptor.options;
|
||||
return headerRight || null;
|
||||
};
|
||||
|
||||
_renderLeft(props) {
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
@@ -366,6 +374,10 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
|
||||
_renderHeader(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
if (options.header === null) {
|
||||
return null;
|
||||
}
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props, {
|
||||
@@ -374,7 +386,6 @@ class Header extends React.PureComponent {
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
|
||||
const wrapperProps = {
|
||||
style: styles.header,
|
||||
@@ -439,7 +450,7 @@ class Header extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
const { options } = this.props.getScreenDetails(scene);
|
||||
const { options } = scene.descriptor;
|
||||
const { headerStyle = {} } = options;
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle);
|
||||
const appBarHeight = getAppBarHeight(isLandscape);
|
||||
@@ -476,14 +487,18 @@ class Header extends React.PureComponent {
|
||||
safeHeaderStyle,
|
||||
];
|
||||
|
||||
const { headerForceInset } = options;
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ top: 'always', bottom: 'never' }}
|
||||
style={containerStyles}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
|
||||
<View style={{ flex: 1 }}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
<Animated.View style={this.props.layoutInterpolator(this.props)}>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
{options.headerBackground}
|
||||
</View>
|
||||
<View style={styles.flexOne}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -551,6 +566,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: -0.5, // resizes down to 20.5
|
||||
alignSelf: 'center',
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
title: {
|
||||
bottom: 0,
|
||||
@@ -578,6 +594,9 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flexOne: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrientation(Header);
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
const defaultBackImage = require('../assets/back-icon.png');
|
||||
|
||||
class HeaderBackButton extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
pressColorAndroid: 'rgba(0, 0, 0, .32)',
|
||||
@@ -17,8 +19,6 @@ class HeaderBackButton extends React.PureComponent {
|
||||
ios: '#037aff',
|
||||
}),
|
||||
truncatedTitle: 'Back',
|
||||
// eslint-disable-next-line global-require
|
||||
buttonImage: require('../assets/back-icon.png'),
|
||||
};
|
||||
|
||||
state = {};
|
||||
@@ -32,9 +32,37 @@ class HeaderBackButton extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
const { backImage, title, tintColor } = this.props;
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
title,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
|
||||
return <BackImage {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonImage,
|
||||
onPress,
|
||||
pressColorAndroid,
|
||||
width,
|
||||
@@ -64,14 +92,7 @@ class HeaderBackButton extends React.PureComponent {
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
]}
|
||||
source={buttonImage}
|
||||
/>
|
||||
{this._renderBackImage()}
|
||||
{Platform.OS === 'ios' &&
|
||||
typeof backButtonTitle === 'string' && (
|
||||
<Text
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user