Compare commits
138 Commits
v2.0.0-rc.
...
2.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde091848a | ||
|
|
824fa32416 | ||
|
|
c518e7f36c | ||
|
|
1cfe01dbdb | ||
|
|
e62a9050fd | ||
|
|
310b909ba8 | ||
|
|
aebe8a5c23 | ||
|
|
e1df2c6c4a | ||
|
|
fa86718a24 | ||
|
|
c8e5673183 | ||
|
|
b312a5e307 | ||
|
|
ee6a6c53b1 | ||
|
|
8edec88341 | ||
|
|
b8d6d4253d | ||
|
|
0adb1ba9f1 | ||
|
|
d3ef3d1271 | ||
|
|
89a24bdc12 | ||
|
|
128a95b496 | ||
|
|
470eaf3b08 | ||
|
|
c91e8206a5 | ||
|
|
da283915f8 | ||
|
|
3031e7bd80 | ||
|
|
98a4f26f26 | ||
|
|
b7f5435c93 | ||
|
|
21ef4fcb82 | ||
|
|
5f64a2a9cb | ||
|
|
992d0fb267 | ||
|
|
6f41379ed1 | ||
|
|
267af01e72 | ||
|
|
b68c3a755d | ||
|
|
7345634493 | ||
|
|
6517169119 | ||
|
|
ea5d14a720 | ||
|
|
313d0726a8 | ||
|
|
b52f153747 | ||
|
|
44621005ff | ||
|
|
bf58364c3d | ||
|
|
b55053cde6 | ||
|
|
9abb2644a9 | ||
|
|
6a946d6ab7 | ||
|
|
395abe5200 | ||
|
|
ba62509ff4 | ||
|
|
45391db7d9 | ||
|
|
7f86362e86 | ||
|
|
99605737e9 | ||
|
|
842f5eb7b2 | ||
|
|
183ea82416 | ||
|
|
108a6504a7 | ||
|
|
f92d671746 | ||
|
|
e0c4a8f7d3 | ||
|
|
bc881c8aa1 | ||
|
|
118c19dcce | ||
|
|
01b43974e6 | ||
|
|
2f90899620 | ||
|
|
6cc86f66e1 | ||
|
|
4be99b6645 | ||
|
|
80016b7218 | ||
|
|
f555a9ec9a | ||
|
|
05cbd85d5c | ||
|
|
51965eac38 | ||
|
|
a3956bf3ce | ||
|
|
ce24c66b5a | ||
|
|
5467f0e22d | ||
|
|
1e7d8d55c3 | ||
|
|
1d2ce862c2 | ||
|
|
d778479e4a | ||
|
|
352dae50e1 | ||
|
|
61385cae59 | ||
|
|
aa3c13891e | ||
|
|
9696d7220d | ||
|
|
2b83b44816 | ||
|
|
ec749023ed | ||
|
|
adc9389eb3 | ||
|
|
54d143fee2 | ||
|
|
d50e74d0c7 | ||
|
|
22926c5230 | ||
|
|
f6c47a6c66 | ||
|
|
046a9f8930 | ||
|
|
72f17538c2 | ||
|
|
550001b053 | ||
|
|
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 |
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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# React Navigation
|
||||
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-community/react-navigation) [](https://reactnavigation.org/docs/contributing.html)
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-navigation/react-navigation) [](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [](https://reactnavigation.org/docs/contributing.html)
|
||||
|
||||
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.
|
||||
|
||||
@@ -55,4 +55,4 @@ This library has adopted a Code of Conduct that we expect project participants t
|
||||
|
||||
## License
|
||||
|
||||
React-navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
|
||||
React Navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
|
||||
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "26.0.0",
|
||||
"sdkVersion": "27.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
|
||||
@@ -26,7 +26,9 @@ import TabsInDrawer from './TabsInDrawer';
|
||||
import ModalStack from './ModalStack';
|
||||
import StacksInTabs from './StacksInTabs';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
import StacksOverTopTabs from './StacksOverTopTabs';
|
||||
import StacksWithKeys from './StacksWithKeys';
|
||||
import InactiveStack from './InactiveStack';
|
||||
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
|
||||
import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
@@ -34,6 +36,7 @@ import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import SwitchWithStacks from './SwitchWithStacks';
|
||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||
import KeyboardHandlingExample from './KeyboardHandlingExample';
|
||||
|
||||
const ExampleInfo = {
|
||||
SimpleStack: {
|
||||
@@ -44,6 +47,11 @@ const ExampleInfo = {
|
||||
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',
|
||||
@@ -98,6 +106,10 @@ const ExampleInfo = {
|
||||
name: 'Stacks over Tabs',
|
||||
description: 'Nested stack navigation that pushes on top of tabs',
|
||||
},
|
||||
StacksOverTopTabs: {
|
||||
name: 'Stacks with non-standard header height',
|
||||
description: 'Tab navigator in stack with custom header heights',
|
||||
},
|
||||
StacksWithKeys: {
|
||||
name: 'Link in Stack with keys',
|
||||
description: 'Use keys to link between screens',
|
||||
@@ -114,6 +126,11 @@ const ExampleInfo = {
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
@@ -125,7 +142,12 @@ const ExampleRoutes = {
|
||||
// screen: MultipleDrawer,
|
||||
// },
|
||||
StackWithCustomHeaderBackImage: StackWithCustomHeaderBackImage,
|
||||
StackWithHeaderPreset: StackWithHeaderPreset,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
StackWithHeaderPreset: StackWithHeaderPreset,
|
||||
},
|
||||
android: {},
|
||||
}),
|
||||
StackWithTranslucentHeader: StackWithTranslucentHeader,
|
||||
TabsInDrawer: TabsInDrawer,
|
||||
CustomTabs: CustomTabs,
|
||||
@@ -134,6 +156,7 @@ const ExampleRoutes = {
|
||||
StacksWithKeys: StacksWithKeys,
|
||||
StacksInTabs: StacksInTabs,
|
||||
StacksOverTabs: StacksOverTabs,
|
||||
StacksOverTopTabs: StacksOverTopTabs,
|
||||
LinkStack: {
|
||||
screen: SimpleStack,
|
||||
path: 'people/Jordan',
|
||||
@@ -143,6 +166,9 @@ const ExampleRoutes = {
|
||||
path: 'settings',
|
||||
},
|
||||
TabsWithNavigationFocus,
|
||||
KeyboardHandlingExample,
|
||||
// This is commented out because it's rarely useful
|
||||
// InactiveStack,
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -307,7 +333,7 @@ const AppNavigator = createStackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
export default () => <AppNavigator persistenceKey="NavState" />;
|
||||
export default AppNavigator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -32,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 }) => (
|
||||
@@ -50,10 +43,7 @@ const DraftsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
|
||||
);
|
||||
DraftsScreen.navigationOptions = {
|
||||
drawerLabel: 'Drafts',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons name="drafts" size={24} style={{ color: tintColor }} />
|
||||
),
|
||||
headerTitle: 'Drafts',
|
||||
};
|
||||
|
||||
const InboxStack = createStackNavigator({
|
||||
@@ -61,11 +51,29 @@ const InboxStack = createStackNavigator({
|
||||
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 DrawerExample = createDrawerNavigator(
|
||||
{
|
||||
Inbox: {
|
||||
|
||||
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
@@ -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),
|
||||
});
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import type {
|
||||
NavigationScreenProp,
|
||||
NavigationState,
|
||||
NavigationStateRoute,
|
||||
NavigationEventSubscription,
|
||||
} from 'react-navigation';
|
||||
|
||||
@@ -13,17 +15,25 @@ 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 (
|
||||
<HeaderButtons>
|
||||
@@ -42,25 +52,55 @@ const MyBackButtonWithNavigation = withNavigation(MyBackButton);
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop, dismiss } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop && dismiss,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() =>
|
||||
navigation.dispatch(
|
||||
StackActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
routeName: 'Photos',
|
||||
params: { name: 'Jane' },
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
title="Reset photos"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack()} title="Go back" />
|
||||
<Button onPress={() => navigation.dismiss()} title="Dismiss" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (navigation.goBack()) {
|
||||
console.log('goBack handled');
|
||||
} else {
|
||||
console.log('goBack unhandled');
|
||||
}
|
||||
}}
|
||||
title="Go back"
|
||||
/>
|
||||
<Button onPress={() => dismiss()} title="Dismiss" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -68,7 +108,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
}
|
||||
|
||||
type MyHomeScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
|
||||
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
@@ -112,7 +152,7 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
}
|
||||
|
||||
type MyPhotosScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
static navigationOptions = {
|
||||
@@ -153,7 +193,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}
|
||||
/>
|
||||
);
|
||||
@@ -162,9 +202,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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -165,7 +165,7 @@ const SimpleTabs = createBottomTabNavigator(
|
||||
},
|
||||
{
|
||||
tabBarOptions: {
|
||||
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
|
||||
activeTintColor: '#e91e63',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { NavigationScreenProp } from 'react-navigation';
|
||||
import * as React from 'react';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import { createStackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
type NavScreenProps = {
|
||||
@@ -19,15 +21,14 @@ class HomeScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push } = navigation;
|
||||
invariant(push, 'missing `push` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenWithNoHeader')}
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
|
||||
@@ -44,18 +45,20 @@ class OtherScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenWithLongTitle')}
|
||||
onPress={() => push('ScreenWithLongTitle')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenWithNoHeader')}
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
@@ -70,10 +73,12 @@ class ScreenWithLongTitle extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { pop } = navigation;
|
||||
invariant(pop, 'missing `pop` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
@@ -89,14 +94,13 @@ class ScreenWithNoHeader extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Header, createStackNavigator } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
@@ -31,11 +33,16 @@ type MyNavScreenProps = {
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
@@ -43,11 +50,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</ScrollView>
|
||||
|
||||
@@ -94,6 +94,19 @@ const TabNav = createBottomTabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
TabNav.navigationOptions = ({ navigation }) => {
|
||||
let { routeName } = navigation.state.routes[navigation.state.index];
|
||||
let title;
|
||||
if (routeName === 'SettingsTab') {
|
||||
title = 'Settings';
|
||||
} else if (routeName === 'MainTab') {
|
||||
title = 'Home';
|
||||
}
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
const StacksOverTabs = createStackNavigator({
|
||||
Root: {
|
||||
screen: TabNav,
|
||||
@@ -107,9 +120,9 @@ const StacksOverTabs = createStackNavigator({
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
path: '/people/:name',
|
||||
navigationOptions: ({ navigation }) => {
|
||||
title: `${navigation.state.params.name}'s Profile!`;
|
||||
},
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
142
examples/NavigationPlayground/js/StacksOverTopTabs.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, ScrollView, StatusBar, StyleSheet } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createMaterialTopTabNavigator,
|
||||
} from 'react-navigation';
|
||||
import { Constants } from 'expo';
|
||||
import { MaterialTopTabBar } from 'react-navigation-tabs';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const HEADER_HEIGHT = 64;
|
||||
|
||||
const MyNavScreen = ({ navigation, banner, statusBarStyle }) => (
|
||||
<ScrollView>
|
||||
<SafeAreaView forceInset={{ horizontal: 'always' }}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Profile', { name: 'Jordan' })}
|
||||
title="Open profile screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('NotifSettings')}
|
||||
title="Open notifications screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('SettingsTab')}
|
||||
title="Go to settings tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
</SafeAreaView>
|
||||
<StatusBar barStyle={statusBarStyle || 'default'} />
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const MyHomeScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner="Home Screen"
|
||||
navigation={navigation}
|
||||
statusBarStyle="light-content"
|
||||
/>
|
||||
);
|
||||
|
||||
const MyProfileScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.name}s Profile`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
const MyNotificationsSettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Notifications Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner="Settings Screen"
|
||||
navigation={navigation}
|
||||
statusBarStyle="light-content"
|
||||
/>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
stackHeader: {
|
||||
height: HEADER_HEIGHT,
|
||||
},
|
||||
tab: {
|
||||
height: HEADER_HEIGHT,
|
||||
},
|
||||
});
|
||||
|
||||
function MaterialTopTabBarWithStatusBar(props) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Constants.statusBarHeight,
|
||||
backgroundColor: '#2196f3',
|
||||
}}
|
||||
>
|
||||
<MaterialTopTabBar {...props} jumpToIndex={() => {}} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const TabNavigator = createMaterialTopTabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MyHomeScreen,
|
||||
navigationOptions: {
|
||||
title: 'Welcome',
|
||||
},
|
||||
},
|
||||
SettingsTab: {
|
||||
screen: MySettingsScreen,
|
||||
navigationOptions: {
|
||||
title: 'Settings',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarComponent: MaterialTopTabBarWithStatusBar,
|
||||
tabBarOptions: {
|
||||
tabStyle: styles.tab,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const StackNavigator = createStackNavigator(
|
||||
{
|
||||
Root: {
|
||||
screen: TabNavigator,
|
||||
navigationOptions: {
|
||||
header: null,
|
||||
},
|
||||
},
|
||||
NotifSettings: {
|
||||
screen: MyNotificationsSettingsScreen,
|
||||
navigationOptions: {
|
||||
title: 'Notifications',
|
||||
},
|
||||
},
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
headerStyle: styles.stackHeader,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default StackNavigator;
|
||||
@@ -4,10 +4,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import { SafeAreaView, StatusBar, Text, View } from 'react-native';
|
||||
import {
|
||||
createBottomTabNavigator,
|
||||
withNavigationFocus,
|
||||
} from 'react-navigation';
|
||||
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';
|
||||
|
||||
@@ -80,16 +78,26 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
return withNavigationFocus(TabScreen);
|
||||
};
|
||||
|
||||
const TabsWithNavigationFocus = createBottomTabNavigator({
|
||||
One: {
|
||||
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
|
||||
const TabsWithNavigationFocus = createMaterialBottomTabNavigator(
|
||||
{
|
||||
One: {
|
||||
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
|
||||
},
|
||||
Two: {
|
||||
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
|
||||
},
|
||||
Three: {
|
||||
screen: createTabScreen(
|
||||
'Three',
|
||||
'numeric-3-box-outline',
|
||||
'numeric-3-box'
|
||||
),
|
||||
},
|
||||
},
|
||||
Two: {
|
||||
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
|
||||
},
|
||||
Three: {
|
||||
screen: createTabScreen('Three', 'numeric-3-box-outline', 'numeric-3-box'),
|
||||
},
|
||||
});
|
||||
{
|
||||
shifting: false,
|
||||
activeTintColor: '#F44336',
|
||||
}
|
||||
);
|
||||
|
||||
export default TabsWithNavigationFocus;
|
||||
|
||||
@@ -11,17 +11,20 @@
|
||||
"test": "flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^26.0.0",
|
||||
"react": "16.3.0-alpha.1",
|
||||
"react-native": "^0.54.0",
|
||||
"expo": "^27.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-header-buttons": "^0.0.4"
|
||||
"react-navigation-header-buttons": "^0.0.4",
|
||||
"react-navigation-material-bottom-tabs": "0.1.3",
|
||||
"react-navigation-tabs": "^0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"flow-bin": "^0.67.0",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
|
||||
@@ -4,19 +4,15 @@ import { Provider } from 'react-redux';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
import AppReducer from './src/reducers';
|
||||
import AppWithNavigationState from './src/navigators/AppNavigator';
|
||||
import { middleware } from './src/utils/redux';
|
||||
import { AppNavigator, middleware } from './src/navigators/AppNavigator';
|
||||
|
||||
const store = createStore(
|
||||
AppReducer,
|
||||
applyMiddleware(middleware),
|
||||
);
|
||||
const store = createStore(AppReducer, applyMiddleware(middleware));
|
||||
|
||||
class ReduxExampleApp extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppWithNavigationState />
|
||||
<AppNavigator />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"icon": "./assets/icons/react-navigation.png",
|
||||
"hideExponentText": false
|
||||
},
|
||||
"sdkVersion": "25.0.0",
|
||||
"sdkVersion": "27.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": ["ttf", "mp4"]
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 20 KiB |
@@ -21,17 +21,18 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^25.0.0",
|
||||
"expo": "^27.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-redux-helpers": "^1.0.0",
|
||||
"react-navigation-redux-helpers": "^2.0.0-beta.1",
|
||||
"react-redux": "^5.0.6",
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^22.4.1",
|
||||
"flow-bin": "^0.74.0",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^25.1.0",
|
||||
"react-native-scripts": "^1.3.1",
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { StackNavigator } from 'react-navigation';
|
||||
import { createStackNavigator } from 'react-navigation';
|
||||
import {
|
||||
reduxifyNavigator,
|
||||
createReactNavigationReduxMiddleware,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
import LoginScreen from '../components/LoginScreen';
|
||||
import MainScreen from '../components/MainScreen';
|
||||
import ProfileScreen from '../components/ProfileScreen';
|
||||
import { addListener } from '../utils/redux';
|
||||
|
||||
export const AppNavigator = StackNavigator({
|
||||
const middleware = createReactNavigationReduxMiddleware(
|
||||
'root',
|
||||
state => state.nav
|
||||
);
|
||||
|
||||
const RootNavigator = createStackNavigator({
|
||||
Login: { screen: LoginScreen },
|
||||
Main: { screen: MainScreen },
|
||||
Profile: { screen: ProfileScreen },
|
||||
});
|
||||
|
||||
class AppWithNavigationState extends React.Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
nav: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
<AppNavigator
|
||||
navigation={{
|
||||
dispatch,
|
||||
state: nav,
|
||||
addListener,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const AppWithNavigationState = reduxifyNavigator(RootNavigator, 'root');
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
nav: state.nav,
|
||||
state: state.nav,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
||||
const AppNavigator = connect(mapStateToProps)(AppWithNavigationState);
|
||||
|
||||
export { RootNavigator, AppNavigator, middleware };
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
|
||||
import { AppNavigator } from '../navigators/AppNavigator';
|
||||
import { RootNavigator } from '../navigators/AppNavigator';
|
||||
|
||||
// Start with two routes: The Main screen, with the Login screen on top.
|
||||
const firstAction = AppNavigator.router.getActionForPathAndParams('Main');
|
||||
const tempNavState = AppNavigator.router.getStateForAction(firstAction);
|
||||
const secondAction = AppNavigator.router.getActionForPathAndParams('Login');
|
||||
const initialNavState = AppNavigator.router.getStateForAction(
|
||||
const firstAction = RootNavigator.router.getActionForPathAndParams('Main');
|
||||
const tempNavState = RootNavigator.router.getStateForAction(firstAction);
|
||||
const secondAction = RootNavigator.router.getActionForPathAndParams('Login');
|
||||
const initialNavState = RootNavigator.router.getStateForAction(
|
||||
secondAction,
|
||||
tempNavState
|
||||
);
|
||||
@@ -16,19 +16,19 @@ function nav(state = initialNavState, action) {
|
||||
let nextState;
|
||||
switch (action.type) {
|
||||
case 'Login':
|
||||
nextState = AppNavigator.router.getStateForAction(
|
||||
nextState = RootNavigator.router.getStateForAction(
|
||||
NavigationActions.back(),
|
||||
state
|
||||
);
|
||||
break;
|
||||
case 'Logout':
|
||||
nextState = AppNavigator.router.getStateForAction(
|
||||
nextState = RootNavigator.router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'Login' }),
|
||||
state
|
||||
);
|
||||
break;
|
||||
default:
|
||||
nextState = AppNavigator.router.getStateForAction(action, state);
|
||||
nextState = RootNavigator.router.getStateForAction(action, state);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import {
|
||||
createReactNavigationReduxMiddleware,
|
||||
createReduxBoundAddListener,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
const middleware = createReactNavigationReduxMiddleware(
|
||||
"root",
|
||||
state => state.nav,
|
||||
);
|
||||
const addListener = createReduxBoundAddListener("root");
|
||||
|
||||
export {
|
||||
middleware,
|
||||
addListener,
|
||||
};
|
||||
327
flow/react-navigation.js
vendored
@@ -69,6 +69,14 @@ declare module 'react-navigation' {
|
||||
[key: string]: mixed,
|
||||
};
|
||||
|
||||
declare export type NavigationBackAction = {|
|
||||
type: 'Navigation/BACK',
|
||||
key?: ?string,
|
||||
|};
|
||||
declare export type NavigationInitAction = {|
|
||||
type: 'Navigation/INIT',
|
||||
params?: NavigationParams,
|
||||
|};
|
||||
declare export type NavigationNavigateAction = {|
|
||||
type: 'Navigation/NAVIGATE',
|
||||
routeName: string,
|
||||
@@ -79,12 +87,6 @@ declare module 'react-navigation' {
|
||||
|
||||
key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationBackAction = {|
|
||||
type: 'Navigation/BACK',
|
||||
key?: ?string,
|
||||
|};
|
||||
|
||||
declare export type NavigationSetParamsAction = {|
|
||||
type: 'Navigation/SET_PARAMS',
|
||||
|
||||
@@ -95,30 +97,6 @@ declare module 'react-navigation' {
|
||||
params: NavigationParams,
|
||||
|};
|
||||
|
||||
declare export type NavigationInitAction = {|
|
||||
type: 'Navigation/INIT',
|
||||
params?: NavigationParams,
|
||||
|};
|
||||
|
||||
declare export type NavigationResetAction = {|
|
||||
type: 'Navigation/RESET',
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
|};
|
||||
|
||||
declare export type NavigationUriAction = {|
|
||||
type: 'Navigation/URI',
|
||||
uri: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationReplaceAction = {|
|
||||
+type: 'Navigation/REPLACE',
|
||||
+key: string,
|
||||
+routeName: string,
|
||||
+params?: NavigationParams,
|
||||
+action?: NavigationNavigateAction,
|
||||
|};
|
||||
declare export type NavigationPopAction = {|
|
||||
+type: 'Navigation/POP',
|
||||
+n?: number,
|
||||
@@ -135,17 +113,51 @@ declare module 'react-navigation' {
|
||||
+action?: NavigationNavigateAction,
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationResetAction = {|
|
||||
type: 'Navigation/RESET',
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
|};
|
||||
declare export type NavigationReplaceAction = {|
|
||||
+type: 'Navigation/REPLACE',
|
||||
+key: string,
|
||||
+routeName: string,
|
||||
+params?: NavigationParams,
|
||||
+action?: NavigationNavigateAction,
|
||||
|};
|
||||
declare export type NavigationCompleteTransitionAction = {|
|
||||
+type: 'Navigation/COMPLETE_TRANSITION',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationOpenDrawerAction = {|
|
||||
+type: 'Navigation/OPEN_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationCloseDrawerAction = {|
|
||||
+type: 'Navigation/CLOSE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationToggleDrawerAction = {|
|
||||
+type: 'Navigation/TOGGLE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationAction =
|
||||
| NavigationBackAction
|
||||
| NavigationInitAction
|
||||
| NavigationNavigateAction
|
||||
| NavigationReplaceAction
|
||||
| NavigationSetParamsAction
|
||||
| NavigationPopAction
|
||||
| NavigationPopToTopAction
|
||||
| NavigationPushAction
|
||||
| NavigationBackAction
|
||||
| NavigationSetParamsAction
|
||||
| NavigationResetAction;
|
||||
| NavigationResetAction
|
||||
| NavigationReplaceAction
|
||||
| NavigationCompleteTransitionAction
|
||||
| NavigationOpenDrawerAction
|
||||
| NavigationCloseDrawerAction
|
||||
| NavigationToggleDrawerAction;
|
||||
|
||||
/**
|
||||
* NavigationState is a tree of routes for a single navigator, where each
|
||||
@@ -269,24 +281,36 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationComponent =
|
||||
| NavigationScreenComponent<NavigationRoute, *, *>
|
||||
| NavigationContainer<*, *, *>
|
||||
| any;
|
||||
| NavigationContainer<*, *, *>;
|
||||
|
||||
declare interface withOptionalNavigationOptions<Options> {
|
||||
navigationOptions?: NavigationScreenConfig<Options>;
|
||||
}
|
||||
|
||||
declare export type NavigationScreenComponent<
|
||||
Route: NavigationRoute,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
|
||||
({} | { navigationOptions: NavigationScreenConfig<Options> });
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, Route>,
|
||||
}> &
|
||||
withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare interface withRouter<State, Options> {
|
||||
router: NavigationRouter<State, Options>;
|
||||
}
|
||||
|
||||
declare export type NavigationNavigator<
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, State>,
|
||||
}> &
|
||||
withRouter<State, Options> &
|
||||
withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare export type NavigationRouteConfig =
|
||||
| NavigationComponent
|
||||
@@ -426,10 +450,10 @@ declare module 'react-navigation' {
|
||||
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
|
||||
tabBarVisible?: boolean,
|
||||
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
|
||||
tabBarOnPress?: (
|
||||
scene: TabScene,
|
||||
jumpToIndex: (index: number) => void
|
||||
) => void,
|
||||
tabBarOnPress?: ({
|
||||
navigation: NavigationScreenProp<NavigationRoute>,
|
||||
defaultHandler: () => void,
|
||||
}) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -485,37 +509,46 @@ declare module 'react-navigation' {
|
||||
declare export type NavigationScreenProp<+S> = {
|
||||
+state: S,
|
||||
dispatch: NavigationDispatch,
|
||||
goBack: (routeKey?: ?string) => boolean,
|
||||
dismiss: () => boolean,
|
||||
navigate: (
|
||||
routeName: | string
|
||||
| {
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
key?: string,
|
||||
},
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
setParams: (newParams: NavigationParams) => boolean,
|
||||
addListener: (
|
||||
eventName: string,
|
||||
callback: NavigationEventCallback
|
||||
) => NavigationEventSubscription,
|
||||
push: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
replace: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop: (params?: { immediate?: boolean }) => boolean,
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
isFocused: () => boolean,
|
||||
// Shared action creators that exist for all routers
|
||||
goBack: (routeKey?: ?string) => boolean,
|
||||
navigate: (
|
||||
routeName:
|
||||
| string
|
||||
| {
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
key?: string,
|
||||
},
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
setParams: (newParams: NavigationParams) => boolean,
|
||||
// StackRouter action creators
|
||||
pop?: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop?: (params?: { immediate?: boolean }) => boolean,
|
||||
push?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
replace?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => 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<{
|
||||
@@ -532,10 +565,12 @@ declare module 'react-navigation' {
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationContainerProps<State, Options>,
|
||||
}> &
|
||||
withRouter<State, Options> &
|
||||
withOptionalNavigationOptions<Options>;
|
||||
|
||||
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
|
||||
uriPrefix?: string | RegExp,
|
||||
@@ -704,44 +739,73 @@ declare module 'react-navigation' {
|
||||
BACK: 'Navigation/BACK',
|
||||
INIT: 'Navigation/INIT',
|
||||
NAVIGATE: 'Navigation/NAVIGATE',
|
||||
RESET: 'Navigation/RESET',
|
||||
SET_PARAMS: 'Navigation/SET_PARAMS',
|
||||
URI: 'Navigation/URI',
|
||||
back: {
|
||||
(payload?: { key?: ?string }): NavigationBackAction,
|
||||
toString: () => string,
|
||||
},
|
||||
init: {
|
||||
(payload?: { params?: NavigationParams }): NavigationInitAction,
|
||||
toString: () => string,
|
||||
},
|
||||
navigate: {
|
||||
(payload: {
|
||||
routeName: string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
}): NavigationNavigateAction,
|
||||
toString: () => string,
|
||||
},
|
||||
reset: {
|
||||
(payload: {
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}): NavigationResetAction,
|
||||
toString: () => string,
|
||||
},
|
||||
setParams: {
|
||||
(payload: {
|
||||
key: string,
|
||||
params: NavigationParams,
|
||||
}): NavigationSetParamsAction,
|
||||
toString: () => string,
|
||||
},
|
||||
uri: {
|
||||
(payload: { uri: string }): NavigationUriAction,
|
||||
toString: () => string,
|
||||
},
|
||||
|
||||
back: (payload?: { key?: ?string }) => NavigationBackAction,
|
||||
init: (payload?: { params?: NavigationParams }) => NavigationInitAction,
|
||||
navigate: (payload: {
|
||||
routeName: string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
key?: string,
|
||||
}) => NavigationNavigateAction,
|
||||
setParams: (payload: {
|
||||
key: string,
|
||||
params: NavigationParams,
|
||||
}) => NavigationSetParamsAction,
|
||||
};
|
||||
|
||||
declare export var StackActions: {
|
||||
POP: 'Navigation/POP',
|
||||
POP_TO_TOP: 'Navigation/POP_TO_TOP',
|
||||
PUSH: 'Navigation/PUSH',
|
||||
RESET: 'Navigation/RESET',
|
||||
REPLACE: 'Navigation/REPLACE',
|
||||
COMPLETE_TRANSITION: 'Navigation/COMPLETE_TRANSITION',
|
||||
|
||||
pop: (payload: {
|
||||
n?: number,
|
||||
immediate?: boolean,
|
||||
}) => NavigationPopAction,
|
||||
popToTop: (payload: {
|
||||
immediate?: boolean,
|
||||
}) => NavigationPopToTopAction,
|
||||
push: (payload: {
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
key?: string,
|
||||
}) => NavigationPushAction,
|
||||
reset: (payload: {
|
||||
index: number,
|
||||
key?: ?string,
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}) => NavigationResetAction,
|
||||
replace: (payload: {
|
||||
key: string,
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
}) => NavigationReplaceAction,
|
||||
completeTransition: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationCompleteTransitionAction,
|
||||
};
|
||||
|
||||
declare export var DrawerActions: {
|
||||
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
|
||||
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
|
||||
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
|
||||
|
||||
openDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationOpenDrawerAction,
|
||||
closeDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationCloseDrawerAction,
|
||||
toggleDrawer: (payload: {
|
||||
key?: string,
|
||||
}) => NavigationToggleDrawerAction,
|
||||
};
|
||||
|
||||
declare type _RouterProp<S: NavigationState, O: {}> = {
|
||||
@@ -764,7 +828,7 @@ declare module 'react-navigation' {
|
||||
view: NavigationView<O, S>,
|
||||
router: NavigationRouter<S, O>,
|
||||
navigatorConfig?: NavigatorConfig
|
||||
): any;
|
||||
): NavigationNavigator<S, O, *>;
|
||||
|
||||
declare export function StackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
@@ -807,10 +871,6 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createMaterialBottomTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createMaterialTopTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
@@ -1086,13 +1146,26 @@ declare module 'react-navigation' {
|
||||
};
|
||||
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
|
||||
|
||||
declare type _NavigationInjectedProps = {
|
||||
navigation: NavigationScreenProp<NavigationStateRoute>,
|
||||
};
|
||||
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 }>>;
|
||||
|
||||
declare export function getNavigation<State: NavigationState, Options: {}>(
|
||||
router: NavigationRouter<State, Options>,
|
||||
state: State,
|
||||
dispatch: NavigationDispatch,
|
||||
actionSubscribers: Set<NavigationEventCallback>,
|
||||
getScreenProps: () => {},
|
||||
getCurrentNavigation: () => NavigationScreenProp<State>
|
||||
): NavigationScreenProp<State>;
|
||||
}
|
||||
|
||||
27
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "2.0.0-rc.1",
|
||||
"version": "2.4.1",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
@@ -34,11 +34,11 @@
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^1.0.2",
|
||||
"react-native-drawer-layout-polyfill": "^1.3.2",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.0.1",
|
||||
"react-navigation-tabs": "0.1.0-alpha.6"
|
||||
"react-lifecycles-compat": "^3",
|
||||
"react-native-safe-area-view": "^0.8.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||
"react-navigation-drawer": "0.3.2",
|
||||
"react-navigation-tabs": "0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
@@ -48,17 +48,17 @@
|
||||
"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",
|
||||
@@ -67,7 +67,7 @@
|
||||
"jest": {
|
||||
"notify": true,
|
||||
"preset": "react-native",
|
||||
"testRegex": "./src/.*\\-test\\.js$",
|
||||
"testRegex": "/__tests__/[^/]+-test\\.js$",
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
],
|
||||
@@ -82,8 +82,11 @@
|
||||
"coveragePathIgnorePatterns": [
|
||||
"jest-setup.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.png$": "<rootDir>/assetsTransformer.js"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"examples"
|
||||
"<rootDir>/examples/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"
|
||||
|
||||
@@ -4,6 +4,6 @@ set -eo pipefail
|
||||
|
||||
case $CIRCLE_NODE_INDEX in
|
||||
0) yarn test && yarn codecov ;;
|
||||
1) cd examples/NavigationPlayground && yarn && yarn test ;;
|
||||
#1) cd examples/NavigationPlayground && yarn && yarn test ;;
|
||||
#2) cd examples/ReduxExample && yarn && yarn test ;;
|
||||
esac
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
@@ -209,7 +208,7 @@ describe('NavigationContainer', () => {
|
||||
let spy = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spy.console = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ Object {
|
||||
"console": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://v2.reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
|
||||
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
|
||||
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
|
||||
remove: () => {},
|
||||
});
|
||||
|
||||
describe('addNavigationHelpers', () => {
|
||||
it('handles dismiss action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
const child = { key: 'A', routeName: 'Home' };
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: child,
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
dangerouslyGetParent: () => ({
|
||||
state: {
|
||||
key: 'P',
|
||||
routeName: 'Parent',
|
||||
routes: [child],
|
||||
},
|
||||
}),
|
||||
}).dismiss()
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.BACK,
|
||||
key: 'P',
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
102
src/__tests__/getNavigation-test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import getNavigation from '../getNavigation';
|
||||
|
||||
test('getNavigation provides default action helpers', () => {
|
||||
const router = {
|
||||
getActionCreators: () => ({}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
|
||||
const dispatch = jest.fn();
|
||||
|
||||
const topNav = getNavigation(
|
||||
router,
|
||||
{},
|
||||
dispatch,
|
||||
new Set(),
|
||||
() => ({}),
|
||||
() => topNav
|
||||
);
|
||||
|
||||
topNav.navigate('GreatRoute');
|
||||
|
||||
expect(dispatch.mock.calls.length).toBe(1);
|
||||
expect(dispatch.mock.calls[0][0].type).toBe('Navigation/NAVIGATE');
|
||||
expect(dispatch.mock.calls[0][0].routeName).toBe('GreatRoute');
|
||||
});
|
||||
|
||||
test('getNavigation provides router action helpers', () => {
|
||||
const router = {
|
||||
getActionCreators: () => ({
|
||||
foo: bar => ({ type: 'FooBarAction', bar }),
|
||||
}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
|
||||
const dispatch = jest.fn();
|
||||
|
||||
const topNav = getNavigation(
|
||||
router,
|
||||
{},
|
||||
dispatch,
|
||||
new Set(),
|
||||
() => ({}),
|
||||
() => topNav
|
||||
);
|
||||
|
||||
topNav.foo('Great');
|
||||
|
||||
expect(dispatch.mock.calls.length).toBe(1);
|
||||
expect(dispatch.mock.calls[0][0].type).toBe('FooBarAction');
|
||||
expect(dispatch.mock.calls[0][0].bar).toBe('Great');
|
||||
});
|
||||
|
||||
test('getNavigation get child navigation with router', () => {
|
||||
const actionSubscribers = new Set();
|
||||
let navigation = null;
|
||||
|
||||
const routerA = {
|
||||
getActionCreators: () => ({}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const router = {
|
||||
childRouters: {
|
||||
RouteA: routerA,
|
||||
},
|
||||
getActionCreators: () => ({}),
|
||||
getStateForAction(action, lastState = {}) {
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
|
||||
const initState = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
key: 'a',
|
||||
routeName: 'RouteA',
|
||||
routes: [{ key: 'c', routeName: 'RouteC' }],
|
||||
index: 0,
|
||||
},
|
||||
{ key: 'b', routeName: 'RouteB' },
|
||||
],
|
||||
};
|
||||
|
||||
const topNav = getNavigation(
|
||||
router,
|
||||
initState,
|
||||
() => {},
|
||||
actionSubscribers,
|
||||
() => ({}),
|
||||
() => navigation
|
||||
);
|
||||
|
||||
const childNavA = topNav.getChildNavigation('a');
|
||||
|
||||
expect(childNavA.router).toBe(routerA);
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
/* Helpers for navigation */
|
||||
|
||||
import NavigationActions from './NavigationActions';
|
||||
import invariant from './utils/invariant';
|
||||
export default function(navigation) {
|
||||
return {
|
||||
...navigation,
|
||||
// Go back from the given key, default to active key
|
||||
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 })
|
||||
);
|
||||
},
|
||||
// Go back from the parent key. If this is a nested stack, the entire
|
||||
// stack will be dismissed.
|
||||
dismiss: () => {
|
||||
let parent = navigation.dangerouslyGetParent();
|
||||
if (parent && parent.state) {
|
||||
return navigation.dispatch(
|
||||
NavigationActions.back({ key: parent.state.key })
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
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,
|
||||
})
|
||||
),
|
||||
|
||||
openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()),
|
||||
closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()),
|
||||
toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Linking, AsyncStorage } from 'react-native';
|
||||
import withLifecyclePolyfill from 'react-lifecycles-compat';
|
||||
import { AsyncStorage, Linking, Platform } from 'react-native';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import { BackHandler } from './PlatformHelpers';
|
||||
import NavigationActions from './NavigationActions';
|
||||
import getNavigation from './getNavigation';
|
||||
import invariant from './utils/invariant';
|
||||
import docsUrl from './utils/docsUrl';
|
||||
|
||||
@@ -184,9 +185,9 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,20 +199,39 @@ export default function createNavigationContainer(Component) {
|
||||
|
||||
if (__DEV__ && !this.props.detached) {
|
||||
if (_statefulContainerCount > 0) {
|
||||
console.error(
|
||||
`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'
|
||||
)}`
|
||||
);
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
@@ -219,15 +239,7 @@ export default function createNavigationContainer(Component) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
let action = this._initialAction;
|
||||
if (!startupState) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Init new Navigation State');
|
||||
startupState = Component.router.getStateForAction(action);
|
||||
}
|
||||
|
||||
const url = await Linking.getInitialURL();
|
||||
const parsedUrl = url && this._urlToPathAndParams(url);
|
||||
// Pull state out of URL
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const urlAction = Component.router.getActionForPathAndParams(
|
||||
@@ -244,11 +256,8 @@ export default function createNavigationContainer(Component) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (startupState === this.state.nav) {
|
||||
return;
|
||||
}
|
||||
this.setState({ nav: startupState }, () => {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
|
||||
const dispatchActions = () =>
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
@@ -257,6 +266,15 @@ export default function createNavigationContainer(Component) {
|
||||
lastState: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (startupState === this.state.nav) {
|
||||
dispatchActions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ nav: startupState }, () => {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
dispatchActions();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,6 +285,8 @@ export default function createNavigationContainer(Component) {
|
||||
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
|
||||
);
|
||||
this.dispatch(NavigationActions.init());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,58 +314,68 @@ export default function createNavigationContainer(Component) {
|
||||
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(nav);
|
||||
this._persistNavigationState(navState);
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
dispatchActionEvents();
|
||||
}
|
||||
|
||||
dispatchActionEvents();
|
||||
return false;
|
||||
};
|
||||
|
||||
_getScreenProps = () => this.props.screenProps;
|
||||
|
||||
render() {
|
||||
let navigation = this.props.navigation;
|
||||
if (this._isStateful()) {
|
||||
const nav = this.state.nav;
|
||||
if (!nav) {
|
||||
const navState = this.state.nav;
|
||||
if (!navState) {
|
||||
return this._renderLoading();
|
||||
}
|
||||
if (!this._navigation || this._navigation.state !== nav) {
|
||||
this._navigation = {
|
||||
dispatch: this.dispatch,
|
||||
state: nav,
|
||||
addListener: (eventName, handler) => {
|
||||
if (eventName !== 'action') {
|
||||
return { remove: () => {} };
|
||||
}
|
||||
this._actionEventSubscribers.add(handler);
|
||||
return {
|
||||
remove: () => {
|
||||
this._actionEventSubscribers.delete(handler);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
if (!this._navigation || this._navigation.state !== navState) {
|
||||
this._navigation = getNavigation(
|
||||
Component.router,
|
||||
navState,
|
||||
this.dispatch,
|
||||
this._actionEventSubscribers,
|
||||
this._getScreenProps,
|
||||
() => this._navigation
|
||||
);
|
||||
}
|
||||
navigation = this._navigation;
|
||||
}
|
||||
@@ -354,5 +384,5 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
}
|
||||
|
||||
return withLifecyclePolyfill(NavigationContainer);
|
||||
return polyfill(NavigationContainer);
|
||||
}
|
||||
|
||||
@@ -138,11 +138,14 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
emit((lastEmittedEvent = 'didBlur'), childPayload);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEmittedEvent === 'didBlur' && !newRoute) {
|
||||
removeAll();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
removeAll,
|
||||
addListener(eventName, eventHandler) {
|
||||
const subscribers = getChildSubscribers(eventName);
|
||||
if (!subscribers) {
|
||||
|
||||
102
src/getChildNavigation.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import getChildEventSubscriber from './getChildEventSubscriber';
|
||||
import getChildRouter from './getChildRouter';
|
||||
import invariant from './utils/invariant';
|
||||
|
||||
const createParamGetter = route => (paramName, defaultValue) => {
|
||||
const params = route.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
|
||||
const children =
|
||||
navigation._childrenNavigation || (navigation._childrenNavigation = {});
|
||||
|
||||
const childRoute = navigation.state.routes.find(r => r.key === childKey);
|
||||
|
||||
if (children[childKey] && children[childKey].state === childRoute) {
|
||||
return children[childKey];
|
||||
}
|
||||
|
||||
const childRouter = getChildRouter(navigation.router, childRoute.routeName);
|
||||
|
||||
// If the route has children, we'll use this to pass in to the action creators
|
||||
// for the childRouter so that any action that depends on the active route will
|
||||
// behave as expected. We don't explicitly require that routers implement routes
|
||||
// and index properties, but if we did then we would put an invariant here to
|
||||
// ensure that a focusedGrandChildRoute exists if childRouter is defined.
|
||||
const focusedGrandChildRoute =
|
||||
childRoute.routes && typeof childRoute.index === 'number'
|
||||
? childRoute.routes[childRoute.index]
|
||||
: null;
|
||||
|
||||
const actionCreators = {
|
||||
...navigation.actions,
|
||||
...navigation.router.getActionCreators(childRoute, navigation.state.key),
|
||||
...(childRouter
|
||||
? childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key)
|
||||
: {}),
|
||||
};
|
||||
const actionHelpers = {};
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
actionHelpers[actionName] = (...args) => {
|
||||
const actionCreator = actionCreators[actionName];
|
||||
const action = actionCreator(...args);
|
||||
return navigation.dispatch(action);
|
||||
};
|
||||
});
|
||||
|
||||
if (children[childKey]) {
|
||||
children[childKey] = {
|
||||
...children[childKey],
|
||||
...actionHelpers,
|
||||
state: childRoute,
|
||||
router: childRouter,
|
||||
actions: actionCreators,
|
||||
getParam: createParamGetter(childRoute),
|
||||
};
|
||||
return children[childKey];
|
||||
}
|
||||
|
||||
const childSubscriber = getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
childKey
|
||||
);
|
||||
|
||||
children[childKey] = {
|
||||
...actionHelpers,
|
||||
|
||||
state: childRoute,
|
||||
router: childRouter,
|
||||
actions: actionCreators,
|
||||
getParam: createParamGetter(childRoute),
|
||||
|
||||
getChildNavigation: grandChildKey =>
|
||||
getChildNavigation(children[childKey], grandChildKey, () =>
|
||||
getCurrentParentNavigation().getChildNavigation(childKey)
|
||||
),
|
||||
|
||||
isFocused: () => {
|
||||
const currentNavigation = getCurrentParentNavigation();
|
||||
const { routes, index } = currentNavigation.state;
|
||||
if (!currentNavigation.isFocused()) {
|
||||
return false;
|
||||
}
|
||||
if (routes[index].key === childKey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
dispatch: navigation.dispatch,
|
||||
getScreenProps: navigation.getScreenProps,
|
||||
dangerouslyGetParent: getCurrentParentNavigation,
|
||||
addListener: childSubscriber.addListener,
|
||||
};
|
||||
return children[childKey];
|
||||
}
|
||||
|
||||
export default getChildNavigation;
|
||||
9
src/getChildRouter.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function getChildRouter(router, routeName) {
|
||||
if (router.childRouters && router.childRouters[routeName]) {
|
||||
return router.childRouters[routeName];
|
||||
}
|
||||
|
||||
const Component = router.getComponentForRouteName(routeName);
|
||||
|
||||
return Component.router;
|
||||
}
|
||||
54
src/getNavigation.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import getChildNavigation from './getChildNavigation';
|
||||
|
||||
export default function getNavigation(
|
||||
router,
|
||||
state,
|
||||
dispatch,
|
||||
actionSubscribers,
|
||||
getScreenProps,
|
||||
getCurrentNavigation
|
||||
) {
|
||||
const actions = router.getActionCreators(state, null);
|
||||
|
||||
const navigation = {
|
||||
actions,
|
||||
router,
|
||||
state,
|
||||
dispatch,
|
||||
getScreenProps,
|
||||
getChildNavigation: childKey =>
|
||||
getChildNavigation(navigation, childKey, getCurrentNavigation),
|
||||
isFocused: childKey => {
|
||||
const { routes, index } = getCurrentNavigation().state;
|
||||
if (childKey == null || routes[index].key === childKey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
addListener: (eventName, handler) => {
|
||||
if (eventName !== 'action') {
|
||||
return { remove: () => {} };
|
||||
}
|
||||
actionSubscribers.add(handler);
|
||||
return {
|
||||
remove: () => {
|
||||
actionSubscribers.delete(handler);
|
||||
},
|
||||
};
|
||||
},
|
||||
dangerouslyGetParent: () => null,
|
||||
};
|
||||
|
||||
const actionCreators = {
|
||||
...getNavigationActionCreators(navigation.state),
|
||||
...actions,
|
||||
};
|
||||
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
navigation[actionName] = (...args) =>
|
||||
navigation.dispatch(actionCreators[actionName](...args));
|
||||
});
|
||||
|
||||
return navigation;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import DrawerNavigator from '../createDrawerNavigator';
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: `Welcome ${
|
||||
navigation.state.params ? navigation.state.params.user : 'anonymous'
|
||||
}`,
|
||||
gesturesEnabled: true,
|
||||
});
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const routeConfig = {
|
||||
Home: {
|
||||
screen: HomeScreen,
|
||||
},
|
||||
};
|
||||
|
||||
describe('DrawerNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MyDrawerNavigator = DrawerNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyDrawerNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
40
src/navigators/__tests__/NestedNavigator-test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import StackNavigator from '../createStackNavigator';
|
||||
|
||||
const SubNavigator = StackNavigator({
|
||||
Home: {
|
||||
screen: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const NavNestedDirect = StackNavigator({
|
||||
Sub: {
|
||||
screen: SubNavigator,
|
||||
},
|
||||
});
|
||||
|
||||
const NavNestedIndirect = StackNavigator({
|
||||
Sub: {
|
||||
// eslint-disable-next-line react/display-name
|
||||
screen: props => <SubNavigator {...props} />,
|
||||
},
|
||||
});
|
||||
|
||||
/* Prevent React error boundaries from swallowing the errors */
|
||||
NavNestedIndirect.prototype.componentDidCatch = null;
|
||||
SubNavigator.prototype.componentDidCatch = null;
|
||||
|
||||
describe('Nested navigators', () => {
|
||||
it('renders succesfully as direct child', () => {
|
||||
const rendered = renderer.create(<NavNestedDirect />).toJSON();
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('throw when trying to pass navigation prop', () => {
|
||||
const tryRender = () => {
|
||||
renderer.create(<NavNestedIndirect />);
|
||||
};
|
||||
expect(tryRender).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,243 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DrawerNavigator renders successfully 1`] = `
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"zIndex": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
accessibilityComponentType={undefined}
|
||||
accessibilityLabel={undefined}
|
||||
accessibilityTraits={undefined}
|
||||
accessible={true}
|
||||
collapsable={undefined}
|
||||
hitSlop={undefined}
|
||||
nativeID={undefined}
|
||||
onLayout={undefined}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
pointerEvents="none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"zIndex": 1000,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
/>
|
||||
<View
|
||||
accessibilityViewIsModal={false}
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "white",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": null,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": -320,
|
||||
},
|
||||
],
|
||||
"width": 320,
|
||||
"zIndex": 1001,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={undefined}
|
||||
alwaysBounceVertical={false}
|
||||
onContentSizeChange={null}
|
||||
onMomentumScrollBegin={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={undefined}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onScrollShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onTouchCancel={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
scrollEventThrottle={undefined}
|
||||
sendMomentumEvents={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "column",
|
||||
"flexGrow": 1,
|
||||
"flexShrink": 1,
|
||||
"overflow": "scroll",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollContentView
|
||||
collapsable={false}
|
||||
removeClippedSubviews={undefined}
|
||||
style={
|
||||
Array [
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"paddingVertical": 4,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityComponentType={undefined}
|
||||
accessibilityLabel={undefined}
|
||||
accessibilityTraits={undefined}
|
||||
accessible={true}
|
||||
collapsable={undefined}
|
||||
hasTVPreferredFocus={undefined}
|
||||
hitSlop={undefined}
|
||||
isTVSelectable={true}
|
||||
nativeID={undefined}
|
||||
onLayout={undefined}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
tvParallaxProperties={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(0, 0, 0, .04)",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
ellipsizeMode="tail"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
"margin": 16,
|
||||
},
|
||||
Object {
|
||||
"color": "#2196f3",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollContentView>
|
||||
</RCTScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -0,0 +1,369 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`Nested navigators throw when trying to pass navigation prop 1`] = `"No \\"routes\\" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop"`;
|
||||
@@ -76,51 +76,36 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
"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,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -128,70 +113,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>
|
||||
@@ -277,51 +281,36 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
"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,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -329,52 +318,71 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
<View
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
exports[`TabNavigator renders successfully 1`] = `
|
||||
<View
|
||||
loaded={
|
||||
Array [
|
||||
0,
|
||||
]
|
||||
}
|
||||
onLayout={[Function]}
|
||||
collapsable={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
@@ -21,56 +16,65 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "stretch",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"alignItems": "stretch",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -143,7 +147,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -156,6 +160,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"minWidth": 30,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
@@ -170,6 +175,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"minWidth": 30,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dimensions, Platform, ScrollView } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import createNavigator from './createNavigator';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import DrawerRouter from '../routers/DrawerRouter';
|
||||
import DrawerScreen from '../views/Drawer/DrawerScreen';
|
||||
import DrawerView from '../views/Drawer/DrawerView';
|
||||
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
|
||||
|
||||
// A stack navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
|
||||
const defaultContentComponent = props => (
|
||||
<ScrollView alwaysBounceVertical={false}>
|
||||
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
|
||||
<DrawerItems {...props} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const DefaultDrawerConfig = {
|
||||
drawerWidth: () => {
|
||||
/*
|
||||
* Default drawer width is screen width - header height
|
||||
* with a max width of 280 on mobile and 320 on tablet
|
||||
* https://material.io/guidelines/patterns/navigation-drawer.html
|
||||
*/
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const smallerAxisSize = Math.min(height, width);
|
||||
const isLandscape = width > height;
|
||||
const isTablet = smallerAxisSize >= 600;
|
||||
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
|
||||
const maxWidth = isTablet ? 320 : 280;
|
||||
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
|
||||
},
|
||||
contentComponent: defaultContentComponent,
|
||||
drawerPosition: 'left',
|
||||
drawerBackgroundColor: 'white',
|
||||
useNativeAnimations: true,
|
||||
};
|
||||
|
||||
const DrawerNavigator = (routeConfigs, config = {}) => {
|
||||
const mergedConfig = { ...DefaultDrawerConfig, ...config };
|
||||
|
||||
const {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
...drawerConfig
|
||||
} = mergedConfig;
|
||||
|
||||
const routerConfig = {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
};
|
||||
|
||||
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
|
||||
|
||||
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
export default DrawerNavigator;
|
||||
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.onGestureCanceled && this.props.onGestureCanceled();
|
||||
};
|
||||
|
||||
_handleGestureFinish = () => {
|
||||
this._previouslyFocusedTextInput = null;
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
};
|
||||
|
||||
_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,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
|
||||
@@ -7,82 +8,37 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
static router = router;
|
||||
static navigationOptions = null;
|
||||
|
||||
childEventSubscribers = {};
|
||||
|
||||
// Cleanup subscriptions for routes that no longer exist
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
|
||||
Object.keys(this.childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
this.childEventSubscribers[key].removeAll();
|
||||
delete this.childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove all subscriptions
|
||||
componentWillUnmount() {
|
||||
Object.values(this.childEventSubscribers).map(s => s.removeAll());
|
||||
}
|
||||
|
||||
_isRouteFocused = route => () => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
state = {
|
||||
descriptors: {},
|
||||
screenProps: this.props.screenProps,
|
||||
};
|
||||
|
||||
_dangerouslyGetParent = () => {
|
||||
return this.props.navigation;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, screenProps } = this.props;
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const prevDescriptors = prevState.descriptors;
|
||||
const { navigation, screenProps } = nextProps;
|
||||
const { dispatch, state, addListener } = navigation;
|
||||
const { routes } = state;
|
||||
if (typeof routes === 'undefined') {
|
||||
throw new TypeError(
|
||||
'No "routes" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop'
|
||||
);
|
||||
}
|
||||
|
||||
const descriptors = { ...prevState.descriptors };
|
||||
|
||||
const descriptors = {};
|
||||
routes.forEach(route => {
|
||||
if (
|
||||
prevDescriptors &&
|
||||
prevDescriptors[route.key] &&
|
||||
route === prevDescriptors[route.key].state &&
|
||||
screenProps === prevState.screenProps
|
||||
) {
|
||||
descriptors[route.key] = prevDescriptors[route.key];
|
||||
return;
|
||||
}
|
||||
const getComponent = () =>
|
||||
router.getComponentForRouteName(route.routeName);
|
||||
|
||||
if (!this.childEventSubscribers[route.key]) {
|
||||
this.childEventSubscribers[route.key] = getChildEventSubscriber(
|
||||
addListener,
|
||||
route.key
|
||||
);
|
||||
}
|
||||
|
||||
const actionCreators = {
|
||||
...navigation.actions,
|
||||
...router.getActionCreators(route, state.key),
|
||||
};
|
||||
const actionHelpers = {};
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
actionHelpers[actionName] = (...args) => {
|
||||
const actionCreator = actionCreators[actionName];
|
||||
const action = actionCreator(...args);
|
||||
dispatch(action);
|
||||
};
|
||||
});
|
||||
const childNavigation = {
|
||||
...actionHelpers,
|
||||
actions: actionCreators,
|
||||
dispatch,
|
||||
state: route,
|
||||
isFocused: () => this._isRouteFocused(route),
|
||||
dangerouslyGetParent: this._dangerouslyGetParent,
|
||||
addListener: this.childEventSubscribers[route.key].addListener,
|
||||
getParam: (paramName, defaultValue) => {
|
||||
const params = route.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
};
|
||||
|
||||
const childNavigation = navigation.getChildNavigation(route.key);
|
||||
const options = router.getScreenOptions(childNavigation, screenProps);
|
||||
descriptors[route.key] = {
|
||||
key: route.key,
|
||||
@@ -93,17 +49,23 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
};
|
||||
});
|
||||
|
||||
return { descriptors, screenProps };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NavigatorView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
{...this.props}
|
||||
screenProps={this.state.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
navigationConfig={navigationConfig}
|
||||
descriptors={descriptors}
|
||||
descriptors={this.state.descriptors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return Navigator;
|
||||
|
||||
return polyfill(Navigator);
|
||||
}
|
||||
|
||||
export default createNavigator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
|
||||
import createNavigator from './createNavigator';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
@@ -11,6 +12,8 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
disableKeyboardHandling,
|
||||
getCustomActionCreators,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
@@ -19,12 +22,16 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
getCustomActionCreators,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
|
||||
// Create a navigator with StackView as the view
|
||||
const Navigator = createNavigator(StackView, router, stackConfig);
|
||||
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);
|
||||
|
||||
29
src/react-navigation.js
vendored
@@ -8,6 +8,9 @@ module.exports = {
|
||||
get StateUtils() {
|
||||
return require('./StateUtils').default;
|
||||
},
|
||||
get getNavigation() {
|
||||
return require('./getNavigation').default;
|
||||
},
|
||||
|
||||
// Navigators
|
||||
get createNavigator() {
|
||||
@@ -32,33 +35,31 @@ module.exports = {
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
},
|
||||
get createDrawerNavigator() {
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
return require('react-navigation-drawer').createDrawerNavigator;
|
||||
},
|
||||
get DrawerNavigator() {
|
||||
console.warn(
|
||||
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
|
||||
);
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
return require('react-navigation-drawer').createDrawerNavigator;
|
||||
},
|
||||
get createTabNavigator() {
|
||||
console.warn(
|
||||
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopNavigator instead.'
|
||||
'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 createMaterialTopNavigator instead.'
|
||||
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').default;
|
||||
return require('react-navigation-deprecated-tab-navigator')
|
||||
.createTabNavigator;
|
||||
},
|
||||
get createBottomTabNavigator() {
|
||||
return require('react-navigation-tabs').createBottomTabNavigator;
|
||||
},
|
||||
get createMaterialBottomTabNavigator() {
|
||||
return require('react-navigation-tabs').createMaterialBottomTabNavigator;
|
||||
},
|
||||
get createMaterialTopTabNavigator() {
|
||||
return require('react-navigation-tabs').createMaterialTopTabNavigator;
|
||||
},
|
||||
@@ -71,7 +72,7 @@ module.exports = {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
return require('react-navigation-drawer').DrawerActions;
|
||||
},
|
||||
|
||||
// Routers
|
||||
@@ -81,6 +82,9 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get DrawerRouter() {
|
||||
return require('react-navigation-drawer').DrawerRouter;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
@@ -118,10 +122,13 @@ module.exports = {
|
||||
|
||||
// DrawerView
|
||||
get DrawerView() {
|
||||
return require('./views/Drawer/DrawerView').default;
|
||||
return require('react-navigation-drawer').DrawerView;
|
||||
},
|
||||
get DrawerItems() {
|
||||
return require('./views/Drawer/DrawerNavigatorItems').default;
|
||||
return require('react-navigation-drawer').DrawerNavigatorItems;
|
||||
},
|
||||
get DrawerSidebar() {
|
||||
return require('react-navigation-drawer').DrawerSidebar;
|
||||
},
|
||||
|
||||
// TabView
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
|
||||
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
|
||||
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
|
||||
|
||||
const openDrawer = payload => ({
|
||||
type: OPEN_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const closeDrawer = payload => ({
|
||||
type: CLOSE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const toggleDrawer = payload => ({
|
||||
type: TOGGLE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
export default {
|
||||
OPEN_DRAWER,
|
||||
CLOSE_DRAWER,
|
||||
TOGGLE_DRAWER,
|
||||
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
toggleDrawer,
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import SwitchRouter from './SwitchRouter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
import invariant from '../utils/invariant';
|
||||
import withDefaultValue from '../utils/withDefaultValue';
|
||||
|
||||
import DrawerActions from './DrawerActions';
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
config = { ...config };
|
||||
config = withDefaultValue(config, 'resetOnBlur', false);
|
||||
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
|
||||
|
||||
const switchRouter = SwitchRouter(routeConfigs, config);
|
||||
|
||||
return {
|
||||
...switchRouter,
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
openDrawer: () => DrawerActions.openDrawer({ key: navStateKey }),
|
||||
closeDrawer: () => DrawerActions.closeDrawer({ key: navStateKey }),
|
||||
toggleDrawer: () => DrawerActions.toggleDrawer({ key: navStateKey }),
|
||||
...switchRouter.getActionCreators(route, navStateKey),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, lastState) {
|
||||
const state = lastState || {
|
||||
...switchRouter.getStateForAction(action, undefined),
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
|
||||
const isRouterTargeted = action.key == null || action.key === state.key;
|
||||
|
||||
if (
|
||||
isRouterTargeted &&
|
||||
action.type === DrawerActions.CLOSE_DRAWER &&
|
||||
state.isDrawerOpen
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isRouterTargeted &&
|
||||
action.type === DrawerActions.OPEN_DRAWER &&
|
||||
!state.isDrawerOpen
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (isRouterTargeted && action.type === DrawerActions.TOGGLE_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: !state.isDrawerOpen,
|
||||
};
|
||||
}
|
||||
|
||||
// Fall back on tab router for screen switching logic
|
||||
const childState = switchRouter.getStateForAction(action, state);
|
||||
if (childState !== null && childState !== state) {
|
||||
// If the tabs have changed, make sure to close the drawer
|
||||
return {
|
||||
...childState,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -27,6 +27,10 @@ function behavesLikePushAction(action) {
|
||||
|
||||
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);
|
||||
@@ -145,6 +149,8 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
paths.sort((a, b) => b[1].priority - a[1].priority);
|
||||
|
||||
return {
|
||||
childRouters,
|
||||
|
||||
getComponentForState(state) {
|
||||
const activeChildRoute = state.routes[state.index];
|
||||
const { routeName } = activeChildRoute;
|
||||
@@ -160,7 +166,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route, navStateKey),
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, navStateKey),
|
||||
pop: (n, params) =>
|
||||
StackActions.pop({
|
||||
@@ -223,7 +229,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 !== StackActions.RESET || action.key !== null) {
|
||||
if (
|
||||
!isResetToRootStack(action) &&
|
||||
action.type !== NavigationActions.NAVIGATE
|
||||
) {
|
||||
const keyIndex = action.key
|
||||
? StateUtils.indexOf(state, action.key)
|
||||
: -1;
|
||||
@@ -243,6 +252,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
|
||||
@@ -295,7 +336,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
isTransitioning:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true
|
||||
: undefined,
|
||||
: state.isTransitioning,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
};
|
||||
@@ -360,7 +401,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
routeName: childRouterName,
|
||||
key: action.key || generateKey(),
|
||||
};
|
||||
return StateUtils.push(state, route);
|
||||
return {
|
||||
...StateUtils.push(state, route),
|
||||
isTransitioning: action.immediate !== true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ export default (routeConfigs, config = {}) => {
|
||||
}
|
||||
|
||||
return {
|
||||
childRouters,
|
||||
|
||||
getInitialState() {
|
||||
const routes = order.map(resetChildRoute);
|
||||
return {
|
||||
@@ -108,7 +110,7 @@ export default (routeConfigs, config = {}) => {
|
||||
|
||||
getActionCreators(route, stateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route, stateKey),
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, stateKey),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/* eslint react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
import DrawerRouter from '../DrawerRouter';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
|
||||
describe('DrawerRouter', () => {
|
||||
test('Handles basic tab logic', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
],
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
expect(state).toEqual(expectedState);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
|
||||
state
|
||||
);
|
||||
const expectedState2 = {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
],
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
expect(state2).toEqual(expectedState2);
|
||||
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
|
||||
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
||||
});
|
||||
|
||||
test('Drawer opens closes and toggles', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.CLOSE_DRAWER },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(false);
|
||||
const state4 = router.getStateForAction(
|
||||
{ type: DrawerActions.TOGGLE_DRAWER },
|
||||
state3
|
||||
);
|
||||
expect(state4.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
|
||||
test('Drawer opens closes with key targeted', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(false);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
/* 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 NavigationActions from '../../NavigationActions';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
@@ -15,6 +16,7 @@ beforeEach(() => {
|
||||
const ROUTERS = {
|
||||
TabRouter,
|
||||
StackRouter,
|
||||
SwitchRouter,
|
||||
};
|
||||
|
||||
const dummyEventSubscriber = (name, handler) => ({
|
||||
@@ -25,7 +27,7 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
const Router = ROUTERS[routerName];
|
||||
|
||||
describe(`General router features - ${routerName}`, () => {
|
||||
test('title is configurable using navigationOptions and getScreenOptions', () => {
|
||||
test(`title is configurable using navigationOptions and getScreenOptions - ${routerName}`, () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
@@ -86,9 +88,103 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
).title
|
||||
).toEqual('Baz-123');
|
||||
});
|
||||
|
||||
test(`set params works in ${routerName}`, () => {
|
||||
class FooView extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const router = Router({
|
||||
Foo: { screen: FooView },
|
||||
Bar: { screen: FooView },
|
||||
});
|
||||
|
||||
const initState = router.getStateForAction(NavigationActions.init());
|
||||
const initRoute = initState.routes[initState.index];
|
||||
expect(initRoute.params).toEqual(undefined);
|
||||
|
||||
const state0 = router.getStateForAction(
|
||||
NavigationActions.setParams({
|
||||
params: { foo: 42 },
|
||||
key: initRoute.key,
|
||||
}),
|
||||
initState
|
||||
);
|
||||
expect(state0.routes[state0.index].params.foo).toEqual(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 />;
|
||||
@@ -157,6 +253,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 />;
|
||||
@@ -249,3 +409,62 @@ 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)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
import StackRouter from '../StackRouter';
|
||||
import StackActions from '../StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import TabRouter from '../TabRouter';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -427,12 +426,47 @@ describe('StackRouter', () => {
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('Handle navigation to nested navigator', () => {
|
||||
const state = TestStackRouter.getStateForAction({
|
||||
type: NavigationActions.INIT,
|
||||
});
|
||||
const action = TestStackRouter.getActionForPathAndParams('fo/22/b/hello');
|
||||
/* $FlowFixMe */
|
||||
const state2 = TestStackRouter.getStateForAction(action);
|
||||
expect(state2).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'id-4',
|
||||
isTransitioning: false,
|
||||
routeName: 'foo',
|
||||
params: {
|
||||
fooThing: '22',
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
routeName: 'bar',
|
||||
key: 'id-3',
|
||||
params: {
|
||||
barThing: 'hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('popToTop bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
@@ -484,6 +518,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 /> },
|
||||
@@ -620,6 +692,7 @@ describe('StackRouter', () => {
|
||||
state
|
||||
);
|
||||
|
||||
expect(state2.isTransitioning).toEqual(true);
|
||||
expect(state2.index).toEqual(1);
|
||||
expect(state2.routes[1].index).toEqual(1);
|
||||
expect(state2.routes[1].routes[1].index).toEqual(1);
|
||||
@@ -642,23 +715,74 @@ describe('StackRouter', () => {
|
||||
expect(pushedState).toEqual(null);
|
||||
});
|
||||
|
||||
test('Navigate with key is idempotent', () => {
|
||||
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' }),
|
||||
initState
|
||||
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
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
|
||||
const second = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'Second2' }),
|
||||
first
|
||||
);
|
||||
expect(pushedTwiceState).toEqual(null);
|
||||
|
||||
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', () => {
|
||||
@@ -1079,15 +1203,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({
|
||||
@@ -1104,10 +1264,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' },
|
||||
},
|
||||
]);
|
||||
@@ -1193,7 +1353,7 @@ describe('StackRouter', () => {
|
||||
});
|
||||
|
||||
test('Handles the reset action with nested Router', () => {
|
||||
const ChildRouter = TabRouter({
|
||||
const ChildRouter = StackRouter({
|
||||
baz: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
@@ -1214,6 +1374,7 @@ describe('StackRouter', () => {
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.RESET,
|
||||
key: null,
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1325,6 +1486,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,
|
||||
@@ -1445,42 +1634,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(
|
||||
{
|
||||
@@ -1761,3 +1914,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');
|
||||
});
|
||||
|
||||
@@ -127,6 +127,93 @@ describe('SwitchRouter', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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 = {}) => {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import TabRouter from '../TabRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
@@ -141,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 },
|
||||
@@ -695,53 +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(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('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 },
|
||||
});
|
||||
@@ -750,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,
|
||||
@@ -779,7 +780,6 @@ describe('TabRouter', () => {
|
||||
routeName: 'Boo',
|
||||
action: NavigationActions.navigate({ routeName: 'Zoo' }),
|
||||
});
|
||||
|
||||
const expectedState = ScreenA.router.getStateForAction(
|
||||
action,
|
||||
screenApreState
|
||||
@@ -787,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)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function docsUrl(path) {
|
||||
return `https://v2.reactnavigation.org/docs/${path}`;
|
||||
return `https://reactnavigation.org/docs/${path}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default (obj, key, defaultValue) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (obj.hasOwnProperty(key) && typeof obj[key] !== 'undefined') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import React from 'react';
|
||||
import { View, Text, Platform, StyleSheet } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
/**
|
||||
* Component that renders the navigation list in the drawer.
|
||||
*/
|
||||
const DrawerNavigatorItems = ({
|
||||
navigation: { state, navigate },
|
||||
items,
|
||||
activeItemKey,
|
||||
activeTintColor,
|
||||
activeBackgroundColor,
|
||||
inactiveTintColor,
|
||||
inactiveBackgroundColor,
|
||||
getLabel,
|
||||
renderIcon,
|
||||
onItemPress,
|
||||
itemsContainerStyle,
|
||||
itemStyle,
|
||||
labelStyle,
|
||||
activeLabelStyle,
|
||||
inactiveLabelStyle,
|
||||
iconContainerStyle,
|
||||
drawerPosition,
|
||||
}) => (
|
||||
<View style={[styles.container, itemsContainerStyle]}>
|
||||
{items.map((route, index) => {
|
||||
const focused = activeItemKey === route.key;
|
||||
const color = focused ? activeTintColor : inactiveTintColor;
|
||||
const backgroundColor = focused
|
||||
? activeBackgroundColor
|
||||
: inactiveBackgroundColor;
|
||||
const scene = { route, index, focused, tintColor: color };
|
||||
const icon = renderIcon(scene);
|
||||
const label = getLabel(scene);
|
||||
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
|
||||
return (
|
||||
<TouchableItem
|
||||
key={route.key}
|
||||
onPress={() => {
|
||||
onItemPress({ route, focused });
|
||||
}}
|
||||
delayPressIn={0}
|
||||
>
|
||||
<SafeAreaView
|
||||
style={{ backgroundColor }}
|
||||
forceInset={{
|
||||
[drawerPosition]: 'always',
|
||||
[drawerPosition === 'left' ? 'right' : 'left']: 'never',
|
||||
vertical: 'never',
|
||||
}}
|
||||
>
|
||||
<View style={[styles.item, itemStyle]}>
|
||||
{icon ? (
|
||||
<View
|
||||
style={[
|
||||
styles.icon,
|
||||
focused ? null : styles.inactiveIcon,
|
||||
iconContainerStyle,
|
||||
]}
|
||||
>
|
||||
{icon}
|
||||
</View>
|
||||
) : null}
|
||||
{typeof label === 'string' ? (
|
||||
<Text
|
||||
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</TouchableItem>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
/* Material design specs - https://material.io/guidelines/patterns/navigation-drawer.html#navigation-drawer-specs */
|
||||
DrawerNavigatorItems.defaultProps = {
|
||||
activeTintColor: '#2196f3',
|
||||
activeBackgroundColor: 'rgba(0, 0, 0, .04)',
|
||||
inactiveTintColor: 'rgba(0, 0, 0, .87)',
|
||||
inactiveBackgroundColor: 'transparent',
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingVertical: 4,
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
marginHorizontal: 16,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
inactiveIcon: {
|
||||
/*
|
||||
* Icons have 0.54 opacity according to guidelines
|
||||
* 100/87 * 54 ~= 62
|
||||
*/
|
||||
opacity: 0.62,
|
||||
},
|
||||
label: {
|
||||
margin: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
export default DrawerNavigatorItems;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import SceneView from '../SceneView';
|
||||
|
||||
/**
|
||||
* Component that renders the child screen of the drawer.
|
||||
*/
|
||||
class DrawerScreen extends React.PureComponent {
|
||||
render() {
|
||||
const { descriptors, navigation, screenProps } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
const descriptor = descriptors[routes[index].key];
|
||||
const Content = descriptor.getComponent();
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
component={Content}
|
||||
navigation={descriptor.navigation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerScreen;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
/**
|
||||
* Component that renders the sidebar screen of the drawer.
|
||||
*/
|
||||
|
||||
class DrawerSidebar extends React.PureComponent {
|
||||
_getScreenOptions = routeKey => {
|
||||
const descriptor = this.props.descriptors[routeKey];
|
||||
invariant(
|
||||
descriptor.options,
|
||||
'Cannot access screen descriptor options from drawer sidebar'
|
||||
);
|
||||
return descriptor.options;
|
||||
};
|
||||
|
||||
_getLabel = ({ focused, tintColor, route }) => {
|
||||
const { drawerLabel, title } = this._getScreenOptions(route.key);
|
||||
if (drawerLabel) {
|
||||
return typeof drawerLabel === 'function'
|
||||
? drawerLabel({ tintColor, focused })
|
||||
: drawerLabel;
|
||||
}
|
||||
|
||||
if (typeof title === 'string') {
|
||||
return title;
|
||||
}
|
||||
|
||||
return route.routeName;
|
||||
};
|
||||
|
||||
_renderIcon = ({ focused, tintColor, route }) => {
|
||||
const { drawerIcon } = this._getScreenOptions(route.key);
|
||||
if (drawerIcon) {
|
||||
return typeof drawerIcon === 'function'
|
||||
? drawerIcon({ tintColor, focused })
|
||||
: drawerIcon;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
_onItemPress = ({ route, focused }) => {
|
||||
if (!focused) {
|
||||
let subAction;
|
||||
// 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({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
routeName: route.routes[0].routeName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
this.props.navigation.navigate(route.routeName, undefined, subAction);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const ContentComponent = this.props.contentComponent;
|
||||
if (!ContentComponent) {
|
||||
return null;
|
||||
}
|
||||
const { state } = this.props.navigation;
|
||||
invariant(typeof state.index === 'number', 'should be set');
|
||||
return (
|
||||
<View style={[styles.container, this.props.style]}>
|
||||
<ContentComponent
|
||||
{...this.props.contentOptions}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
items={state.routes}
|
||||
activeItemKey={
|
||||
state.routes[state.index] ? state.routes[state.index].key : null
|
||||
}
|
||||
screenProps={this.props.screenProps}
|
||||
getLabel={this._getLabel}
|
||||
renderIcon={this._renderIcon}
|
||||
onItemPress={this._onItemPress}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerSidebar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import DrawerLayout from 'react-native-drawer-layout-polyfill';
|
||||
|
||||
import DrawerSidebar from './DrawerSidebar';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
*/
|
||||
export default class DrawerView extends React.PureComponent {
|
||||
state = {
|
||||
drawerWidth:
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { isDrawerOpen } = this.props.navigation.state;
|
||||
const wasDrawerOpen = prevProps.navigation.state.isDrawerOpen;
|
||||
|
||||
if (isDrawerOpen && !wasDrawerOpen) {
|
||||
this._drawer.openDrawer();
|
||||
} else if (wasDrawerOpen && !isDrawerOpen) {
|
||||
this._drawer.closeDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
const { navigation } = this.props;
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (!isDrawerOpen) {
|
||||
navigation.dispatch({ type: DrawerActions.OPEN_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
const { navigation } = this.props;
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (isDrawerOpen) {
|
||||
navigation.dispatch({ type: DrawerActions.CLOSE_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_updateWidth = () => {
|
||||
const drawerWidth =
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth;
|
||||
|
||||
if (this.state.drawerWidth !== drawerWidth) {
|
||||
this.setState({ drawerWidth });
|
||||
}
|
||||
};
|
||||
|
||||
_renderNavigationView = () => {
|
||||
return (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
contentComponent={this.props.navigationConfig.contentComponent}
|
||||
contentOptions={this.props.navigationConfig.contentOptions}
|
||||
drawerPosition={this.props.navigationConfig.drawerPosition}
|
||||
style={this.props.navigationConfig.style}
|
||||
{...this.props.navigationConfig}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state } = this.props.navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = this.props.descriptors[activeKey];
|
||||
|
||||
const DrawerScreen = descriptor.getComponent();
|
||||
|
||||
const { drawerLockMode } = descriptor.options;
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={c => {
|
||||
this._drawer = c;
|
||||
}}
|
||||
drawerLockMode={
|
||||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
|
||||
this.props.navigationConfig.drawerLockMode
|
||||
}
|
||||
drawerBackgroundColor={
|
||||
this.props.navigationConfig.drawerBackgroundColor
|
||||
}
|
||||
drawerWidth={this.state.drawerWidth}
|
||||
onDrawerOpen={this._handleDrawerOpen}
|
||||
onDrawerClose={this._handleDrawerClose}
|
||||
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
|
||||
renderNavigationView={this._renderNavigationView}
|
||||
drawerPosition={
|
||||
this.props.navigationConfig.drawerPosition === 'right'
|
||||
? DrawerLayout.positions.Right
|
||||
: DrawerLayout.positions.Left
|
||||
}
|
||||
>
|
||||
<DrawerScreen
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={descriptor.navigation}
|
||||
/>
|
||||
</DrawerLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
I18nManager,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
||||
@@ -24,7 +25,9 @@ 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;
|
||||
};
|
||||
|
||||
@@ -47,12 +50,6 @@ class Header extends React.PureComponent {
|
||||
widths: {},
|
||||
};
|
||||
|
||||
_handleOnLayout = e => {
|
||||
if (typeof this.props.onLayout === 'function') {
|
||||
this.props.onLayout(e.nativeEvent.layout);
|
||||
}
|
||||
};
|
||||
|
||||
_getHeaderTitleString(scene) {
|
||||
const options = scene.descriptor.options;
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
@@ -172,7 +169,7 @@ class Header extends React.PureComponent {
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
) => {
|
||||
const { options } = props.scene.descriptor;
|
||||
const { options, navigation } = props.scene.descriptor;
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
@@ -184,7 +181,7 @@ class Header extends React.PureComponent {
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
this.props.navigation.goBack(props.scene.descriptor.key);
|
||||
navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -494,10 +491,7 @@ class Header extends React.PureComponent {
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={this.props.layoutInterpolator(this.props)}
|
||||
onLayout={this._handleOnLayout}
|
||||
>
|
||||
<Animated.View style={this.props.layoutInterpolator(this.props)}>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
{options.headerBackground}
|
||||
@@ -572,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,
|
||||
|
||||
@@ -47,7 +47,10 @@ function isGoingBack(scenes) {
|
||||
}
|
||||
|
||||
function forLayout(props) {
|
||||
const { layout, position, scene, scenes } = props;
|
||||
const { layout, position, scene, scenes, mode } = props;
|
||||
if (mode !== 'float') {
|
||||
return {};
|
||||
}
|
||||
const isBack = isGoingBack(scenes);
|
||||
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
@@ -55,8 +58,19 @@ function forLayout(props) {
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
const width = layout.initWidth;
|
||||
|
||||
// Make sure the header stays hidden when transitioning between 2 screens
|
||||
// with no header.
|
||||
if (
|
||||
(isBack && !hasHeader(scenes[index]) && !hasHeader(scenes[last])) ||
|
||||
(!isBack && !hasHeader(scenes[first]) && !hasHeader(scenes[index]))
|
||||
) {
|
||||
return {
|
||||
transform: [{ translateX: width }],
|
||||
};
|
||||
}
|
||||
|
||||
const rtlMult = I18nManager.isRTL ? -1 : 1;
|
||||
const translateX = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
@@ -163,11 +177,11 @@ function forLeftButton(props) {
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* NOTE: this offset calculation is an approximation that gives us
|
||||
* decent results in many cases, but it is ultimately a poor substitute
|
||||
* for text measurement. See the comment on title for more information.
|
||||
*
|
||||
*
|
||||
* - 70 is the width of the left button area.
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
@@ -234,14 +248,14 @@ function forLeftLabel(props) {
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* NOTE: this offset calculation is a an approximation that gives us
|
||||
* decent results in many cases, but it is ultimately a poor substitute
|
||||
* for text measurement. We want the back button label to transition
|
||||
* smoothly into the title text and to do this we need to understand
|
||||
* where the title is positioned within the title container (since it is
|
||||
* centered).
|
||||
*
|
||||
*
|
||||
* - 70 is the width of the left button area.
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import withLifecyclePolyfill from 'react-lifecycles-compat';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import SceneView from './SceneView';
|
||||
|
||||
@@ -77,4 +77,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default withLifecyclePolyfill(ResourceSavingSceneView);
|
||||
export default polyfill(ResourceSavingSceneView);
|
||||
|
||||
@@ -65,6 +65,17 @@ export default function ScenesReducer(
|
||||
prevState,
|
||||
descriptors
|
||||
) {
|
||||
// Always update the descriptors
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
// It will be resolved in a better way when we re-write Transitioner
|
||||
scenes.forEach(scene => {
|
||||
const { route } = scene;
|
||||
if (descriptors && descriptors[route.key]) {
|
||||
scene.descriptor = descriptors[route.key];
|
||||
}
|
||||
});
|
||||
|
||||
// Bail out early if we didn't update the state
|
||||
if (prevState === nextState) {
|
||||
return scenes;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import StackViewLayout from './StackViewLayout';
|
||||
@@ -22,15 +22,14 @@ class StackView extends React.Component {
|
||||
<Transitioner
|
||||
render={this._render}
|
||||
configureTransition={this._configureTransition}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
onTransitionStart={this.props.onTransitionStart}
|
||||
onTransitionEnd={(transition, lastTransition) => {
|
||||
const { onTransitionEnd, navigation } = this.props;
|
||||
if (
|
||||
transition.navigation.state.isTransitioning &&
|
||||
!lastTransition.navigation.state.isTransitioning
|
||||
) {
|
||||
const { navigationConfig, navigation } = this.props;
|
||||
const { onTransitionEnd } = navigationConfig;
|
||||
if (transition.navigation.state.isTransitioning) {
|
||||
navigation.dispatch(
|
||||
StackActions.completeTransition({
|
||||
key: navigation.state.key,
|
||||
@@ -60,6 +59,9 @@ class StackView extends React.Component {
|
||||
return (
|
||||
<StackViewLayout
|
||||
{...navigationConfig}
|
||||
onGestureBegin={this.props.onGestureBegin}
|
||||
onGestureCanceled={this.props.onGestureCanceled}
|
||||
onGestureEnd={this.props.onGestureEnd}
|
||||
screenProps={screenProps}
|
||||
descriptors={this.props.descriptors}
|
||||
transitionProps={transitionProps}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
View,
|
||||
I18nManager,
|
||||
Easing,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
@@ -16,6 +17,7 @@ import Header from '../Header/Header';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import SceneView from '../SceneView';
|
||||
import withOrientation from '../withOrientation';
|
||||
import { NavigationProvider } from '../NavigationContext';
|
||||
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
@@ -23,6 +25,13 @@ import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
|
||||
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
|
||||
const IS_IPHONE_X =
|
||||
Platform.OS === 'ios' &&
|
||||
!Platform.isPad &&
|
||||
!Platform.isTVOS &&
|
||||
(WINDOW_HEIGHT === 812 || WINDOW_WIDTH === 812);
|
||||
|
||||
const EaseInOut = Easing.inOut(Easing.ease);
|
||||
|
||||
/**
|
||||
@@ -80,6 +89,11 @@ class StackViewLayout extends React.Component {
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
state = {
|
||||
// Used when card's header is null and mode is float to make switch animation work correctly
|
||||
floatingHeaderHeight: 0,
|
||||
};
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
const { options } = scene.descriptor;
|
||||
const { header } = options;
|
||||
@@ -94,11 +108,7 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
|
||||
// Handle the case where the header option is a function, and provide the default
|
||||
const renderHeader =
|
||||
header ||
|
||||
(props => (
|
||||
<Header onLayout={layout => (this._headerLayout = layout)} {...props} />
|
||||
));
|
||||
const renderHeader = header || (props => <Header {...props} />);
|
||||
|
||||
const {
|
||||
headerLeftInterpolator,
|
||||
@@ -113,16 +123,20 @@ class StackViewLayout extends React.Component {
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
return renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
});
|
||||
return (
|
||||
<NavigationProvider value={scene.descriptor.navigation}>
|
||||
{renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
})}
|
||||
</NavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
@@ -204,15 +218,201 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_panResponder = PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
const { navigation } = this.props.transitionProps;
|
||||
const { index } = navigation.state;
|
||||
this._isResponding = false;
|
||||
this._reset(index, 0);
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
},
|
||||
onPanResponderGrant: () => {
|
||||
const {
|
||||
transitionProps: { navigation, position, scene },
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
|
||||
if (index !== scene.index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position.stopAnimation((value: number) => {
|
||||
this._isResponding = true;
|
||||
this._gestureStartValue = value;
|
||||
});
|
||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
||||
},
|
||||
onMoveShouldSetPanResponder: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene, scenes },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
if (index !== scene.index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
|
||||
const currentDragPosition =
|
||||
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
|
||||
const axisLength = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const axisHasBeenMeasured = !!axisLength;
|
||||
|
||||
// Measure the distance from the touch to the edge of the screen
|
||||
const screenEdgeDistance = gestureDirectionInverted
|
||||
? axisLength - (currentDragPosition - currentDragDistance)
|
||||
: currentDragPosition - currentDragDistance;
|
||||
// Compare to the gesture distance relavant to card or modal
|
||||
|
||||
const {
|
||||
gestureResponseDistance: userGestureResponseDistance = {},
|
||||
} = options;
|
||||
const gestureResponseDistance = isVertical
|
||||
? userGestureResponseDistance.vertical ||
|
||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
||||
: userGestureResponseDistance.horizontal ||
|
||||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
||||
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
|
||||
if (screenEdgeDistance > gestureResponseDistance) {
|
||||
// Reject touches that started in the middle of the screen
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasDraggedEnough =
|
||||
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
|
||||
|
||||
const isOnFirstCard = immediateIndex === 0;
|
||||
const shouldSetResponder =
|
||||
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
|
||||
return shouldSetResponder;
|
||||
},
|
||||
onPanResponderMove: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
// Handle the moving touches for our granted responder
|
||||
const startValue = this._gestureStartValue;
|
||||
const axis = isVertical ? 'dy' : 'dx';
|
||||
const axisDistance = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const currentValue =
|
||||
axis === 'dx' && gestureDirectionInverted
|
||||
? startValue + gesture[axis] / axisDistance
|
||||
: startValue - gesture[axis] / axisDistance;
|
||||
const value = clamp(index - 1, currentValue, index);
|
||||
position.setValue(value);
|
||||
},
|
||||
onPanResponderTerminationRequest: () =>
|
||||
// Returning false will prevent other views from becoming responder while
|
||||
// the navigation view is the responder (mid-gesture)
|
||||
false,
|
||||
onPanResponderRelease: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
if (!this._isResponding) {
|
||||
return;
|
||||
}
|
||||
this._isResponding = false;
|
||||
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
|
||||
// Calculate animate duration according to gesture speed and moved distance
|
||||
const axisDistance = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const movementDirection = gestureDirectionInverted ? -1 : 1;
|
||||
const movedDistance =
|
||||
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
|
||||
const gestureVelocity =
|
||||
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
|
||||
const defaultVelocity = axisDistance / ANIMATION_DURATION;
|
||||
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
|
||||
const resetDuration = gestureDirectionInverted
|
||||
? (axisDistance - movedDistance) / velocity
|
||||
: movedDistance / velocity;
|
||||
const goBackDuration = gestureDirectionInverted
|
||||
? movedDistance / velocity
|
||||
: (axisDistance - movedDistance) / velocity;
|
||||
|
||||
// To asyncronously get the current animated value, we need to run stopAnimation:
|
||||
position.stopAnimation(value => {
|
||||
// If the speed of the gesture release is significant, use that as the indication
|
||||
// of intent
|
||||
if (gestureVelocity < -0.5) {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
return;
|
||||
}
|
||||
if (gestureVelocity > 0.5) {
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
|
||||
// and the back will happen.
|
||||
if (value <= index - POSITION_THRESHOLD) {
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
} else {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
_onFloatingHeaderLayout = e => {
|
||||
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
|
||||
};
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'float') {
|
||||
const { scene } = this.props.transitionProps;
|
||||
floatingHeader = (
|
||||
<NavigationProvider value={scene.descriptor.navigation}>
|
||||
<View pointerEvents="box-none" onLayout={this._onFloatingHeaderLayout}>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
</NavigationProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const {
|
||||
@@ -222,142 +422,19 @@ class StackViewLayout extends React.Component {
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted = options.gestureDirection === 'inverted';
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
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: number) => {
|
||||
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 { options } = scene.descriptor;
|
||||
|
||||
const {
|
||||
gestureResponseDistance: userGestureResponseDistance = {},
|
||||
} = options;
|
||||
const gestureResponseDistance = isVertical
|
||||
? userGestureResponseDistance.vertical ||
|
||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
||||
: userGestureResponseDistance.horizontal ||
|
||||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
||||
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
|
||||
if (screenEdgeDistance > gestureResponseDistance) {
|
||||
// Reject touches that started in the middle of the screen
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasDraggedEnough =
|
||||
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
|
||||
|
||||
const isOnFirstCard = immediateIndex === 0;
|
||||
const shouldSetResponder =
|
||||
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
|
||||
return shouldSetResponder;
|
||||
},
|
||||
onPanResponderMove: (event, gesture) => {
|
||||
// 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 responder = !gesturesEnabled ? null : this._panResponder;
|
||||
|
||||
const handlers = gesturesEnabled ? responder.panHandlers : {};
|
||||
const containerStyle = [
|
||||
@@ -453,8 +530,8 @@ class StackViewLayout extends React.Component {
|
||||
const hasHeader = options.header !== null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
let marginTop = 0;
|
||||
if (!hasHeader && headerMode === 'float' && this._headerLayout) {
|
||||
marginTop = -this._headerLayout.height;
|
||||
if (!hasHeader && headerMode === 'float') {
|
||||
marginTop = -this.state.floatingHeaderHeight;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -484,4 +561,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default StackViewLayout;
|
||||
export default withOrientation(StackViewLayout);
|
||||
|
||||
@@ -63,6 +63,12 @@ class Transitioner extends React.Component {
|
||||
nextScenes = filterStale(nextScenes);
|
||||
}
|
||||
|
||||
// Update nextScenes when we change screenProps
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
if (nextProps.screenProps !== this.props.screenProps) {
|
||||
this.setState({ nextScenes });
|
||||
}
|
||||
|
||||
if (nextScenes === this.state.scenes) {
|
||||
return;
|
||||
}
|
||||
@@ -90,6 +96,24 @@ class Transitioner extends React.Component {
|
||||
this._prevTransitionProps = this._transitionProps;
|
||||
this._transitionProps = buildTransitionProps(nextProps, nextState);
|
||||
|
||||
const toValue = nextProps.navigation.state.index;
|
||||
|
||||
if (!this._transitionProps.navigation.state.isTransitioning) {
|
||||
this.setState(nextState, async () => {
|
||||
const result = nextProps.onTransitionStart(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
progress.setValue(1);
|
||||
position.setValue(toValue);
|
||||
this._onTransitionEnd();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// get the transition spec.
|
||||
const transitionUserSpec = nextProps.configureTransition
|
||||
? nextProps.configureTransition(
|
||||
@@ -106,7 +130,6 @@ class Transitioner extends React.Component {
|
||||
const { timing } = transitionSpec;
|
||||
delete transitionSpec.timing;
|
||||
|
||||
const toValue = nextProps.navigation.state.index;
|
||||
const positionHasChanged = position.__getValue() !== toValue;
|
||||
|
||||
// if swiped back, indexHasChanged == true && positionHasChanged == false
|
||||
|
||||
49
src/views/__tests__/Transitioner-test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/* eslint react/display-name:0 */
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Transitioner from '../Transitioner';
|
||||
|
||||
describe('Transitioner', () => {
|
||||
it('should not trigger onTransitionStart and onTransitionEnd when route params are changed', () => {
|
||||
const onTransitionStartCallback = jest.fn();
|
||||
const onTransitionEndCallback = jest.fn();
|
||||
|
||||
const transitionerProps = {
|
||||
configureTransition: (transitionProps, prevTransitionProps) => ({}),
|
||||
navigation: {
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', routeName: 'Foo' },
|
||||
{ key: '2', routeName: 'Bar' },
|
||||
],
|
||||
},
|
||||
goBack: () => false,
|
||||
dispatch: () => false,
|
||||
setParams: () => false,
|
||||
navigate: () => false,
|
||||
},
|
||||
render: () => <div />,
|
||||
onTransitionStart: onTransitionStartCallback,
|
||||
onTransitionEnd: onTransitionEndCallback,
|
||||
};
|
||||
|
||||
const nextTransitionerProps = {
|
||||
...transitionerProps,
|
||||
navigation: {
|
||||
...transitionerProps.navigation,
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', routeName: 'Foo', params: { name: 'Zoom' } },
|
||||
{ key: '2', routeName: 'Bar' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const component = renderer.create(<Transitioner {...transitionerProps} />);
|
||||
component.update(<Transitioner {...nextTransitionerProps} />);
|
||||
expect(onTransitionStartCallback).not.toBeCalled();
|
||||
expect(onTransitionEndCallback).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
exports[`TabBarBottom renders successfully 1`] = `
|
||||
<View
|
||||
loaded={
|
||||
Array [
|
||||
0,
|
||||
]
|
||||
}
|
||||
onLayout={[Function]}
|
||||
collapsable={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
@@ -20,73 +15,67 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={false}
|
||||
alwaysBounceVertical={false}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
bounces={false}
|
||||
contentContainerStyle={
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
contentOffset={
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
}
|
||||
directionalLockEnabled={true}
|
||||
horizontal={true}
|
||||
keyboardDismissMode="on-drag"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={null}
|
||||
onMomentumScrollBegin={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={undefined}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onScrollShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onTouchCancel={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
overScrollMode="never"
|
||||
pagingEnabled={true}
|
||||
scrollEnabled={undefined}
|
||||
scrollEventThrottle={1}
|
||||
scrollsToTop={false}
|
||||
sendMomentumEvents={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"flexGrow": 1,
|
||||
"flexShrink": 1,
|
||||
"overflow": "scroll",
|
||||
},
|
||||
>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={false}
|
||||
alwaysBounceVertical={false}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
bounces={false}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<RCTScrollContentView
|
||||
collapsable={false}
|
||||
removeClippedSubviews={undefined}
|
||||
}
|
||||
}
|
||||
contentOffset={
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
}
|
||||
directionalLockEnabled={true}
|
||||
horizontal={true}
|
||||
keyboardDismissMode="on-drag"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={null}
|
||||
onMomentumScrollBegin={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={undefined}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onScrollShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
onTouchCancel={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
overScrollMode="never"
|
||||
pagingEnabled={true}
|
||||
scrollEnabled={undefined}
|
||||
scrollEventThrottle={1}
|
||||
scrollsToTop={false}
|
||||
sendMomentumEvents={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"flexGrow": 1,
|
||||
"flexShrink": 1,
|
||||
"overflow": "scroll",
|
||||
},
|
||||
Object {
|
||||
"flex": 1,
|
||||
@@ -94,47 +83,62 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
<RCTScrollContentView
|
||||
collapsable={false}
|
||||
removeClippedSubviews={undefined}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
},
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
testID={undefined}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={false}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
navigation={
|
||||
style={
|
||||
Object {
|
||||
"state": Object {
|
||||
"key": "s1",
|
||||
"routeName": "s1",
|
||||
},
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
screenProps={undefined}
|
||||
/>
|
||||
>
|
||||
<View
|
||||
navigation={
|
||||
Object {
|
||||
"state": Object {
|
||||
"key": "s1",
|
||||
"routeName": "s1",
|
||||
},
|
||||
}
|
||||
}
|
||||
screenProps={undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollContentView>
|
||||
</RCTScrollView>
|
||||
</RCTScrollContentView>
|
||||
</RCTScrollView>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 659 B |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 786 B After Width: | Height: | Size: 373 B |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 812 B |