mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-19 18:38:16 +08:00
Compare commits
162 Commits
@ericvicen
...
2.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d2ce862c2 | ||
|
|
d778479e4a | ||
|
|
352dae50e1 | ||
|
|
61385cae59 | ||
|
|
aa3c13891e | ||
|
|
9696d7220d | ||
|
|
2b83b44816 | ||
|
|
ec749023ed | ||
|
|
adc9389eb3 | ||
|
|
54d143fee2 | ||
|
|
d50e74d0c7 | ||
|
|
22926c5230 | ||
|
|
f6c47a6c66 | ||
|
|
046a9f8930 | ||
|
|
72f17538c2 | ||
|
|
550001b053 | ||
|
|
d168ab26f9 | ||
|
|
99916328a1 | ||
|
|
08e1b53f2e | ||
|
|
2243528e97 | ||
|
|
931271198b | ||
|
|
1876706bad | ||
|
|
e97d6b26a8 | ||
|
|
ec1694f909 | ||
|
|
93db7d0c95 | ||
|
|
589b80b2fa | ||
|
|
364144d639 | ||
|
|
2fd7284fe2 | ||
|
|
6499f02157 | ||
|
|
39d5714ac0 | ||
|
|
1ceda5f04d | ||
|
|
4533883ba7 | ||
|
|
8ec2466fef | ||
|
|
1bd6593ede | ||
|
|
4e8d8ce12f | ||
|
|
9f95a7f10b | ||
|
|
f6bd3e4306 | ||
|
|
c1a94895f5 | ||
|
|
ad7cde9eb9 | ||
|
|
2643f690a9 | ||
|
|
8e52995ef3 | ||
|
|
8ed3817c90 | ||
|
|
eda9bfd567 | ||
|
|
723c5f2149 | ||
|
|
ab5481a290 | ||
|
|
df281cfed0 | ||
|
|
e0df3cf74a | ||
|
|
2440af66e4 | ||
|
|
47fe858d4e | ||
|
|
c641bee11b | ||
|
|
4b39e2db3c | ||
|
|
f7533a790f | ||
|
|
c56122466f | ||
|
|
7fc992dc58 | ||
|
|
32922cdd7d | ||
|
|
eda51b3b79 | ||
|
|
921ee09587 | ||
|
|
7ae4c60eb8 | ||
|
|
5fff7ef5c6 | ||
|
|
42bb1cc317 | ||
|
|
337fd89ad5 | ||
|
|
acf9b92ff7 | ||
|
|
5072130d6f | ||
|
|
20bbbd62ff | ||
|
|
0890896824 | ||
|
|
0cf14f8e1e | ||
|
|
e5e434c9e2 | ||
|
|
e5d8d2c216 | ||
|
|
abd5200739 | ||
|
|
202609d9cf | ||
|
|
7b4dd98255 | ||
|
|
70c644f522 | ||
|
|
5274d16e3b | ||
|
|
e5e2cbb86d | ||
|
|
a8caa0d93c | ||
|
|
f70a25a6a8 | ||
|
|
6cde6e2558 | ||
|
|
0794c0faaa | ||
|
|
ea28e84e5a | ||
|
|
419ee5318d | ||
|
|
fbbf00875b | ||
|
|
22e09f7186 | ||
|
|
ece6297e8e | ||
|
|
ad52caf57b | ||
|
|
11f5e6e8e5 | ||
|
|
1764b21f34 | ||
|
|
bbacabeba3 | ||
|
|
b140b70555 | ||
|
|
356646cbfa | ||
|
|
6234b5661e | ||
|
|
270c3f0754 | ||
|
|
6f04bdffa6 | ||
|
|
8415378784 | ||
|
|
9e47092d56 | ||
|
|
029d6ac4d2 | ||
|
|
d57d118fda | ||
|
|
2232e394bb | ||
|
|
670d48366b | ||
|
|
99ac5b6c08 | ||
|
|
68a2a106f3 | ||
|
|
b7c6d072a5 | ||
|
|
ecd9fd71e9 | ||
|
|
cfc9690326 | ||
|
|
828e7f2d43 | ||
|
|
9c3fffa47f | ||
|
|
be524e4224 | ||
|
|
095814230b | ||
|
|
9cf557bba0 | ||
|
|
5e4512f3eb | ||
|
|
ee1b5972ce | ||
|
|
2233d0e1d8 | ||
|
|
577d99c165 | ||
|
|
aa362ea776 | ||
|
|
864908a49c | ||
|
|
5cab55b8c9 | ||
|
|
9b9db86bde | ||
|
|
4def39c0f7 | ||
|
|
e6559f5878 | ||
|
|
a9d8f2e03e | ||
|
|
84a070b9d5 | ||
|
|
ee984943c7 | ||
|
|
9fdfec18f6 | ||
|
|
aee16b91a4 | ||
|
|
191439f79a | ||
|
|
b1ac152fec | ||
|
|
c588ab9f9d | ||
|
|
ae8cd41396 | ||
|
|
5038ee2360 | ||
|
|
5bf071e3ee | ||
|
|
fcbf78e658 | ||
|
|
fd75e9c14c | ||
|
|
7d36a3b137 | ||
|
|
175387b22f | ||
|
|
0dd7daecc0 | ||
|
|
42230634fd | ||
|
|
a9943e9b2e | ||
|
|
6475e32dba | ||
|
|
f67872d8f1 | ||
|
|
2c7187b22a | ||
|
|
160d44f58e | ||
|
|
d017ed01b3 | ||
|
|
c2e197f8d3 | ||
|
|
6b3968b601 | ||
|
|
b575200879 | ||
|
|
cd5bd8882e | ||
|
|
3f837c895e | ||
|
|
bc5d35aba3 | ||
|
|
9a6e0bbd98 | ||
|
|
052d22804c | ||
|
|
7a978b1087 | ||
|
|
b06fb7e432 | ||
|
|
a92ed83302 | ||
|
|
0c31bc44ea | ||
|
|
8e5ee4d312 | ||
|
|
4bb8987ab7 | ||
|
|
81a86fa091 | ||
|
|
47f357f332 | ||
|
|
bdda6fa5be | ||
|
|
b097136f74 | ||
|
|
c9b0219cec | ||
|
|
ac83cf804c | ||
|
|
cf63521516 |
@@ -42,9 +42,7 @@
|
||||
"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/guides/contributors)
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-navigation/react-navigation) [](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [](https://reactnavigation.org/docs/contributing.html)
|
||||
|
||||
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.
|
||||
|
||||
|
||||
12
assetsTransformer.js
Normal file
12
assetsTransformer.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file is needed to hijack asset imports so that test files don't attempt
|
||||
* to import them as JavaScript modules.
|
||||
* See https://github.com/facebook/jest/issues/2663#issuecomment-317109798
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
process(src, filename, config, options) {
|
||||
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
|
||||
},
|
||||
};
|
||||
@@ -5,5 +5,6 @@ import renderer from 'react-test-renderer';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const rendered = renderer.create(<App />).toJSON();
|
||||
expect(rendered).toBeTruthy();
|
||||
// Will be null because the playground uses state persistence which happens asyncronously
|
||||
expect(rendered).toEqual(null);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,6 @@ A playground for experimenting with react-navigation in a pure-JS React Native a
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.
|
||||
|
||||
You can view this example application directly on your phone by visiting [our expo demo](https://exp.host/@react-navigation/NavigationPlayground).
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "25.0.0",
|
||||
"sdkVersion": "27.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ import {
|
||||
StatusBar,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, StackNavigator } from 'react-navigation';
|
||||
import { SafeAreaView, createStackNavigator } from 'react-navigation';
|
||||
|
||||
import CustomTabs from './CustomTabs';
|
||||
import CustomTransitioner from './CustomTransitioner';
|
||||
@@ -27,18 +27,34 @@ import ModalStack from './ModalStack';
|
||||
import StacksInTabs from './StacksInTabs';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
import StacksWithKeys from './StacksWithKeys';
|
||||
import InactiveStack from './InactiveStack';
|
||||
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
|
||||
import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import TabAnimations from './TabAnimations';
|
||||
import SwitchWithStacks from './SwitchWithStacks';
|
||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||
import KeyboardHandlingExample from './KeyboardHandlingExample';
|
||||
|
||||
const ExampleInfo = {
|
||||
SimpleStack: {
|
||||
name: 'Stack Example',
|
||||
description: 'A card stack',
|
||||
},
|
||||
SwitchWithStacks: {
|
||||
name: 'Switch between routes',
|
||||
description: 'Jump between routes',
|
||||
},
|
||||
InactiveStack: {
|
||||
name: 'Navigate idempotently to stacks in inactive routes',
|
||||
description:
|
||||
'An inactive route in a stack should be given the opportunity to handle actions',
|
||||
},
|
||||
StackWithCustomHeaderBackImage: {
|
||||
name: 'Custom header back image',
|
||||
description: 'Stack with custom header back image',
|
||||
},
|
||||
SimpleTabs: {
|
||||
name: 'Tabs Example',
|
||||
description: 'Tabs following platform conventions',
|
||||
@@ -51,10 +67,6 @@ const ExampleInfo = {
|
||||
name: 'UIKit-style Header Transitions',
|
||||
description: 'Masked back button and sliding header items. iOS only.',
|
||||
},
|
||||
StackWithHeaderPreset: {
|
||||
name: 'UIKit-style Header Transitions',
|
||||
description: 'Masked back button and sliding header items. iOS only.',
|
||||
},
|
||||
StackWithTranslucentHeader: {
|
||||
name: 'Translucent Header',
|
||||
description: 'Render arbitrary translucent content in header background.',
|
||||
@@ -105,23 +117,26 @@ const ExampleInfo = {
|
||||
name: 'Link to Settings Tab',
|
||||
description: 'Deep linking into a route in tab',
|
||||
},
|
||||
TabAnimations: {
|
||||
name: 'Animated Tabs Example',
|
||||
description: 'Tab transitions have custom animations',
|
||||
},
|
||||
TabsWithNavigationFocus: {
|
||||
name: 'withNavigationFocus',
|
||||
description: 'Receive the focus prop to know when a screen is focused',
|
||||
},
|
||||
KeyboardHandlingExample: {
|
||||
name: 'Keyboard Handling Example',
|
||||
description:
|
||||
'Demo automatic handling of keyboard showing/hiding inside StackNavigator',
|
||||
},
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
SimpleStack: SimpleStack,
|
||||
SimpleStack,
|
||||
SwitchWithStacks,
|
||||
SimpleTabs: SimpleTabs,
|
||||
Drawer: Drawer,
|
||||
// MultipleDrawer: {
|
||||
// screen: MultipleDrawer,
|
||||
// },
|
||||
StackWithCustomHeaderBackImage: StackWithCustomHeaderBackImage,
|
||||
StackWithHeaderPreset: StackWithHeaderPreset,
|
||||
StackWithTranslucentHeader: StackWithTranslucentHeader,
|
||||
TabsInDrawer: TabsInDrawer,
|
||||
@@ -139,8 +154,10 @@ const ExampleRoutes = {
|
||||
screen: SimpleTabs,
|
||||
path: 'settings',
|
||||
},
|
||||
TabAnimations,
|
||||
TabsWithNavigationFocus,
|
||||
KeyboardHandlingExample,
|
||||
// This is commented out because it's rarely useful
|
||||
// InactiveStack,
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -151,7 +168,7 @@ class MainScreen extends React.Component<any, State> {
|
||||
scrollY: new Animated.Value(0),
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
Asset.fromModule(
|
||||
require('react-navigation/src/views/assets/back-icon-mask.png')
|
||||
).downloadAsync();
|
||||
@@ -286,7 +303,7 @@ class MainScreen extends React.Component<any, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const AppNavigator = StackNavigator(
|
||||
const AppNavigator = createStackNavigator(
|
||||
{
|
||||
...ExampleRoutes,
|
||||
Index: {
|
||||
@@ -305,8 +322,7 @@ const AppNavigator = StackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
// export default () => <AppNavigator />;
|
||||
export default SimpleStack;
|
||||
export default AppNavigator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
TabRouter,
|
||||
} from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Button,
|
||||
Easing,
|
||||
Image,
|
||||
Platform,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
createNavigator,
|
||||
} from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<SafeAreaView forceInset={{ top: 'always' }}>
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
|
||||
import { Platform, ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
StackNavigator,
|
||||
DrawerNavigator,
|
||||
createStackNavigator,
|
||||
createDrawerNavigator,
|
||||
SafeAreaView,
|
||||
} from 'react-navigation';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -31,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 }) => (
|
||||
@@ -49,23 +43,38 @@ const DraftsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
|
||||
);
|
||||
DraftsScreen.navigationOptions = {
|
||||
headerTitle: 'Drafts',
|
||||
};
|
||||
|
||||
const InboxStack = createStackNavigator({
|
||||
Inbox: { screen: InboxScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
InboxStack.navigationOptions = {
|
||||
drawerLabel: 'Inbox',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons
|
||||
name="move-to-inbox"
|
||||
size={24}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const DraftsStack = createStackNavigator({
|
||||
Drafts: { screen: DraftsScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
DraftsStack.navigationOptions = {
|
||||
drawerLabel: 'Drafts',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons name="drafts" size={24} style={{ color: tintColor }} />
|
||||
),
|
||||
};
|
||||
|
||||
const InboxStack = StackNavigator({
|
||||
Inbox: { screen: InboxScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
const DraftsStack = StackNavigator({
|
||||
Drafts: { screen: DraftsScreen },
|
||||
Email: { screen: EmailScreen },
|
||||
});
|
||||
|
||||
const DrawerExample = DrawerNavigator(
|
||||
const DrawerExample = createDrawerNavigator(
|
||||
{
|
||||
Inbox: {
|
||||
path: '/',
|
||||
|
||||
96
examples/NavigationPlayground/js/InactiveStack.js
Normal file
96
examples/NavigationPlayground/js/InactiveStack.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { Button, Text, StatusBar, View, StyleSheet } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createSwitchNavigator,
|
||||
NavigationActions,
|
||||
} from 'react-navigation';
|
||||
|
||||
const runSubRoutes = navigation => {
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'Second2' }));
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
|
||||
};
|
||||
|
||||
const runSubRoutesWithIntermediate = navigation => {
|
||||
navigation.dispatch(toFirst1);
|
||||
navigation.dispatch(toSecond2);
|
||||
navigation.dispatch(toFirst);
|
||||
navigation.dispatch(toFirst2);
|
||||
};
|
||||
|
||||
const runSubAction = navigation => {
|
||||
navigation.dispatch(toFirst2);
|
||||
navigation.dispatch(toSecond2);
|
||||
navigation.dispatch(toFirstChild1);
|
||||
};
|
||||
|
||||
const DummyScreen = ({ routeName, navigation, style }) => {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
StyleSheet.absoluteFill,
|
||||
{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontWeight: '800' }}>
|
||||
{routeName}({navigation.state.key})
|
||||
</Text>
|
||||
<View>
|
||||
<Button title="back" onPress={() => navigation.goBack()} />
|
||||
<Button title="dismiss" onPress={() => navigation.dismiss()} />
|
||||
<Button
|
||||
title="between sub-routes"
|
||||
onPress={() => runSubRoutes(navigation)}
|
||||
/>
|
||||
<Button
|
||||
title="between sub-routes (with intermediate)"
|
||||
onPress={() => runSubRoutesWithIntermediate(navigation)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="with sub-action"
|
||||
onPress={() => runSubAction(navigation)}
|
||||
/>
|
||||
</View>
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const createDummyScreen = routeName => {
|
||||
const BoundDummyScreen = props => DummyScreen({ ...props, routeName });
|
||||
return BoundDummyScreen;
|
||||
};
|
||||
|
||||
const toFirst = NavigationActions.navigate({ routeName: 'First' });
|
||||
const toFirst1 = NavigationActions.navigate({ routeName: 'First1' });
|
||||
const toFirst2 = NavigationActions.navigate({ routeName: 'First2' });
|
||||
const toSecond2 = NavigationActions.navigate({ routeName: 'Second2' });
|
||||
const toFirstChild1 = NavigationActions.navigate({
|
||||
routeName: 'First',
|
||||
action: NavigationActions.navigate({ routeName: 'First1' }),
|
||||
});
|
||||
|
||||
export default createStackNavigator(
|
||||
{
|
||||
Other: createDummyScreen('Leaf'),
|
||||
First: createStackNavigator({
|
||||
First1: createDummyScreen('First1'),
|
||||
First2: createDummyScreen('First2'),
|
||||
}),
|
||||
Second: createStackNavigator({
|
||||
Second1: createDummyScreen('Second1'),
|
||||
Second2: createDummyScreen('Second2'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
headerMode: 'none',
|
||||
}
|
||||
);
|
||||
63
examples/NavigationPlayground/js/KeyboardHandlingExample.js
Normal file
63
examples/NavigationPlayground/js/KeyboardHandlingExample.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { StatusBar, View, TextInput, InteractionManager } from 'react-native';
|
||||
import { createStackNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class ScreenOne extends React.Component {
|
||||
static navigationOptions = {
|
||||
title: 'Home',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<View style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('ScreenTwo')}
|
||||
title="Push screen with focused text input"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenTwo extends React.Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: navigation.getParam('inputValue', 'Screen w/ Input'),
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this._textInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<View style={{ paddingTop: 30 }}>
|
||||
<View style={{ alignSelf: 'center', paddingVertical: 20 }}>
|
||||
<TextInput
|
||||
ref={c => (this._textInput = c)}
|
||||
onChangeText={inputValue => navigation.setParams({ inputValue })}
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
height: 24,
|
||||
width: 150,
|
||||
borderColor: '#555',
|
||||
borderWidth: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default createStackNavigator({
|
||||
ScreenOne,
|
||||
ScreenTwo: withNavigationFocus(ScreenTwo),
|
||||
});
|
||||
@@ -3,9 +3,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, StatusBar, Text } from 'react-native';
|
||||
import { SafeAreaView, StackNavigator } from 'react-navigation';
|
||||
import { ScrollView, StatusBar, Text } from 'react-native';
|
||||
import { SafeAreaView, createStackNavigator } from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -31,7 +32,8 @@ const MyNavScreen = ({ navigation, banner }) => (
|
||||
headerVisible:
|
||||
!navigation.state.params ||
|
||||
!navigation.state.params.headerVisible,
|
||||
})}
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
@@ -57,7 +59,7 @@ MyProfileScreen.navigationOptions = ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
});
|
||||
|
||||
const ProfileNavigator = StackNavigator(
|
||||
const ProfileNavigator = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
@@ -87,7 +89,7 @@ MyHeaderTestScreen.navigationOptions = ({ navigation }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const ModalStack = StackNavigator(
|
||||
const ModalStack = createStackNavigator(
|
||||
{
|
||||
ProfileNavigator: {
|
||||
screen: ProfileNavigator,
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView, StyleSheet } from 'react-native';
|
||||
import { DrawerNavigator } from 'react-navigation';
|
||||
import { Platform, ScrollView, StyleSheet } from 'react-native';
|
||||
import { createDrawerNavigator } from 'react-navigation';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView style={styles.container}>
|
||||
@@ -40,7 +41,7 @@ DraftsScreen.navigationOptions = {
|
||||
),
|
||||
};
|
||||
|
||||
const DrawerExample = DrawerNavigator(
|
||||
const DrawerExample = createDrawerNavigator(
|
||||
{
|
||||
Inbox: {
|
||||
path: '/',
|
||||
@@ -59,7 +60,7 @@ const DrawerExample = DrawerNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
const MainDrawerExample = DrawerNavigator({
|
||||
const MainDrawerExample = createDrawerNavigator({
|
||||
Drafts: {
|
||||
screen: DrawerExample,
|
||||
},
|
||||
|
||||
@@ -4,22 +4,40 @@
|
||||
|
||||
import type {
|
||||
NavigationScreenProp,
|
||||
NavigationState,
|
||||
NavigationStateRoute,
|
||||
NavigationEventSubscription,
|
||||
} from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, SafeAreaView, withNavigation } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
SafeAreaView,
|
||||
withNavigation,
|
||||
} from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
banner: React.Node,
|
||||
};
|
||||
|
||||
class MyBackButton extends React.Component<any, any> {
|
||||
type BackButtonProps = {
|
||||
navigation: NavigationScreenProp<NavigationStateRoute>,
|
||||
};
|
||||
|
||||
class MyBackButton extends React.Component<BackButtonProps, any> {
|
||||
render() {
|
||||
return <Button onPress={this._navigateBack} title="Custom Back" />;
|
||||
return (
|
||||
<HeaderButtons>
|
||||
<HeaderButtons.Item title="Back" onPress={this._navigateBack} />
|
||||
</HeaderButtons>
|
||||
);
|
||||
}
|
||||
|
||||
_navigateBack = () => {
|
||||
@@ -32,11 +50,16 @@ 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
|
||||
@@ -44,12 +67,13 @@ 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={() => navigation.goBack(null)} title="Go back" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack()} title="Go back" />
|
||||
<Button onPress={() => dismiss()} title="Dismiss" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -57,7 +81,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
}
|
||||
|
||||
type MyHomeScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
|
||||
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
@@ -101,7 +125,7 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
}
|
||||
|
||||
type MyPhotosScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
static navigationOptions = {
|
||||
@@ -142,7 +166,7 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.name}'s Photos`}
|
||||
banner={`${navigation.getParam('name')}'s Photos`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
@@ -151,9 +175,9 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
|
||||
const MyProfileScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
|
||||
navigation.state.params.name
|
||||
}'s Profile`}
|
||||
banner={`${
|
||||
navigation.getParam('mode') === 'edit' ? 'Now Editing ' : ''
|
||||
}${navigation.getParam('name')}'s Profile`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
@@ -168,17 +192,19 @@ MyProfileScreen.navigationOptions = props => {
|
||||
// Render a button on the right side of the header.
|
||||
// When pressed switches the screen to edit mode.
|
||||
headerRight: (
|
||||
<Button
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
<HeaderButtons>
|
||||
<HeaderButtons.Item
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
</HeaderButtons>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const SimpleStack = StackNavigator({
|
||||
const SimpleStack = createStackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
|
||||
@@ -8,10 +8,11 @@ import type {
|
||||
} from 'react-navigation';
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView, StatusBar, View } from 'react-native';
|
||||
import { SafeAreaView, TabNavigator } from 'react-navigation';
|
||||
import { Platform, ScrollView, StatusBar, View } from 'react-native';
|
||||
import { SafeAreaView, createBottomTabNavigator } from 'react-navigation';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
@@ -143,7 +144,7 @@ MySettingsScreen.navigationOptions = {
|
||||
),
|
||||
};
|
||||
|
||||
const SimpleTabs = TabNavigator(
|
||||
const SimpleTabs = createBottomTabNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
@@ -163,8 +164,6 @@ const SimpleTabs = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
lazy: true,
|
||||
removeClippedSubviews: true,
|
||||
tabBarOptions: {
|
||||
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { NavigationScreenProp } from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, Button, StatusBar, StyleSheet } from 'react-native';
|
||||
import { createStackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
banner: React.Node,
|
||||
};
|
||||
|
||||
class MyCustomHeaderBackImage extends React.Component<any, any> {
|
||||
render() {
|
||||
const source = require('./assets/back.png');
|
||||
return (
|
||||
<Image
|
||||
source={source}
|
||||
style={[styles.myCustomHeaderBackImage, this.props.style]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type MyHomeScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
|
||||
static navigationOptions = {
|
||||
title: 'Welcome',
|
||||
headerBackTitle: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="Home Screen" navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
type MyPhotosScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s photos`,
|
||||
headerBackTitle: null,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{`${navigation.state.params.name}'s Photos`}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
|
||||
title="Navigate to a profile screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type MyProfileScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyProfileScreen extends React.Component<MyProfileScreenProps> {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: 'Profile',
|
||||
headerBackImage: (
|
||||
<MyCustomHeaderBackImage style={styles.myCustomHeaderBackImageAlt} />
|
||||
),
|
||||
});
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SampleText>{`${navigation.state.params.name}'s Profile`}</SampleText>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StackWithCustomHeaderBackImage = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
Photos: {
|
||||
path: 'photos/:name',
|
||||
screen: MyPhotosScreen,
|
||||
},
|
||||
Profile: {
|
||||
path: 'profile/:name',
|
||||
screen: MyProfileScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
headerBackImage: MyCustomHeaderBackImage,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default StackWithCustomHeaderBackImage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
myCustomHeaderBackImage: {
|
||||
height: 14.5,
|
||||
width: 24,
|
||||
marginLeft: 9,
|
||||
marginRight: 12,
|
||||
marginVertical: 12,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
myCustomHeaderBackImageAlt: {
|
||||
tintColor: '#f00',
|
||||
},
|
||||
});
|
||||
@@ -4,8 +4,11 @@
|
||||
import type { NavigationScreenProp } from 'react-navigation';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import { createStackNavigator, SafeAreaView } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
type NavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
@@ -18,15 +21,17 @@ class HomeScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push } = navigation;
|
||||
invariant(push, 'missing `push` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
title="Push another screen"
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -40,14 +45,20 @@ class OtherScreen extends React.Component<NavScreenProps> {
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button
|
||||
onPress={() => navigation.push('Other')}
|
||||
onPress={() => push('ScreenWithLongTitle')}
|
||||
title="Push another screen"
|
||||
/>
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button
|
||||
onPress={() => push('ScreenWithNoHeader')}
|
||||
title="Push screen with no header"
|
||||
/>
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
@@ -55,10 +66,54 @@ class OtherScreen extends React.Component<NavScreenProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const StackWithHeaderPreset = StackNavigator(
|
||||
class ScreenWithLongTitle extends React.Component<NavScreenProps> {
|
||||
static navigationOptions = {
|
||||
title: "Another title that's kind of long",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { pop } = navigation;
|
||||
invariant(pop, 'missing `pop` action creator for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenWithNoHeader extends React.Component<NavScreenProps> {
|
||||
static navigationOptions = {
|
||||
header: null,
|
||||
title: 'No Header',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { push, pop } = navigation;
|
||||
invariant(push && pop, 'missing action creators for StackNavigator');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ paddingTop: 30 }}>
|
||||
<Button onPress={() => push('Other')} title="Push another screen" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StackWithHeaderPreset = createStackNavigator(
|
||||
{
|
||||
Home: HomeScreen,
|
||||
Other: OtherScreen,
|
||||
ScreenWithNoHeader: ScreenWithNoHeader,
|
||||
ScreenWithLongTitle: ScreenWithLongTitle,
|
||||
},
|
||||
{
|
||||
headerTransitionPreset: 'uikit',
|
||||
|
||||
@@ -12,15 +12,18 @@ import { isIphoneX } from 'react-native-iphone-x-helper';
|
||||
import * as React from 'react';
|
||||
import { BlurView, Constants } from 'expo';
|
||||
import {
|
||||
Button,
|
||||
Dimensions,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Header, StackNavigator } from 'react-navigation';
|
||||
import { Header, createStackNavigator } from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
import { HeaderButtons } from './commonComponents/HeaderButtons';
|
||||
|
||||
type MyNavScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
@@ -30,11 +33,16 @@ type MyNavScreenProps = {
|
||||
class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
render() {
|
||||
const { navigation, banner } = this.props;
|
||||
const { push, replace, popToTop, pop } = navigation;
|
||||
invariant(
|
||||
push && replace && popToTop && pop,
|
||||
'missing action creators for StackNavigator'
|
||||
);
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.push('Profile', { name: 'Jane' })}
|
||||
onPress={() => push('Profile', { name: 'Jane' })}
|
||||
title="Push a profile screen"
|
||||
/>
|
||||
<Button
|
||||
@@ -42,11 +50,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
|
||||
title="Navigate to a photos screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
|
||||
onPress={() => replace('Profile', { name: 'Lucy' })}
|
||||
title="Replace with profile"
|
||||
/>
|
||||
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => navigation.pop()} title="Pop" />
|
||||
<Button onPress={() => popToTop()} title="Pop to top" />
|
||||
<Button onPress={() => pop()} title="Pop" />
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</ScrollView>
|
||||
@@ -193,17 +201,19 @@ MyProfileScreen.navigationOptions = props => {
|
||||
// Render a button on the right side of the header.
|
||||
// When pressed switches the screen to edit mode.
|
||||
headerRight: (
|
||||
<Button
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
<HeaderButtons>
|
||||
<HeaderButtons.Item
|
||||
title={params.mode === 'edit' ? 'Done' : 'Edit'}
|
||||
onPress={() =>
|
||||
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
|
||||
}
|
||||
/>
|
||||
</HeaderButtons>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const StackWithTranslucentHeader = StackNavigator(
|
||||
const StackWithTranslucentHeader = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { SafeAreaView, StackNavigator, TabNavigator } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createBottomTabNavigator,
|
||||
} from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -51,7 +56,7 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MainTab = StackNavigator({
|
||||
const MainTab = createStackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
path: '/',
|
||||
@@ -68,7 +73,7 @@ const MainTab = StackNavigator({
|
||||
},
|
||||
});
|
||||
|
||||
const SettingsTab = StackNavigator({
|
||||
const SettingsTab = createStackNavigator({
|
||||
Settings: {
|
||||
screen: MySettingsScreen,
|
||||
path: '/',
|
||||
@@ -84,7 +89,7 @@ const SettingsTab = StackNavigator({
|
||||
},
|
||||
});
|
||||
|
||||
const StacksInTabs = TabNavigator(
|
||||
const StacksInTabs = createBottomTabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MainTab,
|
||||
@@ -116,9 +121,9 @@ const StacksInTabs = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: false,
|
||||
swipeEnabled: false,
|
||||
tabBarOptions: {
|
||||
showLabel: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { SafeAreaView, StackNavigator, TabNavigator } from 'react-navigation';
|
||||
import { ScrollView, StatusBar } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createBottomTabNavigator,
|
||||
} from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
@@ -50,7 +55,7 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const TabNav = TabNavigator(
|
||||
const TabNav = createBottomTabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MyHomeScreen,
|
||||
@@ -89,7 +94,20 @@ const TabNav = TabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
const StacksOverTabs = StackNavigator({
|
||||
TabNav.navigationOptions = ({ navigation }) => {
|
||||
let { routeName } = navigation.state.routes[navigation.state.index];
|
||||
let title;
|
||||
if (routeName === 'SettingsTab') {
|
||||
title = 'Settings';
|
||||
} else if (routeName === 'MainTab') {
|
||||
title = 'Home';
|
||||
}
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
const StacksOverTabs = createStackNavigator({
|
||||
Root: {
|
||||
screen: TabNav,
|
||||
},
|
||||
@@ -102,9 +120,9 @@ const StacksOverTabs = StackNavigator({
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
path: '/people/:name',
|
||||
navigationOptions: ({ navigation }) => {
|
||||
title: `${navigation.state.params.name}'s Profile!`;
|
||||
},
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Button, StatusBar, Text, View } from 'react-native';
|
||||
import { StackNavigator } from 'react-navigation';
|
||||
import { StatusBar, Text, View } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class HomeScreen extends React.Component<any, any> {
|
||||
render() {
|
||||
@@ -81,7 +82,7 @@ class SettingsScreen extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
const Stack = StackNavigator(
|
||||
const Stack = createStackNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: HomeScreen,
|
||||
|
||||
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { createStackNavigator, createSwitchNavigator } from 'react-navigation';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class SignInScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Please sign in',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="Sign in!" onPress={this._signInAsync} />
|
||||
<Button
|
||||
title="Go back to other examples"
|
||||
onPress={() => this.props.navigation.goBack(null)}
|
||||
/>
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_signInAsync = async () => {
|
||||
await AsyncStorage.setItem('userToken', 'abc');
|
||||
this.props.navigation.navigate('Home');
|
||||
};
|
||||
}
|
||||
|
||||
class HomeScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Welcome to the app!',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="Show me more of the app" onPress={this._showMoreApp} />
|
||||
<Button title="Actually, sign me out :)" onPress={this._signOutAsync} />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_showMoreApp = () => {
|
||||
this.props.navigation.navigate('Other');
|
||||
};
|
||||
|
||||
_signOutAsync = async () => {
|
||||
await AsyncStorage.clear();
|
||||
this.props.navigation.navigate('Auth');
|
||||
};
|
||||
}
|
||||
|
||||
class OtherScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Lots of features here',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="I'm done, sign me out" onPress={this._signOutAsync} />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_signOutAsync = async () => {
|
||||
await AsyncStorage.clear();
|
||||
this.props.navigation.navigate('Auth');
|
||||
};
|
||||
}
|
||||
|
||||
class LoadingScreen extends React.Component<any, any> {
|
||||
componentDidMount() {
|
||||
this._bootstrapAsync();
|
||||
}
|
||||
|
||||
_bootstrapAsync = async () => {
|
||||
const userToken = await AsyncStorage.getItem('userToken');
|
||||
let initialRouteName = userToken ? 'App' : 'Auth';
|
||||
this.props.navigation.navigate(initialRouteName);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const AppStack = createStackNavigator({ Home: HomeScreen, Other: OtherScreen });
|
||||
const AuthStack = createStackNavigator({ SignIn: SignInScreen });
|
||||
|
||||
export default createSwitchNavigator({
|
||||
Loading: LoadingScreen,
|
||||
App: AppStack,
|
||||
Auth: AuthStack,
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Animated, Button, ScrollView, StatusBar } from 'react-native';
|
||||
import { StackNavigator, TabNavigator } from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Profile', { name: 'Jordan' })}
|
||||
title="Open profile screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('NotifSettings')}
|
||||
title="Open notifications screen"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('SettingsTab')}
|
||||
title="Go to settings tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const MyHomeScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Home Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MyProfileScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.name}s Profile`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
const MyNotificationsSettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Notifications Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Screen" navigation={navigation} />
|
||||
);
|
||||
|
||||
const MainTab = StackNavigator({
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
path: '/',
|
||||
navigationOptions: {
|
||||
title: 'Welcome',
|
||||
},
|
||||
},
|
||||
Profile: {
|
||||
screen: MyProfileScreen,
|
||||
path: '/people/:name',
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: `${navigation.state.params.name}'s Profile!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const SettingsTab = StackNavigator({
|
||||
Settings: {
|
||||
screen: MySettingsScreen,
|
||||
path: '/',
|
||||
navigationOptions: () => ({
|
||||
title: 'Settings',
|
||||
}),
|
||||
},
|
||||
NotifSettings: {
|
||||
screen: MyNotificationsSettingsScreen,
|
||||
navigationOptions: {
|
||||
title: 'Notifications',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const TabAnimations = TabNavigator(
|
||||
{
|
||||
MainTab: {
|
||||
screen: MainTab,
|
||||
path: '/',
|
||||
navigationOptions: {
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-home' : 'ios-home-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
SettingsTab: {
|
||||
screen: SettingsTab,
|
||||
path: '/settings',
|
||||
navigationOptions: {
|
||||
tabBarLabel: 'Settings',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-settings' : 'ios-settings-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: true,
|
||||
configureTransition: (currentTransitionProps,nextTransitionProps) => ({
|
||||
timing: Animated.spring,
|
||||
tension: 1,
|
||||
friction: 35,
|
||||
}),
|
||||
swipeEnabled: false,
|
||||
}
|
||||
);
|
||||
|
||||
export default TabAnimations;
|
||||
@@ -3,13 +3,13 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Platform, ScrollView } from 'react-native';
|
||||
import { TabNavigator, DrawerNavigator } from 'react-navigation';
|
||||
import { Platform, ScrollView } from 'react-native';
|
||||
import { createDrawerNavigator } from 'react-navigation';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
|
||||
const TabsInDrawer = DrawerNavigator({
|
||||
const TabsInDrawer = createDrawerNavigator({
|
||||
SimpleTabs: {
|
||||
screen: SimpleTabs,
|
||||
navigationOptions: {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, SafeAreaView, Text } from 'react-native';
|
||||
import { TabNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import { SafeAreaView, StatusBar, Text, View } from 'react-native';
|
||||
import { withNavigationFocus } from 'react-navigation';
|
||||
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
|
||||
@@ -65,9 +67,10 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={() => this.props.navigation.goBack(null)}
|
||||
onPress={() => this.props.navigation.pop()}
|
||||
title="Back to other examples"
|
||||
/>
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -75,7 +78,7 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
return withNavigationFocus(TabScreen);
|
||||
};
|
||||
|
||||
const TabsWithNavigationFocus = TabNavigator(
|
||||
const TabsWithNavigationFocus = createMaterialBottomTabNavigator(
|
||||
{
|
||||
One: {
|
||||
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
|
||||
@@ -92,9 +95,8 @@ const TabsWithNavigationFocus = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: true,
|
||||
swipeEnabled: true,
|
||||
shifting: false,
|
||||
activeTintColor: '#F44336',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
BIN
examples/NavigationPlayground/js/assets/back.png
Normal file
BIN
examples/NavigationPlayground/js/assets/back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,18 @@
|
||||
import { Button as RNButton, StyleSheet, View, Platform } from 'react-native';
|
||||
import React from 'react';
|
||||
|
||||
export const Button = props => (
|
||||
<View style={styles.margin}>
|
||||
<RNButton {...props} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
margin: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
margin: 10,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import DefaultHeaderButtons from 'react-navigation-header-buttons';
|
||||
import * as React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export class HeaderButtons extends React.PureComponent {
|
||||
static Item = DefaultHeaderButtons.Item;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DefaultHeaderButtons
|
||||
color={Platform.OS === 'ios' ? '#037aff' : 'black'}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,26 @@
|
||||
"eject": "react-native-scripts eject",
|
||||
"android": "react-native-scripts android",
|
||||
"ios": "react-native-scripts ios",
|
||||
"test": "node node_modules/jest/bin/jest.js && flow"
|
||||
"test": "flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^25.0.0",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"expo": "^27.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../.."
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-header-buttons": "^0.0.4",
|
||||
"react-navigation-material-bottom-tabs": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^21.0.0",
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"jest": "^21.0.1",
|
||||
"jest-expo": "^25.1.0",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
"react-test-renderer": "16.0.0"
|
||||
"react-test-renderer": "16.3.0-alpha.1"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo",
|
||||
|
||||
@@ -11,7 +11,9 @@ module.exports = {
|
||||
return blacklist([
|
||||
/react\-navigation\/examples\/(?!NavigationPlayground).*/,
|
||||
/react\-navigation\/node_modules\/react-native\/(.*)/,
|
||||
/react\-navigation\/node_modules\/react\/(.*)/
|
||||
/react\-navigation\/node_modules\/react\/(.*)/,
|
||||
/react\-navigation\/node_modules\/react-native-paper\/(.*)/,
|
||||
/react\-navigation\/node_modules\/@expo\/vector-icons\/(.*)/,
|
||||
]);
|
||||
},
|
||||
extraNodeModules: getNodeModulesForDirectory(path.resolve('.')),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,4 +2,4 @@
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.
|
||||
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^21.0.0",
|
||||
"jest": "^21.0.1",
|
||||
"babel-jest": "^22.4.1",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^25.1.0",
|
||||
"react-native-scripts": "^1.3.1",
|
||||
"react-test-renderer": "16.0.0"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
|
||||
import { StackNavigator } from 'react-navigation';
|
||||
import { initializeListeners } from 'react-navigation-redux-helpers';
|
||||
|
||||
import LoginScreen from '../components/LoginScreen';
|
||||
import MainScreen from '../components/MainScreen';
|
||||
import ProfileScreen from '../components/ProfileScreen';
|
||||
import { addListener } from '../utils/redux';
|
||||
import { navigationPropConstructor } from '../utils/redux';
|
||||
|
||||
export const AppNavigator = StackNavigator({
|
||||
Login: { screen: LoginScreen },
|
||||
@@ -20,17 +21,14 @@ class AppWithNavigationState extends React.Component {
|
||||
nav: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
initializeListeners('root', this.props.nav);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
<AppNavigator
|
||||
navigation={addNavigationHelpers({
|
||||
dispatch,
|
||||
state: nav,
|
||||
addListener,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
const navigation = navigationPropConstructor(dispatch, nav);
|
||||
return <AppNavigator navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {
|
||||
createReactNavigationReduxMiddleware,
|
||||
createReduxBoundAddListener,
|
||||
createNavigationPropConstructor,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
const middleware = createReactNavigationReduxMiddleware(
|
||||
"root",
|
||||
state => state.nav,
|
||||
'root',
|
||||
state => state.nav
|
||||
);
|
||||
const addListener = createReduxBoundAddListener("root");
|
||||
const navigationPropConstructor = createNavigationPropConstructor('root');
|
||||
|
||||
export {
|
||||
middleware,
|
||||
addListener,
|
||||
};
|
||||
export { middleware, navigationPropConstructor };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
148
flow/react-navigation.js
vendored
148
flow/react-navigation.js
vendored
@@ -269,21 +269,26 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationComponent =
|
||||
| NavigationScreenComponent<NavigationRoute, *, *>
|
||||
| NavigationContainer<*, *, *>
|
||||
| any;
|
||||
| NavigationContainer<*, *, *>;
|
||||
|
||||
declare export type NavigationScreenComponent<
|
||||
Route: NavigationRoute,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, Route>,
|
||||
}> &
|
||||
({} | { navigationOptions: NavigationScreenConfig<Options> });
|
||||
|
||||
declare export type NavigationNavigator<
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationNavigatorProps<Options, State>,
|
||||
}> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
@@ -296,7 +301,6 @@ declare module 'react-navigation' {
|
||||
} & NavigationScreenRouteConfig);
|
||||
|
||||
declare export type NavigationScreenRouteConfig =
|
||||
| NavigationComponent
|
||||
| {
|
||||
screen: NavigationComponent,
|
||||
}
|
||||
@@ -344,7 +348,7 @@ declare module 'react-navigation' {
|
||||
headerTintColor?: string,
|
||||
headerLeft?: React$Node | React$ElementType,
|
||||
headerBackTitle?: string,
|
||||
headerBackImage?: ImageSource,
|
||||
headerBackImage?: React$Node | React$ElementType,
|
||||
headerTruncatedBackTitle?: string,
|
||||
headerBackTitleStyle?: TextStyleProp,
|
||||
headerPressColorAndroid?: string,
|
||||
@@ -380,6 +384,20 @@ declare module 'react-navigation' {
|
||||
...NavigationStackRouterConfig,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Switch Navigator
|
||||
*/
|
||||
|
||||
declare export type NavigationSwitchRouterConfig = {|
|
||||
initialRouteName?: string,
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
order?: Array<string>,
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults to `'none'`
|
||||
resetOnBlur?: boolean, // defaults to `true`
|
||||
|};
|
||||
|
||||
/**
|
||||
* Tab Navigator
|
||||
*/
|
||||
@@ -391,7 +409,6 @@ declare module 'react-navigation' {
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
// todo: type these as the real route names rather than 'string'
|
||||
order?: Array<string>,
|
||||
|
||||
// Does the back button cause the router to switch to the initial tab
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|
||||
|};
|
||||
@@ -414,10 +431,9 @@ declare module 'react-navigation' {
|
||||
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
|
||||
tabBarVisible?: boolean,
|
||||
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
|
||||
tabBarOnPress?: (
|
||||
scene: TabScene,
|
||||
jumpToIndex: (index: number) => void
|
||||
) => void,
|
||||
tabBarOnPress?: ({
|
||||
navigation: NavigationScreenProp<NavigationRoute>,
|
||||
}) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -473,29 +489,46 @@ declare module 'react-navigation' {
|
||||
declare export type NavigationScreenProp<+S> = {
|
||||
+state: S,
|
||||
dispatch: NavigationDispatch,
|
||||
goBack: (routeKey?: ?string) => boolean,
|
||||
navigate: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
setParams: (newParams: NavigationParams) => boolean,
|
||||
addListener: (
|
||||
eventName: string,
|
||||
callback: NavigationEventCallback
|
||||
) => NavigationEventSubscription,
|
||||
push: (
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
isFocused: () => boolean,
|
||||
// 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: (
|
||||
replace?: (
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
|
||||
popToTop: (params?: { immediate?: boolean }) => boolean,
|
||||
reset?: (actions: NavigationAction[], index: number) => boolean,
|
||||
dismiss?: () => boolean,
|
||||
// DrawerRouter action creators
|
||||
openDrawer?: () => boolean,
|
||||
closeDrawer?: () => boolean,
|
||||
toggleDrawer?: () => boolean,
|
||||
};
|
||||
|
||||
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
|
||||
@@ -512,19 +545,24 @@ declare module 'react-navigation' {
|
||||
State: NavigationState,
|
||||
Options: {},
|
||||
Props: {}
|
||||
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
|
||||
> = React$ComponentType<{
|
||||
...Props,
|
||||
...NavigationContainerProps<State, Options>,
|
||||
}> & {
|
||||
router: NavigationRouter<State, Options>,
|
||||
navigationOptions?: ?NavigationScreenConfig<Options>,
|
||||
};
|
||||
|
||||
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
|
||||
uriPrefix?: string | RegExp,
|
||||
onNavigationStateChange?: (
|
||||
onNavigationStateChange?: ?(
|
||||
NavigationState,
|
||||
NavigationState,
|
||||
NavigationAction
|
||||
) => void,
|
||||
navigation?: NavigationScreenProp<S>,
|
||||
persistenceKey?: ?string,
|
||||
renderLoadingExperimental?: React$ComponentType<{}>,
|
||||
screenProps?: *,
|
||||
navigationOptions?: O,
|
||||
}>;
|
||||
@@ -742,12 +780,16 @@ declare module 'react-navigation' {
|
||||
view: NavigationView<O, S>,
|
||||
router: NavigationRouter<S, O>,
|
||||
navigatorConfig?: NavigatorConfig
|
||||
): any;
|
||||
): NavigationNavigator<S, O, *>;
|
||||
|
||||
declare export function StackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
stackConfig?: StackNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createStackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
stackConfig?: StackNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare type _TabViewConfig = {|
|
||||
tabBarComponent?: React$ElementType,
|
||||
@@ -772,6 +814,30 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
/* TODO: fix the config for each of these tab navigator types */
|
||||
declare export function createBottomTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createMaterialTopTabNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare type _SwitchNavigatorConfig = {|
|
||||
...NavigationSwitchRouterConfig,
|
||||
|};
|
||||
declare export function SwitchNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _SwitchNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createSwitchNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _SwitchNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare type _DrawerViewConfig = {|
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
@@ -793,6 +859,10 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _DrawerNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare export function createDrawerNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _DrawerNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare export function StackRouter(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
@@ -878,12 +948,14 @@ declare module 'react-navigation' {
|
||||
vertical?: _SafeAreaViewForceInsetValue,
|
||||
horizontal?: _SafeAreaViewForceInsetValue,
|
||||
},
|
||||
children: React$Node,
|
||||
children?: React$Node,
|
||||
style?: AnimatedViewStyleProp,
|
||||
};
|
||||
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>;
|
||||
|
||||
declare export var Header: React$ComponentType<HeaderProps>;
|
||||
declare export var Header: React$ComponentType<HeaderProps> & {
|
||||
HEIGHT: number,
|
||||
};
|
||||
|
||||
declare type _HeaderTitleProps = {
|
||||
children: React$Node,
|
||||
@@ -1026,13 +1098,17 @@ 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 }>>;
|
||||
}
|
||||
|
||||
55
package.json
55
package.json
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "1.2.1",
|
||||
"version": "2.0.3",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
"url": "git@github.com:react-navigation/react-navigation.git",
|
||||
"type": "git"
|
||||
},
|
||||
"author":
|
||||
"Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>",
|
||||
"author": "Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>, Brent Vatne <brent@expo.io>",
|
||||
"license": "BSD-2-Clause",
|
||||
"scripts": {
|
||||
"start": "npm run ios",
|
||||
@@ -22,38 +21,44 @@
|
||||
"format": "eslint --fix .",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"files": ["src"],
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"clamp": "^1.0.1",
|
||||
"create-react-context": "^0.2.1",
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^3",
|
||||
"react-native-drawer-layout-polyfill": "^1.3.2",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"react-native-tab-view": "^0.0.74"
|
||||
"react-native-safe-area-view": "^0.8.0",
|
||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||
"react-navigation-tabs": "0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-jest": "^20.0.3",
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-preset-react-native": "^2.1.0",
|
||||
"codecov": "^2.2.0",
|
||||
"eslint": "^4.2.0",
|
||||
"eslint-config-prettier": "^2.3.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.2",
|
||||
"eslint-plugin-prettier": "^2.1.2",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-react": "^7.1.0",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^22.1.3",
|
||||
"jest-expo": "^25.1.0",
|
||||
"lint-staged": "^4.2.1",
|
||||
"prettier": "^1.5.3",
|
||||
"prettier-eslint": "^6.4.2",
|
||||
"prettier": "^1.12.1",
|
||||
"prettier-eslint": "^8.8.1",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"react-native-vector-icons": "^4.2.0",
|
||||
@@ -62,14 +67,30 @@
|
||||
"jest": {
|
||||
"notify": true,
|
||||
"preset": "react-native",
|
||||
"testRegex": "./src/.*\\-test\\.js$",
|
||||
"setupFiles": ["<rootDir>/jest-setup.js"],
|
||||
"testRegex": "/__tests__/[^/]+-test\\.js$",
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
],
|
||||
"coverageDirectory": "./coverage/",
|
||||
"collectCoverage": true,
|
||||
"coverageReporters": ["lcov"],
|
||||
"collectCoverageFrom": ["src/**/*.js"],
|
||||
"coveragePathIgnorePatterns": ["jest-setup.js"],
|
||||
"modulePathIgnorePatterns": ["examples"]
|
||||
"coverageReporters": [
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"jest-setup.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.png$": "<rootDir>/assetsTransformer.js"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/examples/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
|
||||
@@ -4,6 +4,6 @@ set -eo pipefail
|
||||
|
||||
case $CIRCLE_NODE_INDEX in
|
||||
0) yarn test && yarn codecov ;;
|
||||
1) yarn link && cd examples/NavigationPlayground && yarn && yarn link react-navigation && yarn test ;;
|
||||
1) cd examples/NavigationPlayground && yarn && yarn test ;;
|
||||
#2) cd examples/ReduxExample && yarn && yarn test ;;
|
||||
esac
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
const BACK = 'Navigation/BACK';
|
||||
const INIT = 'Navigation/INIT';
|
||||
const NAVIGATE = 'Navigation/NAVIGATE';
|
||||
const POP = 'Navigation/POP';
|
||||
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
|
||||
const PUSH = 'Navigation/PUSH';
|
||||
const RESET = 'Navigation/RESET';
|
||||
const REPLACE = 'Navigation/REPLACE';
|
||||
const SET_PARAMS = 'Navigation/SET_PARAMS';
|
||||
const URI = 'Navigation/URI';
|
||||
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
|
||||
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
|
||||
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
|
||||
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
|
||||
|
||||
const createAction = (type, fn) => {
|
||||
fn.toString = () => type;
|
||||
return fn;
|
||||
};
|
||||
|
||||
const back = createAction(BACK, (payload = {}) => ({
|
||||
const back = (payload = {}) => ({
|
||||
type: BACK,
|
||||
key: payload.key,
|
||||
immediate: payload.immediate,
|
||||
}));
|
||||
});
|
||||
|
||||
const init = createAction(INIT, (payload = {}) => {
|
||||
const init = (payload = {}) => {
|
||||
const action = {
|
||||
type: INIT,
|
||||
};
|
||||
@@ -32,9 +17,9 @@ const init = createAction(INIT, (payload = {}) => {
|
||||
action.params = payload.params;
|
||||
}
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
const navigate = createAction(NAVIGATE, payload => {
|
||||
const navigate = payload => {
|
||||
const action = {
|
||||
type: NAVIGATE,
|
||||
routeName: payload.routeName,
|
||||
@@ -49,107 +34,24 @@ const navigate = createAction(NAVIGATE, payload => {
|
||||
action.key = payload.key;
|
||||
}
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
const pop = createAction(POP, payload => ({
|
||||
type: POP,
|
||||
n: payload && payload.n,
|
||||
immediate: payload && payload.immediate,
|
||||
}));
|
||||
|
||||
const popToTop = createAction(POP_TO_TOP, payload => ({
|
||||
type: POP_TO_TOP,
|
||||
immediate: payload && payload.immediate,
|
||||
key: payload && payload.key,
|
||||
}));
|
||||
|
||||
const push = createAction(PUSH, payload => {
|
||||
const action = {
|
||||
type: PUSH,
|
||||
routeName: payload.routeName,
|
||||
};
|
||||
if (payload.params) {
|
||||
action.params = payload.params;
|
||||
}
|
||||
if (payload.action) {
|
||||
action.action = payload.action;
|
||||
}
|
||||
return action;
|
||||
});
|
||||
|
||||
const reset = createAction(RESET, payload => ({
|
||||
type: RESET,
|
||||
index: payload.index,
|
||||
key: payload.key,
|
||||
actions: payload.actions,
|
||||
}));
|
||||
|
||||
const replace = createAction(REPLACE, payload => ({
|
||||
type: REPLACE,
|
||||
key: payload.key,
|
||||
newKey: payload.newKey,
|
||||
params: payload.params,
|
||||
action: payload.action,
|
||||
routeName: payload.routeName,
|
||||
immediate: payload.immediate,
|
||||
}));
|
||||
|
||||
const setParams = createAction(SET_PARAMS, payload => ({
|
||||
const setParams = payload => ({
|
||||
type: SET_PARAMS,
|
||||
key: payload.key,
|
||||
params: payload.params,
|
||||
}));
|
||||
|
||||
const uri = createAction(URI, payload => ({
|
||||
type: URI,
|
||||
uri: payload.uri,
|
||||
}));
|
||||
|
||||
const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
|
||||
type: COMPLETE_TRANSITION,
|
||||
key: payload && payload.key,
|
||||
}));
|
||||
|
||||
const openDrawer = createAction(OPEN_DRAWER, payload => ({
|
||||
type: OPEN_DRAWER,
|
||||
}));
|
||||
const closeDrawer = createAction(CLOSE_DRAWER, payload => ({
|
||||
type: CLOSE_DRAWER,
|
||||
}));
|
||||
const toggleDrawer = createAction(TOGGLE_DRAWER, payload => ({
|
||||
type: TOGGLE_DRAWER,
|
||||
}));
|
||||
});
|
||||
|
||||
export default {
|
||||
// Action constants
|
||||
BACK,
|
||||
INIT,
|
||||
NAVIGATE,
|
||||
POP,
|
||||
POP_TO_TOP,
|
||||
PUSH,
|
||||
RESET,
|
||||
REPLACE,
|
||||
SET_PARAMS,
|
||||
URI,
|
||||
COMPLETE_TRANSITION,
|
||||
OPEN_DRAWER,
|
||||
CLOSE_DRAWER,
|
||||
TOGGLE_DRAWER,
|
||||
|
||||
// Action creators
|
||||
back,
|
||||
init,
|
||||
navigate,
|
||||
pop,
|
||||
popToTop,
|
||||
push,
|
||||
reset,
|
||||
replace,
|
||||
setParams,
|
||||
uri,
|
||||
completeTransition,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
toggleDrawer,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ const StateUtils = {
|
||||
* routes of the navigation state, or -1 if it is not present.
|
||||
*/
|
||||
indexOf(state, key) {
|
||||
return state.routes.map(route => route.key).indexOf(key);
|
||||
return state.routes.findIndex(route => route.key === key);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -116,8 +116,23 @@ const StateUtils = {
|
||||
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the positon to where the new route in the
|
||||
* stack is at.
|
||||
* Note that this moves the index to the position to where the new route in the
|
||||
* stack is at and updates the routes array accordingly.
|
||||
*/
|
||||
replaceAndPrune(state, key, route) {
|
||||
const index = StateUtils.indexOf(state, key);
|
||||
const replaced = StateUtils.replaceAtIndex(state, index, route);
|
||||
|
||||
return {
|
||||
...replaced,
|
||||
routes: replaced.routes.slice(0, index + 1),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the position to where the new route in the
|
||||
* stack is at. Does not prune the routes.
|
||||
*/
|
||||
replaceAt(state, key, route) {
|
||||
const index = StateUtils.indexOf(state, key);
|
||||
@@ -137,7 +152,7 @@ const StateUtils = {
|
||||
route.key
|
||||
);
|
||||
|
||||
if (state.routes[index] === route) {
|
||||
if (state.routes[index] === route && index === state.index) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -153,7 +168,7 @@ const StateUtils = {
|
||||
|
||||
/**
|
||||
* Resets all routes.
|
||||
* Note that this moves the index to the positon to where the last route in the
|
||||
* Note that this moves the index to the position to where the last route in the
|
||||
* stack is at if the param `index` isn't provided.
|
||||
*/
|
||||
reset(state, routes, index) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
describe('actions', () => {
|
||||
describe('generic navigation actions', () => {
|
||||
const params = { foo: 'bar' };
|
||||
const navigateAction = NavigationActions.navigate({ routeName: 'another' });
|
||||
|
||||
it('exports back action and type', () => {
|
||||
expect(NavigationActions.back.toString()).toEqual(NavigationActions.BACK);
|
||||
expect(NavigationActions.back()).toEqual({ type: NavigationActions.BACK });
|
||||
expect(NavigationActions.back({ key: 'test' })).toEqual({
|
||||
type: NavigationActions.BACK,
|
||||
@@ -14,7 +13,6 @@ describe('actions', () => {
|
||||
});
|
||||
|
||||
it('exports init action and type', () => {
|
||||
expect(NavigationActions.init.toString()).toEqual(NavigationActions.INIT);
|
||||
expect(NavigationActions.init()).toEqual({ type: NavigationActions.INIT });
|
||||
expect(NavigationActions.init({ params })).toEqual({
|
||||
type: NavigationActions.INIT,
|
||||
@@ -23,9 +21,6 @@ describe('actions', () => {
|
||||
});
|
||||
|
||||
it('exports navigate action and type', () => {
|
||||
expect(NavigationActions.navigate.toString()).toEqual(
|
||||
NavigationActions.NAVIGATE
|
||||
);
|
||||
expect(NavigationActions.navigate({ routeName: 'test' })).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'test',
|
||||
@@ -47,36 +42,7 @@ describe('actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('exports reset action and type', () => {
|
||||
expect(NavigationActions.reset.toString()).toEqual(NavigationActions.RESET);
|
||||
expect(NavigationActions.reset({ index: 0, actions: [] })).toEqual({
|
||||
type: NavigationActions.RESET,
|
||||
index: 0,
|
||||
actions: [],
|
||||
});
|
||||
expect(
|
||||
NavigationActions.reset({
|
||||
index: 0,
|
||||
key: 'test',
|
||||
actions: [navigateAction],
|
||||
})
|
||||
).toEqual({
|
||||
type: NavigationActions.RESET,
|
||||
index: 0,
|
||||
key: 'test',
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'another',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('exports setParams action and type', () => {
|
||||
expect(NavigationActions.setParams.toString()).toEqual(
|
||||
NavigationActions.SET_PARAMS
|
||||
);
|
||||
expect(
|
||||
NavigationActions.setParams({
|
||||
key: 'test',
|
||||
@@ -88,12 +54,4 @@ describe('actions', () => {
|
||||
params,
|
||||
});
|
||||
});
|
||||
|
||||
it('exports uri action and type', () => {
|
||||
expect(NavigationActions.uri.toString()).toEqual(NavigationActions.URI);
|
||||
expect(NavigationActions.uri({ uri: 'http://google.com' })).toEqual({
|
||||
type: NavigationActions.URI,
|
||||
uri: 'http://google.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import React from 'react';
|
||||
import 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackNavigator from '../navigators/createStackNavigator';
|
||||
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const BazScreen = () => <div />;
|
||||
const CarScreen = () => <div />;
|
||||
const DogScreen = () => <div />;
|
||||
const ElkScreen = () => <div />;
|
||||
const NavigationContainer = StackNavigator(
|
||||
{
|
||||
foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
baz: {
|
||||
screen: BazScreen,
|
||||
},
|
||||
car: {
|
||||
screen: CarScreen,
|
||||
},
|
||||
dog: {
|
||||
screen: DogScreen,
|
||||
},
|
||||
elk: {
|
||||
screen: ElkScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'foo',
|
||||
}
|
||||
);
|
||||
|
||||
jest.useFakeTimers();
|
||||
import createStackNavigator from '../navigators/createStackNavigator';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../createNavigationContainer';
|
||||
|
||||
describe('NavigationContainer', () => {
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const BazScreen = () => <div />;
|
||||
const CarScreen = () => <div />;
|
||||
const DogScreen = () => <div />;
|
||||
const ElkScreen = () => <div />;
|
||||
const NavigationContainer = createStackNavigator(
|
||||
{
|
||||
foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
baz: {
|
||||
screen: BazScreen,
|
||||
},
|
||||
car: {
|
||||
screen: CarScreen,
|
||||
},
|
||||
dog: {
|
||||
screen: DogScreen,
|
||||
},
|
||||
elk: {
|
||||
screen: ElkScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'foo',
|
||||
}
|
||||
);
|
||||
|
||||
describe('state.nav', () => {
|
||||
it("should be preloaded with the router's initial state", () => {
|
||||
const navigationContainer = renderer
|
||||
@@ -198,4 +202,57 @@ describe('NavigationContainer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('warnings', () => {
|
||||
function spyConsole() {
|
||||
let spy = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
spy.console.mockRestore();
|
||||
});
|
||||
|
||||
return spy;
|
||||
}
|
||||
|
||||
describe('detached navigators', () => {
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
let spy = spyConsole();
|
||||
|
||||
it('warns when you render more than one navigator explicitly', () => {
|
||||
class BlankScreen extends React.Component {
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
class RootScreen extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<ChildNavigator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ChildNavigator = createStackNavigator({
|
||||
Child: BlankScreen,
|
||||
});
|
||||
|
||||
const RootStack = createStackNavigator({
|
||||
Root: RootScreen,
|
||||
});
|
||||
|
||||
renderer.create(<RootStack />).toJSON();
|
||||
expect(spy).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.get(state, 'a')).toEqual({
|
||||
key: 'a',
|
||||
@@ -20,6 +21,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.indexOf(state, 'a')).toBe(0);
|
||||
expect(NavigationStateUtils.indexOf(state, 'b')).toBe(1);
|
||||
@@ -30,6 +32,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.has(state, 'b')).toBe(true);
|
||||
expect(NavigationStateUtils.has(state, 'c')).toBe(false);
|
||||
@@ -40,9 +43,11 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
};
|
||||
expect(NavigationStateUtils.push(state, { key: 'b', routeName })).toEqual(
|
||||
@@ -54,6 +59,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() =>
|
||||
NavigationStateUtils.push(state, { key: 'a', routeName })
|
||||
@@ -65,10 +71,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.pop(state)).toEqual(newState);
|
||||
});
|
||||
@@ -77,6 +85,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.pop(state)).toBe(state);
|
||||
});
|
||||
@@ -86,10 +95,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.jumpToIndex(state, 0)).toBe(state);
|
||||
expect(NavigationStateUtils.jumpToIndex(state, 1)).toEqual(newState);
|
||||
@@ -99,6 +110,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
|
||||
});
|
||||
@@ -107,10 +119,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.jumpTo(state, 'a')).toBe(state);
|
||||
expect(NavigationStateUtils.jumpTo(state, 'b')).toEqual(newState);
|
||||
@@ -120,6 +134,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
|
||||
});
|
||||
@@ -128,10 +143,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.back(state)).toEqual(newState);
|
||||
expect(NavigationStateUtils.back(newState)).toBe(newState);
|
||||
@@ -141,10 +158,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.forward(state)).toEqual(newState);
|
||||
expect(NavigationStateUtils.forward(newState)).toBe(newState);
|
||||
@@ -155,10 +174,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAt(state, 'b', { key: 'c', routeName })
|
||||
@@ -169,24 +190,27 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, { key: 'c', routeName })
|
||||
).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 }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
||||
).toEqual(state);
|
||||
).toEqual({ ...state, index: 1 });
|
||||
});
|
||||
|
||||
// Reset
|
||||
@@ -194,10 +218,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.reset(state, [
|
||||
@@ -215,10 +241,12 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 0,
|
||||
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.reset(
|
||||
|
||||
13
src/__tests__/__snapshots__/NavigationContainer-test.js.snap
Normal file
13
src/__tests__/__snapshots__/NavigationContainer-test.js.snap
Normal file
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NavigationContainer warnings detached navigators warns when you render more than one navigator explicitly 1`] = `
|
||||
Object {
|
||||
"console": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://v2.reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -1,118 +0,0 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
|
||||
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
|
||||
remove: () => {},
|
||||
});
|
||||
|
||||
describe('addNavigationHelpers', () => {
|
||||
it('handles Back action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'A', routeName: 'Home' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).goBack('A')
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.BACK,
|
||||
key: 'A',
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles Back action when the key is not defined', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { routeName: 'Home' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).goBack()
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({ type: NavigationActions.BACK });
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles Navigate action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { routeName: 'Home' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).navigate('Profile', { name: 'Matt' })
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
params: { name: 'Matt' },
|
||||
routeName: 'Profile',
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles SetParams action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).setParams({ notificationsEnabled: 'yes' })
|
||||
).toEqual(true);
|
||||
expect(mockedDispatch).toBeCalledWith({
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
key: 'B',
|
||||
params: { notificationsEnabled: 'yes' },
|
||||
});
|
||||
expect(mockedDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('handles GetParams action', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings', params: { name: 'Peter' } },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).getParam('name', 'Brent')
|
||||
).toEqual('Peter');
|
||||
});
|
||||
|
||||
it('handles GetParams action with default param', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings' },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).getParam('name', 'Brent')
|
||||
).toEqual('Brent');
|
||||
});
|
||||
|
||||
it('handles GetParams action with param value as null', () => {
|
||||
const mockedDispatch = jest
|
||||
.fn(() => false)
|
||||
.mockImplementationOnce(() => true);
|
||||
expect(
|
||||
addNavigationHelpers({
|
||||
state: { key: 'B', routeName: 'Settings', params: { name: null } },
|
||||
dispatch: mockedDispatch,
|
||||
addListener: dummyEventSubscriber,
|
||||
}).getParam('name')
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
/* Helpers for navigation */
|
||||
|
||||
import NavigationActions from './NavigationActions';
|
||||
import invariant from './utils/invariant';
|
||||
|
||||
export default function(navigation) {
|
||||
return {
|
||||
...navigation,
|
||||
goBack: key => {
|
||||
let actualizedKey = key;
|
||||
if (key === undefined && navigation.state.key) {
|
||||
invariant(
|
||||
typeof navigation.state.key === 'string',
|
||||
'key should be a string'
|
||||
);
|
||||
actualizedKey = navigation.state.key;
|
||||
}
|
||||
return navigation.dispatch(
|
||||
NavigationActions.back({ key: actualizedKey })
|
||||
);
|
||||
},
|
||||
navigate: (navigateTo, params, action) => {
|
||||
if (typeof navigateTo === 'string') {
|
||||
return navigation.dispatch(
|
||||
NavigationActions.navigate({ routeName: navigateTo, params, action })
|
||||
);
|
||||
}
|
||||
invariant(
|
||||
typeof navigateTo === 'object',
|
||||
'Must navigateTo an object or a string'
|
||||
);
|
||||
invariant(
|
||||
params == null,
|
||||
'Params must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
action == null,
|
||||
'Child action must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
return navigation.dispatch(NavigationActions.navigate(navigateTo));
|
||||
},
|
||||
pop: (n, params) =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.pop({ n, immediate: params && params.immediate })
|
||||
),
|
||||
popToTop: params =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.popToTop({ immediate: params && params.immediate })
|
||||
),
|
||||
/**
|
||||
* For updating current route params. For example the nav bar title and
|
||||
* buttons are based on the route params.
|
||||
* This means `setParams` can be used to update nav bar for example.
|
||||
*/
|
||||
setParams: params => {
|
||||
invariant(
|
||||
navigation.state.key && typeof navigation.state.key === 'string',
|
||||
'setParams cannot be called by root navigator'
|
||||
);
|
||||
const key = navigation.state.key;
|
||||
return navigation.dispatch(NavigationActions.setParams({ params, key }));
|
||||
},
|
||||
|
||||
getParam: (paramName, defaultValue) => {
|
||||
const params = navigation.state.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
push: (routeName, params, action) =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.push({ routeName, params, action })
|
||||
),
|
||||
|
||||
replace: (routeName, params, action) =>
|
||||
navigation.dispatch(
|
||||
NavigationActions.replace({
|
||||
routeName,
|
||||
params,
|
||||
action,
|
||||
key: navigation.state.key,
|
||||
})
|
||||
),
|
||||
|
||||
openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()),
|
||||
closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()),
|
||||
toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Linking } from 'react-native';
|
||||
import { AsyncStorage, Linking, Platform } from 'react-native';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import { BackHandler } from './PlatformHelpers';
|
||||
import NavigationActions from './NavigationActions';
|
||||
import addNavigationHelpers from './addNavigationHelpers';
|
||||
import invariant from './utils/invariant';
|
||||
import getNavigationActionCreators from './routers/getNavigationActionCreators';
|
||||
import docsUrl from './utils/docsUrl';
|
||||
|
||||
function isStateful(props) {
|
||||
return !props.navigation;
|
||||
}
|
||||
|
||||
function validateProps(props) {
|
||||
if (isStateful(props)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { navigation, screenProps, ...containerProps } = props;
|
||||
|
||||
const keys = Object.keys(containerProps);
|
||||
|
||||
if (keys.length !== 0) {
|
||||
throw new Error(
|
||||
'This navigator has both navigation and container props, so it is ' +
|
||||
`unclear if it should own its own state. Remove props: "${keys.join(
|
||||
', '
|
||||
)}" ` +
|
||||
'if the navigator should get its state from the navigation prop. If the ' +
|
||||
'navigator should maintain its own state, do not pass a navigation prop.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Track the number of stateful container instances. Warn if >0 and not using the
|
||||
// detached prop to explicitly acknowledge the behavior. We should deprecated implicit
|
||||
// stateful navigation containers in a future release and require a provider style pattern
|
||||
// instead in order to eliminate confusion entirely.
|
||||
let _statefulContainerCount = 0;
|
||||
export function _TESTING_ONLY_reset_container_count() {
|
||||
_statefulContainerCount = 0;
|
||||
}
|
||||
|
||||
// We keep a global flag to catch errors during the state persistence hydrating scenario.
|
||||
// The innermost navigator who catches the error will dispatch a new init action.
|
||||
let _reactNavigationIsHydratingState = false;
|
||||
// Unfortunate to use global state here, but it seems necessesary for the time
|
||||
// being. There seems to be some problems with cascading componentDidCatch
|
||||
// handlers. Ideally the inner non-stateful navigator catches the error and
|
||||
// re-throws it, to be caught by the top-level stateful navigator.
|
||||
|
||||
/**
|
||||
* Create an HOC that injects the navigation and manages the navigation state
|
||||
@@ -18,12 +63,17 @@ export default function createNavigationContainer(Component) {
|
||||
static router = Component.router;
|
||||
static navigationOptions = null;
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
validateProps(nextProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
_actionEventSubscribers = new Set();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._validateProps(props);
|
||||
validateProps(props);
|
||||
|
||||
this._initialAction = NavigationActions.init();
|
||||
|
||||
@@ -41,14 +91,21 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
nav: this._isStateful()
|
||||
? Component.router.getStateForAction(this._initialAction)
|
||||
: null,
|
||||
nav:
|
||||
this._isStateful() && !props.persistenceKey
|
||||
? Component.router.getStateForAction(this._initialAction)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
_renderLoading() {
|
||||
return this.props.renderLoadingExperimental
|
||||
? this.props.renderLoadingExperimental()
|
||||
: null;
|
||||
}
|
||||
|
||||
_isStateful() {
|
||||
return !this.props.navigation;
|
||||
return isStateful(this.props);
|
||||
}
|
||||
|
||||
_validateProps(props) {
|
||||
@@ -127,74 +184,177 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._validateProps(nextProps);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Clear cached _nav every tick
|
||||
if (this._nav === this.state.nav) {
|
||||
this._nav = null;
|
||||
// Clear cached _navState every tick
|
||||
if (this._navState === this.state.nav) {
|
||||
this._navState = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
this._isMounted = true;
|
||||
if (!this._isStateful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__ && !this.props.detached) {
|
||||
if (_statefulContainerCount > 0) {
|
||||
// Temporarily only show this on iOS due to this issue:
|
||||
// https://github.com/react-navigation/react-navigation/issues/4196#issuecomment-390827829
|
||||
if (Platform.OS === 'ios') {
|
||||
console.warn(
|
||||
`You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl(
|
||||
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_statefulContainerCount++;
|
||||
Linking.addEventListener('url', this._handleOpenURL);
|
||||
|
||||
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
|
||||
// Pull out anything that can impact state
|
||||
const { persistenceKey } = this.props;
|
||||
const startupStateJSON =
|
||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
||||
const url = await Linking.getInitialURL();
|
||||
const parsedUrl = url && this._urlToPathAndParams(url);
|
||||
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action: this._initialAction,
|
||||
state: this.state.nav,
|
||||
lastState: null,
|
||||
})
|
||||
);
|
||||
// Initialize state. This must be done *after* any async code
|
||||
// so we don't end up with a different value for this.state.nav
|
||||
// due to changes while async function was resolving
|
||||
let action = this._initialAction;
|
||||
let startupState = this.state.nav;
|
||||
if (!startupState) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Init new Navigation State');
|
||||
startupState = Component.router.getStateForAction(action);
|
||||
}
|
||||
|
||||
// Pull persisted state from AsyncStorage
|
||||
if (startupStateJSON) {
|
||||
try {
|
||||
startupState = JSON.parse(startupStateJSON);
|
||||
_reactNavigationIsHydratingState = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Pull state out of URL
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const urlAction = Component.router.getActionForPathAndParams(
|
||||
path,
|
||||
params
|
||||
);
|
||||
if (urlAction) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Applying Navigation Action for Initial URL:', url);
|
||||
action = urlAction;
|
||||
startupState = Component.router.getStateForAction(
|
||||
urlAction,
|
||||
startupState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dispatchActions = () =>
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action,
|
||||
state: this.state.nav,
|
||||
lastState: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (startupState === this.state.nav) {
|
||||
dispatchActions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ nav: startupState }, () => {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
dispatchActions();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidCatch(e, errorInfo) {
|
||||
if (_reactNavigationIsHydratingState) {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
console.warn(
|
||||
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
|
||||
);
|
||||
this.dispatch(NavigationActions.init());
|
||||
} else {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
_persistNavigationState = async nav => {
|
||||
const { persistenceKey } = this.props;
|
||||
if (!persistenceKey) {
|
||||
return;
|
||||
}
|
||||
await AsyncStorage.setItem(persistenceKey, JSON.stringify(nav));
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
Linking.removeEventListener('url', this._handleOpenURL);
|
||||
this.subs && this.subs.remove();
|
||||
|
||||
if (this._isStateful()) {
|
||||
_statefulContainerCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-tick temporary storage for state.nav
|
||||
|
||||
dispatch = action => {
|
||||
if (!this._isStateful()) {
|
||||
return false;
|
||||
if (this.props.navigation) {
|
||||
return this.props.navigation.dispatch(action);
|
||||
}
|
||||
this._nav = this._nav || this.state.nav;
|
||||
const oldNav = this._nav;
|
||||
invariant(oldNav, 'should be set in constructor if stateful');
|
||||
const nav = Component.router.getStateForAction(action, oldNav);
|
||||
|
||||
// navState will have the most up-to-date value, because setState sometimes behaves asyncronously
|
||||
this._navState = this._navState || this.state.nav;
|
||||
const lastNavState = this._navState;
|
||||
invariant(lastNavState, 'should be set in constructor if stateful');
|
||||
const reducedState = Component.router.getStateForAction(
|
||||
action,
|
||||
lastNavState
|
||||
);
|
||||
const navState = reducedState === null ? lastNavState : reducedState;
|
||||
|
||||
const dispatchActionEvents = () => {
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action,
|
||||
state: nav,
|
||||
lastState: oldNav,
|
||||
state: navState,
|
||||
lastState: lastNavState,
|
||||
})
|
||||
);
|
||||
};
|
||||
if (nav && nav !== oldNav) {
|
||||
|
||||
if (reducedState === null) {
|
||||
// The router will return null when action has been handled and the state hasn't changed.
|
||||
// dispatch returns true when something has been handled.
|
||||
dispatchActionEvents();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (navState !== lastNavState) {
|
||||
// Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
|
||||
this._nav = nav;
|
||||
this.setState({ nav }, () => {
|
||||
this._onNavigationStateChange(oldNav, nav, action);
|
||||
this._navState = navState;
|
||||
this.setState({ nav: navState }, () => {
|
||||
this._onNavigationStateChange(lastNavState, navState, action);
|
||||
dispatchActionEvents();
|
||||
this._persistNavigationState(navState);
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
dispatchActionEvents();
|
||||
}
|
||||
|
||||
dispatchActionEvents();
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -202,9 +362,11 @@ export default function createNavigationContainer(Component) {
|
||||
let navigation = this.props.navigation;
|
||||
if (this._isStateful()) {
|
||||
const nav = this.state.nav;
|
||||
invariant(nav, 'should be set in constructor if stateful');
|
||||
if (!nav) {
|
||||
return this._renderLoading();
|
||||
}
|
||||
if (!this._navigation || this._navigation.state !== nav) {
|
||||
this._navigation = addNavigationHelpers({
|
||||
this._navigation = {
|
||||
dispatch: this.dispatch,
|
||||
state: nav,
|
||||
addListener: (eventName, handler) => {
|
||||
@@ -218,6 +380,11 @@ export default function createNavigationContainer(Component) {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const actionCreators = getNavigationActionCreators(nav);
|
||||
Object.keys(actionCreators).forEach(actionName => {
|
||||
this._navigation[actionName] = (...args) =>
|
||||
this.dispatch(actionCreators[actionName](...args));
|
||||
});
|
||||
}
|
||||
navigation = this._navigation;
|
||||
@@ -227,5 +394,5 @@ export default function createNavigationContainer(Component) {
|
||||
}
|
||||
}
|
||||
|
||||
return NavigationContainer;
|
||||
return polyfill(NavigationContainer);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
action,
|
||||
type: eventName,
|
||||
};
|
||||
const isTransitioning = !!state && !!state.transitioningFromKey;
|
||||
const isTransitioning = !!state && state.isTransitioning;
|
||||
|
||||
const previouslyLastEmittedEvent = lastEmittedEvent;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import DrawerNavigator from '../DrawerNavigator';
|
||||
import DrawerNavigator from '../createDrawerNavigator';
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
|
||||
@@ -3,6 +3,8 @@ import { StyleSheet, View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import StackNavigator from '../createStackNavigator';
|
||||
import withNavigation from '../../views/withNavigation';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
@@ -31,6 +33,10 @@ const routeConfig = {
|
||||
};
|
||||
|
||||
describe('StackNavigator', () => {
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
it('renders successfully', () => {
|
||||
const MyStackNavigator = StackNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyStackNavigator />).toJSON();
|
||||
@@ -51,4 +57,37 @@ describe('StackNavigator', () => {
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('passes navigation to headerRight when wrapped in withNavigation', () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
class TestComponent extends React.Component {
|
||||
render() {
|
||||
return <View>{this.props.onPress(this.props.navigation)}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
const TestComponentWithNavigation = withNavigation(TestComponent);
|
||||
|
||||
class A extends React.Component {
|
||||
static navigationOptions = {
|
||||
headerRight: <TestComponentWithNavigation onPress={spy} />,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
const Nav = StackNavigator({ A: { screen: A } });
|
||||
|
||||
renderer.create(<Nav />);
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
navigate: expect.any(Function),
|
||||
addListener: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import SwitchNavigator from '../createSwitchNavigator';
|
||||
|
||||
const A = () => <View />;
|
||||
const B = () => <View />;
|
||||
const routeConfig = { A, B };
|
||||
|
||||
describe('SwitchNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MySwitchNavigator = SwitchNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MySwitchNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,9 @@ import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import TabNavigator from '../createTabNavigator';
|
||||
const {
|
||||
createTabNavigator,
|
||||
} = require('react-navigation-deprecated-tab-navigator');
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
@@ -25,7 +27,7 @@ const routeConfig = {
|
||||
|
||||
describe('TabNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MyTabNavigator = TabNavigator(routeConfig);
|
||||
const MyTabNavigator = createTabNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyTabNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
|
||||
@@ -51,6 +51,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
@@ -76,37 +77,31 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
</View>
|
||||
<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,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -114,70 +109,89 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -237,6 +251,7 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
@@ -262,37 +277,31 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
</View>
|
||||
<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,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -300,52 +309,71 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
<View
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwitchNavigator renders successfully 1`] = `<View />`;
|
||||
@@ -137,9 +137,15 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"height": 29,
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -150,6 +156,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"minWidth": 30,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
@@ -164,6 +171,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"minWidth": 30,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
|
||||
55
src/navigators/createKeyboardAwareNavigator.js
Normal file
55
src/navigators/createKeyboardAwareNavigator.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
export default Navigator =>
|
||||
class KeyboardAwareNavigator extends React.Component {
|
||||
static router = Navigator.router;
|
||||
_previouslyFocusedTextInput = null;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Navigator
|
||||
{...this.props}
|
||||
onGestureBegin={this._handleGestureBegin}
|
||||
onGestureCanceled={this._handleGestureCanceled}
|
||||
onGestureFinish={this._handleGestureFinish}
|
||||
onTransitionStart={this._handleTransitionStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_handleGestureBegin = () => {
|
||||
this._previouslyFocusedTextInput = TextInput.State.currentlyFocusedField();
|
||||
if (this._previouslyFocusedTextInput) {
|
||||
TextInput.State.blurTextInput(this._previouslyFocusedTextInput);
|
||||
}
|
||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
||||
};
|
||||
|
||||
_handleGestureCanceled = () => {
|
||||
if (this._previouslyFocusedTextInput) {
|
||||
TextInput.State.focusTextInput(this._previouslyFocusedTextInput);
|
||||
}
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
};
|
||||
|
||||
_handleGestureFinish = () => {
|
||||
this._previouslyFocusedTextInput = null;
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
};
|
||||
|
||||
_handleTransitionStart = (transitionProps, prevTransitionProps) => {
|
||||
// TODO: We should not even have received the transition start event
|
||||
// in the case where the index did not change, I believe. We
|
||||
// should revisit this after 2.0 release.
|
||||
if (transitionProps.index !== prevTransitionProps.index) {
|
||||
const currentField = TextInput.State.currentlyFocusedField();
|
||||
if (currentField) {
|
||||
TextInput.State.blurTextInput(currentField);
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onTransitionStart &&
|
||||
this.props.onTransitionStart(transitionProps, prevTransitionProps);
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
|
||||
function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
class Navigator extends React.Component {
|
||||
@@ -15,23 +14,26 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
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
|
||||
// Remove all subscription references
|
||||
componentWillUnmount() {
|
||||
Object.values(this.childEventSubscribers).map(s => s.removeAll());
|
||||
this.childEventSubscribers = {};
|
||||
}
|
||||
|
||||
_isRouteFocused = route => () => {
|
||||
_isRouteFocused = route => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_dangerouslyGetParent = () => {
|
||||
return this.props.navigation;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, screenProps } = this.props;
|
||||
const { dispatch, state, addListener } = navigation;
|
||||
@@ -49,12 +51,37 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
const childNavigation = addNavigationHelpers({
|
||||
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,
|
||||
isFocused: this._isRouteFocused.bind(this, route),
|
||||
});
|
||||
getParam: (paramName, defaultValue) => {
|
||||
const params = route.params;
|
||||
|
||||
if (params && paramName in params) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
};
|
||||
|
||||
const options = router.getScreenOptions(childNavigation, screenProps);
|
||||
descriptors[route.key] = {
|
||||
key: route.key,
|
||||
@@ -67,6 +94,7 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
|
||||
return (
|
||||
<NavigatorView
|
||||
{...this.props}
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
navigationConfig={navigationConfig}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
|
||||
import createNavigator from './createNavigator';
|
||||
import StackView from '../views/StackView/StackView2';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
|
||||
function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
const {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
disableKeyboardHandling,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
@@ -22,7 +26,10 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
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);
|
||||
|
||||
13
src/navigators/createSwitchNavigator.js
Normal file
13
src/navigators/createSwitchNavigator.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from '../navigators/createNavigator';
|
||||
import SwitchRouter from '../routers/SwitchRouter';
|
||||
import SwitchView from '../views/SwitchView/SwitchView';
|
||||
|
||||
function createSwitchNavigator(routeConfigMap, switchConfig = {}) {
|
||||
const router = SwitchRouter(routeConfigMap, switchConfig);
|
||||
const Navigator = createNavigator(SwitchView, router, switchConfig);
|
||||
return createNavigationContainer(Navigator);
|
||||
}
|
||||
|
||||
export default createSwitchNavigator;
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import createNavigator from './createNavigator';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import TabRouter from '../routers/TabRouter';
|
||||
import TabView from '../views/TabView/TabView';
|
||||
import TabBarTop from '../views/TabView/TabBarTop';
|
||||
import TabBarBottom from '../views/TabView/TabBarBottom';
|
||||
|
||||
const TabNavigator = (routeConfigs, config = {}) => {
|
||||
// Use the look native to the platform by default
|
||||
const tabsConfig = { ...TabNavigator.Presets.Default, ...config };
|
||||
|
||||
const router = TabRouter(routeConfigs, tabsConfig);
|
||||
|
||||
const navigator = createNavigator(TabView, router, tabsConfig);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
const Presets = {
|
||||
iOSBottomTabs: {
|
||||
tabBarComponent: TabBarBottom,
|
||||
tabBarPosition: 'bottom',
|
||||
swipeEnabled: false,
|
||||
animationEnabled: false,
|
||||
initialLayout: undefined,
|
||||
},
|
||||
AndroidTopTabs: {
|
||||
tabBarComponent: TabBarTop,
|
||||
tabBarPosition: 'top',
|
||||
swipeEnabled: true,
|
||||
animationEnabled: true,
|
||||
initialLayout: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Use these to get Android-style top tabs even on iOS or vice versa.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const HomeScreenTabNavigator = TabNavigator({
|
||||
* Chat: {
|
||||
* screen: ChatScreen,
|
||||
* },
|
||||
* ...
|
||||
* }, {
|
||||
* ...TabNavigator.Presets.AndroidTopTabs,
|
||||
* tabBarOptions: {
|
||||
* ...
|
||||
* },
|
||||
* });
|
||||
*```
|
||||
*/
|
||||
TabNavigator.Presets = {
|
||||
iOSBottomTabs: Presets.iOSBottomTabs,
|
||||
AndroidTopTabs: Presets.AndroidTopTabs,
|
||||
Default:
|
||||
Platform.OS === 'ios' ? Presets.iOSBottomTabs : Presets.AndroidTopTabs,
|
||||
};
|
||||
|
||||
export default TabNavigator;
|
||||
101
src/react-navigation.js
vendored
101
src/react-navigation.js
vendored
@@ -8,25 +8,68 @@ module.exports = {
|
||||
get StateUtils() {
|
||||
return require('./StateUtils').default;
|
||||
},
|
||||
get addNavigationHelpers() {
|
||||
return require('./addNavigationHelpers').default;
|
||||
},
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
|
||||
// Navigators
|
||||
get createNavigator() {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
get StackNavigator() {
|
||||
get createStackNavigator() {
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
},
|
||||
get TabNavigator() {
|
||||
return require('./navigators/createTabNavigator').default;
|
||||
get StackNavigator() {
|
||||
console.warn(
|
||||
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
|
||||
);
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
},
|
||||
get createSwitchNavigator() {
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
},
|
||||
get SwitchNavigator() {
|
||||
console.warn(
|
||||
'The SwitchNavigator function name is deprecated, please use createSwitchNavigator instead'
|
||||
);
|
||||
return require('./navigators/createSwitchNavigator').default;
|
||||
},
|
||||
get createDrawerNavigator() {
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
},
|
||||
get DrawerNavigator() {
|
||||
return require('./navigators/DrawerNavigator').default;
|
||||
console.warn(
|
||||
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
|
||||
);
|
||||
return require('./navigators/createDrawerNavigator').default;
|
||||
},
|
||||
get createTabNavigator() {
|
||||
console.warn(
|
||||
'createTabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator')
|
||||
.createTabNavigator;
|
||||
},
|
||||
get TabNavigator() {
|
||||
console.warn(
|
||||
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator')
|
||||
.createTabNavigator;
|
||||
},
|
||||
get createBottomTabNavigator() {
|
||||
return require('react-navigation-tabs').createBottomTabNavigator;
|
||||
},
|
||||
get createMaterialTopTabNavigator() {
|
||||
return require('react-navigation-tabs').createMaterialTopTabNavigator;
|
||||
},
|
||||
|
||||
// Actions
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
get StackActions() {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
},
|
||||
|
||||
// Routers
|
||||
@@ -36,6 +79,12 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get DrawerRouter() {
|
||||
return require('./routers/DrawerRouter').default;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
|
||||
// Views
|
||||
get Transitioner() {
|
||||
@@ -50,6 +99,12 @@ module.exports = {
|
||||
get SafeAreaView() {
|
||||
return require('react-native-safe-area-view').default;
|
||||
},
|
||||
get SceneView() {
|
||||
return require('./views/SceneView').default;
|
||||
},
|
||||
get ResourceSavingSceneView() {
|
||||
return require('./views/ResourceSavingSceneView').default;
|
||||
},
|
||||
|
||||
// Header
|
||||
get Header() {
|
||||
@@ -69,16 +124,33 @@ module.exports = {
|
||||
get DrawerItems() {
|
||||
return require('./views/Drawer/DrawerNavigatorItems').default;
|
||||
},
|
||||
get DrawerSidebar() {
|
||||
return require('./views/Drawer/DrawerSidebar').default;
|
||||
},
|
||||
|
||||
// TabView
|
||||
get TabView() {
|
||||
return require('./views/TabView/TabView').default;
|
||||
console.warn(
|
||||
'TabView is deprecated. Please use the react-navigation-tabs package instead: https://github.com/react-navigation/react-navigation-tabs'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').TabView;
|
||||
},
|
||||
get TabBarTop() {
|
||||
return require('./views/TabView/TabBarTop').default;
|
||||
console.warn(
|
||||
'TabBarTop is deprecated. Please use the react-navigation-tabs package instead: https://github.com/react-navigation/react-navigation-tabs'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').TabBarTop;
|
||||
},
|
||||
get TabBarBottom() {
|
||||
return require('./views/TabView/TabBarBottom').default;
|
||||
console.warn(
|
||||
'TabBarBottom is deprecated. Please use the react-navigation-tabs package instead: https://github.com/react-navigation/react-navigation-tabs'
|
||||
);
|
||||
return require('react-navigation-deprecated-tab-navigator').TabBarBottom;
|
||||
},
|
||||
|
||||
// SwitchView
|
||||
get SwitchView() {
|
||||
return require('./views/SwitchView/SwitchView').default;
|
||||
},
|
||||
|
||||
// HOCs
|
||||
@@ -88,4 +160,7 @@ module.exports = {
|
||||
get withNavigationFocus() {
|
||||
return require('./views/withNavigationFocus').default;
|
||||
},
|
||||
get withOrientation() {
|
||||
return require('./views/withOrientation').default;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,18 +8,23 @@ module.exports = {
|
||||
get StateUtils() {
|
||||
return require('./StateUtils').default;
|
||||
},
|
||||
get addNavigationHelpers() {
|
||||
return require('./addNavigationHelpers').default;
|
||||
},
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
|
||||
// Navigators
|
||||
get createNavigator() {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
|
||||
// Actions
|
||||
get NavigationActions() {
|
||||
return require('./NavigationActions').default;
|
||||
},
|
||||
get StackActions() {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
},
|
||||
|
||||
// Routers
|
||||
get StackRouter() {
|
||||
return require('./routers/StackRouter').default;
|
||||
@@ -27,6 +32,9 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
|
||||
// HOCs
|
||||
get withNavigation() {
|
||||
|
||||
28
src/routers/DrawerActions.js
Normal file
28
src/routers/DrawerActions.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
|
||||
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
|
||||
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
|
||||
|
||||
const openDrawer = payload => ({
|
||||
type: OPEN_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const closeDrawer = payload => ({
|
||||
type: CLOSE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const toggleDrawer = payload => ({
|
||||
type: TOGGLE_DRAWER,
|
||||
...payload,
|
||||
});
|
||||
|
||||
export default {
|
||||
OPEN_DRAWER,
|
||||
CLOSE_DRAWER,
|
||||
TOGGLE_DRAWER,
|
||||
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
toggleDrawer,
|
||||
};
|
||||
@@ -1,54 +1,87 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import TabRouter from './TabRouter';
|
||||
|
||||
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 = {}) => {
|
||||
const tabRouter = TabRouter(routeConfigs, config);
|
||||
config = { ...config };
|
||||
config = withDefaultValue(config, 'resetOnBlur', false);
|
||||
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
|
||||
|
||||
const switchRouter = SwitchRouter(routeConfigs, config);
|
||||
|
||||
return {
|
||||
...tabRouter,
|
||||
...switchRouter,
|
||||
|
||||
getStateForAction(action, lastState) {
|
||||
const state = lastState || {
|
||||
...tabRouter.getStateForAction(action, undefined),
|
||||
isDrawerOpen: false,
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
openDrawer: () => DrawerActions.openDrawer({ key: navStateKey }),
|
||||
closeDrawer: () => DrawerActions.closeDrawer({ key: navStateKey }),
|
||||
toggleDrawer: () => DrawerActions.toggleDrawer({ key: navStateKey }),
|
||||
...switchRouter.getActionCreators(route, navStateKey),
|
||||
};
|
||||
},
|
||||
|
||||
// Handle explicit drawer actions
|
||||
if (
|
||||
state.isDrawerOpen &&
|
||||
action.type === NavigationActions.CLOSE_DRAWER
|
||||
) {
|
||||
getStateForAction(action, state) {
|
||||
// Set up the initial state if needed
|
||||
if (!state) {
|
||||
return {
|
||||
...state,
|
||||
...switchRouter.getStateForAction(action, undefined),
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
if (
|
||||
!state.isDrawerOpen &&
|
||||
action.type === NavigationActions.OPEN_DRAWER
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: true,
|
||||
};
|
||||
}
|
||||
if (action.type === NavigationActions.TOGGLE_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: !state.isDrawerOpen,
|
||||
};
|
||||
|
||||
const isRouterTargeted = action.key == null || action.key === state.key;
|
||||
|
||||
if (isRouterTargeted) {
|
||||
// Only handle actions that are meant for this drawer, as specified by action.key.
|
||||
|
||||
if (action.type === DrawerActions.CLOSE_DRAWER && state.isDrawerOpen) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DrawerActions.OPEN_DRAWER && !state.isDrawerOpen) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DrawerActions.TOGGLE_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
isDrawerOpen: !state.isDrawerOpen,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back on tab router for screen switching logic
|
||||
const tabState = tabRouter.getStateForAction(action, state);
|
||||
if (tabState !== null && tabState !== state) {
|
||||
// If the tabs have changed, make sure to close the drawer
|
||||
return {
|
||||
...tabState,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
// Fall back on switch router for screen switching logic, and handling of child routers
|
||||
const switchedState = switchRouter.getStateForAction(action, state);
|
||||
|
||||
if (switchedState === null) {
|
||||
// The switch router or a child router is attempting to swallow this action. We return null to allow this.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (switchedState !== state) {
|
||||
if (switchedState.index !== state.index) {
|
||||
// If the tabs have changed, make sure to close the drawer
|
||||
return {
|
||||
...switchedState,
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
// Return the state new state, as returned by the switch router.
|
||||
// The index hasn't changed, so this most likely means that a child router has returned a new state
|
||||
return switchedState;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
};
|
||||
|
||||
52
src/routers/StackActions.js
Normal file
52
src/routers/StackActions.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const POP = 'Navigation/POP';
|
||||
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
|
||||
const PUSH = 'Navigation/PUSH';
|
||||
const RESET = 'Navigation/RESET';
|
||||
const REPLACE = 'Navigation/REPLACE';
|
||||
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
|
||||
|
||||
const pop = payload => ({
|
||||
type: POP,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const popToTop = payload => ({
|
||||
type: POP_TO_TOP,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const push = payload => ({
|
||||
type: PUSH,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const reset = payload => ({
|
||||
type: RESET,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const replace = payload => ({
|
||||
type: REPLACE,
|
||||
...payload,
|
||||
});
|
||||
|
||||
const completeTransition = payload => ({
|
||||
type: COMPLETE_TRANSITION,
|
||||
...payload,
|
||||
});
|
||||
|
||||
export default {
|
||||
POP,
|
||||
POP_TO_TOP,
|
||||
PUSH,
|
||||
RESET,
|
||||
REPLACE,
|
||||
COMPLETE_TRANSITION,
|
||||
|
||||
pop,
|
||||
popToTop,
|
||||
push,
|
||||
reset,
|
||||
replace,
|
||||
completeTransition,
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackActions from './StackActions';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import StateUtils from '../StateUtils';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import invariant from '../utils/invariant';
|
||||
import { generateKey } from './KeyGenerator';
|
||||
import getNavigationActionCreators from './getNavigationActionCreators';
|
||||
|
||||
function isEmpty(obj) {
|
||||
if (!obj) return true;
|
||||
@@ -19,10 +21,16 @@ function isEmpty(obj) {
|
||||
function behavesLikePushAction(action) {
|
||||
return (
|
||||
action.type === NavigationActions.NAVIGATE ||
|
||||
action.type === NavigationActions.PUSH
|
||||
action.type === StackActions.PUSH
|
||||
);
|
||||
}
|
||||
|
||||
const defaultActionCreators = (route, navStateKey) => ({});
|
||||
|
||||
function isResetToRootStack(action) {
|
||||
return action.type === StackActions.RESET && action.key === null;
|
||||
}
|
||||
|
||||
export default (routeConfigs, stackConfig = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
@@ -43,6 +51,8 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
});
|
||||
|
||||
const { initialRouteParams } = stackConfig;
|
||||
const getCustomActionCreators =
|
||||
stackConfig.getCustomActionCreators || defaultActionCreators;
|
||||
|
||||
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
|
||||
|
||||
@@ -65,7 +75,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
@@ -100,7 +110,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
};
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
transitioningFromKey: false,
|
||||
isTransitioning: false,
|
||||
index: 0,
|
||||
routes: [route],
|
||||
};
|
||||
@@ -136,7 +146,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
});
|
||||
|
||||
paths = Object.entries(pathsByRouteNames);
|
||||
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
|
||||
paths.sort((a, b) => b[1].priority - a[1].priority);
|
||||
|
||||
return {
|
||||
getComponentForState(state) {
|
||||
@@ -152,16 +162,75 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getActionCreators(route, navStateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, navStateKey),
|
||||
pop: (n, params) =>
|
||||
StackActions.pop({
|
||||
n,
|
||||
...params,
|
||||
}),
|
||||
popToTop: params => StackActions.popToTop(params),
|
||||
push: (routeName, params, action) =>
|
||||
StackActions.push({
|
||||
routeName,
|
||||
params,
|
||||
action,
|
||||
}),
|
||||
replace: (replaceWith, params, action, newKey) => {
|
||||
if (typeof replaceWith === 'string') {
|
||||
return StackActions.replace({
|
||||
routeName: replaceWith,
|
||||
params,
|
||||
action,
|
||||
key: route.key,
|
||||
newKey,
|
||||
});
|
||||
}
|
||||
invariant(
|
||||
typeof replaceWith === 'object',
|
||||
'Must replaceWith an object or a string'
|
||||
);
|
||||
invariant(
|
||||
params == null,
|
||||
'Params must not be provided to .replace() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
action == null,
|
||||
'Child action must not be provided to .replace() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
newKey == null,
|
||||
'Child action must not be provided to .replace() when specifying an object'
|
||||
);
|
||||
return StackActions.replace(replaceWith);
|
||||
},
|
||||
reset: (actions, index) =>
|
||||
StackActions.reset({
|
||||
actions,
|
||||
index: index == null ? actions.length - 1 : index,
|
||||
key: navStateKey,
|
||||
}),
|
||||
dismiss: () =>
|
||||
NavigationActions.back({
|
||||
key: navStateKey,
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, state) {
|
||||
// Set up the initial state if needed
|
||||
if (!state) {
|
||||
return getInitialState(action);
|
||||
}
|
||||
const lastRouteKey = state.routes[state.index].key;
|
||||
|
||||
// Check if the focused child scene wants to handle the action, as long as
|
||||
// it is not a reset to the root stack
|
||||
if (action.type !== NavigationActions.RESET || action.key !== null) {
|
||||
if (
|
||||
!isResetToRootStack(action) &&
|
||||
action.type !== NavigationActions.NAVIGATE
|
||||
) {
|
||||
const keyIndex = action.key
|
||||
? StateUtils.indexOf(state, action.key)
|
||||
: -1;
|
||||
@@ -181,6 +250,31 @@ 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) {
|
||||
return StateUtils.replaceAndPrune(
|
||||
state,
|
||||
nextRouteState ? nextRouteState.key : childRoute.key,
|
||||
nextRouteState ? nextRouteState : childRoute
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle explicit push navigation action. This must happen after the
|
||||
@@ -193,46 +287,50 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
let route;
|
||||
|
||||
invariant(
|
||||
action.type !== NavigationActions.PUSH || action.key == null,
|
||||
action.type !== StackActions.PUSH || action.key == null,
|
||||
'StackRouter does not support key on the push action'
|
||||
);
|
||||
|
||||
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
|
||||
if (action.key) {
|
||||
const lastRouteIndex = state.routes.findIndex(
|
||||
r => r.key === action.key
|
||||
);
|
||||
if (lastRouteIndex !== -1) {
|
||||
// If index is unchanged and params are not being set, leave state identity intact
|
||||
if (state.index === lastRouteIndex && !action.params) {
|
||||
return state;
|
||||
}
|
||||
// Before pushing a new route we first try to find one in the existing route stack
|
||||
// More information on this: https://github.com/react-navigation/rfcs/blob/master/text/0004-less-pushy-navigate.md
|
||||
const lastRouteIndex = state.routes.findIndex(r => {
|
||||
if (action.key) {
|
||||
return r.key === action.key;
|
||||
} else {
|
||||
return r.routeName === action.routeName;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the now unused routes at the tail of the routes array
|
||||
const routes = state.routes.slice(0, lastRouteIndex + 1);
|
||||
if (action.type !== StackActions.PUSH && lastRouteIndex !== -1) {
|
||||
// If index is unchanged and params are not being set, leave state identity intact
|
||||
if (state.index === lastRouteIndex && !action.params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply params if provided, otherwise leave route identity intact
|
||||
if (action.params) {
|
||||
const route = state.routes.find(r => r.key === action.key);
|
||||
routes[lastRouteIndex] = {
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return state with new index. Change transitioningFromKey only if index has changed
|
||||
return {
|
||||
...state,
|
||||
transitioningFromKey:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true ? lastRouteKey : null
|
||||
: null,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
// Remove the now unused routes at the tail of the routes array
|
||||
const routes = state.routes.slice(0, lastRouteIndex + 1);
|
||||
|
||||
// Apply params if provided, otherwise leave route identity intact
|
||||
if (action.params) {
|
||||
const route = state.routes[lastRouteIndex];
|
||||
routes[lastRouteIndex] = {
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return state with new index. Change isTransitioning only if index has changed
|
||||
return {
|
||||
...state,
|
||||
isTransitioning:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true
|
||||
: state.isTransitioning,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
if (childRouter) {
|
||||
@@ -254,18 +352,14 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
return {
|
||||
...StateUtils.push(state, route),
|
||||
transitioningFromKey: action.immediate !== true ? lastRouteKey : null,
|
||||
isTransitioning: action.immediate !== true,
|
||||
};
|
||||
} else if (
|
||||
action.type === NavigationActions.PUSH &&
|
||||
action.type === StackActions.PUSH &&
|
||||
childRouters[action.routeName] === undefined
|
||||
) {
|
||||
// If we've made it this far with a push action, we return the
|
||||
// state with a new identity to prevent the action from bubbling
|
||||
// back up.
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
// Return the state identity to bubble the action up
|
||||
return state;
|
||||
}
|
||||
|
||||
// Handle navigation to other child routers that are not yet pushed
|
||||
@@ -305,7 +399,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
|
||||
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
|
||||
if (action.type === NavigationActions.POP_TO_TOP) {
|
||||
if (action.type === StackActions.POP_TO_TOP) {
|
||||
// Refuse to handle pop to top if a key is given that doesn't correspond
|
||||
// to this router
|
||||
if (action.key && state.key !== action.key) {
|
||||
@@ -314,14 +408,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// If we're already at the top, then we return the state with a new
|
||||
// identity so that the action is handled by this router.
|
||||
if (state.index === 0) {
|
||||
if (state.index > 0) {
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
lastRouteKey: action.immediate !== true ? lastRouteKey : null,
|
||||
isTransitioning: action.immediate !== true,
|
||||
index: 0,
|
||||
routes: [state.routes[0]],
|
||||
};
|
||||
@@ -330,7 +420,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
|
||||
// Handle replace action
|
||||
if (action.type === NavigationActions.REPLACE) {
|
||||
if (action.type === StackActions.REPLACE) {
|
||||
const routeIndex = state.routes.findIndex(r => r.key === action.key);
|
||||
// Only replace if the key matches one of our routes
|
||||
if (routeIndex !== -1) {
|
||||
@@ -356,13 +446,13 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// Update transitioning state
|
||||
if (
|
||||
action.type === NavigationActions.COMPLETE_TRANSITION &&
|
||||
action.type === StackActions.COMPLETE_TRANSITION &&
|
||||
(action.key == null || action.key === state.key) &&
|
||||
state.transitioningFromKey
|
||||
state.isTransitioning
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -386,7 +476,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.RESET) {
|
||||
if (action.type === StackActions.RESET) {
|
||||
// Only handle reset actions that are unspecified or match this state key
|
||||
if (action.key != null && action.key != state.key) {
|
||||
// Deliberately use != instead of !== so we can match null with
|
||||
@@ -423,11 +513,11 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
if (
|
||||
action.type === NavigationActions.BACK ||
|
||||
action.type === NavigationActions.POP
|
||||
action.type === StackActions.POP
|
||||
) {
|
||||
const { key, n, immediate } = action;
|
||||
let backRouteIndex = state.index;
|
||||
if (action.type === NavigationActions.POP && n != null) {
|
||||
if (action.type === StackActions.POP && n != null) {
|
||||
// determine the index to go back *from*. In this case, n=1 means to go
|
||||
// back from state.index, as if it were a normal "BACK" action
|
||||
backRouteIndex = Math.max(1, state.index - n + 1);
|
||||
@@ -441,17 +531,11 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
...state,
|
||||
routes: state.routes.slice(0, backRouteIndex),
|
||||
index: backRouteIndex - 1,
|
||||
transitioningFromKey: immediate !== true ? lastRouteKey : null,
|
||||
};
|
||||
} else if (
|
||||
backRouteIndex === 0 &&
|
||||
action.type === NavigationActions.POP
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
isTransitioning: immediate !== true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
@@ -482,6 +566,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (!pathToResolve) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
params: inputParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -552,7 +637,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (key.asterisk || !key) {
|
||||
return result;
|
||||
}
|
||||
const nextResult = result || {};
|
||||
const nextResult = result || inputParams || {};
|
||||
const paramName = key.name;
|
||||
|
||||
let decodedMatchResult;
|
||||
|
||||
388
src/routers/SwitchRouter.js
Normal file
388
src/routers/SwitchRouter.js
Normal file
@@ -0,0 +1,388 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import StackActions from './StackActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getNavigationActionCreators from './getNavigationActionCreators';
|
||||
|
||||
const defaultActionCreators = (route, navStateKey) => ({});
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
NavigationActions.SET_PARAMS,
|
||||
// Todo: make SwitchRouter not depend on StackActions..
|
||||
StackActions.COMPLETE_TRANSITION,
|
||||
].includes(actionType);
|
||||
}
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
const getCustomActionCreators =
|
||||
config.getCustomActionCreators || defaultActionCreators;
|
||||
|
||||
const initialRouteParams = config.initialRouteParams;
|
||||
const initialRouteName = config.initialRouteName || order[0];
|
||||
const backBehavior = config.backBehavior || 'none';
|
||||
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
|
||||
const resetOnBlur = config.hasOwnProperty('resetOnBlur')
|
||||
? config.resetOnBlur
|
||||
: true;
|
||||
const initialRouteIndex = order.indexOf(initialRouteName);
|
||||
const childRouters = {};
|
||||
order.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
if (!paths[routeName]) {
|
||||
paths[routeName] =
|
||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
||||
}
|
||||
childRouters[routeName] = null;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
if (screen.router) {
|
||||
childRouters[routeName] = screen.router;
|
||||
}
|
||||
});
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}'.` +
|
||||
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
function resetChildRoute(routeName) {
|
||||
const params =
|
||||
routeName === initialRouteName ? initialRouteParams : undefined;
|
||||
const childRouter = childRouters[routeName];
|
||||
if (childRouter) {
|
||||
const childAction = NavigationActions.init();
|
||||
return {
|
||||
...childRouter.getStateForAction(childAction),
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getInitialState() {
|
||||
const routes = order.map(resetChildRoute);
|
||||
return {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
};
|
||||
},
|
||||
|
||||
getNextState(prevState, possibleNextState) {
|
||||
if (!prevState) {
|
||||
return possibleNextState;
|
||||
}
|
||||
|
||||
let nextState;
|
||||
if (prevState.index !== possibleNextState.index && resetOnBlur) {
|
||||
const prevRouteName = prevState.routes[prevState.index].routeName;
|
||||
const nextRoutes = [...possibleNextState.routes];
|
||||
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
|
||||
|
||||
return {
|
||||
...possibleNextState,
|
||||
routes: nextRoutes,
|
||||
};
|
||||
} else {
|
||||
nextState = possibleNextState;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
},
|
||||
|
||||
getActionCreators(route, stateKey) {
|
||||
return {
|
||||
...getNavigationActionCreators(route),
|
||||
...getCustomActionCreators(route, stateKey),
|
||||
};
|
||||
},
|
||||
|
||||
getStateForAction(action, inputState) {
|
||||
let prevState = inputState ? { ...inputState } : inputState;
|
||||
let state = inputState || this.getInitialState();
|
||||
let activeChildIndex = state.index;
|
||||
|
||||
if (action.type === NavigationActions.INIT) {
|
||||
// NOTE(brentvatne): this seems weird... why are we merging these
|
||||
// params into child routes?
|
||||
// ---------------------------------------------------------------
|
||||
// Merge any params from the action into all the child routes
|
||||
const { params } = action;
|
||||
if (params) {
|
||||
state.routes = state.routes.map(route => ({
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...params,
|
||||
...(route.routeName === initialRouteName
|
||||
? initialRouteParams
|
||||
: null),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Let the current child handle it
|
||||
const activeChildLastState = state.routes[state.index];
|
||||
const activeChildRouter = childRouters[order[state.index]];
|
||||
if (activeChildRouter) {
|
||||
const activeChildState = activeChildRouter.getStateForAction(
|
||||
action,
|
||||
activeChildLastState
|
||||
);
|
||||
if (!activeChildState && inputState) {
|
||||
return null;
|
||||
}
|
||||
if (activeChildState && activeChildState !== activeChildLastState) {
|
||||
const routes = [...state.routes];
|
||||
routes[state.index] = activeChildState;
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tab changing. Do this after letting the current tab try to
|
||||
// handle the action, to allow inner children to change first
|
||||
const isBackEligible =
|
||||
action.key == null || action.key === activeChildLastState.key;
|
||||
if (action.type === NavigationActions.BACK) {
|
||||
if (isBackEligible && shouldBackNavigateToInitialRoute) {
|
||||
activeChildIndex = initialRouteIndex;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
let didNavigate = false;
|
||||
if (action.type === NavigationActions.NAVIGATE) {
|
||||
didNavigate = !!order.find((childId, i) => {
|
||||
if (childId === action.routeName) {
|
||||
activeChildIndex = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (didNavigate) {
|
||||
const childState = state.routes[activeChildIndex];
|
||||
const childRouter = childRouters[action.routeName];
|
||||
let newChildState;
|
||||
|
||||
if (action.action) {
|
||||
newChildState = childRouter
|
||||
? childRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!action.action && !childRouter && action.params) {
|
||||
newChildState = {
|
||||
...childState,
|
||||
params: {
|
||||
...(childState.params || {}),
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (newChildState && newChildState !== childState) {
|
||||
const routes = [...state.routes];
|
||||
routes[activeChildIndex] = newChildState;
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
} else if (
|
||||
!newChildState &&
|
||||
state.index === activeChildIndex &&
|
||||
prevState
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.SET_PARAMS) {
|
||||
const key = action.key;
|
||||
const lastRoute = state.routes.find(route => route.key === key);
|
||||
if (lastRoute) {
|
||||
const params = {
|
||||
...lastRoute.params,
|
||||
...action.params,
|
||||
};
|
||||
const routes = [...state.routes];
|
||||
routes[state.routes.indexOf(lastRoute)] = {
|
||||
...lastRoute,
|
||||
params,
|
||||
};
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (activeChildIndex !== state.index) {
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
} else if (didNavigate && !inputState) {
|
||||
return state;
|
||||
} else if (didNavigate) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
// Let other children handle it and switch to the first child that returns a new state
|
||||
let index = state.index;
|
||||
let routes = state.routes;
|
||||
order.find((childId, i) => {
|
||||
const childRouter = childRouters[childId];
|
||||
if (i === index) {
|
||||
return false;
|
||||
}
|
||||
let childState = routes[i];
|
||||
if (childRouter) {
|
||||
childState = childRouter.getStateForAction(action, childState);
|
||||
}
|
||||
if (!childState) {
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
if (childState !== routes[i]) {
|
||||
routes = [...routes];
|
||||
routes[i] = childState;
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Nested routers can be updated after switching children with actions such as SET_PARAMS
|
||||
// and COMPLETE_TRANSITION.
|
||||
// NOTE: This may be problematic with custom routers because we whitelist the actions
|
||||
// that can be handled by child routers without automatically changing index.
|
||||
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
|
||||
index = state.index;
|
||||
}
|
||||
|
||||
if (index !== state.index || routes !== state.routes) {
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
getComponentForState(state) {
|
||||
const routeName = state.routes[state.index].routeName;
|
||||
invariant(
|
||||
routeName,
|
||||
`There is no route defined for index ${state.index}. Check that
|
||||
that you passed in a navigation state with a valid tab/screen index.`
|
||||
);
|
||||
const childRouter = childRouters[routeName];
|
||||
if (childRouter) {
|
||||
return childRouter.getComponentForState(state.routes[state.index]);
|
||||
}
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getComponentForRouteName(routeName) {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = order[state.index];
|
||||
const subPath = paths[routeName];
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an optional action, based on a relative path and query params.
|
||||
*
|
||||
* This will return null if there is no action matched
|
||||
*/
|
||||
getActionForPathAndParams(path, params) {
|
||||
if (!path) {
|
||||
return NavigationActions.navigate({
|
||||
routeName: initialRouteName,
|
||||
params,
|
||||
});
|
||||
}
|
||||
return (
|
||||
order
|
||||
.map(childId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[childId];
|
||||
const partsInTestPath = pathToTest.split('/').length;
|
||||
const pathPartsToTest = parts.slice(0, partsInTestPath).join('/');
|
||||
if (pathPartsToTest === pathToTest) {
|
||||
const childRouter = childRouters[childId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: childId,
|
||||
});
|
||||
if (childRouter && childRouter.getActionForPathAndParams) {
|
||||
action.action = childRouter.getActionForPathAndParams(
|
||||
parts.slice(partsInTestPath).join('/'),
|
||||
params
|
||||
);
|
||||
}
|
||||
if (params) {
|
||||
action.params = params;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
order
|
||||
.map(childId => {
|
||||
const childRouter = childRouters[childId];
|
||||
return (
|
||||
childRouter && childRouter.getActionForPathAndParams(path, params)
|
||||
);
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
routeConfigs,
|
||||
config.navigationOptions
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,326 +1,11 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
NavigationActions.SET_PARAMS,
|
||||
NavigationActions.COMPLETE_TRANSITION,
|
||||
].includes(actionType);
|
||||
}
|
||||
import SwitchRouter from './SwitchRouter';
|
||||
import withDefaultValue from '../utils/withDefaultValue';
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
config = { ...config };
|
||||
config = withDefaultValue(config, 'resetOnBlur', false);
|
||||
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
const initialRouteParams = config.initialRouteParams;
|
||||
const initialRouteName = config.initialRouteName || order[0];
|
||||
const initialRouteIndex = order.indexOf(initialRouteName);
|
||||
const backBehavior = config.backBehavior || 'initialRoute';
|
||||
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
|
||||
const tabRouters = {};
|
||||
order.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
paths[routeName] =
|
||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
||||
tabRouters[routeName] = null;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
if (screen.router) {
|
||||
tabRouters[routeName] = screen.router;
|
||||
}
|
||||
});
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}' for TabRouter. ` +
|
||||
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
getStateForAction(action, inputState) {
|
||||
// Establish a default state
|
||||
let state = inputState;
|
||||
if (!state) {
|
||||
const routes = order.map(routeName => {
|
||||
const params =
|
||||
routeName === initialRouteName ? initialRouteParams : undefined;
|
||||
const tabRouter = tabRouters[routeName];
|
||||
if (tabRouter) {
|
||||
const childAction = NavigationActions.init();
|
||||
return {
|
||||
...tabRouter.getStateForAction(childAction),
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
});
|
||||
state = {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
transitioningFromKey: null,
|
||||
};
|
||||
// console.log(`${order.join('-')}: Initial state`, {state});
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.INIT) {
|
||||
// Merge any params from the action into all the child routes
|
||||
const { params } = action;
|
||||
if (params) {
|
||||
state.routes = state.routes.map(route => ({
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...params,
|
||||
...(route.routeName === initialRouteName
|
||||
? initialRouteParams
|
||||
: null),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Let the current tab handle it
|
||||
const activeTabLastState = state.routes[state.index];
|
||||
const activeTabRouter = tabRouters[order[state.index]];
|
||||
if (activeTabRouter) {
|
||||
const activeTabState = activeTabRouter.getStateForAction(
|
||||
action,
|
||||
activeTabLastState
|
||||
);
|
||||
if (!activeTabState && inputState) {
|
||||
return null;
|
||||
}
|
||||
if (activeTabState && activeTabState !== activeTabLastState) {
|
||||
const routes = [...state.routes];
|
||||
routes[state.index] = activeTabState;
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tab changing. Do this after letting the current tab try to
|
||||
// handle the action, to allow inner tabs to change first
|
||||
let activeTabIndex = state.index;
|
||||
const isBackEligible =
|
||||
action.key == null || action.key === activeTabLastState.key;
|
||||
if (action.type === NavigationActions.BACK) {
|
||||
if (isBackEligible && shouldBackNavigateToInitialRoute) {
|
||||
activeTabIndex = initialRouteIndex;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
let didNavigate = false;
|
||||
if (action.type === NavigationActions.NAVIGATE) {
|
||||
const navigateAction = action;
|
||||
didNavigate = !!order.find((tabId, i) => {
|
||||
if (tabId === navigateAction.routeName) {
|
||||
activeTabIndex = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (didNavigate) {
|
||||
const childState = state.routes[activeTabIndex];
|
||||
let newChildState;
|
||||
|
||||
const tabRouter = tabRouters[action.routeName];
|
||||
|
||||
if (action.action) {
|
||||
newChildState = tabRouter
|
||||
? tabRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!tabRouter && action.params) {
|
||||
newChildState = {
|
||||
...childState,
|
||||
params: {
|
||||
...(childState.params || {}),
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (newChildState && newChildState !== childState) {
|
||||
const routes = [...state.routes];
|
||||
routes[activeTabIndex] = newChildState;
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
index: activeTabIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.type === NavigationActions.SET_PARAMS) {
|
||||
const key = action.key;
|
||||
const lastRoute = state.routes.find(route => route.key === key);
|
||||
if (lastRoute) {
|
||||
const params = {
|
||||
...lastRoute.params,
|
||||
...action.params,
|
||||
};
|
||||
const routes = [...state.routes];
|
||||
routes[state.routes.indexOf(lastRoute)] = {
|
||||
...lastRoute,
|
||||
params,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (activeTabIndex !== state.index) {
|
||||
return {
|
||||
...state,
|
||||
index: activeTabIndex,
|
||||
};
|
||||
} else if (didNavigate && !inputState) {
|
||||
return state;
|
||||
} else if (didNavigate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Let other tabs handle it and switch to the first tab that returns a new state
|
||||
let index = state.index;
|
||||
let routes = state.routes;
|
||||
order.find((tabId, i) => {
|
||||
const tabRouter = tabRouters[tabId];
|
||||
if (i === index) {
|
||||
return false;
|
||||
}
|
||||
let tabState = routes[i];
|
||||
if (tabRouter) {
|
||||
// console.log(`${order.join('-')}: Processing child router:`, {action, tabState});
|
||||
tabState = tabRouter.getStateForAction(action, tabState);
|
||||
}
|
||||
if (!tabState) {
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
if (tabState !== routes[i]) {
|
||||
routes = [...routes];
|
||||
routes[i] = tabState;
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// console.log(`${order.join('-')}: Processed other tabs:`, {lastIndex: state.index, index});
|
||||
|
||||
// Nested routers can be updated after switching tabs with actions such as SET_PARAMS
|
||||
// and COMPLETE_TRANSITION.
|
||||
// NOTE: This may be problematic with custom routers because we whitelist the actions
|
||||
// that can be handled by child routers without automatically changing index.
|
||||
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
|
||||
index = state.index;
|
||||
}
|
||||
|
||||
if (index !== state.index || routes !== state.routes) {
|
||||
return {
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
getComponentForState(state) {
|
||||
const routeName = state.routes[state.index].routeName;
|
||||
invariant(
|
||||
routeName,
|
||||
`There is no route defined for index ${state.index}. Check that
|
||||
that you passed in a navigation state with a valid tab/screen index.`
|
||||
);
|
||||
const childRouter = tabRouters[routeName];
|
||||
if (childRouter) {
|
||||
return childRouter.getComponentForState(state.routes[state.index]);
|
||||
}
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getComponentForRouteName(routeName) {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = order[state.index];
|
||||
const subPath = paths[routeName];
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an optional action, based on a relative path and query params.
|
||||
*
|
||||
* This will return null if there is no action matched
|
||||
*/
|
||||
getActionForPathAndParams(path, params) {
|
||||
return (
|
||||
order
|
||||
.map(tabId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[tabId];
|
||||
if (parts[0] === pathToTest) {
|
||||
const tabRouter = tabRouters[tabId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: tabId,
|
||||
});
|
||||
if (tabRouter && tabRouter.getActionForPathAndParams) {
|
||||
action.action = tabRouter.getActionForPathAndParams(
|
||||
parts.slice(1).join('/'),
|
||||
params
|
||||
);
|
||||
} else if (params) {
|
||||
action.params = params;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
order
|
||||
.map(tabId => {
|
||||
const tabRouter = tabRouters[tabId];
|
||||
return (
|
||||
tabRouter && tabRouter.getActionForPathAndParams(path, params)
|
||||
);
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
routeConfigs,
|
||||
config.navigationOptions
|
||||
),
|
||||
};
|
||||
const switchRouter = SwitchRouter(routeConfigs, config);
|
||||
return switchRouter;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import DrawerRouter from '../DrawerRouter';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
|
||||
@@ -18,7 +19,7 @@ describe('DrawerRouter', () => {
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
@@ -32,7 +33,7 @@ describe('DrawerRouter', () => {
|
||||
);
|
||||
const expectedState2 = {
|
||||
index: 1,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
@@ -44,6 +45,43 @@ describe('DrawerRouter', () => {
|
||||
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
|
||||
});
|
||||
|
||||
test('Handles initial route navigation', () => {
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const router = DrawerRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
Bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
},
|
||||
{ initialRouteName: 'Bar' }
|
||||
);
|
||||
const state = router.getStateForAction({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
});
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isDrawerOpen: false,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{
|
||||
key: 'Foo',
|
||||
params: undefined,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
{
|
||||
key: 'Bar',
|
||||
params: undefined,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Drawer opens closes and toggles', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
@@ -54,19 +92,85 @@ describe('DrawerRouter', () => {
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.OPEN_DRAWER },
|
||||
{ type: DrawerActions.OPEN_DRAWER },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.CLOSE_DRAWER },
|
||||
{ type: DrawerActions.CLOSE_DRAWER },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(false);
|
||||
const state4 = router.getStateForAction(
|
||||
{ type: NavigationActions.TOGGLE_DRAWER },
|
||||
{ type: DrawerActions.TOGGLE_DRAWER },
|
||||
state3
|
||||
);
|
||||
expect(state4.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
|
||||
test('Drawer opens closes with key targeted', () => {
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(false);
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state2
|
||||
);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Nested routers bubble up blocked actions', () => {
|
||||
const ScreenA = () => <div />;
|
||||
ScreenA.router = {
|
||||
getStateForAction(action, lastState) {
|
||||
if (action.type === 'CHILD_ACTION') return null;
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const ScreenB = () => <div />;
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
Bar: { screen: ScreenB },
|
||||
});
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
|
||||
const state2 = router.getStateForAction({ type: 'CHILD_ACTION' }, state);
|
||||
expect(state2).toEqual(null);
|
||||
});
|
||||
|
||||
test('Drawer stays open when child routers return new state', () => {
|
||||
const ScreenA = () => <div />;
|
||||
ScreenA.router = {
|
||||
getStateForAction(action, lastState = { changed: false }) {
|
||||
if (action.type === 'CHILD_ACTION')
|
||||
return { ...lastState, changed: true };
|
||||
return lastState;
|
||||
},
|
||||
};
|
||||
const router = DrawerRouter({
|
||||
Foo: { screen: ScreenA },
|
||||
});
|
||||
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
expect(state.isDrawerOpen).toEqual(false);
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
|
||||
state
|
||||
);
|
||||
expect(state2.isDrawerOpen).toEqual(true);
|
||||
|
||||
const state3 = router.getStateForAction({ type: 'CHILD_ACTION' }, state2);
|
||||
expect(state3.isDrawerOpen).toEqual(true);
|
||||
expect(state3.routes[0].changed).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* 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 addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -58,31 +58,31 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
];
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[0],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[1],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('BarTitle');
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
addNavigationHelpers({
|
||||
{
|
||||
state: routes[2],
|
||||
dispatch: () => false,
|
||||
addListener: dummyEventSubscriber,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).toEqual('Baz-123');
|
||||
@@ -90,6 +90,75 @@ Object.keys(ROUTERS).forEach(routerName => {
|
||||
});
|
||||
});
|
||||
|
||||
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 />;
|
||||
@@ -135,7 +204,7 @@ test('Handles deep action', () => {
|
||||
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
transitioningFromKey: false,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -158,6 +227,70 @@ test('Handles deep action', () => {
|
||||
expect(state2 && state2.routes[1].index).toEqual(1);
|
||||
});
|
||||
|
||||
test('Handles the navigate action with params', () => {
|
||||
const FooTabNavigator = () => <div />;
|
||||
FooTabNavigator.router = TabRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Boo: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const TestRouter = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: FooTabNavigator },
|
||||
});
|
||||
const state = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
immediate: true,
|
||||
routeName: 'Bar',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.routes[1].params).toEqual({ foo: '42' });
|
||||
expect(state2 && state2.routes[1].routes).toEqual([
|
||||
{
|
||||
key: 'Baz',
|
||||
routeName: 'Baz',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
{
|
||||
key: 'Boo',
|
||||
routeName: 'Boo',
|
||||
params: { foo: '42' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Handles the setParams action', () => {
|
||||
const FooTabNavigator = () => <div />;
|
||||
FooTabNavigator.router = TabRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
});
|
||||
const TestRouter = StackRouter({
|
||||
Foo: { screen: FooTabNavigator },
|
||||
Bar: { screen: () => <div /> },
|
||||
});
|
||||
const state = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
params: { name: 'foobar' },
|
||||
key: 'Baz',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.routes[0].routes).toEqual([
|
||||
{
|
||||
key: 'Baz',
|
||||
routeName: 'Baz',
|
||||
params: { name: 'foobar' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Supports lazily-evaluated getScreen', () => {
|
||||
const BarView = () => <div />;
|
||||
const FooTabNavigator = () => <div />;
|
||||
@@ -250,3 +383,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)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import StackRouter from '../StackRouter';
|
||||
import TabRouter from '../TabRouter';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
import StackActions from '../StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_normalize_keys();
|
||||
@@ -92,7 +91,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -103,7 +102,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 1,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -127,7 +126,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -138,7 +137,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 1,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -353,7 +352,7 @@ describe('StackRouter', () => {
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
expect(initState).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [{ key: 'id-0', routeName: 'foo' }],
|
||||
});
|
||||
@@ -366,7 +365,38 @@ describe('StackRouter', () => {
|
||||
expect(pushedState.routes[1].routes[1].routeName).toEqual('qux');
|
||||
});
|
||||
|
||||
test('pop does not bubble up', () => {
|
||||
test('push bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
Bad: { screen: () => <div /> },
|
||||
});
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.PUSH,
|
||||
routeName: 'Bad',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(2);
|
||||
expect(state3 && state3.routes.length).toEqual(3);
|
||||
});
|
||||
|
||||
test('pop bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
@@ -389,46 +419,14 @@ describe('StackRouter', () => {
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.POP,
|
||||
type: StackActions.POP,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.routes[1].index).toEqual(0);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('push does not bubble up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
Foo: { screen: () => <div /> },
|
||||
Bar: { screen: ChildNavigator },
|
||||
Bad: { screen: () => <div /> },
|
||||
});
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.PUSH,
|
||||
routeName: 'Bad',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('popToTop does not bubble up', () => {
|
||||
test('popToTop bubbles up', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
@@ -449,12 +447,11 @@ describe('StackRouter', () => {
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.POP_TO_TOP,
|
||||
type: StackActions.POP_TO_TOP,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.routes[1].index).toEqual(0);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('popToTop targets StackRouter by key if specified', () => {
|
||||
@@ -478,7 +475,7 @@ describe('StackRouter', () => {
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.POP_TO_TOP,
|
||||
type: StackActions.POP_TO_TOP,
|
||||
key: state2.key,
|
||||
},
|
||||
state2
|
||||
@@ -486,6 +483,44 @@ describe('StackRouter', () => {
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('pop action works as expected', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 3,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'A', routeName: 'foo' },
|
||||
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
|
||||
{ key: 'C', routeName: 'foo' },
|
||||
{ key: 'D', routeName: 'bar' },
|
||||
],
|
||||
};
|
||||
const poppedState = TestRouter.getStateForAction(StackActions.pop(), state);
|
||||
expect(poppedState.routes.length).toBe(3);
|
||||
expect(poppedState.index).toBe(2);
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
StackActions.pop({ n: 2, immediate: true }),
|
||||
state
|
||||
);
|
||||
expect(poppedState2.routes.length).toBe(2);
|
||||
expect(poppedState2.index).toBe(1);
|
||||
expect(poppedState2.isTransitioning).toBe(false);
|
||||
|
||||
const poppedState3 = TestRouter.getStateForAction(
|
||||
StackActions.pop({ n: 5 }),
|
||||
state
|
||||
);
|
||||
expect(poppedState3.routes.length).toBe(1);
|
||||
expect(poppedState3.index).toBe(0);
|
||||
expect(poppedState3.isTransitioning).toBe(true);
|
||||
});
|
||||
|
||||
test('popToTop works as expected', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
@@ -494,7 +529,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 2,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'A', routeName: 'foo' },
|
||||
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
|
||||
@@ -502,27 +537,79 @@ describe('StackRouter', () => {
|
||||
],
|
||||
};
|
||||
const poppedState = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop(),
|
||||
StackActions.popToTop(),
|
||||
state
|
||||
);
|
||||
expect(poppedState.routes.length).toBe(1);
|
||||
expect(poppedState.index).toBe(0);
|
||||
expect(poppedState.transitioningFromKey).toBe('C');
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop(),
|
||||
StackActions.popToTop(),
|
||||
poppedState
|
||||
);
|
||||
expect(poppedState).toEqual(poppedState2);
|
||||
const poppedImmediatelyState = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop({ immediate: true }),
|
||||
StackActions.popToTop({ immediate: true }),
|
||||
state
|
||||
);
|
||||
expect(poppedImmediatelyState.routes.length).toBe(1);
|
||||
expect(poppedImmediatelyState.index).toBe(0);
|
||||
expect(poppedImmediatelyState.transitioningFromKey).toBe(null);
|
||||
expect(poppedImmediatelyState.isTransitioning).toBe(false);
|
||||
});
|
||||
|
||||
test('Navigate Pushes duplicate routeName', () => {
|
||||
test('Navigate does not push duplicate routeName', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteName: 'foo' }
|
||||
);
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const barState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
initState
|
||||
);
|
||||
expect(barState.index).toEqual(1);
|
||||
expect(barState.routes[1].routeName).toEqual('bar');
|
||||
const navigateOnBarState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
barState
|
||||
);
|
||||
expect(navigateOnBarState).toEqual(null);
|
||||
});
|
||||
|
||||
test('Navigate focuses given routeName if already active in stack', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
baz: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteName: 'foo' }
|
||||
);
|
||||
const initialState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const fooBarState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
initialState
|
||||
);
|
||||
const fooBarBazState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'baz' }),
|
||||
fooBarState
|
||||
);
|
||||
expect(fooBarBazState.index).toEqual(2);
|
||||
expect(fooBarBazState.routes[2].routeName).toEqual('baz');
|
||||
|
||||
const fooState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo' }),
|
||||
fooBarBazState
|
||||
);
|
||||
expect(fooState.index).toEqual(0);
|
||||
expect(fooState.routes.length).toEqual(1);
|
||||
expect(fooState.routes[0].routeName).toEqual('foo');
|
||||
});
|
||||
|
||||
test('Navigate pushes duplicate routeName if unique key is provided', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
@@ -535,7 +622,7 @@ describe('StackRouter', () => {
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar' }),
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'new-unique-key!' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState.index).toEqual(2);
|
||||
@@ -589,28 +676,90 @@ describe('StackRouter', () => {
|
||||
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(0);
|
||||
expect(pushedState.routes[0].routeName).toEqual('foo');
|
||||
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' }),
|
||||
for (key of ['a', null]) {
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState).toEqual(null);
|
||||
}
|
||||
});
|
||||
|
||||
// https://github.com/react-navigation/react-navigation/issues/4063
|
||||
test('Navigate on inactive stackrouter is idempotent', () => {
|
||||
const FirstChildNavigator = () => <div />;
|
||||
FirstChildNavigator.router = StackRouter({
|
||||
First1: () => <div />,
|
||||
First2: () => <div />,
|
||||
});
|
||||
|
||||
const SecondChildNavigator = () => <div />;
|
||||
SecondChildNavigator.router = StackRouter({
|
||||
Second1: () => <div />,
|
||||
Second2: () => <div />,
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Leaf: () => <div />,
|
||||
First: FirstChildNavigator,
|
||||
Second: SecondChildNavigator,
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
|
||||
const first = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'First2' }),
|
||||
state
|
||||
);
|
||||
|
||||
const second = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'Second2' }),
|
||||
first
|
||||
);
|
||||
|
||||
const firstAgain = router.getStateForAction(
|
||||
NavigationActions.navigate({
|
||||
routeName: 'First2',
|
||||
params: { debug: true },
|
||||
}),
|
||||
second
|
||||
);
|
||||
|
||||
expect(first.routes.length).toEqual(2);
|
||||
expect(first.index).toEqual(1);
|
||||
expect(second.routes.length).toEqual(3);
|
||||
expect(second.index).toEqual(2);
|
||||
|
||||
expect(firstAgain.index).toEqual(1);
|
||||
expect(firstAgain.routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('Navigate to current routeName returns null to indicate handled action', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const navigatedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState.index).toEqual(1);
|
||||
expect(pushedTwiceState.routes[1].routeName).toEqual('bar');
|
||||
expect(navigatedState).toBe(null);
|
||||
});
|
||||
|
||||
test('Push behaves like navigate, except for key', () => {
|
||||
@@ -620,19 +769,39 @@ describe('StackRouter', () => {
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.push({ routeName: 'bar' }),
|
||||
StackActions.push({ routeName: 'bar' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
expect(() => {
|
||||
TestRouter.getStateForAction(
|
||||
{ type: NavigationActions.PUSH, routeName: 'bar', key: 'a' },
|
||||
{ type: StackActions.PUSH, routeName: 'bar', key: 'a' },
|
||||
pushedState
|
||||
);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test('Push adds new routes every time', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
StackActions.push({ routeName: 'bar' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const secondPushedState = TestRouter.getStateForAction(
|
||||
StackActions.push({ routeName: 'bar' }),
|
||||
pushedState
|
||||
);
|
||||
expect(secondPushedState.index).toEqual(2);
|
||||
expect(secondPushedState.routes[2].routeName).toEqual('bar');
|
||||
});
|
||||
|
||||
test('Navigate backwards with key removes leading routes', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
@@ -678,7 +847,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -706,7 +875,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -726,7 +895,7 @@ describe('StackRouter', () => {
|
||||
NavigationActions.navigate({ routeName: 'foo' })
|
||||
);
|
||||
const replacedState = TestRouter.getStateForAction(
|
||||
NavigationActions.replace({
|
||||
StackActions.replace({
|
||||
routeName: 'bar',
|
||||
params: { meaning: 42 },
|
||||
key: initState.routes[0].key,
|
||||
@@ -739,7 +908,7 @@ describe('StackRouter', () => {
|
||||
expect(replacedState.routes[0].routeName).toEqual('bar');
|
||||
expect(replacedState.routes[0].params.meaning).toEqual(42);
|
||||
const replacedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.replace({
|
||||
StackActions.replace({
|
||||
routeName: 'bar',
|
||||
key: initState.routes[0].key,
|
||||
newKey: 'wow',
|
||||
@@ -773,15 +942,15 @@ describe('StackRouter', () => {
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(1);
|
||||
expect(state2 && state2.transitioningFromKey).toEqual(state.routes[0].key);
|
||||
expect(state2 && state2.isTransitioning).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.COMPLETE_TRANSITION,
|
||||
type: StackActions.COMPLETE_TRANSITION,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.transitioningFromKey).toEqual(null);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
});
|
||||
|
||||
test('Handle basic stack logic for components with router', () => {
|
||||
@@ -803,7 +972,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -831,7 +1000,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -905,7 +1074,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -929,7 +1098,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -998,15 +1167,51 @@ describe('StackRouter', () => {
|
||||
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
|
||||
});
|
||||
|
||||
test('Handles the SetParams action for inactive routes', () => {
|
||||
const router = StackRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Bar',
|
||||
initialRouteParams: { name: 'Zoo' },
|
||||
}
|
||||
);
|
||||
const initialState = {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
key: 'RouteA',
|
||||
routeName: 'Foo',
|
||||
params: { name: 'InitialParam', other: 'Unchanged' },
|
||||
},
|
||||
{ key: 'RouteB', routeName: 'Bar', params: {} },
|
||||
],
|
||||
};
|
||||
const state = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
params: { name: 'NewParam' },
|
||||
key: 'RouteA',
|
||||
},
|
||||
initialState
|
||||
);
|
||||
expect(state.index).toEqual(1);
|
||||
expect(state.routes[0].params).toEqual({
|
||||
name: 'NewParam',
|
||||
other: 'Unchanged',
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles the setParams action with nested routers', () => {
|
||||
const ChildNavigator = () => <div />;
|
||||
const GrandChildNavigator = () => <div />;
|
||||
GrandChildNavigator.router = StackRouter({
|
||||
Quux: { screen: () => <div /> },
|
||||
Corge: { screen: () => <div /> },
|
||||
});
|
||||
ChildNavigator.router = TabRouter({
|
||||
Baz: { screen: GrandChildNavigator },
|
||||
ChildNavigator.router = StackRouter({
|
||||
Baz: { screen: () => <div /> },
|
||||
Qux: { screen: () => <div /> },
|
||||
});
|
||||
const router = StackRouter({
|
||||
@@ -1023,10 +1228,10 @@ describe('StackRouter', () => {
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
|
||||
expect(state2 && state2.routes[0].routes).toEqual([
|
||||
{
|
||||
key: 'id-0',
|
||||
routeName: 'Quux',
|
||||
routeName: 'Baz',
|
||||
params: { name: 'foobar' },
|
||||
},
|
||||
]);
|
||||
@@ -1044,7 +1249,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1079,7 +1284,7 @@ describe('StackRouter', () => {
|
||||
});
|
||||
const state1 = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const resetAction = {
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: 'Bad Key',
|
||||
actions: [
|
||||
{
|
||||
@@ -1112,7 +1317,7 @@ describe('StackRouter', () => {
|
||||
});
|
||||
|
||||
test('Handles the reset action with nested Router', () => {
|
||||
const ChildRouter = TabRouter({
|
||||
const ChildRouter = StackRouter({
|
||||
baz: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
@@ -1132,7 +1337,8 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: null,
|
||||
actions: [
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
@@ -1184,7 +1390,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: 'Init',
|
||||
actions: [
|
||||
{
|
||||
@@ -1199,7 +1405,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.RESET,
|
||||
type: StackActions.RESET,
|
||||
key: null,
|
||||
actions: [
|
||||
{
|
||||
@@ -1274,19 +1480,19 @@ describe('StackRouter', () => {
|
||||
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-2',
|
||||
params: { code: 'test', foo: 'bar' },
|
||||
routeName: 'main',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-1',
|
||||
params: { code: 'test', foo: 'bar', id: '4' },
|
||||
routeName: 'profile',
|
||||
@@ -1333,19 +1539,19 @@ describe('StackRouter', () => {
|
||||
|
||||
expect(state2).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-5',
|
||||
params: { code: '', foo: 'bar' },
|
||||
routeName: 'main',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-4',
|
||||
params: { code: '', foo: 'bar', id: '4' },
|
||||
routeName: 'profile',
|
||||
@@ -1364,42 +1570,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(
|
||||
{
|
||||
@@ -1448,7 +1618,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
@@ -1664,21 +1834,136 @@ test('Handles deep navigate completion action', () => {
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(0);
|
||||
expect(state2.transitioningFromKey).toEqual(null);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].transitioningFromKey).toEqual(
|
||||
state.routes[0].routes[state.routes[0].index].key
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.isTransitioning).toEqual(false);
|
||||
expect(state2 && state2.routes[0].index).toEqual(1);
|
||||
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
|
||||
expect(!!key).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.COMPLETE_TRANSITION,
|
||||
type: StackActions.COMPLETE_TRANSITION,
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3.index).toEqual(0);
|
||||
expect(state3.transitioningFromKey).toEqual(null);
|
||||
expect(state3.routes[0].index).toEqual(1);
|
||||
expect(state3.routes[0].transitioningFromKey).toEqual(null);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
expect(state3 && state3.routes[0].index).toEqual(1);
|
||||
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
let nestedRouter = StackRouter({
|
||||
Foo: Screen,
|
||||
Bar: Screen,
|
||||
});
|
||||
|
||||
NestedStack.router = nestedRouter;
|
||||
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
Bar: Screen,
|
||||
Baz: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Baz',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('Baz');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3.routes[state3.index].routeName).toEqual('Baz');
|
||||
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Foo',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo');
|
||||
|
||||
const state5 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state4
|
||||
);
|
||||
let activeState5 = state5.routes[state5.index];
|
||||
expect(activeState5.routeName).toEqual('NestedStack');
|
||||
expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const NestedStack = () => <div />;
|
||||
const OtherNestedStack = () => <div />;
|
||||
|
||||
let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen });
|
||||
let otherNestedRouter = StackRouter({ Foo: Screen });
|
||||
NestedStack.router = nestedRouter;
|
||||
OtherNestedStack.router = otherNestedRouter;
|
||||
|
||||
let router = StackRouter(
|
||||
{
|
||||
NestedStack,
|
||||
OtherNestedStack,
|
||||
Bar: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'OtherNestedStack',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedStack');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'NestedStack',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedStack');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
252
src/routers/__tests__/SwitchRouter-test.js
Normal file
252
src/routers/__tests__/SwitchRouter-test.js
Normal file
@@ -0,0 +1,252 @@
|
||||
/* eslint react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
import SwitchRouter from '../SwitchRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
describe('SwitchRouter', () => {
|
||||
test('resets the route when unfocusing a tab by default', () => {
|
||||
const router = getExampleRouter();
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
|
||||
state
|
||||
);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].routes.length).toEqual(2);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.routes[0].index).toEqual(0);
|
||||
expect(state3.routes[0].routes.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('does not reset the route on unfocus if resetOnBlur is false', () => {
|
||||
const router = getExampleRouter({ resetOnBlur: false });
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
|
||||
state
|
||||
);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].routes.length).toEqual(2);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.routes[0].index).toEqual(1);
|
||||
expect(state3.routes[0].routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('ignores back by default', () => {
|
||||
const router = getExampleRouter();
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.index).toEqual(1);
|
||||
});
|
||||
|
||||
test('handles back if given a backBehavior', () => {
|
||||
const router = getExampleRouter({ backBehavior: 'initialRoute' });
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.index).toEqual(0);
|
||||
});
|
||||
|
||||
test('paths option on SwitchRouter overrides path from route config', () => {
|
||||
const router = getExampleRouter({ paths: { A: 'overridden' } });
|
||||
const action = router.getActionForPathAndParams('overridden', {});
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('A');
|
||||
});
|
||||
|
||||
test('provides correct action for getActionForPathAndParams', () => {
|
||||
const router = getExampleRouter({ backBehavior: 'initialRoute' });
|
||||
const action = router.getActionForPathAndParams('A1', { foo: 'bar' });
|
||||
expect(action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.routeName).toEqual('A1');
|
||||
|
||||
const action1 = router.getActionForPathAndParams('', {});
|
||||
expect(action1.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action1.routeName).toEqual('A');
|
||||
|
||||
const action2 = router.getActionForPathAndParams(null, {});
|
||||
expect(action2.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action2.routeName).toEqual('A');
|
||||
|
||||
const action3 = router.getActionForPathAndParams('great/path', {
|
||||
foo: 'baz',
|
||||
});
|
||||
expect(action3).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B',
|
||||
params: { foo: 'baz' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B1',
|
||||
params: { foo: 'baz' },
|
||||
},
|
||||
});
|
||||
|
||||
const action4 = router.getActionForPathAndParams('great/path/B2', {
|
||||
foo: 'baz',
|
||||
});
|
||||
expect(action4).toEqual({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B',
|
||||
params: { foo: 'baz' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'B2',
|
||||
params: { foo: 'baz' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('order of handling navigate action is correct for nested switchrouters', () => {
|
||||
// router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
|
||||
// if we are focused on Other and navigate to Bar, what should happen?
|
||||
|
||||
const Screen = () => <div />;
|
||||
const NestedSwitch = () => <div />;
|
||||
const OtherNestedSwitch = () => <div />;
|
||||
|
||||
let nestedRouter = SwitchRouter({ Foo: Screen, Bar: Screen });
|
||||
let otherNestedRouter = SwitchRouter({ Foo: Screen });
|
||||
NestedSwitch.router = nestedRouter;
|
||||
OtherNestedSwitch.router = otherNestedRouter;
|
||||
|
||||
let router = SwitchRouter(
|
||||
{
|
||||
NestedSwitch,
|
||||
OtherNestedSwitch,
|
||||
Bar: Screen,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'OtherNestedSwitch',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('OtherNestedSwitch');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Bar');
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'NestedSwitch',
|
||||
},
|
||||
state2
|
||||
);
|
||||
const state4 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
},
|
||||
state3
|
||||
);
|
||||
let activeState4 = state4.routes[state4.index];
|
||||
expect(activeState4.routeName).toEqual('NestedSwitch');
|
||||
expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar');
|
||||
});
|
||||
|
||||
// https://github.com/react-navigation/react-navigation.github.io/issues/117#issuecomment-385597628
|
||||
test('order of handling navigate action is correct for nested stackrouters', () => {
|
||||
const Screen = () => <div />;
|
||||
const MainStack = () => <div />;
|
||||
const LoginStack = () => <div />;
|
||||
MainStack.router = StackRouter({ Home: Screen, Profile: Screen });
|
||||
LoginStack.router = StackRouter({ Form: Screen, ForgotPassword: Screen });
|
||||
|
||||
let router = SwitchRouter(
|
||||
{
|
||||
Home: Screen,
|
||||
Login: LoginStack,
|
||||
Main: MainStack,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Login',
|
||||
}
|
||||
);
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state.routes[state.index].routeName).toEqual('Login');
|
||||
|
||||
const state2 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Home',
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2.routes[state2.index].routeName).toEqual('Home');
|
||||
});
|
||||
});
|
||||
|
||||
const getExampleRouter = (config = {}) => {
|
||||
const PlainScreen = () => <div />;
|
||||
const StackA = () => <div />;
|
||||
const StackB = () => <div />;
|
||||
|
||||
StackA.router = StackRouter({
|
||||
A1: PlainScreen,
|
||||
A2: PlainScreen,
|
||||
});
|
||||
|
||||
StackB.router = StackRouter({
|
||||
B1: PlainScreen,
|
||||
B2: PlainScreen,
|
||||
});
|
||||
|
||||
const router = SwitchRouter(
|
||||
{
|
||||
A: {
|
||||
screen: StackA,
|
||||
path: '',
|
||||
},
|
||||
B: {
|
||||
screen: StackB,
|
||||
path: 'great/path',
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'A',
|
||||
...config,
|
||||
}
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import TabRouter from '../TabRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
@@ -140,6 +140,46 @@ describe('TabRouter', () => {
|
||||
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
|
||||
});
|
||||
|
||||
test('Handles the SetParams action for inactive routes', () => {
|
||||
const router = TabRouter(
|
||||
{
|
||||
Foo: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
Bar: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'Bar',
|
||||
}
|
||||
);
|
||||
const initialState = {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
key: 'RouteA',
|
||||
routeName: 'Foo',
|
||||
params: { name: 'InitialParam', other: 'Unchanged' },
|
||||
},
|
||||
{ key: 'RouteB', routeName: 'Bar', params: {} },
|
||||
],
|
||||
};
|
||||
const state = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.SET_PARAMS,
|
||||
params: { name: 'NewParam' },
|
||||
key: 'RouteA',
|
||||
},
|
||||
initialState
|
||||
);
|
||||
expect(state.index).toEqual(1);
|
||||
expect(state.routes[0].params).toEqual({
|
||||
name: 'NewParam',
|
||||
other: 'Unchanged',
|
||||
});
|
||||
});
|
||||
|
||||
test('getStateForAction returns null when navigating to same tab', () => {
|
||||
const router = TabRouter(
|
||||
{ Foo: BareLeafRouteConfig, Bar: BareLeafRouteConfig },
|
||||
@@ -181,6 +221,7 @@ describe('TabRouter', () => {
|
||||
const navAction = {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Baz',
|
||||
params: { foo: '42' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
@@ -351,7 +392,7 @@ describe('TabRouter', () => {
|
||||
});
|
||||
const MidNavigator = () => <div />;
|
||||
MidNavigator.router = TabRouter({
|
||||
Foo: { screen: ChildNavigator0 },
|
||||
Fee: { screen: ChildNavigator0 },
|
||||
Bar: { screen: ChildNavigator1 },
|
||||
});
|
||||
const router = TabRouter({
|
||||
@@ -371,8 +412,8 @@ describe('TabRouter', () => {
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
key: 'Fee',
|
||||
routeName: 'Fee',
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
@@ -410,8 +451,8 @@ describe('TabRouter', () => {
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
key: 'Fee',
|
||||
routeName: 'Fee',
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
@@ -444,7 +485,10 @@ describe('TabRouter', () => {
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Bar',
|
||||
action: { type: NavigationActions.NAVIGATE, routeName: 'Zap' },
|
||||
action: {
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'Zap',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(state4).toEqual({
|
||||
@@ -459,8 +503,8 @@ describe('TabRouter', () => {
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
key: 'Foo',
|
||||
routeName: 'Foo',
|
||||
key: 'Fee',
|
||||
routeName: 'Fee',
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'Boo', routeName: 'Boo' },
|
||||
@@ -690,56 +734,15 @@ describe('TabRouter', () => {
|
||||
expect(state2).toEqual(state0);
|
||||
});
|
||||
|
||||
test('pop action works as expected', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
|
||||
const state = {
|
||||
index: 3,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'A', routeName: 'foo' },
|
||||
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
|
||||
{ key: 'C', routeName: 'foo' },
|
||||
{ key: 'D', routeName: 'bar' },
|
||||
],
|
||||
};
|
||||
const poppedState = TestRouter.getStateForAction(
|
||||
NavigationActions.pop(),
|
||||
state
|
||||
);
|
||||
expect(poppedState.routes.length).toBe(3);
|
||||
expect(poppedState.index).toBe(2);
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.pop({ n: 2, immediate: true }),
|
||||
state
|
||||
);
|
||||
expect(poppedState2.routes.length).toBe(2);
|
||||
expect(poppedState2.index).toBe(1);
|
||||
expect(poppedState2.isTransitioning).toBe(false);
|
||||
|
||||
const poppedState3 = TestRouter.getStateForAction(
|
||||
NavigationActions.pop({ n: 5 }),
|
||||
state
|
||||
);
|
||||
expect(poppedState3.routes.length).toBe(1);
|
||||
expect(poppedState3.index).toBe(0);
|
||||
expect(poppedState3.isTransitioning).toBe(true);
|
||||
});
|
||||
|
||||
test('Inner actions are only unpacked if the current tab matches', () => {
|
||||
const PlainScreen = () => <div />;
|
||||
const ScreenA = () => <div />;
|
||||
const ScreenB = () => <div />;
|
||||
ScreenB.router = StackRouter({
|
||||
ScreenB.router = TabRouter({
|
||||
Baz: { screen: PlainScreen },
|
||||
Zoo: { screen: PlainScreen },
|
||||
});
|
||||
ScreenA.router = StackRouter({
|
||||
ScreenA.router = TabRouter({
|
||||
Bar: { screen: PlainScreen },
|
||||
Boo: { screen: ScreenB },
|
||||
});
|
||||
@@ -748,10 +751,10 @@ describe('TabRouter', () => {
|
||||
});
|
||||
const screenApreState = {
|
||||
index: 0,
|
||||
key: 'Init',
|
||||
key: 'Foo',
|
||||
isTransitioning: false,
|
||||
routeName: 'Foo',
|
||||
routes: [{ key: 'Init', routeName: 'Bar' }],
|
||||
routes: [{ key: 'Bar', routeName: 'Bar' }],
|
||||
};
|
||||
const preState = {
|
||||
index: 0,
|
||||
@@ -777,7 +780,6 @@ describe('TabRouter', () => {
|
||||
routeName: 'Boo',
|
||||
action: NavigationActions.navigate({ routeName: 'Zoo' }),
|
||||
});
|
||||
|
||||
const expectedState = ScreenA.router.getStateForAction(
|
||||
action,
|
||||
screenApreState
|
||||
@@ -785,8 +787,25 @@ describe('TabRouter', () => {
|
||||
const state = router.getStateForAction(action, preState);
|
||||
const innerState = state ? state.routes[0] : state;
|
||||
|
||||
expect(innerState.routes[1].index).toEqual(1);
|
||||
expect(expectedState && comparable(expectedState)).toEqual(
|
||||
innerState && comparable(innerState)
|
||||
);
|
||||
|
||||
const noMatchAction = NavigationActions.navigate({
|
||||
routeName: 'Qux',
|
||||
action: NavigationActions.navigate({ routeName: 'Zoo' }),
|
||||
});
|
||||
const expectedState2 = ScreenA.router.getStateForAction(
|
||||
noMatchAction,
|
||||
screenApreState
|
||||
);
|
||||
const state2 = router.getStateForAction(noMatchAction, preState);
|
||||
const innerState2 = state2 ? state2.routes[0] : state2;
|
||||
|
||||
expect(innerState2.routes[1].index).toEqual(0);
|
||||
expect(expectedState2 && comparable(expectedState2)).toEqual(
|
||||
innerState2 && comparable(innerState2)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -38,7 +38,8 @@ export default (routeConfigs, navigatorScreenConfig) => (
|
||||
|
||||
const routeConfig = routeConfigs[route.routeName];
|
||||
|
||||
const routeScreenConfig = routeConfig.navigationOptions;
|
||||
const routeScreenConfig =
|
||||
routeConfig === Component ? null : routeConfig.navigationOptions;
|
||||
const componentScreenConfig = Component.navigationOptions;
|
||||
|
||||
const configOptions = { navigation, screenProps: screenProps || {} };
|
||||
|
||||
46
src/routers/getNavigationActionCreators.js
Normal file
46
src/routers/getNavigationActionCreators.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
const getNavigationActionCreators = route => {
|
||||
return {
|
||||
goBack: key => {
|
||||
let actualizedKey = key;
|
||||
if (key === undefined && route.key) {
|
||||
invariant(typeof route.key === 'string', 'key should be a string');
|
||||
actualizedKey = route.key;
|
||||
}
|
||||
return NavigationActions.back({ key: actualizedKey });
|
||||
},
|
||||
navigate: (navigateTo, params, action) => {
|
||||
if (typeof navigateTo === 'string') {
|
||||
return NavigationActions.navigate({
|
||||
routeName: navigateTo,
|
||||
params,
|
||||
action,
|
||||
});
|
||||
}
|
||||
invariant(
|
||||
typeof navigateTo === 'object',
|
||||
'Must navigateTo an object or a string'
|
||||
);
|
||||
invariant(
|
||||
params == null,
|
||||
'Params must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
invariant(
|
||||
action == null,
|
||||
'Child action must not be provided to .navigate() when specifying an object'
|
||||
);
|
||||
return NavigationActions.navigate(navigateTo);
|
||||
},
|
||||
setParams: params => {
|
||||
invariant(
|
||||
route.key && typeof route.key === 'string',
|
||||
'setParams cannot be called by root navigator'
|
||||
);
|
||||
return NavigationActions.setParams({ params, key: route.key });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getNavigationActionCreators;
|
||||
3
src/utils/docsUrl.js
Normal file
3
src/utils/docsUrl.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function docsUrl(path) {
|
||||
return `https://v2.reactnavigation.org/docs/${path}`;
|
||||
}
|
||||
8
src/utils/withDefaultValue.js
Normal file
8
src/utils/withDefaultValue.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default (obj, key, defaultValue) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj[key] = defaultValue;
|
||||
return obj;
|
||||
};
|
||||
@@ -1,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,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
/**
|
||||
@@ -47,9 +47,10 @@ class DrawerSidebar extends React.PureComponent {
|
||||
_onItemPress = ({ route, focused }) => {
|
||||
if (!focused) {
|
||||
let subAction;
|
||||
// TODO (v3): Revisit and repeal this behavior:
|
||||
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
|
||||
if (route.index !== undefined && route.index !== 0) {
|
||||
subAction = NavigationActions.reset({
|
||||
if (route.index != null && route.index !== 0) {
|
||||
subAction = StackActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({
|
||||
@@ -58,7 +59,12 @@ class DrawerSidebar extends React.PureComponent {
|
||||
],
|
||||
});
|
||||
}
|
||||
this.props.navigation.navigate(route.routeName, undefined, subAction);
|
||||
this.props.navigation.dispatch(
|
||||
NavigationActions.navigate({
|
||||
routeName: route.routeName,
|
||||
action: subAction,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import DrawerLayout from 'react-native-drawer-layout-polyfill';
|
||||
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import DrawerSidebar from './DrawerSidebar';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import DrawerActions from '../../routers/DrawerActions';
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
@@ -17,7 +17,7 @@ export default class DrawerView extends React.PureComponent {
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ export default class DrawerView extends React.PureComponent {
|
||||
Dimensions.removeEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { isDrawerOpen } = nextProps.navigation.state;
|
||||
const wasDrawerOpen = this.props.navigation.state.isDrawerOpen;
|
||||
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) {
|
||||
@@ -37,12 +38,18 @@ export default class DrawerView extends React.PureComponent {
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.dispatch({ type: NavigationActions.OPEN_DRAWER });
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (!isDrawerOpen) {
|
||||
navigation.dispatch({ type: DrawerActions.OPEN_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.dispatch({ type: NavigationActions.CLOSE_DRAWER });
|
||||
const { isDrawerOpen } = navigation.state;
|
||||
if (isDrawerOpen) {
|
||||
navigation.dispatch({ type: DrawerActions.CLOSE_DRAWER });
|
||||
}
|
||||
};
|
||||
|
||||
_updateWidth = () => {
|
||||
@@ -86,6 +93,7 @@ export default class DrawerView extends React.PureComponent {
|
||||
this._drawer = c;
|
||||
}}
|
||||
drawerLockMode={
|
||||
drawerLockMode ||
|
||||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
|
||||
this.props.navigationConfig.drawerLockMode
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
I18nManager,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
||||
@@ -24,12 +25,15 @@ const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
return Platform.OS === 'ios'
|
||||
? isLandscape && !Platform.isPad ? 32 : 44
|
||||
? isLandscape && !Platform.isPad
|
||||
? 32
|
||||
: 44
|
||||
: 56;
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
layoutInterpolator: HeaderStyleInterpolator.forLayout,
|
||||
leftInterpolator: HeaderStyleInterpolator.forLeft,
|
||||
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
|
||||
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
|
||||
@@ -143,7 +147,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);
|
||||
props.scene.descriptor.navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -151,7 +155,7 @@ class Header extends React.PureComponent {
|
||||
onPress={goBack}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
@@ -165,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
|
||||
@@ -174,14 +178,21 @@ class Header extends React.PureComponent {
|
||||
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
|
||||
: undefined;
|
||||
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModularHeaderBackButton
|
||||
onPress={this._navigateBack}
|
||||
onPress={goBack}
|
||||
ButtonContainerComponent={ButtonContainerComponent}
|
||||
LabelContainerComponent={LabelContainerComponent}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
@@ -363,6 +374,10 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
|
||||
_renderHeader(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
if (options.header === null) {
|
||||
return null;
|
||||
}
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props, {
|
||||
@@ -371,7 +386,6 @@ class Header extends React.PureComponent {
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
const wrapperProps = {
|
||||
style: styles.header,
|
||||
@@ -477,10 +491,14 @@ class Header extends React.PureComponent {
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
|
||||
<View style={{ flex: 1 }}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
<Animated.View style={this.props.layoutInterpolator(this.props)}>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
{options.headerBackground}
|
||||
</View>
|
||||
<View style={styles.flexOne}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -548,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,
|
||||
@@ -575,6 +594,9 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flexOne: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrientation(Header);
|
||||
|
||||
@@ -1,594 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Image,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import ModularHeaderBackButton from './ModularHeaderBackButton';
|
||||
import HeaderStyleInterpolator from './HeaderStyleInterpolator2';
|
||||
import withOrientation from '../withOrientation';
|
||||
import { last } from 'rxjs/operators';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
return Platform.OS === 'ios'
|
||||
? isLandscape && !Platform.isPad ? 32 : 44
|
||||
: 56;
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
leftInterpolator: HeaderStyleInterpolator.forLeft,
|
||||
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
|
||||
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
|
||||
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
|
||||
titleInterpolator: HeaderStyleInterpolator.forCenter,
|
||||
rightInterpolator: HeaderStyleInterpolator.forRight,
|
||||
};
|
||||
|
||||
static get HEIGHT() {
|
||||
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
|
||||
}
|
||||
|
||||
state = {
|
||||
widths: {},
|
||||
};
|
||||
|
||||
_getHeaderTitleString({ options }) {
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
}
|
||||
return options.title;
|
||||
}
|
||||
|
||||
_getLastSceneDescriptor(descriptor) {
|
||||
const { state } = this.props.navigation;
|
||||
const index = state.routes.findIndex(r => r.key === descriptor.key);
|
||||
if (index < 1) {
|
||||
return null;
|
||||
}
|
||||
const lastKey = state.routes[index - 1].key;
|
||||
return this.props.descriptors[lastKey];
|
||||
}
|
||||
|
||||
_getBackButtonTitleString(descriptor) {
|
||||
const lastSceneDescriptor = this._getLastSceneDescriptor(descriptor);
|
||||
if (!lastSceneDescriptor) {
|
||||
return null;
|
||||
}
|
||||
const { headerBackTitle } = lastSceneDescriptor.options;
|
||||
if (headerBackTitle || headerBackTitle === null) {
|
||||
return headerBackTitle;
|
||||
}
|
||||
return this._getHeaderTitleString(lastSceneDescriptor);
|
||||
}
|
||||
|
||||
_getTruncatedBackButtonTitle(descriptor) {
|
||||
const lastSceneDescriptor = this._getLastSceneDescriptor(descriptor);
|
||||
if (!lastSceneDescriptor) {
|
||||
return null;
|
||||
}
|
||||
return lastSceneDescriptor.options.headerTruncatedBackTitle;
|
||||
}
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
const { options } = props.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
return headerTitle;
|
||||
}
|
||||
const titleString = this._getHeaderTitleString(props.descriptor);
|
||||
|
||||
const titleStyle = options.headerTitleStyle;
|
||||
const color = options.headerTintColor;
|
||||
const allowFontScaling = options.headerTitleAllowFontScaling;
|
||||
|
||||
// On iOS, width of left/right components depends on the calculated
|
||||
// size of the title.
|
||||
const onLayoutIOS =
|
||||
Platform.OS === 'ios'
|
||||
? e => {
|
||||
this.setState({
|
||||
widths: {
|
||||
...this.state.widths,
|
||||
[props.descriptor.key]: e.nativeEvent.layout.width,
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const RenderedHeaderTitle =
|
||||
headerTitle && typeof headerTitle !== 'string'
|
||||
? headerTitle
|
||||
: HeaderTitle;
|
||||
return (
|
||||
<RenderedHeaderTitle
|
||||
onLayout={onLayoutIOS}
|
||||
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
|
||||
style={[color ? { color } : null, titleStyle]}
|
||||
>
|
||||
{titleString}
|
||||
</RenderedHeaderTitle>
|
||||
);
|
||||
};
|
||||
|
||||
_renderLeftComponent = props => {
|
||||
const { options } = props.descriptor;
|
||||
if (
|
||||
React.isValidElement(options.headerLeft) ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return options.headerLeft;
|
||||
}
|
||||
|
||||
if (props.index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.descriptor);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.descriptor
|
||||
);
|
||||
const width = this.state.widths[props.descriptor.key]
|
||||
? (this.props.layout.initWidth -
|
||||
this.state.widths[props.descriptor.key]) /
|
||||
2
|
||||
: undefined;
|
||||
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
this.props.navigation.goBack(props.descriptor.key);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<RenderedLeftComponent
|
||||
onPress={goBack}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderModularLeftComponent = (
|
||||
props,
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
) => {
|
||||
const { options } = props.descriptor;
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.descriptor);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.descriptor
|
||||
);
|
||||
const width = this.state.widths[props.descriptor.key]
|
||||
? (this.props.layout.initWidth -
|
||||
this.state.widths[props.descriptor.key]) /
|
||||
2
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ModularHeaderBackButton
|
||||
onPress={this._navigateBack}
|
||||
ButtonContainerComponent={ButtonContainerComponent}
|
||||
LabelContainerComponent={LabelContainerComponent}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderRightComponent = props => {
|
||||
const { headerRight } = props.descriptor.options;
|
||||
return headerRight || null;
|
||||
};
|
||||
|
||||
_renderLeft(props) {
|
||||
const { options } = props.descriptor;
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
// On Android, or if we have a custom header left, or if we have a custom back image, we
|
||||
// do not use the modular header (which is the one that imitates UINavigationController)
|
||||
if (
|
||||
transitionPreset !== 'uikit' ||
|
||||
options.headerBackImage ||
|
||||
options.headerLeft ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'left',
|
||||
this._renderLeftComponent,
|
||||
this.props.leftInterpolator
|
||||
);
|
||||
} else {
|
||||
return this._renderModularSubView(
|
||||
props,
|
||||
'left',
|
||||
this._renderModularLeftComponent,
|
||||
this.props.leftLabelInterpolator,
|
||||
this.props.leftButtonInterpolator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_renderTitle(props, options) {
|
||||
const style = {};
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (!options.hasLeftComponent) {
|
||||
style.left = 0;
|
||||
}
|
||||
if (!options.hasRightComponent) {
|
||||
style.right = 0;
|
||||
}
|
||||
} else if (
|
||||
Platform.OS === 'ios' &&
|
||||
!options.hasLeftComponent &&
|
||||
!options.hasRightComponent
|
||||
) {
|
||||
style.left = 0;
|
||||
style.right = 0;
|
||||
}
|
||||
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
'title',
|
||||
this._renderTitleComponent,
|
||||
transitionPreset === 'uikit'
|
||||
? this.props.titleFromLeftInterpolator
|
||||
: this.props.titleInterpolator
|
||||
);
|
||||
}
|
||||
|
||||
_renderRight(props) {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'right',
|
||||
this._renderRightComponent,
|
||||
this.props.rightInterpolator
|
||||
);
|
||||
}
|
||||
|
||||
_renderModularSubView(
|
||||
props,
|
||||
name,
|
||||
renderer,
|
||||
labelStyleInterpolator,
|
||||
buttonStyleInterpolator
|
||||
) {
|
||||
const { descriptor, index, navigation } = props;
|
||||
const { key } = descriptor;
|
||||
|
||||
// Never render a modular back button on the first screen in a stack.
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = navigation.state.index - index;
|
||||
|
||||
if (Math.abs(offset) > 2) {
|
||||
// Scene is far away from the active scene. Hides it to avoid unnecessary
|
||||
// rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
const ButtonContainer = ({ children }) => (
|
||||
<Animated.View style={[buttonStyleInterpolator(props)]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const LabelContainer = ({ children }) => (
|
||||
<Animated.View style={[labelStyleInterpolator(props)]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const subView = renderer(props, ButtonContainer, LabelContainer);
|
||||
|
||||
if (subView === null) {
|
||||
return subView;
|
||||
}
|
||||
const isTransitioning = !!navigation.state.transitioningFromKey;
|
||||
|
||||
const pointerEvents = offset !== 0 || isTransitioning ? 'none' : 'box-none';
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`${name}_${key}`}
|
||||
pointerEvents={pointerEvents}
|
||||
style={[styles.item, styles[name], props.style]}
|
||||
>
|
||||
{subView}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSubView(props, name, renderer, styleInterpolator) {
|
||||
const { descriptor, index, navigation } = props;
|
||||
const { key } = descriptor;
|
||||
|
||||
const offset = navigation.state.index - index;
|
||||
|
||||
if (Math.abs(offset) > 2) {
|
||||
// Scene is far away from the active scene. Hides it to avoid unnecessary
|
||||
// rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
const subView = renderer(props);
|
||||
|
||||
if (subView == null) {
|
||||
return null;
|
||||
}
|
||||
const isTransitioning = !!navigation.state.transitioningFromKey;
|
||||
|
||||
const pointerEvents = offset !== 0 || isTransitioning ? 'none' : 'box-none';
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={pointerEvents}
|
||||
key={`${name}_${key}`}
|
||||
style={[
|
||||
styles.item,
|
||||
styles[name],
|
||||
props.style,
|
||||
styleInterpolator({
|
||||
// todo: determine if we really need to splat all this.props
|
||||
...this.props,
|
||||
...props,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{subView}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderHeader(props) {
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props, {
|
||||
hasLeftComponent: !!left,
|
||||
hasRightComponent: !!right,
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
const { options } = props.descriptor;
|
||||
|
||||
const wrapperProps = {
|
||||
style: styles.header,
|
||||
key: `header_${props.descriptor.key}`,
|
||||
};
|
||||
|
||||
if (
|
||||
options.headerLeft ||
|
||||
options.headerBackImage ||
|
||||
Platform.OS !== 'ios' ||
|
||||
transitionPreset !== 'uikit'
|
||||
) {
|
||||
return (
|
||||
<View {...wrapperProps}>
|
||||
{title}
|
||||
{left}
|
||||
{right}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MaskedViewIOS
|
||||
{...wrapperProps}
|
||||
maskElement={
|
||||
<View style={styles.iconMaskContainer}>
|
||||
<Image
|
||||
source={require('../assets/back-icon-mask.png')}
|
||||
style={styles.iconMask}
|
||||
/>
|
||||
<View style={styles.iconMaskFillerRect} />
|
||||
</View>
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{left}
|
||||
{right}
|
||||
</MaskedViewIOS>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let appBar;
|
||||
const {
|
||||
mode,
|
||||
isLandscape,
|
||||
navigation,
|
||||
descriptors,
|
||||
descriptor,
|
||||
transition,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
if (mode === 'float') {
|
||||
const scenesDescriptorsByIndex = [];
|
||||
const { state } = navigation;
|
||||
state.routes.forEach((route, routeIndex) => {
|
||||
scenesDescriptorsByIndex[routeIndex] = descriptors[route.key];
|
||||
});
|
||||
const scenesProps = scenesDescriptorsByIndex.map(
|
||||
(descriptor, descriptorIndex) => ({
|
||||
...this.props,
|
||||
descriptor,
|
||||
index: descriptorIndex,
|
||||
})
|
||||
);
|
||||
appBar = scenesProps.map(this._renderHeader, this);
|
||||
} else {
|
||||
appBar = this._renderHeader({ ...this.props, index });
|
||||
}
|
||||
|
||||
const { options } = descriptor;
|
||||
const { headerStyle = {} } = options;
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle);
|
||||
const appBarHeight = getAppBarHeight(isLandscape);
|
||||
|
||||
const {
|
||||
alignItems,
|
||||
justifyContent,
|
||||
flex,
|
||||
flexDirection,
|
||||
flexGrow,
|
||||
flexShrink,
|
||||
flexBasis,
|
||||
flexWrap,
|
||||
...safeHeaderStyle
|
||||
} = headerStyleObj;
|
||||
|
||||
if (__DEV__) {
|
||||
warnIfHeaderStyleDefined(alignItems, 'alignItems');
|
||||
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
|
||||
warnIfHeaderStyleDefined(flex, 'flex');
|
||||
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
|
||||
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
|
||||
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
||||
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
||||
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
|
||||
}
|
||||
|
||||
// TODO: warn if any unsafe styles are provided
|
||||
const containerStyles = [
|
||||
options.headerTransparent
|
||||
? styles.transparentContainer
|
||||
: styles.container,
|
||||
{ height: appBarHeight },
|
||||
safeHeaderStyle,
|
||||
];
|
||||
|
||||
const { headerForceInset } = options;
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
|
||||
<View style={{ flex: 1 }}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function warnIfHeaderStyleDefined(value, styleProp) {
|
||||
if (value !== undefined) {
|
||||
console.warn(
|
||||
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let platformContainerStyles;
|
||||
if (Platform.OS === 'ios') {
|
||||
platformContainerStyles = {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#A7A7AA',
|
||||
};
|
||||
} else {
|
||||
platformContainerStyles = {
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: StyleSheet.hairlineWidth,
|
||||
shadowOffset: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
},
|
||||
elevation: 4,
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
|
||||
...platformContainerStyles,
|
||||
},
|
||||
transparentContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
...platformContainerStyles,
|
||||
},
|
||||
header: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
item: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
iconMaskContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconMaskFillerRect: {
|
||||
flex: 1,
|
||||
backgroundColor: '#d8d8d8',
|
||||
marginLeft: -3,
|
||||
},
|
||||
iconMask: {
|
||||
// These are mostly the same as the icon in ModularHeaderBackButton
|
||||
height: 21,
|
||||
width: 12,
|
||||
marginLeft: 9,
|
||||
marginTop: -0.5, // resizes down to 20.5
|
||||
alignSelf: 'center',
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
title: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: TITLE_OFFSET,
|
||||
right: TITLE_OFFSET,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
|
||||
},
|
||||
left: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
right: {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrientation(Header);
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
const defaultBackImage = require('../assets/back-icon.png');
|
||||
|
||||
class HeaderBackButton extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
pressColorAndroid: 'rgba(0, 0, 0, .32)',
|
||||
@@ -17,8 +19,6 @@ class HeaderBackButton extends React.PureComponent {
|
||||
ios: '#037aff',
|
||||
}),
|
||||
truncatedTitle: 'Back',
|
||||
// eslint-disable-next-line global-require
|
||||
buttonImage: require('../assets/back-icon.png'),
|
||||
};
|
||||
|
||||
state = {};
|
||||
@@ -32,9 +32,37 @@ class HeaderBackButton extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
const { backImage, title, tintColor } = this.props;
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
title,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
|
||||
return <BackImage {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonImage,
|
||||
onPress,
|
||||
pressColorAndroid,
|
||||
width,
|
||||
@@ -64,14 +92,7 @@ class HeaderBackButton extends React.PureComponent {
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
]}
|
||||
source={buttonImage}
|
||||
/>
|
||||
{this._renderBackImage()}
|
||||
{Platform.OS === 'ios' &&
|
||||
typeof backButtonTitle === 'string' && (
|
||||
<Text
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
import { Dimensions, I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
|
||||
const crossFadeInterpolation = (first, index, last) => ({
|
||||
inputRange: [first, index - 0.9, index - 0.2, index, last],
|
||||
outputRange: [0, 0, 0.3, 1, 0],
|
||||
function hasHeader(scene) {
|
||||
if (!scene) {
|
||||
return true;
|
||||
}
|
||||
const { descriptor } = scene;
|
||||
return descriptor.options.header !== null;
|
||||
}
|
||||
|
||||
const crossFadeInterpolation = (scenes, first, index, last) => ({
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
index - 0.9,
|
||||
index - 0.2,
|
||||
index,
|
||||
last - 0.001,
|
||||
last,
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0.3 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility that builds the style for the navigation header.
|
||||
* Utilities that build the style for the navigation header.
|
||||
*
|
||||
* +-------------+-------------+-------------+
|
||||
* | | | |
|
||||
@@ -17,6 +41,51 @@ const crossFadeInterpolation = (first, index, last) => ({
|
||||
* +-------------+-------------+-------------+
|
||||
*/
|
||||
|
||||
function isGoingBack(scenes) {
|
||||
const lastSceneIndexInScenes = scenes.length - 1;
|
||||
return !scenes[lastSceneIndexInScenes].isActive;
|
||||
}
|
||||
|
||||
function forLayout(props) {
|
||||
const { layout, position, scene, scenes, mode } = props;
|
||||
if (mode !== 'float') {
|
||||
return {};
|
||||
}
|
||||
const isBack = isGoingBack(scenes);
|
||||
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
if (!interpolate) return {};
|
||||
|
||||
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],
|
||||
outputRange: [
|
||||
rtlMult * (hasHeader(scenes[first]) ? 0 : width),
|
||||
rtlMult * (hasHeader(scenes[index]) ? 0 : isBack ? width : -width),
|
||||
rtlMult * (hasHeader(scenes[last]) ? 0 : -width),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
transform: [{ translateX }],
|
||||
};
|
||||
}
|
||||
|
||||
function forLeft(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
@@ -27,12 +96,14 @@ function forLeft(props) {
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
opacity: position.interpolate(
|
||||
crossFadeInterpolation(scenes, first, index, last)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function forCenter(props) {
|
||||
const { position, scene } = props;
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
@@ -41,12 +112,14 @@ function forCenter(props) {
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
opacity: position.interpolate(
|
||||
crossFadeInterpolation(scenes, first, index, last)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function forRight(props) {
|
||||
const { position, scene } = props;
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
@@ -54,7 +127,9 @@ function forRight(props) {
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
opacity: position.interpolate(
|
||||
crossFadeInterpolation(scenes, first, index, last)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,25 +146,42 @@ function forLeftButton(props) {
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
// The gist of what we're doing here is animating the left button _normally_ (fast fade)
|
||||
// when both scenes in transition have headers. When the current, next, or previous scene _don't_
|
||||
// have a header, we don't fade the button, and only set it's opacity to 0 at the last moment
|
||||
// of the transition.
|
||||
const inputRange = [
|
||||
first,
|
||||
first + 0.001,
|
||||
first + Math.abs(index - first) / 2,
|
||||
index,
|
||||
last - Math.abs(last - index) / 2,
|
||||
last - 0.001,
|
||||
last,
|
||||
];
|
||||
const outputRange = [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0.1 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0.1 : 1,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
];
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [
|
||||
first,
|
||||
first + Math.abs(index - first) / 2,
|
||||
index,
|
||||
last - Math.abs(last - index) / 2,
|
||||
last,
|
||||
],
|
||||
outputRange: [0, 0.5, 1, 0.5, 0],
|
||||
inputRange,
|
||||
outputRange,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: this offset calculation is a an approximation that gives us
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
@@ -105,41 +197,71 @@ function forLeftLabel(props) {
|
||||
|
||||
const offset = LEFT_LABEL_OFFSET;
|
||||
|
||||
// Similarly to the animation of the left label, when animating to or from a scene without
|
||||
// a header, we keep the label at full opacity and in the same position for as long as possible.
|
||||
return {
|
||||
// For now we fade out the label before fading in the title, so the
|
||||
// differences between the label and title position can be hopefully not so
|
||||
// noticable to the user
|
||||
opacity: position.interpolate({
|
||||
inputRange: [first, index - 0.35, index, index + 0.5, last],
|
||||
outputRange: [0, 0, 1, 0.5, 0],
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
index - 0.35,
|
||||
index,
|
||||
index + 0.5,
|
||||
last - 0.001,
|
||||
last,
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0.5 : 1,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
inputRange: [first, first + 0.001, index, last - 0.001, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-offset, 0, offset]
|
||||
: [offset, 0, -offset * 1.5],
|
||||
? [
|
||||
-offset * 1.5,
|
||||
hasHeader(scenes[first]) ? -offset * 1.5 : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? offset : 0,
|
||||
offset,
|
||||
]
|
||||
: [
|
||||
offset,
|
||||
hasHeader(scenes[first]) ? offset : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? -offset * 1.5 : 0,
|
||||
-offset * 1.5,
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
|
||||
function forCenterFromLeft(props) {
|
||||
const { position, scene } = props;
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
@@ -151,16 +273,44 @@ function forCenterFromLeft(props) {
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [first, index - 0.5, index, index + 0.7, last],
|
||||
outputRange: [0, 0, 1, 0, 0],
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
index - 0.5,
|
||||
index,
|
||||
index + 0.7,
|
||||
last - 0.001,
|
||||
last,
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
inputRange: [first, first + 0.001, index, last - 0.001, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-offset, 0, offset]
|
||||
: [offset, 0, -offset],
|
||||
? [
|
||||
-offset,
|
||||
hasHeader(scenes[first]) ? -offset : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? offset : 0,
|
||||
offset,
|
||||
]
|
||||
: [
|
||||
offset,
|
||||
hasHeader(scenes[first]) ? offset : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? -offset : 0,
|
||||
-offset,
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -168,6 +318,7 @@ function forCenterFromLeft(props) {
|
||||
}
|
||||
|
||||
export default {
|
||||
forLayout,
|
||||
forLeft,
|
||||
forLeftButton,
|
||||
forLeftLabel,
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { Dimensions, I18nManager } from 'react-native';
|
||||
|
||||
const crossFadeInterpolation = (first, index, last) => ({
|
||||
inputRange: [first, index - 0.9, index - 0.2, index, last],
|
||||
outputRange: [0, 0, 0.3, 1, 0],
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility that builds the style for the navigation header.
|
||||
*
|
||||
* +-------------+-------------+-------------+
|
||||
* | | | |
|
||||
* | Left | Title | Right |
|
||||
* | Component | Component | Component |
|
||||
* | | | |
|
||||
* +-------------+-------------+-------------+
|
||||
*/
|
||||
|
||||
function forLeft(props) {
|
||||
const { position, descriptor, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
};
|
||||
}
|
||||
|
||||
function forCenter(props) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
};
|
||||
}
|
||||
|
||||
function forRight(props) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS UINavigationController style interpolators
|
||||
*/
|
||||
|
||||
function forLeftButton(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [
|
||||
first,
|
||||
first + Math.abs(index - first) / 2,
|
||||
index,
|
||||
last - Math.abs(last - index) / 2,
|
||||
last,
|
||||
],
|
||||
outputRange: [0, 0.5, 1, 0.5, 0],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* 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. 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)
|
||||
*/
|
||||
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
|
||||
function forLeftLabel(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
const offset = LEFT_LABEL_OFFSET;
|
||||
|
||||
return {
|
||||
// For now we fade out the label before fading in the title, so the
|
||||
// differences between the label and title position can be hopefully not so
|
||||
// noticable to the user
|
||||
opacity: position.interpolate({
|
||||
inputRange: [first, index - 0.35, index, index + 0.5, last],
|
||||
outputRange: [0, 0, 1, 0.5, 0],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-offset, 0, offset]
|
||||
: [offset, 0, -offset * 1.5],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
|
||||
function forCenterFromLeft(props) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const inputRange = [first, index - 0.5, index, index + 0.5, last];
|
||||
const offset = TITLE_OFFSET_IOS;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [first, index - 0.5, index, index + 0.7, last],
|
||||
outputRange: [0, 0, 1, 0, 0],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-offset, 0, offset]
|
||||
: [offset, 0, -offset],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
forLeft,
|
||||
forLeftButton,
|
||||
forLeftLabel,
|
||||
forCenterFromLeft,
|
||||
forCenter,
|
||||
forRight,
|
||||
};
|
||||
@@ -3,12 +3,12 @@ import { I18nManager, Image, Text, View, StyleSheet } from 'react-native';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
const defaultBackImage = require('../assets/back-icon.png');
|
||||
|
||||
class ModularHeaderBackButton extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
tintColor: '#037aff',
|
||||
truncatedTitle: 'Back',
|
||||
// eslint-disable-next-line global-require
|
||||
buttonImage: require('../assets/back-icon.png'),
|
||||
};
|
||||
|
||||
state = {};
|
||||
@@ -22,9 +22,37 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
const { backImage, title, tintColor } = this.props;
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
title,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
|
||||
return <BackImage {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonImage,
|
||||
onPress,
|
||||
width,
|
||||
title,
|
||||
@@ -61,14 +89,7 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<ButtonContainerComponent>
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
]}
|
||||
source={buttonImage}
|
||||
/>
|
||||
{this._renderBackImage()}
|
||||
</ButtonContainerComponent>
|
||||
{typeof backButtonTitle === 'string' && (
|
||||
<LabelContainerComponent>
|
||||
|
||||
8
src/views/NavigationContext.js
Normal file
8
src/views/NavigationContext.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import createReactContext from 'create-react-context';
|
||||
|
||||
const NavigationContext = createReactContext();
|
||||
|
||||
export const NavigationProvider = NavigationContext.Provider;
|
||||
export const NavigationConsumer = NavigationContext.Consumer;
|
||||
@@ -1,40 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import SceneView from './SceneView';
|
||||
|
||||
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
|
||||
|
||||
export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
class ResourceSavingSceneView extends React.PureComponent {
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.isFocused && !prevState.awake) {
|
||||
return { awake: true };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
const key = props.childNavigation.state.key;
|
||||
const focusedIndex = props.navigation.state.index;
|
||||
const focusedKey = props.navigation.state.routes[focusedIndex].key;
|
||||
const isFocused = key === focusedKey;
|
||||
|
||||
this.state = {
|
||||
awake: props.lazy ? isFocused : true,
|
||||
visible: isFocused,
|
||||
awake: props.lazy ? props.isFocused : true,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._actionListener = this.props.navigation.addListener(
|
||||
'action',
|
||||
this._onAction
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._actionListener.remove();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { awake, visible } = this.state;
|
||||
const { awake } = this.state;
|
||||
const {
|
||||
isFocused,
|
||||
childNavigation,
|
||||
navigation,
|
||||
removeClippedSubviews,
|
||||
@@ -49,12 +42,12 @@ export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
removeClippedSubviews={
|
||||
Platform.OS === 'android'
|
||||
? removeClippedSubviews
|
||||
: !visible && removeClippedSubviews
|
||||
: !isFocused && removeClippedSubviews
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
this._mustAlwaysBeVisible() || visible
|
||||
this._mustAlwaysBeVisible() || isFocused
|
||||
? styles.innerAttached
|
||||
: styles.innerDetached
|
||||
}
|
||||
@@ -68,33 +61,6 @@ export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
_mustAlwaysBeVisible = () => {
|
||||
return this.props.animationEnabled || this.props.swipeEnabled;
|
||||
};
|
||||
|
||||
_onAction = payload => {
|
||||
// We do not care about transition complete events, they won't actually change the state
|
||||
if (
|
||||
payload.action.type == 'Navigation/COMPLETE_TRANSITION' ||
|
||||
!payload.state
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { routes, index } = payload.state;
|
||||
const key = this.props.childNavigation.state.key;
|
||||
|
||||
if (routes[index].key === key) {
|
||||
if (!this.state.visible) {
|
||||
let nextState = { visible: true };
|
||||
if (!this.state.awake) {
|
||||
nextState.awake = true;
|
||||
}
|
||||
this.setState(nextState);
|
||||
}
|
||||
} else {
|
||||
if (this.state.visible) {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -110,3 +76,5 @@ const styles = StyleSheet.create({
|
||||
top: FAR_FAR_AWAY,
|
||||
},
|
||||
});
|
||||
|
||||
export default polyfill(ResourceSavingSceneView);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { NavigationProvider } from './NavigationContext';
|
||||
|
||||
export default class SceneView extends React.PureComponent {
|
||||
static childContextTypes = {
|
||||
navigation: propTypes.object.isRequired,
|
||||
};
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
navigation: this.props.navigation,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { screenProps, navigation, component: Component } = this.props;
|
||||
return <Component screenProps={screenProps} navigation={navigation} />;
|
||||
const { screenProps, component: Component, navigation } = this.props;
|
||||
return (
|
||||
<NavigationProvider value={navigation}>
|
||||
<Component screenProps={screenProps} navigation={navigation} />
|
||||
</NavigationProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NativeModules } from 'react-native';
|
||||
import StackViewLayout from './StackViewLayout';
|
||||
import Transitioner from '../Transitioner';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
|
||||
const NativeAnimatedModule =
|
||||
@@ -24,14 +25,16 @@ class StackView extends React.Component {
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
onTransitionStart={this.props.onTransitionStart}
|
||||
onTransitionEnd={(lastTransition, transition) => {
|
||||
onTransitionEnd={(transition, lastTransition) => {
|
||||
const { onTransitionEnd, navigation } = this.props;
|
||||
navigation.dispatch(
|
||||
NavigationActions.completeTransition({
|
||||
key: navigation.state.key,
|
||||
})
|
||||
);
|
||||
onTransitionEnd && onTransitionEnd(lastTransition, transition);
|
||||
if (transition.navigation.state.isTransitioning) {
|
||||
navigation.dispatch(
|
||||
StackActions.completeTransition({
|
||||
key: navigation.state.key,
|
||||
})
|
||||
);
|
||||
}
|
||||
onTransitionEnd && onTransitionEnd(transition, lastTransition);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -54,6 +57,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,546 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import Transitioner from './Transitioner2';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import Transitions from './StackViewTransitions';
|
||||
|
||||
const NativeAnimatedModule =
|
||||
NativeModules && NativeModules.NativeAnimatedModule;
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
PanResponder,
|
||||
Platform,
|
||||
View,
|
||||
I18nManager,
|
||||
Easing,
|
||||
NativeModules,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
// import Header from '../Header/Header2'; // WIP.. interpolation reconfiguration, fun!
|
||||
import SceneView from '../SceneView';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
|
||||
const EaseInOut = Easing.inOut(Easing.ease);
|
||||
|
||||
/**
|
||||
* The max duration of the card animation in milliseconds after released gesture.
|
||||
* The actual duration should be always less then that because the rest distance
|
||||
* is always less then the full distance of the layout.
|
||||
*/
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
/**
|
||||
* The gesture distance threshold to trigger the back behavior. For instance,
|
||||
* `1/2` means that moving greater than 1/2 of the width of the screen will
|
||||
* trigger a back action
|
||||
*/
|
||||
const POSITION_THRESHOLD = 1 / 2;
|
||||
|
||||
/**
|
||||
* The threshold (in pixels) to start the gesture action.
|
||||
*/
|
||||
const RESPOND_THRESHOLD = 20;
|
||||
|
||||
/**
|
||||
* The distance of touch start from the edge of the screen where the gesture will be recognized
|
||||
*/
|
||||
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
|
||||
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
||||
|
||||
const animatedSubscribeValue = animatedValue => {
|
||||
if (!animatedValue.__isNative) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(animatedValue._listeners).length === 0) {
|
||||
animatedValue.addListener(emptyFunction);
|
||||
}
|
||||
};
|
||||
|
||||
class StackViewLayout extends React.Component {
|
||||
/**
|
||||
* Used to identify the starting point of the position when the gesture starts, such that it can
|
||||
* be updated according to its relative position. This means that a card can effectively be
|
||||
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
|
||||
* corresponding location for the touch.
|
||||
*/
|
||||
_gestureStartValue = 0;
|
||||
|
||||
// tracks if a touch is currently happening
|
||||
_isResponding = false;
|
||||
|
||||
/**
|
||||
* immediateIndex is used to represent the expected index that we will be on after a
|
||||
* transition. To achieve a smooth animation when swiping back, the action to go back
|
||||
* doesn't actually fire until the transition completes. The immediateIndex is used during
|
||||
* the transition so that gestures can be handled correctly. This is a work-around for
|
||||
* cases when the user quickly swipes back several times.
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
// _panResponder = 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);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
|
||||
_renderHeader(descriptor, headerMode) {
|
||||
const { options } = descriptor;
|
||||
const { header } = options;
|
||||
|
||||
if (typeof header !== 'undefined' && typeof header !== 'function') {
|
||||
return header;
|
||||
}
|
||||
|
||||
// const renderHeader = header || (props => <Header {...props} />);
|
||||
const renderHeader = header || (props => null);
|
||||
const {
|
||||
headerLeftInterpolator,
|
||||
headerTitleInterpolator,
|
||||
headerRightInterpolator,
|
||||
} = this._getTransitionConfig();
|
||||
|
||||
const {
|
||||
mode,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
return renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
descriptor,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_animatedSubscribe(props) {
|
||||
// Hack to make this work with native driven animations. We add a single listener
|
||||
// so the JS value of the following animated values gets updated. We rely on
|
||||
// some Animated private APIs and not doing so would require using a bunch of
|
||||
// value listeners but we'd have to remove them to not leak and I'm not sure
|
||||
// when we'd do that with the current structure we have. `stopAnimation` callback
|
||||
// is also broken with native animated values that have no listeners so if we
|
||||
// want to remove this we have to fix this too.
|
||||
animatedSubscribeValue(props.layout.width);
|
||||
animatedSubscribeValue(props.layout.height);
|
||||
animatedSubscribeValue(props.position);
|
||||
}
|
||||
|
||||
// _reset(resetToIndex, duration) {
|
||||
// if (
|
||||
// Platform.OS === 'ios' &&
|
||||
// ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
// ) {
|
||||
// Animated.spring(this.props.transitionProps.position, {
|
||||
// toValue: resetToIndex,
|
||||
// stiffness: 5000,
|
||||
// damping: 600,
|
||||
// mass: 3,
|
||||
// useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
// }).start();
|
||||
// } else {
|
||||
// Animated.timing(this.props.transitionProps.position, {
|
||||
// toValue: resetToIndex,
|
||||
// duration,
|
||||
// easing: EaseInOut,
|
||||
// useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
// }).start();
|
||||
// }
|
||||
// }
|
||||
|
||||
// _goBack(backFromIndex, duration) {
|
||||
// const { navigation, position, scenes } = this.props.transitionProps;
|
||||
// const toValue = Math.max(backFromIndex - 1, 0);
|
||||
|
||||
// // set temporary index for gesture handler to respect until the action is
|
||||
// // dispatched at the end of the transition.
|
||||
// this._immediateIndex = toValue;
|
||||
|
||||
// const onCompleteAnimation = () => {
|
||||
// this._immediateIndex = null;
|
||||
// const backFromScene = scenes.find(s => s.index === toValue + 1);
|
||||
// if (!this._isResponding && backFromScene) {
|
||||
// navigation.dispatch(
|
||||
// NavigationActions.back({
|
||||
// key: backFromScene.route.key,
|
||||
// immediate: true,
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// };
|
||||
|
||||
// if (
|
||||
// Platform.OS === 'ios' &&
|
||||
// ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
// ) {
|
||||
// Animated.spring(position, {
|
||||
// toValue,
|
||||
// stiffness: 5000,
|
||||
// damping: 600,
|
||||
// mass: 3,
|
||||
// useNativeDriver: position.__isNative,
|
||||
// }).start(onCompleteAnimation);
|
||||
// } else {
|
||||
// Animated.timing(position, {
|
||||
// toValue,
|
||||
// duration,
|
||||
// easing: EaseInOut,
|
||||
// useNativeDriver: position.__isNative,
|
||||
// }).start(onCompleteAnimation);
|
||||
// }
|
||||
// }
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
const {
|
||||
navigation,
|
||||
transition,
|
||||
descriptor,
|
||||
descriptors,
|
||||
layout,
|
||||
mode,
|
||||
} = this.props;
|
||||
if (headerMode === 'float') {
|
||||
floatingHeader = this._renderHeader(descriptor, headerMode);
|
||||
}
|
||||
const { index, routes } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = descriptor;
|
||||
|
||||
const gestureDirectionInverted = options.gestureDirection === 'inverted';
|
||||
|
||||
const gesturesEnabled =
|
||||
typeof options.gesturesEnabled === 'boolean'
|
||||
? options.gesturesEnabled
|
||||
: Platform.OS === 'ios';
|
||||
|
||||
// const handlers = gesturesEnabled ? this._panResponder.panHandlers : {};
|
||||
const handlers = {};
|
||||
|
||||
const containerStyle = [
|
||||
styles.container,
|
||||
this._getTransitionConfig().containerStyle,
|
||||
];
|
||||
|
||||
let forwardScene = null;
|
||||
let backwardScene = null;
|
||||
|
||||
if (transition) {
|
||||
const { fromDescriptor, toDescriptor } = transition;
|
||||
const fromKey = fromDescriptor.key;
|
||||
const toKey = toDescriptor.key;
|
||||
const toIndex = navigation.state.routes.findIndex(r => r.key === toKey);
|
||||
invariant(
|
||||
toIndex !== -1,
|
||||
`Could not find toIndex in navigation state for ${fromKey}`
|
||||
);
|
||||
const fromIndex = navigation.state.routes.findIndex(
|
||||
r => r.key === fromKey
|
||||
);
|
||||
if (fromIndex == -1) {
|
||||
// we are coming from a screen that is no longer in the stack
|
||||
backwardScene = fromDescriptor;
|
||||
} else if (toIndex > fromIndex) {
|
||||
// presumably we are going doing a push.
|
||||
backwardScene = fromDescriptor;
|
||||
} else {
|
||||
// we are navigating back, and the forward scene is on top
|
||||
forwardScene = fromDescriptor;
|
||||
}
|
||||
} else if (index > 0) {
|
||||
// when we aren't transitioning, render the previous screen in case we swipe back.
|
||||
const previousKey = routes[index - 1].key;
|
||||
const previousDescriptor = descriptors[previousKey];
|
||||
backwardScene = previousDescriptor;
|
||||
}
|
||||
|
||||
return (
|
||||
<View {...handlers} style={containerStyle}>
|
||||
<View style={styles.scenes}>
|
||||
{backwardScene && this._renderScene(backwardScene, index - 1)}
|
||||
{this._renderScene(descriptor, index)}
|
||||
{forwardScene && this._renderScene(forwardScene, index + 1)}
|
||||
</View>
|
||||
{floatingHeader}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_getHeaderMode() {
|
||||
if (this.props.headerMode) {
|
||||
return this.props.headerMode;
|
||||
}
|
||||
if (Platform.OS === 'android' || this.props.mode === 'modal') {
|
||||
return 'screen';
|
||||
}
|
||||
return 'float';
|
||||
}
|
||||
|
||||
_getHeaderTransitionPreset() {
|
||||
// On Android or with header mode screen, we always just use in-place,
|
||||
// we ignore the option entirely (at least until we have other presets)
|
||||
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
// TODO: validations: 'fade-in-place' or 'uikit' are valid
|
||||
if (this.props.headerTransitionPreset) {
|
||||
return this.props.headerTransitionPreset;
|
||||
} else {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
}
|
||||
|
||||
_renderInnerScene(descriptor) {
|
||||
const { options, navigation, getComponent } = descriptor;
|
||||
const SceneComponent = getComponent();
|
||||
|
||||
const { screenProps } = this.props;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
</View>
|
||||
{this._renderHeader(descriptor, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getTransitionConfig = () => {
|
||||
const isModal = this.props.mode === 'modal';
|
||||
|
||||
return Transitions.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
this.props,
|
||||
isModal
|
||||
);
|
||||
};
|
||||
|
||||
_renderScene = (descriptor, index) => {
|
||||
const { screenInterpolator } = this._getTransitionConfig();
|
||||
const style =
|
||||
screenInterpolator &&
|
||||
screenInterpolator({ ...this.props, descriptor, index });
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...this.props}
|
||||
// providing this descriptor will override this.props.descriptor, to tell the card exactly which scene to render, instead of this.props.descriptor, which defines what scene is active
|
||||
descriptor={descriptor}
|
||||
index={index}
|
||||
key={`card_${descriptor.key}`}
|
||||
style={[style, this.props.cardStyle]}
|
||||
>
|
||||
{this._renderInnerScene(descriptor)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
// Header is physically rendered after scenes so that Header won't be
|
||||
// covered by the shadows of the scenes.
|
||||
// That said, we'd have use `flexDirection: 'column-reverse'` to move
|
||||
// Header above the scenes.
|
||||
flexDirection: 'column-reverse',
|
||||
},
|
||||
scenes: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
class StackView extends React.Component {
|
||||
static defaultProps = {
|
||||
navigationConfig: {
|
||||
mode: 'card',
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Transitioner
|
||||
render={this._render}
|
||||
configureTransition={this._configureTransition}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
// onTransitionStart={this.props.onTransitionStart}
|
||||
// onTransitionEnd={(lastTransition, transition) => {
|
||||
// const { onTransitionEnd, navigation } = this.props;
|
||||
// navigation.dispatch(
|
||||
// NavigationActions.completeTransition({
|
||||
// key: navigation.state.key,
|
||||
// })
|
||||
// );
|
||||
// onTransitionEnd && onTransitionEnd(lastTransition, transition);
|
||||
// }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_configureTransition = transitionProps => {
|
||||
return {
|
||||
...Transitions.getTransitionConfig(
|
||||
this.props.navigationConfig.transitionConfig,
|
||||
transitionProps,
|
||||
this.props.navigationConfig.mode === 'modal'
|
||||
).transitionSpec,
|
||||
useNativeDriver: !!NativeAnimatedModule,
|
||||
};
|
||||
};
|
||||
|
||||
_render = transitionProps => {
|
||||
const { screenProps } = this.props;
|
||||
return <StackViewLayout {...transitionProps} screenProps={screenProps} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default StackView;
|
||||
@@ -9,18 +9,29 @@ import {
|
||||
View,
|
||||
I18nManager,
|
||||
Easing,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
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';
|
||||
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);
|
||||
|
||||
/**
|
||||
@@ -82,11 +93,18 @@ class StackViewLayout extends React.Component {
|
||||
const { options } = scene.descriptor;
|
||||
const { header } = options;
|
||||
|
||||
if (typeof header !== 'undefined' && typeof header !== 'function') {
|
||||
if (header === null && headerMode === 'screen') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if it's a react element
|
||||
if (React.isValidElement(header)) {
|
||||
return header;
|
||||
}
|
||||
|
||||
const renderHeader = header || ((props: *) => <Header {...props} />);
|
||||
// Handle the case where the header option is a function, and provide the default
|
||||
const renderHeader = header || (props => <Header {...props} />);
|
||||
|
||||
const {
|
||||
headerLeftInterpolator,
|
||||
headerTitleInterpolator,
|
||||
@@ -166,6 +184,7 @@ class StackViewLayout extends React.Component {
|
||||
immediate: true,
|
||||
})
|
||||
);
|
||||
navigation.dispatch(StackActions.completeTransition());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,13 +209,195 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_panResponder = PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'float') {
|
||||
floatingHeader = this._renderHeader(
|
||||
this.props.transitionProps.scene,
|
||||
headerMode
|
||||
const { scene } = this.props.transitionProps;
|
||||
floatingHeader = (
|
||||
<NavigationProvider value={scene.descriptor.navigation}>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
</NavigationProvider>
|
||||
);
|
||||
}
|
||||
const {
|
||||
@@ -206,142 +407,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 = [
|
||||
@@ -352,7 +430,7 @@ class StackViewLayout extends React.Component {
|
||||
return (
|
||||
<View {...handlers} style={containerStyle}>
|
||||
<View style={styles.scenes}>
|
||||
{scenes.map((s: *) => this._renderCard(s))}
|
||||
{scenes.map(s => this._renderCard(s))}
|
||||
</View>
|
||||
{floatingHeader}
|
||||
</View>
|
||||
@@ -393,7 +471,7 @@ class StackViewLayout extends React.Component {
|
||||
if (headerMode === 'screen') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.scenes}>
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
@@ -430,11 +508,36 @@ class StackViewLayout extends React.Component {
|
||||
screenInterpolator &&
|
||||
screenInterpolator({ ...this.props.transitionProps, scene });
|
||||
|
||||
// If this screen has "header" set to `null` in it's navigation options, but
|
||||
// it exists in a stack with headerMode float, add a negative margin to
|
||||
// compensate for the hidden header
|
||||
const { options } = scene.descriptor;
|
||||
const hasHeader = options.header !== null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
let marginTop = 0;
|
||||
if (!hasHeader && headerMode === 'float') {
|
||||
const { isLandscape } = this.props;
|
||||
let headerHeight;
|
||||
if (Platform.OS === 'ios') {
|
||||
if (isLandscape && !Platform.isPad) {
|
||||
headerHeight = 52;
|
||||
} else if (IS_IPHONE_X) {
|
||||
headerHeight = 88;
|
||||
} else {
|
||||
headerHeight = 64;
|
||||
}
|
||||
} else {
|
||||
headerHeight = 56;
|
||||
// TODO (Android only): Need to handle translucent status bar.
|
||||
}
|
||||
marginTop = -headerHeight;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...this.props.transitionProps}
|
||||
key={`card_${scene.key}`}
|
||||
style={[style, this.props.cardStyle]}
|
||||
style={[style, { marginTop }, this.props.cardStyle]}
|
||||
scene={scene}
|
||||
>
|
||||
{this._renderInnerScene(scene)}
|
||||
@@ -457,4 +560,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default StackViewLayout;
|
||||
export default withOrientation(StackViewLayout);
|
||||
|
||||
@@ -60,19 +60,21 @@ const FadeOutToBottomAndroid = {
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(transitionProps, isModal) {
|
||||
function defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
if (Platform.OS === 'android') {
|
||||
// todo, uncomment and fix, stop using prevTransitionProps
|
||||
|
||||
// // Use the default Android animation no matter if the screen is a modal.
|
||||
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
// if (
|
||||
// prevTransitionProps &&
|
||||
// transitionProps.index < prevTransitionProps.index
|
||||
// ) {
|
||||
// // Navigating back to the previous screen
|
||||
// return FadeOutToBottomAndroid;
|
||||
// }
|
||||
// Use the default Android animation no matter if the screen is a modal.
|
||||
// Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
if (
|
||||
prevTransitionProps &&
|
||||
transitionProps.index < prevTransitionProps.index
|
||||
) {
|
||||
// Navigating back to the previous screen
|
||||
return FadeOutToBottomAndroid;
|
||||
}
|
||||
return FadeInFromBottomAndroid;
|
||||
}
|
||||
// iOS and other platforms
|
||||
@@ -82,12 +84,21 @@ function defaultTransitionConfig(transitionProps, isModal) {
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
|
||||
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
|
||||
function getTransitionConfig(
|
||||
transitionConfigurer,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
const defaultConfig = defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
);
|
||||
if (transitionConfigurer) {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...transitionConfigurer(transitionProps, isModal),
|
||||
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import { Animated, Easing, Platform } from 'react-native';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
import { I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
|
||||
/**
|
||||
* Utility that builds the style for the card in the cards stack.
|
||||
*
|
||||
* +------------+
|
||||
* +-+ |
|
||||
* +-+ | |
|
||||
* | | | |
|
||||
* | | | Focused |
|
||||
* | | | Card |
|
||||
* | | | |
|
||||
* +-+ | |
|
||||
* +-+ |
|
||||
* +------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render the initial style when the initial layout isn't measured yet.
|
||||
*/
|
||||
function forInitial(props) {
|
||||
const { navigation, descriptor } = props;
|
||||
const { state } = navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
|
||||
const focused = descriptor.key === activeKey;
|
||||
const opacity = focused ? 1 : 0;
|
||||
// If not focused, move the card far away.
|
||||
const translate = focused ? 0 : 1000000;
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX: translate }, { translateY: translate }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard iOS-style slide in from the right.
|
||||
*/
|
||||
function forHorizontal(props) {
|
||||
const { layout, transition, navigation, index } = props;
|
||||
const { state } = navigation;
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const first = index - 1;
|
||||
const last = index + 1;
|
||||
const opacity = transition
|
||||
? transition.progress.interpolate({
|
||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
||||
outputRange: [0, 1, 1, 0.85, 0],
|
||||
})
|
||||
: 1;
|
||||
|
||||
const width = layout.initWidth;
|
||||
const translateX = transition
|
||||
? transition.progress.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-width, 0, width * 0.3]
|
||||
: [width, 0, width * -0.3],
|
||||
})
|
||||
: 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard iOS-style slide in from the bottom (used for modals).
|
||||
*/
|
||||
function forVertical(props) {
|
||||
const { layout, transition, descriptor } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = transition.progress.interpolate({
|
||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
||||
outputRange: [0, 1, 1, 0.85, 0],
|
||||
});
|
||||
|
||||
const height = layout.initHeight;
|
||||
const translateY = transition.progress.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [height, 0, 0],
|
||||
});
|
||||
const translateX = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Android-style fade in from the bottom.
|
||||
*/
|
||||
function forFadeFromBottomAndroid(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const inputRange = [first, index, last - 0.01, last];
|
||||
|
||||
const opacity = position.interpolate({
|
||||
inputRange,
|
||||
outputRange: [0, 1, 1, 0],
|
||||
});
|
||||
|
||||
const translateY = position.interpolate({
|
||||
inputRange,
|
||||
outputRange: [50, 0, 0, 0],
|
||||
});
|
||||
const translateX = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* fadeIn and fadeOut
|
||||
*/
|
||||
function forFade(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [0, 1, 1],
|
||||
});
|
||||
|
||||
return {
|
||||
opacity,
|
||||
};
|
||||
}
|
||||
|
||||
const StyleInterpolator = {
|
||||
forHorizontal,
|
||||
forVertical,
|
||||
forFadeFromBottomAndroid,
|
||||
forFade,
|
||||
};
|
||||
|
||||
let IOSTransitionSpec;
|
||||
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
|
||||
// These are the exact values from UINavigationController's animation configuration
|
||||
IOSTransitionSpec = {
|
||||
timing: Animated.spring,
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
};
|
||||
} else {
|
||||
// This is an approximation of the IOS spring animation using a derived bezier curve
|
||||
IOSTransitionSpec = {
|
||||
duration: 500,
|
||||
easing: Easing.bezier(0.2833, 0.99, 0.31833, 0.99),
|
||||
timing: Animated.timing,
|
||||
};
|
||||
}
|
||||
|
||||
// Standard iOS navigation transition
|
||||
const SlideFromRightIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forHorizontal,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
// Standard iOS navigation transition for modals
|
||||
const ModalSlideFromBottomIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forVertical,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when opening an Activity
|
||||
const FadeInFromBottomAndroid = {
|
||||
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
|
||||
transitionSpec: {
|
||||
duration: 350,
|
||||
easing: Easing.out(Easing.poly(5)), // decelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when closing an Activity
|
||||
const FadeOutToBottomAndroid = {
|
||||
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
|
||||
transitionSpec: {
|
||||
duration: 230,
|
||||
easing: Easing.in(Easing.poly(4)), // accelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(transitionProps, isModal) {
|
||||
if (Platform.OS === 'android') {
|
||||
// todo, uncomment and fix, stop using prevTransitionProps
|
||||
|
||||
// // Use the default Android animation no matter if the screen is a modal.
|
||||
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
// if (
|
||||
// prevTransitionProps &&
|
||||
// transitionProps.index < prevTransitionProps.index
|
||||
// ) {
|
||||
// // Navigating back to the previous screen
|
||||
// return FadeOutToBottomAndroid;
|
||||
// }
|
||||
return FadeInFromBottomAndroid;
|
||||
}
|
||||
// iOS and other platforms
|
||||
if (isModal) {
|
||||
return ModalSlideFromBottomIOS;
|
||||
}
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
|
||||
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
|
||||
if (transitionConfigurer) {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...transitionConfigurer(transitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
export default {
|
||||
defaultTransitionConfig,
|
||||
getTransitionConfig,
|
||||
StyleInterpolator,
|
||||
};
|
||||
@@ -1,225 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Animated, Easing, StyleSheet, View } from 'react-native';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
// Used for all animations unless overriden
|
||||
const DefaultTransitionSpec = {
|
||||
duration: 250,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
timing: Animated.timing,
|
||||
};
|
||||
|
||||
class Transitioner extends React.Component {
|
||||
_isMounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps = (props, lastState) => {
|
||||
const { navigation, descriptors } = props;
|
||||
const { state } = navigation;
|
||||
const canGoBack = state.index > 0;
|
||||
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = descriptors[activeKey];
|
||||
|
||||
if (!lastState) {
|
||||
lastState = {
|
||||
backProgress: canGoBack ? new Animaged.Value(1) : null,
|
||||
descriptor,
|
||||
descriptors,
|
||||
navigation,
|
||||
transition: null,
|
||||
layout: {
|
||||
height: new Animated.Value(0),
|
||||
initHeight: 0,
|
||||
initWidth: 0,
|
||||
isMeasured: false,
|
||||
width: new Animated.Value(0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// const lastNavState = this.props.navigation.state;
|
||||
|
||||
const lastNavState = lastState.navigation.state;
|
||||
const lastActiveKey = lastNavState.routes[lastNavState.index].key;
|
||||
|
||||
// const transitionFromKey =
|
||||
// lastActiveKey !== activeKey ? lastActiveKey : null;
|
||||
const transitionFromKey = state.transitioningFromKey;
|
||||
const transitionFromDescriptor =
|
||||
transitionFromKey &&
|
||||
lastState.descriptor &&
|
||||
lastState.descriptor.key === transitionFromKey;
|
||||
|
||||
// We can only perform a transition if we have been told to via state.transitioningFromKey, and if our previous descriptor matches, indicating that the transitioningFromKey is currently being presented.
|
||||
if (transitionFromDescriptor) {
|
||||
if (lastState.transition) {
|
||||
// there is already a transition in progress.. Don't interrupt it!
|
||||
// At the end of the transition, we will compare props and start again
|
||||
return lastState;
|
||||
}
|
||||
|
||||
return {
|
||||
...lastState,
|
||||
navigation,
|
||||
descriptor,
|
||||
backProgress: null,
|
||||
transition: {
|
||||
fromDescriptor: lastState.descriptor,
|
||||
toDescriptor: descriptor,
|
||||
progress: new Animated.Value(0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// No transition is being performed. If the key has changed, present it immediately without transition
|
||||
if (lastActiveKey !== activeKey) {
|
||||
return {
|
||||
...lastState,
|
||||
backProgress: canGoBack ? new Animaged.Value(1) : null,
|
||||
descriptor,
|
||||
transition: null,
|
||||
};
|
||||
}
|
||||
|
||||
return lastState;
|
||||
};
|
||||
|
||||
// React doesn't handle getDerivedStateFromProps yet, but the polyfill is simple..
|
||||
state = Transitioner.getDerivedStateFromProps(this.props);
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const nextState = Transitioner.getDerivedStateFromProps(
|
||||
nextProps,
|
||||
this.state
|
||||
);
|
||||
if (this.state !== nextState) {
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
_startTransition(transition) {
|
||||
const { configureTransition } = this.props;
|
||||
const { descriptors } = this.state;
|
||||
const { progress, fromDescriptor, toDescriptor } = transition;
|
||||
progress.setValue(0);
|
||||
|
||||
// get the transition spec.
|
||||
// passing the new transitionProps format (this.state) into configureTransition is a breaking change that I haven't documented yet!
|
||||
const transitionUserSpec =
|
||||
(configureTransition && configureTransition(this.state)) || null;
|
||||
|
||||
const transitionSpec = {
|
||||
...DefaultTransitionSpec,
|
||||
...transitionUserSpec,
|
||||
};
|
||||
|
||||
const { timing } = transitionSpec;
|
||||
|
||||
// mutating a prop, this is terrible!
|
||||
// it was in the previous transitioner implementation, so I'm leaving it as-is for now:
|
||||
delete transitionSpec.timing;
|
||||
|
||||
timing(progress, {
|
||||
...transitionSpec,
|
||||
toValue: 1,
|
||||
}).start(didComplete => {
|
||||
this._completeTransition(transition, didComplete);
|
||||
});
|
||||
}
|
||||
|
||||
_completeTransition(transition, didComplete) {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
const { progress, fromDescriptor, toDescriptor } = transition;
|
||||
const { navigation, descriptors } = this.props;
|
||||
|
||||
const nextState = navigation.state;
|
||||
const activeKey = nextState.routes[nextState.index].key;
|
||||
const nextDescriptor =
|
||||
descriptors[activeKey] || this.state.descriptors[activeKey];
|
||||
|
||||
if (activeKey !== toDescriptor.key) {
|
||||
// The user has changed navigation states during the transition! This is known as a queued transition.
|
||||
// Now we set state for a new transition to the current navigation state
|
||||
this.setState({
|
||||
navigation,
|
||||
descriptors,
|
||||
descriptor: nextDescriptor,
|
||||
transition: {
|
||||
fromDescriptor: toDescriptor,
|
||||
toDescriptor: nextDescriptor,
|
||||
progress: new Animated.Value(0),
|
||||
},
|
||||
backProgress: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const canGoBack = navigation.state.index > 0;
|
||||
|
||||
// All transitions are complete. Reset to normal state:
|
||||
this.setState({
|
||||
navigation,
|
||||
descriptors,
|
||||
descriptor: nextDescriptor,
|
||||
transition: null,
|
||||
backProgress: canGoBack ? new Animated.Value(1) : null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('Rendering Transitioner', this.state);
|
||||
return (
|
||||
<View onLayout={this._onLayout} style={[styles.main]}>
|
||||
{this.props.render(this.state)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(lastProps, lastState) {
|
||||
// start transition if it needs it
|
||||
if (
|
||||
this.state.transition &&
|
||||
(!lastState.transition ||
|
||||
lastState.transition.toDescriptor !==
|
||||
this.state.transition.toDescriptor)
|
||||
) {
|
||||
this._startTransition(this.state.transition);
|
||||
}
|
||||
}
|
||||
|
||||
_onLayout = event => {
|
||||
const lastLayout = this.state.layout;
|
||||
const { height, width } = event.nativeEvent.layout;
|
||||
if (lastLayout.initWidth === width && lastLayout.initHeight === height) {
|
||||
return;
|
||||
}
|
||||
const layout = {
|
||||
...lastLayout,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
isMeasured: true,
|
||||
};
|
||||
|
||||
layout.height.setValue(height);
|
||||
layout.width.setValue(width);
|
||||
|
||||
this.setState({ layout });
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default Transitioner;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user