Compare commits

..

5 Commits

Author SHA1 Message Date
Eric Vicenti
0ea01673e5 transitioner2
Introducing a wild and hacky prototype of Transitioner 2!

- "transitionProps" concept is now identical to transitioner.state
- Far simpler state management than before
- Descriptors only, no "scenes"
- No more "position"

Also, StackView is seeing improvements:
- Easier to understand when "transition props" are just this.props

- StackView only renders the current sceen and the one before it

Currently broken:
- Interpolation configuration, beyond the first push
- Attempting to move away from getSceneIndicesForInterpolationInputRange because it works with position and it is confusing as hell, but haven't worked around it yet
- Gestures, although some "backProgress" code is in the works
- Interpolation is super buggy
- onTransitionStart/End
- Header, although a lot of code is moved over
2018-03-11 00:11:58 -08:00
Brent Vatne
71adb7cc4f Update DrawerView.js 2018-03-06 14:22:31 -08:00
Eric Vicenti
77343cb096 Merge branch 'master' into @ericvicenti/drawer-fix 2018-03-06 11:43:25 -08:00
Brent Vatne
accee76951 Merge branch 'master' into @ericvicenti/drawer-fix 2018-03-05 16:23:10 -08:00
Eric Vicenti
bbb8c4d8d3 Fix issue in drawer actions 2018-03-05 11:38:13 -08:00
129 changed files with 9438 additions and 11395 deletions

View File

@@ -7,18 +7,18 @@
"prettier/react"
],
"parser": "babel-eslint",
"plugins": ["react", "prettier"],
"plugins": [
"react",
"prettier"
],
"env": {
"jasmine": true
},
"rules": {
"prettier/prettier": [
"error",
{
"trailingComma": "es5",
"singleQuote": true
}
],
"prettier/prettier": ["error", {
"trailingComma": "es5",
"singleQuote": true
}],
"no-underscore-dangle": "off",
"no-use-before-define": "off",
@@ -27,20 +27,24 @@
"no-plusplus": "off",
"no-class-assign": "off",
"no-duplicate-imports": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": "off",
"import/no-unresolved": "off",
"react/jsx-filename-extension": ["off", { "extensions": [".js", ".jsx"] }],
"react/jsx-filename-extension": [
"off", { "extensions": [".js", ".jsx"] }
],
"react/sort-comp": "off",
"react/prefer-stateless-function": "off",
"react/no-deprecated": "off",
"react/forbid-prop-types": "warn",
"react/prop-types": "off",
"react/require-default-props": "off",
"react/no-unused-prop-types": "off"
"react/no-unused-prop-types": "off",
},
"settings": {
},
"parserOptions": {
"ecmaVersion": 6,

View File

@@ -1,6 +1,6 @@
# React Navigation
[![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-navigation/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-navigation/react-navigation) [![CircleCI badge](https://circleci.com/gh/react-navigation/react-navigation/tree/master.svg?style=shield)](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/contributing.html)
[![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-community/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-community/react-navigation) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/guides/contributors)
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.
@@ -55,4 +55,4 @@ This library has adopted a Code of Conduct that we expect project participants t
## License
React Navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).
React-navigation is licensed under the [BSD 2-clause "Simplified" License](https://github.com/react-community/react-navigation/blob/master/LICENSE).

View File

@@ -1,12 +0,0 @@
/**
* 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)) + ';';
},
};

View File

@@ -55,6 +55,8 @@ module.system=haste
emoji=true
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
@@ -75,5 +77,7 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.67.0
^0.61.0

View File

@@ -5,6 +5,5 @@ import renderer from 'react-test-renderer';
it('renders without crashing', () => {
const rendered = renderer.create(<App />).toJSON();
// Will be null because the playground uses state persistence which happens asyncronously
expect(rendered).toEqual(null);
expect(rendered).toBeTruthy();
});

View File

@@ -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/contributing.html#run-the-example-app) for instructions on running these example apps.
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#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).

View File

@@ -11,7 +11,7 @@
"splash": {
"image": "./assets/icons/splash.png"
},
"sdkVersion": "27.0.0",
"sdkVersion": "25.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

View File

@@ -16,7 +16,7 @@ import {
StatusBar,
View,
} from 'react-native';
import { SafeAreaView, createStackNavigator } from 'react-navigation';
import { SafeAreaView, StackNavigator } from 'react-navigation';
import CustomTabs from './CustomTabs';
import CustomTransitioner from './CustomTransitioner';
@@ -27,34 +27,18 @@ 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 SwitchWithStacks from './SwitchWithStacks';
import TabAnimations from './TabAnimations';
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',
@@ -67,6 +51,10 @@ 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.',
@@ -117,26 +105,23 @@ 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,
SwitchWithStacks,
SimpleStack: SimpleStack,
SimpleTabs: SimpleTabs,
Drawer: Drawer,
// MultipleDrawer: {
// screen: MultipleDrawer,
// },
StackWithCustomHeaderBackImage: StackWithCustomHeaderBackImage,
StackWithHeaderPreset: StackWithHeaderPreset,
StackWithTranslucentHeader: StackWithTranslucentHeader,
TabsInDrawer: TabsInDrawer,
@@ -154,10 +139,8 @@ const ExampleRoutes = {
screen: SimpleTabs,
path: 'settings',
},
TabAnimations,
TabsWithNavigationFocus,
KeyboardHandlingExample,
// This is commented out because it's rarely useful
// InactiveStack,
};
type State = {
@@ -168,7 +151,7 @@ class MainScreen extends React.Component<any, State> {
scrollY: new Animated.Value(0),
};
componentDidMount() {
componentWillMount() {
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon-mask.png')
).downloadAsync();
@@ -303,7 +286,7 @@ class MainScreen extends React.Component<any, State> {
}
}
const AppNavigator = createStackNavigator(
const AppNavigator = StackNavigator(
{
...ExampleRoutes,
Index: {
@@ -322,7 +305,8 @@ const AppNavigator = createStackNavigator(
}
);
export default AppNavigator;
// export default () => <AppNavigator />;
export default SimpleStack;
const styles = StyleSheet.create({
item: {

View File

@@ -4,6 +4,7 @@
import React from 'react';
import {
Button,
Platform,
ScrollView,
StyleSheet,
@@ -19,7 +20,6 @@ import {
TabRouter,
} from 'react-navigation';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView>

View File

@@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import {
Animated,
Button,
Easing,
Image,
Platform,
@@ -16,7 +17,6 @@ import {
createNavigator,
} from 'react-navigation';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
const MyNavScreen = ({ navigation, banner }) => (
<SafeAreaView forceInset={{ top: 'always' }}>

View File

@@ -3,15 +3,14 @@
*/
import React from 'react';
import { Platform, ScrollView, StatusBar } from 'react-native';
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
import {
createStackNavigator,
createDrawerNavigator,
StackNavigator,
DrawerNavigator,
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>
@@ -32,26 +31,6 @@ const InboxScreen = ({ navigation }) => (
<MyNavScreen banner={'Inbox Screen'} navigation={navigation} />
);
InboxScreen.navigationOptions = {
headerTitle: 'Inbox',
};
const EmailScreen = ({ navigation }) => (
<MyNavScreen banner={'Email Screen'} navigation={navigation} />
);
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
@@ -62,19 +41,31 @@ InboxStack.navigationOptions = {
),
};
const DraftsStack = createStackNavigator({
Drafts: { screen: DraftsScreen },
Email: { screen: EmailScreen },
});
const EmailScreen = ({ navigation }) => (
<MyNavScreen banner={'Email Screen'} navigation={navigation} />
);
DraftsStack.navigationOptions = {
const DraftsScreen = ({ navigation }) => (
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
);
DraftsScreen.navigationOptions = {
drawerLabel: 'Drafts',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="drafts" size={24} style={{ color: tintColor }} />
),
};
const DrawerExample = createDrawerNavigator(
const InboxStack = StackNavigator({
Inbox: { screen: InboxScreen },
Email: { screen: EmailScreen },
});
const DraftsStack = StackNavigator({
Drafts: { screen: DraftsScreen },
Email: { screen: EmailScreen },
});
const DrawerExample = DrawerNavigator(
{
Inbox: {
path: '/',

View File

@@ -1,96 +0,0 @@
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',
}
);

View File

@@ -1,63 +0,0 @@
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),
});

View File

@@ -3,10 +3,9 @@
*/
import React from 'react';
import { ScrollView, StatusBar, Text } from 'react-native';
import { SafeAreaView, createStackNavigator } from 'react-navigation';
import { Button, ScrollView, StatusBar, Text } from 'react-native';
import { SafeAreaView, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView>
@@ -32,8 +31,7 @@ const MyNavScreen = ({ navigation, banner }) => (
headerVisible:
!navigation.state.params ||
!navigation.state.params.headerVisible,
})
}
})}
/>
)}
<Button onPress={() => navigation.goBack(null)} title="Go back" />
@@ -59,7 +57,7 @@ MyProfileScreen.navigationOptions = ({ navigation }) => ({
title: `${navigation.state.params.name}'s Profile!`,
});
const ProfileNavigator = createStackNavigator(
const ProfileNavigator = StackNavigator(
{
Home: {
screen: MyHomeScreen,
@@ -89,7 +87,7 @@ MyHeaderTestScreen.navigationOptions = ({ navigation }) => {
};
};
const ModalStack = createStackNavigator(
const ModalStack = StackNavigator(
{
ProfileNavigator: {
screen: ProfileNavigator,

View File

@@ -3,11 +3,10 @@
*/
import React from 'react';
import { Platform, ScrollView, StyleSheet } from 'react-native';
import { createDrawerNavigator } from 'react-navigation';
import { Button, Platform, ScrollView, StyleSheet } from 'react-native';
import { DrawerNavigator } 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}>
@@ -41,7 +40,7 @@ DraftsScreen.navigationOptions = {
),
};
const DrawerExample = createDrawerNavigator(
const DrawerExample = DrawerNavigator(
{
Inbox: {
path: '/',
@@ -60,7 +59,7 @@ const DrawerExample = createDrawerNavigator(
}
);
const MainDrawerExample = createDrawerNavigator({
const MainDrawerExample = DrawerNavigator({
Drafts: {
screen: DrawerExample,
},

View File

@@ -4,42 +4,22 @@
import type {
NavigationScreenProp,
NavigationState,
NavigationStateRoute,
NavigationEventSubscription,
} from 'react-navigation';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import {
createStackNavigator,
SafeAreaView,
withNavigation,
NavigationActions,
StackActions,
} from 'react-navigation';
import invariant from 'invariant';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView, withNavigation } from 'react-navigation';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
type MyNavScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<*>,
banner: React.Node,
};
type BackButtonProps = {
navigation: NavigationScreenProp<NavigationStateRoute>,
};
class MyBackButton extends React.Component<BackButtonProps, any> {
class MyBackButton extends React.Component<any, any> {
render() {
return (
<HeaderButtons>
<HeaderButtons.Item title="Back" onPress={this._navigateBack} />
</HeaderButtons>
);
return <Button onPress={this._navigateBack} title="Custom Back" />;
}
_navigateBack = () => {
@@ -52,55 +32,24 @@ 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={() => push('Profile', { name: 'Jane' })}
onPress={() => navigation.push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
onPress={() =>
navigation.dispatch(
StackActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName: 'Photos',
params: { name: 'Jane' },
}),
],
})
)
}
title="Reset photos"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Navigate to a photos screen"
/>
<Button
onPress={() => replace('Profile', { name: 'Lucy' })}
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => popToTop()} title="Pop to top" />
<Button onPress={() => pop()} title="Pop" />
<Button
onPress={() => {
if (navigation.goBack()) {
console.log('goBack handled');
} else {
console.log('goBack unhandled');
}
}}
title="Go back"
/>
<Button onPress={() => dismiss()} title="Dismiss" />
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
@@ -108,7 +57,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
}
type MyHomeScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<*>,
};
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
@@ -152,7 +101,7 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
}
type MyPhotosScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<*>,
};
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
@@ -193,7 +142,7 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
const { navigation } = this.props;
return (
<MyNavScreen
banner={`${navigation.getParam('name')}'s Photos`}
banner={`${navigation.state.params.name}'s Photos`}
navigation={navigation}
/>
);
@@ -202,9 +151,9 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
const MyProfileScreen = ({ navigation }) => (
<MyNavScreen
banner={`${
navigation.getParam('mode') === 'edit' ? 'Now Editing ' : ''
}${navigation.getParam('name')}'s Profile`}
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
navigation.state.params.name
}'s Profile`}
navigation={navigation}
/>
);
@@ -219,19 +168,17 @@ MyProfileScreen.navigationOptions = props => {
// Render a button on the right side of the header.
// When pressed switches the screen to edit mode.
headerRight: (
<HeaderButtons>
<HeaderButtons.Item
title={params.mode === 'edit' ? 'Done' : 'Edit'}
onPress={() =>
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
}
/>
</HeaderButtons>
<Button
title={params.mode === 'edit' ? 'Done' : 'Edit'}
onPress={() =>
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
}
/>
),
};
};
const SimpleStack = createStackNavigator({
const SimpleStack = StackNavigator({
Home: {
screen: MyHomeScreen,
},

View File

@@ -8,11 +8,10 @@ import type {
} from 'react-navigation';
import React from 'react';
import { Platform, ScrollView, StatusBar, View } from 'react-native';
import { SafeAreaView, createBottomTabNavigator } from 'react-navigation';
import { Button, Platform, ScrollView, StatusBar, View } from 'react-native';
import { SafeAreaView, TabNavigator } 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' }}>
@@ -144,7 +143,7 @@ MySettingsScreen.navigationOptions = {
),
};
const SimpleTabs = createBottomTabNavigator(
const SimpleTabs = TabNavigator(
{
Home: {
screen: MyHomeScreen,
@@ -164,6 +163,8 @@ const SimpleTabs = createBottomTabNavigator(
},
},
{
lazy: true,
removeClippedSubviews: true,
tabBarOptions: {
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
},

View File

@@ -1,145 +0,0 @@
/**
* @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',
},
});

View File

@@ -4,11 +4,8 @@
import type { NavigationScreenProp } from 'react-navigation';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { createStackNavigator, SafeAreaView } from 'react-navigation';
import invariant from 'invariant';
import { Button } from './commonComponents/ButtonWithMargin';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
type NavScreenProps = {
navigation: NavigationScreenProp<*>,
@@ -21,17 +18,15 @@ 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={() => push('ScreenWithNoHeader')}
title="Push screen with no header"
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
@@ -45,20 +40,14 @@ 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={() => push('ScreenWithLongTitle')}
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button
onPress={() => push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
@@ -66,54 +55,10 @@ class OtherScreen extends React.Component<NavScreenProps> {
}
}
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(
const StackWithHeaderPreset = StackNavigator(
{
Home: HomeScreen,
Other: OtherScreen,
ScreenWithNoHeader: ScreenWithNoHeader,
ScreenWithLongTitle: ScreenWithLongTitle,
},
{
headerTransitionPreset: 'uikit',

View File

@@ -12,18 +12,15 @@ 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, createStackNavigator } from 'react-navigation';
import invariant from 'invariant';
import { Header, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
type MyNavScreenProps = {
navigation: NavigationScreenProp<*>,
@@ -33,16 +30,11 @@ 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={() => push('Profile', { name: 'Jane' })}
onPress={() => navigation.push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
@@ -50,11 +42,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
title="Navigate to a photos screen"
/>
<Button
onPress={() => replace('Profile', { name: 'Lucy' })}
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => popToTop()} title="Pop to top" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</ScrollView>
@@ -201,19 +193,17 @@ MyProfileScreen.navigationOptions = props => {
// Render a button on the right side of the header.
// When pressed switches the screen to edit mode.
headerRight: (
<HeaderButtons>
<HeaderButtons.Item
title={params.mode === 'edit' ? 'Done' : 'Edit'}
onPress={() =>
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
}
/>
</HeaderButtons>
<Button
title={params.mode === 'edit' ? 'Done' : 'Edit'}
onPress={() =>
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
}
/>
),
};
};
const StackWithTranslucentHeader = createStackNavigator(
const StackWithTranslucentHeader = StackNavigator(
{
Home: {
screen: MyHomeScreen,

View File

@@ -3,16 +3,11 @@
*/
import React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import {
SafeAreaView,
createStackNavigator,
createBottomTabNavigator,
} from 'react-navigation';
import { Button, ScrollView, StatusBar } from 'react-native';
import { SafeAreaView, StackNavigator, TabNavigator } 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>
@@ -56,7 +51,7 @@ const MySettingsScreen = ({ navigation }) => (
<MyNavScreen banner="Settings Screen" navigation={navigation} />
);
const MainTab = createStackNavigator({
const MainTab = StackNavigator({
Home: {
screen: MyHomeScreen,
path: '/',
@@ -73,7 +68,7 @@ const MainTab = createStackNavigator({
},
});
const SettingsTab = createStackNavigator({
const SettingsTab = StackNavigator({
Settings: {
screen: MySettingsScreen,
path: '/',
@@ -89,7 +84,7 @@ const SettingsTab = createStackNavigator({
},
});
const StacksInTabs = createBottomTabNavigator(
const StacksInTabs = TabNavigator(
{
MainTab: {
screen: MainTab,
@@ -121,9 +116,9 @@ const StacksInTabs = createBottomTabNavigator(
},
},
{
tabBarOptions: {
showLabel: false,
},
tabBarPosition: 'bottom',
animationEnabled: false,
swipeEnabled: false,
}
);

View File

@@ -3,16 +3,11 @@
*/
import React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import {
SafeAreaView,
createStackNavigator,
createBottomTabNavigator,
} from 'react-navigation';
import { Button, ScrollView, StatusBar } from 'react-native';
import { SafeAreaView, StackNavigator, TabNavigator } 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>
@@ -55,7 +50,7 @@ const MySettingsScreen = ({ navigation }) => (
<MyNavScreen banner="Settings Screen" navigation={navigation} />
);
const TabNav = createBottomTabNavigator(
const TabNav = TabNavigator(
{
MainTab: {
screen: MyHomeScreen,
@@ -94,20 +89,7 @@ const TabNav = createBottomTabNavigator(
}
);
TabNav.navigationOptions = ({ navigation }) => {
let { routeName } = navigation.state.routes[navigation.state.index];
let title;
if (routeName === 'SettingsTab') {
title = 'Settings';
} else if (routeName === 'MainTab') {
title = 'Home';
}
return {
title,
};
};
const StacksOverTabs = createStackNavigator({
const StacksOverTabs = StackNavigator({
Root: {
screen: TabNav,
},
@@ -120,9 +102,9 @@ const StacksOverTabs = createStackNavigator({
Profile: {
screen: MyProfileScreen,
path: '/people/:name',
navigationOptions: ({ navigation }) => ({
title: `${navigation.state.params.name}'s Profile!`,
}),
navigationOptions: ({ navigation }) => {
title: `${navigation.state.params.name}'s Profile!`;
},
},
});

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { StatusBar, Text, View } from 'react-native';
import { createStackNavigator } from 'react-navigation';
import { Button } from './commonComponents/ButtonWithMargin';
import { Button, StatusBar, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
class HomeScreen extends React.Component<any, any> {
render() {
@@ -82,7 +81,7 @@ class SettingsScreen extends React.Component<any, any> {
}
}
const Stack = createStackNavigator(
const Stack = StackNavigator(
{
Home: {
screen: HomeScreen,

View File

@@ -1,121 +0,0 @@
/**
* @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,
});

View File

@@ -0,0 +1,127 @@
/**
* @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;

View File

@@ -3,13 +3,13 @@
*/
import React from 'react';
import { Platform, ScrollView } from 'react-native';
import { createDrawerNavigator } from 'react-navigation';
import { Button, Platform, ScrollView } from 'react-native';
import { TabNavigator, DrawerNavigator } from 'react-navigation';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import SimpleTabs from './SimpleTabs';
import StacksOverTabs from './StacksOverTabs';
const TabsInDrawer = createDrawerNavigator({
const TabsInDrawer = DrawerNavigator({
SimpleTabs: {
screen: SimpleTabs,
navigationOptions: {

View File

@@ -3,11 +3,9 @@
*/
import React from 'react';
import { SafeAreaView, StatusBar, Text, View } from 'react-native';
import { withNavigationFocus } from 'react-navigation';
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
import { Button, SafeAreaView, Text } from 'react-native';
import { TabNavigator, withNavigationFocus } from 'react-navigation';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Button } from './commonComponents/ButtonWithMargin';
import SampleText from './SampleText';
@@ -67,10 +65,9 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
/>
)}
<Button
onPress={() => this.props.navigation.pop()}
onPress={() => this.props.navigation.goBack(null)}
title="Back to other examples"
/>
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
@@ -78,7 +75,7 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
return withNavigationFocus(TabScreen);
};
const TabsWithNavigationFocus = createMaterialBottomTabNavigator(
const TabsWithNavigationFocus = TabNavigator(
{
One: {
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
@@ -95,8 +92,9 @@ const TabsWithNavigationFocus = createMaterialBottomTabNavigator(
},
},
{
shifting: false,
activeTintColor: '#F44336',
tabBarPosition: 'bottom',
animationEnabled: true,
swipeEnabled: true,
}
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,18 +0,0 @@
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,
},
}),
},
});

View File

@@ -1,16 +0,0 @@
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}
/>
);
}
}

View File

@@ -8,26 +8,23 @@
"eject": "react-native-scripts eject",
"android": "react-native-scripts android",
"ios": "react-native-scripts ios",
"test": "flow"
"test": "node node_modules/jest/bin/jest.js && flow"
},
"dependencies": {
"expo": "^27.0.0",
"invariant": "^2.2.4",
"react": "16.3.1",
"react-native": "^0.55.0",
"expo": "^25.0.0",
"react": "16.2.0",
"react-native": "^0.52.0",
"react-native-iphone-x-helper": "^1.0.2",
"react-navigation": "link:../..",
"react-navigation-header-buttons": "^0.0.4",
"react-navigation-material-bottom-tabs": "0.1.3"
"react-navigation": "link:../.."
},
"devDependencies": {
"babel-jest": "^22.4.1",
"babel-jest": "^21.0.0",
"babel-plugin-transform-remove-console": "^6.9.0",
"flow-bin": "^0.67.0",
"jest": "^22.1.3",
"jest-expo": "^26.0.0",
"flow-bin": "^0.61.0",
"jest": "^21.0.1",
"jest-expo": "^25.1.0",
"react-native-scripts": "^1.5.0",
"react-test-renderer": "16.3.0-alpha.1"
"react-test-renderer": "16.0.0"
},
"jest": {
"preset": "jest-expo",

View File

@@ -11,9 +11,7 @@ module.exports = {
return blacklist([
/react\-navigation\/examples\/(?!NavigationPlayground).*/,
/react\-navigation\/node_modules\/react-native\/(.*)/,
/react\-navigation\/node_modules\/react\/(.*)/,
/react\-navigation\/node_modules\/react-native-paper\/(.*)/,
/react\-navigation\/node_modules\/@expo\/vector-icons\/(.*)/,
/react\-navigation\/node_modules\/react\/(.*)/
]);
},
extraNodeModules: getNodeModulesForDirectory(path.resolve('.')),

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,4 @@
## Usage
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.

View File

@@ -31,8 +31,8 @@
"redux": "^3.7.2"
},
"devDependencies": {
"babel-jest": "^22.4.1",
"jest": "^22.1.3",
"babel-jest": "^21.0.0",
"jest": "^21.0.1",
"jest-expo": "^25.1.0",
"react-native-scripts": "^1.3.1",
"react-test-renderer": "16.0.0"

View File

@@ -1,15 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStackNavigator } from 'react-navigation';
import { initializeListeners } from 'react-navigation-redux-helpers';
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
import ProfileScreen from '../components/ProfileScreen';
import { navigationPropConstructor } from '../utils/redux';
import { addListener } from '../utils/redux';
export const AppNavigator = createStackNavigator({
export const AppNavigator = StackNavigator({
Login: { screen: LoginScreen },
Main: { screen: MainScreen },
Profile: { screen: ProfileScreen },
@@ -21,14 +20,17 @@ class AppWithNavigationState extends React.Component {
nav: PropTypes.object.isRequired,
};
componentDidMount() {
initializeListeners('root', this.props.nav);
}
render() {
const { dispatch, nav } = this.props;
const navigation = navigationPropConstructor(dispatch, nav);
return <AppNavigator navigation={navigation} />;
return (
<AppNavigator
navigation={addNavigationHelpers({
dispatch,
state: nav,
addListener,
})}
/>
);
}
}

View File

@@ -1,12 +1,15 @@
import {
createReactNavigationReduxMiddleware,
createNavigationPropConstructor,
createReduxBoundAddListener,
} from 'react-navigation-redux-helpers';
const middleware = createReactNavigationReduxMiddleware(
'root',
state => state.nav
"root",
state => state.nav,
);
const navigationPropConstructor = createNavigationPropConstructor('root');
const addListener = createReduxBoundAddListener("root");
export { middleware, navigationPropConstructor };
export {
middleware,
addListener,
};

File diff suppressed because it is too large Load Diff

View File

@@ -69,14 +69,6 @@ declare module 'react-navigation' {
[key: string]: mixed,
};
declare export type NavigationBackAction = {|
type: 'Navigation/BACK',
key?: ?string,
|};
declare export type NavigationInitAction = {|
type: 'Navigation/INIT',
params?: NavigationParams,
|};
declare export type NavigationNavigateAction = {|
type: 'Navigation/NAVIGATE',
routeName: string,
@@ -87,6 +79,12 @@ declare module 'react-navigation' {
key?: string,
|};
declare export type NavigationBackAction = {|
type: 'Navigation/BACK',
key?: ?string,
|};
declare export type NavigationSetParamsAction = {|
type: 'Navigation/SET_PARAMS',
@@ -97,6 +95,30 @@ declare module 'react-navigation' {
params: NavigationParams,
|};
declare export type NavigationInitAction = {|
type: 'Navigation/INIT',
params?: NavigationParams,
|};
declare export type NavigationResetAction = {|
type: 'Navigation/RESET',
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
|};
declare export type NavigationUriAction = {|
type: 'Navigation/URI',
uri: string,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationPopAction = {|
+type: 'Navigation/POP',
+n?: number,
@@ -113,51 +135,17 @@ declare module 'react-navigation' {
+action?: NavigationNavigateAction,
+key?: string,
|};
declare export type NavigationResetAction = {|
type: 'Navigation/RESET',
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationCompleteTransitionAction = {|
+type: 'Navigation/COMPLETE_TRANSITION',
+key?: string,
|};
declare export type NavigationOpenDrawerAction = {|
+type: 'Navigation/OPEN_DRAWER',
+key?: string,
|};
declare export type NavigationCloseDrawerAction = {|
+type: 'Navigation/CLOSE_DRAWER',
+key?: string,
|};
declare export type NavigationToggleDrawerAction = {|
+type: 'Navigation/TOGGLE_DRAWER',
+key?: string,
|};
declare export type NavigationAction =
| NavigationBackAction
| NavigationInitAction
| NavigationNavigateAction
| NavigationSetParamsAction
| NavigationReplaceAction
| NavigationPopAction
| NavigationPopToTopAction
| NavigationPushAction
| NavigationResetAction
| NavigationReplaceAction
| NavigationCompleteTransitionAction
| NavigationOpenDrawerAction
| NavigationCloseDrawerAction
| NavigationToggleDrawerAction;
| NavigationBackAction
| NavigationSetParamsAction
| NavigationResetAction;
/**
* NavigationState is a tree of routes for a single navigator, where each
@@ -281,33 +269,24 @@ declare module 'react-navigation' {
declare export type NavigationComponent =
| NavigationScreenComponent<NavigationRoute, *, *>
| NavigationContainer<*, *, *>;
declare interface withOptionalNavigationOptions<Options> {
navigationOptions?: NavigationScreenConfig<Options>,
}
| NavigationContainer<*, *, *>
| any;
declare export type NavigationScreenComponent<
Route: NavigationRoute,
Options: {},
Props: {}
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, Route>,
}> & withOptionalNavigationOptions<Options>;
declare interface withRouter<State, Options> {
router: NavigationRouter<State, Options>,
}
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
({} | { navigationOptions: NavigationScreenConfig<Options> });
declare export type NavigationNavigator<
State: NavigationState,
Options: {},
Props: {}
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, State>,
}> & withRouter<State, Options> & withOptionalNavigationOptions<Options>;
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
router: NavigationRouter<State, Options>,
navigationOptions?: ?NavigationScreenConfig<Options>,
};
declare export type NavigationRouteConfig =
| NavigationComponent
@@ -317,6 +296,7 @@ declare module 'react-navigation' {
} & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig =
| NavigationComponent
| {
screen: NavigationComponent,
}
@@ -364,7 +344,7 @@ declare module 'react-navigation' {
headerTintColor?: string,
headerLeft?: React$Node | React$ElementType,
headerBackTitle?: string,
headerBackImage?: React$Node | React$ElementType,
headerBackImage?: ImageSource,
headerTruncatedBackTitle?: string,
headerBackTitleStyle?: TextStyleProp,
headerPressColorAndroid?: string,
@@ -400,20 +380,6 @@ 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
*/
@@ -425,6 +391,7 @@ 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`
|};
@@ -447,9 +414,10 @@ declare module 'react-navigation' {
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
tabBarVisible?: boolean,
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
tabBarOnPress?: ({
navigation: NavigationScreenProp<NavigationRoute>,
}) => void,
tabBarOnPress?: (
scene: TabScene,
jumpToIndex: (index: number) => void
) => void,
|};
/**
@@ -503,37 +471,31 @@ declare module 'react-navigation' {
};
declare export type NavigationScreenProp<+S> = {
...$ObjMap<
_DefaultActionCreators,
<Args>((...args: Args) => *) => (...args: Args) => boolean
>,
+state: S,
dispatch: NavigationDispatch,
goBack: (routeKey?: ?string) => boolean,
navigate: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
setParams: (newParams: NavigationParams) => boolean,
addListener: (
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
getParam: (paramName: string, fallback?: any) => any,
isFocused: () => boolean,
// StackRouter action creators
pop?: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop?: (params?: { immediate?: boolean }) => boolean,
push?: (
push: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace?: (
replace: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
reset?: (actions: NavigationAction[], index: number) => boolean,
dismiss?: () => boolean,
// DrawerRouter action creators
openDrawer?: () => boolean,
closeDrawer?: () => boolean,
toggleDrawer?: () => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
};
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
@@ -550,21 +512,19 @@ declare module 'react-navigation' {
State: NavigationState,
Options: {},
Props: {}
> = React$ComponentType<{
...Props,
...NavigationContainerProps<State, Options>,
}> & withRouter<State, Options> & withOptionalNavigationOptions<Options>;
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
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,
}>;
@@ -722,95 +682,46 @@ declare module 'react-navigation' {
BACK: 'Navigation/BACK',
INIT: 'Navigation/INIT',
NAVIGATE: 'Navigation/NAVIGATE',
SET_PARAMS: 'Navigation/SET_PARAMS',
back: (payload?: { key?: ?string }) => NavigationBackAction,
init: (payload?: { params?: NavigationParams }) => NavigationInitAction,
navigate: (payload: {
routeName: string,
params?: ?NavigationParams,
action?: ?NavigationNavigateAction,
key?: string,
}) => NavigationNavigateAction,
setParams: (payload: {
key: string,
params: NavigationParams,
}) => NavigationSetParamsAction,
};
declare export var StackActions: {
POP: 'Navigation/POP',
POP_TO_TOP: 'Navigation/POP_TO_TOP',
PUSH: 'Navigation/PUSH',
RESET: 'Navigation/RESET',
REPLACE: 'Navigation/REPLACE',
COMPLETE_TRANSITION: 'Navigation/COMPLETE_TRANSITION',
pop: (payload: {
n?: number,
immediate?: boolean,
}) => NavigationPopAction,
popToTop: (payload: {
immediate?: boolean,
}) => NavigationPopToTopAction,
push: (payload: {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
}) => NavigationPushAction,
reset: (payload: {
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
}) => NavigationResetAction,
replace: (payload: {
key: string,
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
}) => NavigationReplaceAction,
completeTransition: (payload: {
key?: string,
}) => NavigationCompleteTransitionAction,
SET_PARAMS: 'Navigation/SET_PARAMS',
URI: 'Navigation/URI',
back: {
(payload?: { key?: ?string }): NavigationBackAction,
toString: () => string,
},
init: {
(payload?: { params?: NavigationParams }): NavigationInitAction,
toString: () => string,
},
navigate: {
(payload: {
routeName: string,
params?: ?NavigationParams,
action?: ?NavigationNavigateAction,
}): NavigationNavigateAction,
toString: () => string,
},
reset: {
(payload: {
index: number,
key?: ?string,
actions: Array<NavigationNavigateAction>,
}): NavigationResetAction,
toString: () => string,
},
setParams: {
(payload: {
key: string,
params: NavigationParams,
}): NavigationSetParamsAction,
toString: () => string,
},
uri: {
(payload: { uri: string }): NavigationUriAction,
toString: () => string,
},
};
declare export var DrawerActions: {
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
openDrawer: (payload: {
key?: string,
}) => NavigationOpenDrawerAction,
closeDrawer: (payload: {
key?: string,
}) => NavigationCloseDrawerAction,
toggleDrawer: (payload: {
key?: string,
}) => NavigationToggleDrawerAction,
};
declare type _DefaultActionCreators = {|
goBack: (routeKey?: ?string) => NavigationBackAction,
navigate: (
routeName:
| string
| {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
},
params?: NavigationParams,
action?: NavigationNavigateAction
) => NavigationNavigateAction,
setParams: (newParams: NavigationParams) => NavigationSetParamsAction,
|};
declare export function getNavigationActionCreators(
route: NavigationRoute | NavigationState
): _DefaultActionCreators;
declare type _RouterProp<S: NavigationState, O: {}> = {
router: NavigationRouter<S, O>,
};
@@ -831,16 +742,12 @@ declare module 'react-navigation' {
view: NavigationView<O, S>,
router: NavigationRouter<S, O>,
navigatorConfig?: NavigatorConfig
): NavigationNavigator<S, O, *>;
): any;
declare export function StackNavigator(
routeConfigMap: NavigationRouteConfigMap,
stackConfig?: StackNavigatorConfig
): NavigationContainer<*, *, *>;
declare export function createStackNavigator(
routeConfigMap: NavigationRouteConfigMap,
stackConfig?: StackNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _TabViewConfig = {|
tabBarComponent?: React$ElementType,
@@ -865,30 +772,6 @@ 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',
@@ -910,10 +793,6 @@ declare module 'react-navigation' {
routeConfigs: NavigationRouteConfigMap,
config?: _DrawerNavigatorConfig
): NavigationContainer<*, *, *>;
declare export function createDrawerNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _DrawerNavigatorConfig
): NavigationContainer<*, *, *>;
declare export function StackRouter(
routeConfigs: NavigationRouteConfigMap,
@@ -999,14 +878,12 @@ 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> & {
HEIGHT: number,
};
declare export var Header: React$ComponentType<HeaderProps>;
declare type _HeaderTitleProps = {
children: React$Node,
@@ -1149,17 +1026,13 @@ declare module 'react-navigation' {
};
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
declare export function withNavigation<Props: {}>(
Component: React$ComponentType<Props>
): React$ComponentType<
$Diff<
Props,
{
navigation: NavigationScreenProp<NavigationStateRoute> | void,
}
>
>;
declare export function withNavigationFocus<Props: {}>(
Component: React$ComponentType<Props>
): React$ComponentType<$Diff<Props, { isFocused: boolean | void }>>;
declare 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>;
}

View File

@@ -1,13 +1,14 @@
{
"name": "react-navigation",
"version": "2.3.0-beta.1",
"version": "1.2.1",
"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>, Brent Vatne <brent@expo.io>",
"author":
"Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>",
"license": "BSD-2-Clause",
"scripts": {
"start": "npm run ios",
@@ -21,44 +22,38 @@
"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-safe-area-view": "^0.8.0",
"react-navigation-deprecated-tab-navigator": "1.3.0",
"react-navigation-drawer": "0.3.0",
"react-navigation-tabs": "0.5.1"
"react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.7.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-eslint": "^7.2.3",
"babel-jest": "^22.4.1",
"babel-jest": "^20.0.3",
"babel-preset-react-native": "^2.1.0",
"codecov": "^2.2.0",
"eslint": "^4.2.0",
"eslint-config-prettier": "^2.9.0",
"eslint-config-prettier": "^2.3.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-prettier": "^2.1.2",
"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.12.1",
"prettier-eslint": "^8.8.1",
"prettier": "^1.5.3",
"prettier-eslint": "^6.4.2",
"react": "16.2.0",
"react-native": "^0.52.0",
"react-native-vector-icons": "^4.2.0",
@@ -67,30 +62,14 @@
"jest": {
"notify": true,
"preset": "react-native",
"testRegex": "/__tests__/[^/]+-test\\.js$",
"setupFiles": [
"<rootDir>/jest-setup.js"
],
"testRegex": "./src/.*\\-test\\.js$",
"setupFiles": ["<rootDir>/jest-setup.js"],
"coverageDirectory": "./coverage/",
"collectCoverage": true,
"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)"
]
"coverageReporters": ["lcov"],
"collectCoverageFrom": ["src/**/*.js"],
"coveragePathIgnorePatterns": ["jest-setup.js"],
"modulePathIgnorePatterns": ["examples"]
},
"lint-staged": {
"*.js": [

View File

@@ -4,6 +4,6 @@ set -eo pipefail
case $CIRCLE_NODE_INDEX in
0) yarn test && yarn codecov ;;
#1) cd examples/NavigationPlayground && yarn && yarn test ;;
1) yarn link && cd examples/NavigationPlayground && yarn && yarn link react-navigation && yarn test ;;
#2) cd examples/ReduxExample && yarn && yarn test ;;
esac

View File

@@ -1,15 +1,30 @@
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 back = (payload = {}) => ({
const createAction = (type, fn) => {
fn.toString = () => type;
return fn;
};
const back = createAction(BACK, (payload = {}) => ({
type: BACK,
key: payload.key,
immediate: payload.immediate,
});
}));
const init = (payload = {}) => {
const init = createAction(INIT, (payload = {}) => {
const action = {
type: INIT,
};
@@ -17,9 +32,9 @@ const init = (payload = {}) => {
action.params = payload.params;
}
return action;
};
});
const navigate = payload => {
const navigate = createAction(NAVIGATE, payload => {
const action = {
type: NAVIGATE,
routeName: payload.routeName,
@@ -34,24 +49,107 @@ const navigate = payload => {
action.key = payload.key;
}
return action;
};
});
const setParams = payload => ({
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 => ({
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,
};

View File

@@ -21,7 +21,7 @@ const StateUtils = {
* routes of the navigation state, or -1 if it is not present.
*/
indexOf(state, key) {
return state.routes.findIndex(route => route.key === key);
return state.routes.map(route => route.key).indexOf(key);
},
/**
@@ -116,23 +116,8 @@ const StateUtils = {
/**
* Replace a route by a key.
* Note that this moves the index to the position to where the new route in the
* stack is at 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.
* Note that this moves the index to the positon to where the new route in the
* stack is at.
*/
replaceAt(state, key, route) {
const index = StateUtils.indexOf(state, key);
@@ -152,7 +137,7 @@ const StateUtils = {
route.key
);
if (state.routes[index] === route && index === state.index) {
if (state.routes[index] === route) {
return state;
}
@@ -168,7 +153,7 @@ const StateUtils = {
/**
* Resets all routes.
* Note that this moves the index to the position to where the last route in the
* Note that this moves the index to the positon to where the last route in the
* stack is at if the param `index` isn't provided.
*/
reset(state, routes, index) {

View File

@@ -1,10 +1,11 @@
import NavigationActions from '../NavigationActions';
describe('generic navigation actions', () => {
describe('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,
@@ -13,6 +14,7 @@ describe('generic navigation 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,
@@ -21,6 +23,9 @@ describe('generic navigation 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',
@@ -42,7 +47,36 @@ describe('generic navigation 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',
@@ -54,4 +88,12 @@ describe('generic navigation 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',
});
});
});

View File

@@ -1,50 +1,46 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import 'react-native';
import renderer from 'react-test-renderer';
import NavigationActions from '../NavigationActions';
import createStackNavigator from '../navigators/createStackNavigator';
import { _TESTING_ONLY_reset_container_count } from '../createNavigationContainer';
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();
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
@@ -202,57 +198,4 @@ 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();
});
});
});
});

View File

@@ -8,7 +8,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.get(state, 'a')).toEqual({
key: 'a',
@@ -21,7 +20,6 @@ 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);
@@ -32,7 +30,6 @@ 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);
@@ -43,11 +40,9 @@ 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(
@@ -59,7 +54,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(() =>
NavigationStateUtils.push(state, { key: 'a', routeName })
@@ -71,12 +65,10 @@ 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);
});
@@ -85,7 +77,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.pop(state)).toBe(state);
});
@@ -95,12 +86,10 @@ 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);
@@ -110,7 +99,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
});
@@ -119,12 +107,10 @@ 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);
@@ -134,7 +120,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
});
@@ -143,12 +128,10 @@ 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);
@@ -158,12 +141,10 @@ 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);
@@ -174,12 +155,10 @@ 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 })
@@ -190,27 +169,24 @@ 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 with updated index if route is unchanged but index changes', () => {
it('Returns the state if index matches the route', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
).toEqual({ ...state, index: 1 });
).toEqual(state);
});
// Reset
@@ -218,12 +194,10 @@ 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, [
@@ -241,12 +215,10 @@ 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(

View File

@@ -1,13 +0,0 @@
// 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",
],
],
},
}
`;

View File

@@ -0,0 +1,118 @@
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);
});
});

View File

@@ -1,102 +0,0 @@
import getNavigation from '../getNavigation';
test('getNavigation provides default action helpers', () => {
const router = {
getActionCreators: () => ({}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const dispatch = jest.fn();
const topNav = getNavigation(
router,
{},
dispatch,
new Set(),
() => ({}),
() => topNav
);
topNav.navigate('GreatRoute');
expect(dispatch.mock.calls.length).toBe(1);
expect(dispatch.mock.calls[0][0].type).toBe('Navigation/NAVIGATE');
expect(dispatch.mock.calls[0][0].routeName).toBe('GreatRoute');
});
test('getNavigation provides router action helpers', () => {
const router = {
getActionCreators: () => ({
foo: bar => ({ type: 'FooBarAction', bar }),
}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const dispatch = jest.fn();
const topNav = getNavigation(
router,
{},
dispatch,
new Set(),
() => ({}),
() => topNav
);
topNav.foo('Great');
expect(dispatch.mock.calls.length).toBe(1);
expect(dispatch.mock.calls[0][0].type).toBe('FooBarAction');
expect(dispatch.mock.calls[0][0].bar).toBe('Great');
});
test('getNavigation get child navigation with router', () => {
const actionSubscribers = new Set();
let navigation = null;
const routerA = {
getActionCreators: () => ({}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const router = {
childRouters: {
RouteA: routerA,
},
getActionCreators: () => ({}),
getStateForAction(action, lastState = {}) {
return lastState;
},
};
const initState = {
index: 0,
routes: [
{
key: 'a',
routeName: 'RouteA',
routes: [{ key: 'c', routeName: 'RouteC' }],
index: 0,
},
{ key: 'b', routeName: 'RouteB' },
],
};
const topNav = getNavigation(
router,
initState,
() => {},
actionSubscribers,
() => ({}),
() => navigation
);
const childNavA = topNav.getChildNavigation('a');
expect(childNavA.router).toBe(routerA);
});

View File

@@ -0,0 +1,93 @@
/* 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()),
};
}

View File

@@ -1,54 +1,9 @@
import React from 'react';
import { AsyncStorage, Linking, Platform } from 'react-native';
import { polyfill } from 'react-lifecycles-compat';
import { Linking } from 'react-native';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
import getNavigation from './getNavigation';
import addNavigationHelpers from './addNavigationHelpers';
import invariant from './utils/invariant';
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
@@ -63,17 +18,12 @@ 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);
validateProps(props);
this._validateProps(props);
this._initialAction = NavigationActions.init();
@@ -91,21 +41,14 @@ export default function createNavigationContainer(Component) {
}
this.state = {
nav:
this._isStateful() && !props.persistenceKey
? Component.router.getStateForAction(this._initialAction)
: null,
nav: this._isStateful()
? Component.router.getStateForAction(this._initialAction)
: null,
};
}
_renderLoading() {
return this.props.renderLoadingExperimental
? this.props.renderLoadingExperimental()
: null;
}
_isStateful() {
return isStateful(this.props);
return !this.props.navigation;
}
_validateProps(props) {
@@ -184,198 +127,98 @@ export default function createNavigationContainer(Component) {
}
}
componentWillReceiveProps(nextProps) {
this._validateProps(nextProps);
}
componentDidUpdate() {
// Clear cached _navState every tick
if (this._navState === this.state.nav) {
this._navState = null;
// Clear cached _nav every tick
if (this._nav === this.state.nav) {
this._nav = null;
}
}
async componentDidMount() {
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);
// 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);
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
// Initialize state. This must be done *after* any async code
// so we don't end up with a different value for this.state.nav
// due to changes while async function was resolving
let action = this._initialAction;
let startupState = this.state.nav;
if (!startupState) {
!!process.env.REACT_NAV_LOGGING &&
console.log('Init new Navigation State');
startupState = Component.router.getStateForAction(action);
}
// Pull persisted state from AsyncStorage
if (startupStateJSON) {
try {
startupState = JSON.parse(startupStateJSON);
_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();
});
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action: this._initialAction,
state: this.state.nav,
lastState: null,
})
);
}
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.props.navigation) {
return this.props.navigation.dispatch(action);
if (!this._isStateful()) {
return false;
}
// 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;
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);
const dispatchActionEvents = () => {
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action,
state: navState,
lastState: lastNavState,
state: nav,
lastState: 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) {
if (nav && nav !== oldNav) {
// Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
this._navState = navState;
this.setState({ nav: navState }, () => {
this._onNavigationStateChange(lastNavState, navState, action);
this._nav = nav;
this.setState({ nav }, () => {
this._onNavigationStateChange(oldNav, nav, action);
dispatchActionEvents();
this._persistNavigationState(navState);
});
return true;
} else {
dispatchActionEvents();
}
dispatchActionEvents();
return false;
};
_getScreenProps = () => this.props.screenProps;
render() {
let navigation = this.props.navigation;
if (this._isStateful()) {
const navState = this.state.nav;
if (!navState) {
return this._renderLoading();
}
if (!this._navigation || this._navigation.state !== navState) {
this._navigation = getNavigation(
Component.router,
navState,
this.dispatch,
this._actionEventSubscribers,
this._getScreenProps,
() => this._navigation
);
const nav = this.state.nav;
invariant(nav, 'should be set in constructor if stateful');
if (!this._navigation || this._navigation.state !== nav) {
this._navigation = addNavigationHelpers({
dispatch: this.dispatch,
state: nav,
addListener: (eventName, handler) => {
if (eventName !== 'action') {
return { remove: () => {} };
}
this._actionEventSubscribers.add(handler);
return {
remove: () => {
this._actionEventSubscribers.delete(handler);
},
};
},
});
}
navigation = this._navigation;
}
@@ -384,5 +227,5 @@ export default function createNavigationContainer(Component) {
}
}
return polyfill(NavigationContainer);
return NavigationContainer;
}

View File

@@ -84,7 +84,7 @@ export default function getChildEventSubscriber(addListener, key) {
action,
type: eventName,
};
const isTransitioning = !!state && state.isTransitioning;
const isTransitioning = !!state && !!state.transitioningFromKey;
const previouslyLastEmittedEvent = lastEmittedEvent;
@@ -138,14 +138,11 @@ 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) {

View File

@@ -1,102 +0,0 @@
import getChildEventSubscriber from './getChildEventSubscriber';
import getChildRouter from './getChildRouter';
import invariant from './utils/invariant';
const createParamGetter = route => (paramName, defaultValue) => {
const params = route.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
};
function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
const children =
navigation._childrenNavigation || (navigation._childrenNavigation = {});
const childRoute = navigation.state.routes.find(r => r.key === childKey);
if (children[childKey] && children[childKey].state === childRoute) {
return children[childKey];
}
const childRouter = getChildRouter(navigation.router, childRoute.routeName);
// If the route has children, we'll use this to pass in to the action creators
// for the childRouter so that any action that depends on the active route will
// behave as expected. We don't explicitly require that routers implement routes
// and index properties, but if we did then we would put an invariant here to
// ensure that a focusedGrandChildRoute exists if childRouter is defined.
const focusedGrandChildRoute =
childRoute.routes && typeof childRoute.index === 'number'
? childRoute.routes[childRoute.index]
: null;
const actionCreators = {
...navigation.actions,
...navigation.router.getActionCreators(childRoute, navigation.state.key),
...(childRouter
? childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key)
: {}),
};
const actionHelpers = {};
Object.keys(actionCreators).forEach(actionName => {
actionHelpers[actionName] = (...args) => {
const actionCreator = actionCreators[actionName];
const action = actionCreator(...args);
return navigation.dispatch(action);
};
});
if (children[childKey]) {
children[childKey] = {
...children[childKey],
...actionHelpers,
state: childRoute,
router: childRouter,
actions: actionCreators,
getParam: createParamGetter(childRoute),
};
return children[childKey];
}
const childSubscriber = getChildEventSubscriber(
navigation.addListener,
childKey
);
children[childKey] = {
...actionHelpers,
state: childRoute,
router: childRouter,
actions: actionCreators,
getParam: createParamGetter(childRoute),
getChildNavigation: grandChildKey =>
getChildNavigation(children[childKey], grandChildKey, () =>
getCurrentParentNavigation().getChildNavigation(childKey)
),
isFocused: () => {
const currentNavigation = getCurrentParentNavigation();
const { routes, index } = currentNavigation.state;
if (!currentNavigation.isFocused()) {
return false;
}
if (routes[index].key === childKey) {
return true;
}
return false;
},
dispatch: navigation.dispatch,
getScreenProps: navigation.getScreenProps,
dangerouslyGetParent: getCurrentParentNavigation,
addListener: childSubscriber.addListener,
};
return children[childKey];
}
export default getChildNavigation;

View File

@@ -1,9 +0,0 @@
export default function getChildRouter(router, routeName) {
if (router.childRouters && router.childRouters[routeName]) {
return router.childRouters[routeName];
}
const Component = router.getComponentForRouteName(routeName);
return Component.router;
}

View File

@@ -1,54 +0,0 @@
import getNavigationActionCreators from './routers/getNavigationActionCreators';
import getChildNavigation from './getChildNavigation';
export default function getNavigation(
router,
state,
dispatch,
actionSubscribers,
getScreenProps,
getCurrentNavigation
) {
const actions = router.getActionCreators(state, null);
const navigation = {
actions,
router,
state,
dispatch,
getScreenProps,
getChildNavigation: childKey =>
getChildNavigation(navigation, childKey, getCurrentNavigation),
isFocused: childKey => {
const { routes, index } = getCurrentNavigation().state;
if (childKey == null || routes[index].key === childKey) {
return true;
}
return false;
},
addListener: (eventName, handler) => {
if (eventName !== 'action') {
return { remove: () => {} };
}
actionSubscribers.add(handler);
return {
remove: () => {
actionSubscribers.delete(handler);
},
};
},
dangerouslyGetParent: () => null,
};
const actionCreators = {
...getNavigationActionCreators(navigation.state),
...actions,
};
Object.keys(actionCreators).forEach(actionName => {
navigation[actionName] = (...args) =>
navigation.dispatch(actionCreators[actionName](...args));
});
return navigation;
}

View File

@@ -0,0 +1,71 @@
import React from 'react';
import { Dimensions, Platform, ScrollView } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
import DrawerRouter from '../routers/DrawerRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)
// and the view's props
const defaultContentComponent = props => (
<ScrollView alwaysBounceVertical={false}>
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
<DrawerItems {...props} />
</SafeAreaView>
</ScrollView>
);
const DefaultDrawerConfig = {
drawerWidth: () => {
/*
* Default drawer width is screen width - header height
* with a max width of 280 on mobile and 320 on tablet
* https://material.io/guidelines/patterns/navigation-drawer.html
*/
const { height, width } = Dimensions.get('window');
const smallerAxisSize = Math.min(height, width);
const isLandscape = width > height;
const isTablet = smallerAxisSize >= 600;
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
const maxWidth = isTablet ? 320 : 280;
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
},
contentComponent: defaultContentComponent,
drawerPosition: 'left',
drawerBackgroundColor: 'white',
useNativeAnimations: true,
};
const DrawerNavigator = (routeConfigs, config = {}) => {
const mergedConfig = { ...DefaultDrawerConfig, ...config };
const {
order,
paths,
initialRouteName,
backBehavior,
...drawerConfig
} = mergedConfig;
const routerConfig = {
order,
paths,
initialRouteName,
backBehavior,
};
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
return createNavigationContainer(navigator);
};
export default DrawerNavigator;

View File

@@ -0,0 +1,33 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import DrawerNavigator from '../DrawerNavigator';
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: `Welcome ${
navigation.state.params ? navigation.state.params.user : 'anonymous'
}`,
gesturesEnabled: true,
});
render() {
return null;
}
}
const routeConfig = {
Home: {
screen: HomeScreen,
},
};
describe('DrawerNavigator', () => {
it('renders successfully', () => {
const MyDrawerNavigator = DrawerNavigator(routeConfig);
const rendered = renderer.create(<MyDrawerNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
});

View File

@@ -1,40 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import StackNavigator from '../createStackNavigator';
const SubNavigator = StackNavigator({
Home: {
screen: () => null,
},
});
const NavNestedDirect = StackNavigator({
Sub: {
screen: SubNavigator,
},
});
const NavNestedIndirect = StackNavigator({
Sub: {
// eslint-disable-next-line react/display-name
screen: props => <SubNavigator {...props} />,
},
});
/* Prevent React error boundaries from swallowing the errors */
NavNestedIndirect.prototype.componentDidCatch = null;
SubNavigator.prototype.componentDidCatch = null;
describe('Nested navigators', () => {
it('renders succesfully as direct child', () => {
const rendered = renderer.create(<NavNestedDirect />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('throw when trying to pass navigation prop', () => {
const tryRender = () => {
renderer.create(<NavNestedIndirect />);
};
expect(tryRender).toThrowErrorMatchingSnapshot();
});
});

View File

@@ -3,8 +3,6 @@ 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: {
@@ -33,10 +31,6 @@ const routeConfig = {
};
describe('StackNavigator', () => {
beforeEach(() => {
_TESTING_ONLY_reset_container_count();
});
it('renders successfully', () => {
const MyStackNavigator = StackNavigator(routeConfig);
const rendered = renderer.create(<MyStackNavigator />).toJSON();
@@ -57,37 +51,4 @@ 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),
})
);
});
});

View File

@@ -1,18 +0,0 @@
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();
});
});

View File

@@ -2,9 +2,7 @@ import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
const {
createTabNavigator,
} = require('react-navigation-deprecated-tab-navigator');
import TabNavigator from '../createTabNavigator';
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
@@ -27,7 +25,7 @@ const routeConfig = {
describe('TabNavigator', () => {
it('renders successfully', () => {
const MyTabNavigator = createTabNavigator(routeConfig);
const MyTabNavigator = TabNavigator(routeConfig);
const rendered = renderer.create(<MyTabNavigator />).toJSON();
expect(rendered).toMatchSnapshot();

View File

@@ -0,0 +1,243 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DrawerNavigator renders successfully 1`] = `
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Object {
"backgroundColor": "transparent",
"flex": 1,
}
}
>
<View
collapsable={undefined}
style={
Object {
"flex": 1,
"zIndex": 0,
}
}
/>
<View
accessibilityComponentType={undefined}
accessibilityLabel={undefined}
accessibilityTraits={undefined}
accessible={true}
collapsable={undefined}
hitSlop={undefined}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
pointerEvents="none"
style={
Object {
"backgroundColor": "#000",
"bottom": 0,
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"zIndex": 1000,
}
}
testID={undefined}
/>
<View
accessibilityViewIsModal={false}
collapsable={undefined}
style={
Object {
"backgroundColor": "white",
"bottom": 0,
"left": 0,
"position": "absolute",
"right": null,
"top": 0,
"transform": Array [
Object {
"translateX": -320,
},
],
"width": 320,
"zIndex": 1001,
}
}
>
<View
style={
Array [
Object {
"flex": 1,
},
undefined,
]
}
>
<RCTScrollView
DEPRECATED_sendUpdatedChildFrames={false}
alwaysBounceHorizontal={undefined}
alwaysBounceVertical={false}
onContentSizeChange={null}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onResponderGrant={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={undefined}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
scrollEventThrottle={undefined}
sendMomentumEvents={false}
style={
Array [
Object {
"flexDirection": "column",
"flexGrow": 1,
"flexShrink": 1,
"overflow": "scroll",
},
undefined,
]
}
>
<RCTScrollContentView
collapsable={false}
removeClippedSubviews={undefined}
style={
Array [
undefined,
undefined,
]
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Array [
Object {
"paddingVertical": 4,
},
undefined,
]
}
>
<View
accessibilityComponentType={undefined}
accessibilityLabel={undefined}
accessibilityTraits={undefined}
accessible={true}
collapsable={undefined}
hasTVPreferredFocus={undefined}
hitSlop={undefined}
isTVSelectable={true}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
testID={undefined}
tvParallaxProperties={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "rgba(0, 0, 0, .04)",
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 0,
}
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
},
undefined,
]
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Array [
Object {
"fontWeight": "bold",
"margin": 16,
},
Object {
"color": "#2196f3",
},
undefined,
undefined,
]
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>
</View>
</View>
</RCTScrollContentView>
</RCTScrollView>
</View>
</View>
</View>
`;

View File

@@ -1,359 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nested navigators renders succesfully as direct child 1`] = `
<View
onLayout={[Function]}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
>
<View
onLayout={[Function]}
style={
Array [
Object {
"flex": 1,
},
]
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
/>
</View>
<View
collapsable={undefined}
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
<View
collapsable={undefined}
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;
exports[`Nested navigators throw when trying to pass navigation prop 1`] = `"No \\"routes\\" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop"`;

View File

@@ -51,7 +51,6 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
@@ -77,31 +76,37 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
</View>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<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,
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
@@ -109,89 +114,70 @@ 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",
"left": 0,
"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
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>
</View>
@@ -251,7 +237,6 @@ exports[`StackNavigator renders successfully 1`] = `
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
@@ -277,31 +262,37 @@ exports[`StackNavigator renders successfully 1`] = `
</View>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"transform": Array [
Object {
"translateX": 0,
},
],
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<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,
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
@@ -309,71 +300,52 @@ 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,
}
}
>
<View
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
pointerEvents="box-none"
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
<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>
Welcome anonymous
</Text>
</View>
</View>
</View>

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwitchNavigator renders successfully 1`] = `<View />`;

View File

@@ -2,7 +2,12 @@
exports[`TabNavigator renders successfully 1`] = `
<View
collapsable={false}
loaded={
Array [
0,
]
}
onLayout={[Function]}
style={
Array [
Object {
@@ -16,65 +21,56 @@ exports[`TabNavigator renders successfully 1`] = `
}
>
<View
onLayout={[Function]}
collapsable={undefined}
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Object {
"alignItems": "stretch",
"flex": 1,
"flexDirection": "row",
}
}
>
<View
collapsable={undefined}
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Object {
"alignItems": "stretch",
"flex": 1,
"flexDirection": "row",
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
"flex": 1,
"overflow": "hidden",
}
}
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
>
<View
style={
Object {
"flex": 1,
}
}
/>
</View>
/>
</View>
</View>
</View>
@@ -141,15 +137,9 @@ exports[`TabNavigator renders successfully 1`] = `
>
<View
style={
Array [
Object {
"height": 29,
},
false,
Object {
"flex": 1,
},
]
Object {
"flexGrow": 1,
}
}
>
<View
@@ -160,7 +150,6 @@ exports[`TabNavigator renders successfully 1`] = `
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"minWidth": 30,
"opacity": 1,
"position": "absolute",
"width": "100%",
@@ -175,7 +164,6 @@ exports[`TabNavigator renders successfully 1`] = `
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"minWidth": 30,
"opacity": 0,
"position": "absolute",
"width": "100%",

View File

@@ -1,55 +0,0 @@
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);
};
};

View File

@@ -1,44 +1,60 @@
import React from 'react';
import { polyfill } from 'react-lifecycles-compat';
import getChildEventSubscriber from '../getChildEventSubscriber';
import addNavigationHelpers from '../addNavigationHelpers';
function createNavigator(NavigatorView, router, navigationConfig) {
class Navigator extends React.Component {
static router = router;
static navigationOptions = null;
state = {
descriptors: {},
screenProps: this.props.screenProps,
childEventSubscribers = {};
// Cleanup subscriptions for routes that no longer exist
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
Object.keys(this.childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
this.childEventSubscribers[key].removeAll();
delete this.childEventSubscribers[key];
}
});
}
// Remove all subscriptions
componentWillUnmount() {
Object.values(this.childEventSubscribers).map(s => s.removeAll());
}
_isRouteFocused = route => () => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
static getDerivedStateFromProps(nextProps, prevState) {
const prevDescriptors = prevState.descriptors;
const { navigation, screenProps } = nextProps;
render() {
const { navigation, screenProps } = this.props;
const { dispatch, state, addListener } = navigation;
const { routes } = state;
if (typeof routes === 'undefined') {
throw new TypeError(
'No "routes" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop'
);
}
const descriptors = { ...prevState.descriptors };
const descriptors = {};
routes.forEach(route => {
if (
prevDescriptors &&
prevDescriptors[route.key] &&
route === prevDescriptors[route.key].state &&
screenProps === prevState.screenProps
) {
descriptors[route.key] = prevDescriptors[route.key];
return;
}
const getComponent = () =>
router.getComponentForRouteName(route.routeName);
const childNavigation = navigation.getChildNavigation(route.key);
if (!this.childEventSubscribers[route.key]) {
this.childEventSubscribers[route.key] = getChildEventSubscriber(
addListener,
route.key
);
}
const childNavigation = addNavigationHelpers({
dispatch,
state: route,
addListener: this.childEventSubscribers[route.key].addListener,
isFocused: this._isRouteFocused.bind(this, route),
});
const options = router.getScreenOptions(childNavigation, screenProps);
descriptors[route.key] = {
key: route.key,
@@ -49,23 +65,17 @@ function createNavigator(NavigatorView, router, navigationConfig) {
};
});
return { descriptors, screenProps };
}
render() {
return (
<NavigatorView
{...this.props}
screenProps={this.state.screenProps}
navigation={this.props.navigation}
screenProps={screenProps}
navigation={navigation}
navigationConfig={navigationConfig}
descriptors={this.state.descriptors}
descriptors={descriptors}
/>
);
}
}
return polyfill(Navigator);
return Navigator;
}
export default createNavigator;

View File

@@ -1,37 +1,28 @@
import React from 'react';
import * as React from 'react';
import createNavigationContainer from '../createNavigationContainer';
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
import createNavigator from './createNavigator';
import StackView from '../views/StackView/StackView';
import StackView from '../views/StackView/StackView2';
import StackRouter from '../routers/StackRouter';
function createStackNavigator(routeConfigMap, stackConfig = {}) {
const {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
disableKeyboardHandling,
getCustomActionCreators,
} = stackConfig;
const stackRouterConfig = {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
getCustomActionCreators,
};
const router = StackRouter(routeConfigMap, stackRouterConfig);
// Create a navigator with StackView as the view
let Navigator = createNavigator(StackView, router, stackConfig);
if (!disableKeyboardHandling) {
Navigator = createKeyboardAwareNavigator(Navigator);
}
const Navigator = createNavigator(StackView, router, stackConfig);
// HOC to provide the navigation prop for the top-level navigator (when the prop is missing)
return createNavigationContainer(Navigator);

View File

@@ -1,13 +0,0 @@
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;

View File

@@ -0,0 +1,64 @@
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;

View File

@@ -8,71 +8,25 @@ 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 createStackNavigator() {
return require('./navigators/createStackNavigator').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('react-navigation-drawer').createDrawerNavigator;
},
get DrawerNavigator() {
console.warn(
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
);
return require('react-navigation-drawer').createDrawerNavigator;
},
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;
return require('./navigators/createTabNavigator').default;
},
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('react-navigation-drawer').DrawerActions;
},
get getNavigationActionCreators() {
return require('./routers/getNavigationActionCreators').default;
get DrawerNavigator() {
return require('./navigators/DrawerNavigator').default;
},
// Routers
@@ -82,12 +36,6 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get DrawerRouter() {
return require('react-navigation-drawer').DrawerRouter;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// Views
get Transitioner() {
@@ -102,12 +50,6 @@ 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() {
@@ -122,38 +64,21 @@ module.exports = {
// DrawerView
get DrawerView() {
return require('react-navigation-drawer').DrawerView;
return require('./views/Drawer/DrawerView').default;
},
get DrawerItems() {
return require('react-navigation-drawer').DrawerNavigatorItems;
},
get DrawerSidebar() {
return require('react-navigation-drawer').DrawerSidebar;
return require('./views/Drawer/DrawerNavigatorItems').default;
},
// TabView
get TabView() {
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;
return require('./views/TabView/TabView').default;
},
get TabBarTop() {
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;
return require('./views/TabView/TabBarTop').default;
},
get TabBarBottom() {
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;
return require('./views/TabView/TabBarBottom').default;
},
// HOCs
@@ -163,7 +88,4 @@ module.exports = {
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
get withOrientation() {
return require('./views/withOrientation').default;
},
};

View File

@@ -8,26 +8,18 @@ module.exports = {
get StateUtils() {
return require('./StateUtils').default;
},
get addNavigationHelpers() {
return require('./addNavigationHelpers').default;
},
get NavigationActions() {
return require('./NavigationActions').default;
},
// Navigators
get createNavigator() {
return require('./navigators/createNavigator').default;
},
// Actions
get NavigationActions() {
return require('./NavigationActions').default;
},
get StackActions() {
return require('./routers/StackActions').default;
},
get DrawerActions() {
return require('./routers/DrawerActions').default;
},
get getNavigationActionCreators() {
return require('./routers/getNavigationActionCreators').default;
},
// Routers
get StackRouter() {
return require('./routers/StackRouter').default;
@@ -35,9 +27,6 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// HOCs
get withNavigation() {

View File

@@ -0,0 +1,55 @@
import invariant from '../utils/invariant';
import TabRouter from './TabRouter';
import NavigationActions from '../NavigationActions';
export default (routeConfigs, config = {}) => {
const tabRouter = TabRouter(routeConfigs, config);
return {
...tabRouter,
getStateForAction(action, lastState) {
const state = lastState || {
...tabRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
// Handle explicit drawer actions
if (
state.isDrawerOpen &&
action.type === NavigationActions.CLOSE_DRAWER
) {
return {
...state,
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,
};
}
// 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,
};
}
return state;
},
};
};

View File

@@ -1,52 +0,0 @@
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,
};

View File

@@ -1,14 +1,12 @@
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;
@@ -21,16 +19,10 @@ function isEmpty(obj) {
function behavesLikePushAction(action) {
return (
action.type === NavigationActions.NAVIGATE ||
action.type === StackActions.PUSH
action.type === NavigationActions.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);
@@ -51,8 +43,6 @@ export default (routeConfigs, stackConfig = {}) => {
});
const { initialRouteParams } = stackConfig;
const getCustomActionCreators =
stackConfig.getCustomActionCreators || defaultActionCreators;
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
@@ -75,7 +65,7 @@ export default (routeConfigs, stackConfig = {}) => {
}
return {
key: 'StackRouterRoot',
isTransitioning: false,
transitioningFromKey: null,
index: 0,
routes: [
{
@@ -110,7 +100,7 @@ export default (routeConfigs, stackConfig = {}) => {
};
return {
key: 'StackRouterRoot',
isTransitioning: false,
transitioningFromKey: false,
index: 0,
routes: [route],
};
@@ -146,11 +136,9 @@ export default (routeConfigs, stackConfig = {}) => {
});
paths = Object.entries(pathsByRouteNames);
paths.sort((a, b) => b[1].priority - a[1].priority);
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
return {
childRouters,
getComponentForState(state) {
const activeChildRoute = state.routes[state.index];
const { routeName } = activeChildRoute;
@@ -164,75 +152,16 @@ 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 (
!isResetToRootStack(action) &&
action.type !== NavigationActions.NAVIGATE
) {
if (action.type !== NavigationActions.RESET || action.key !== null) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
: -1;
@@ -252,38 +181,6 @@ export default (routeConfigs, stackConfig = {}) => {
return StateUtils.replaceAt(state, childRoute.key, route);
}
}
} else if (action.type === NavigationActions.NAVIGATE) {
// Traverse routes from the top of the stack to the bottom, so the
// active route has the first opportunity, then the one before it, etc.
for (let childRoute of state.routes.slice().reverse()) {
let childRouter = childRouters[childRoute.routeName];
let childAction =
action.routeName === childRoute.routeName && action.action
? action.action
: action;
if (childRouter) {
const nextRouteState = childRouter.getStateForAction(
childAction,
childRoute
);
if (nextRouteState === null || nextRouteState !== childRoute) {
const newState = StateUtils.replaceAndPrune(
state,
nextRouteState ? nextRouteState.key : childRoute.key,
nextRouteState ? nextRouteState : childRoute
);
return {
...newState,
isTransitioning:
state.index !== newState.index
? action.immediate !== true
: state.isTransitioning,
};
}
}
}
}
// Handle explicit push navigation action. This must happen after the
@@ -296,50 +193,46 @@ export default (routeConfigs, stackConfig = {}) => {
let route;
invariant(
action.type !== StackActions.PUSH || action.key == null,
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// 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;
}
});
// 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;
}
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;
}
// Remove the now unused routes at the tail of the routes array
const routes = state.routes.slice(0, lastRouteIndex + 1);
// 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,
},
// 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,
};
}
// 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) {
@@ -361,14 +254,18 @@ export default (routeConfigs, stackConfig = {}) => {
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
transitioningFromKey: action.immediate !== true ? lastRouteKey : null,
};
} else if (
action.type === StackActions.PUSH &&
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
// Return the state identity to bubble the action up
return state;
// 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,
};
}
// Handle navigation to other child routers that are not yet pushed
@@ -408,7 +305,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 === StackActions.POP_TO_TOP) {
if (action.type === NavigationActions.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) {
@@ -417,10 +314,14 @@ 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,
isTransitioning: action.immediate !== true,
};
} else {
return {
...state,
lastRouteKey: action.immediate !== true ? lastRouteKey : null,
index: 0,
routes: [state.routes[0]],
};
@@ -429,7 +330,7 @@ export default (routeConfigs, stackConfig = {}) => {
}
// Handle replace action
if (action.type === StackActions.REPLACE) {
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
@@ -455,13 +356,13 @@ export default (routeConfigs, stackConfig = {}) => {
// Update transitioning state
if (
action.type === StackActions.COMPLETE_TRANSITION &&
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
state.transitioningFromKey
) {
return {
...state,
isTransitioning: false,
transitioningFromKey: null,
};
}
@@ -485,7 +386,7 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
if (action.type === StackActions.RESET) {
if (action.type === NavigationActions.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
@@ -522,11 +423,11 @@ export default (routeConfigs, stackConfig = {}) => {
if (
action.type === NavigationActions.BACK ||
action.type === StackActions.POP
action.type === NavigationActions.POP
) {
const { key, n, immediate } = action;
let backRouteIndex = state.index;
if (action.type === StackActions.POP && n != null) {
if (action.type === NavigationActions.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);
@@ -540,11 +441,17 @@ export default (routeConfigs, stackConfig = {}) => {
...state,
routes: state.routes.slice(0, backRouteIndex),
index: backRouteIndex - 1,
isTransitioning: immediate !== true,
transitioningFromKey: immediate !== true ? lastRouteKey : null,
};
} else if (
backRouteIndex === 0 &&
action.type === NavigationActions.POP
) {
return {
...state,
};
}
}
return state;
},
@@ -575,7 +482,6 @@ export default (routeConfigs, stackConfig = {}) => {
if (!pathToResolve) {
return NavigationActions.navigate({
routeName: initialRouteName,
params: inputParams,
});
}
@@ -646,7 +552,7 @@ export default (routeConfigs, stackConfig = {}) => {
if (key.asterisk || !key) {
return result;
}
const nextResult = result || inputParams || {};
const nextResult = result || {};
const paramName = key.name;
let decodedMatchResult;

View File

@@ -1,390 +0,0 @@
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 {
childRouters,
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
),
};
};

View File

@@ -1,11 +1,326 @@
import SwitchRouter from './SwitchRouter';
import withDefaultValue from '../utils/withDefaultValue';
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);
}
export default (routeConfigs, config = {}) => {
config = { ...config };
config = withDefaultValue(config, 'resetOnBlur', false);
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
// Fail fast on invalid route definitions
validateRouteConfigMap(routeConfigs);
const switchRouter = SwitchRouter(routeConfigs, config);
return switchRouter;
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
),
};
};

View File

@@ -0,0 +1,72 @@
/* eslint react/display-name:0 */
import React from 'react';
import DrawerRouter from '../DrawerRouter';
import NavigationActions from '../../NavigationActions';
const INIT_ACTION = { type: NavigationActions.INIT };
describe('DrawerRouter', () => {
test('Handles basic tab logic', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
transitioningFromKey: null,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
state
);
const expectedState2 = {
index: 1,
transitioningFromKey: null,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
});
test('Drawer opens closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: NavigationActions.OPEN_DRAWER },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction(
{ type: NavigationActions.CLOSE_DRAWER },
state2
);
expect(state3.isDrawerOpen).toEqual(false);
const state4 = router.getStateForAction(
{ type: NavigationActions.TOGGLE_DRAWER },
state3
);
expect(state4.isDrawerOpen).toEqual(true);
});
});

View File

@@ -1,12 +1,12 @@
/* eslint react/no-multi-comp:0, react/display-name:0 */
/* eslint react/no-multi-comp: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(() => {
@@ -16,7 +16,6 @@ beforeEach(() => {
const ROUTERS = {
TabRouter,
StackRouter,
SwitchRouter,
};
const dummyEventSubscriber = (name, handler) => ({
@@ -27,7 +26,7 @@ Object.keys(ROUTERS).forEach(routerName => {
const Router = ROUTERS[routerName];
describe(`General router features - ${routerName}`, () => {
test(`title is configurable using navigationOptions and getScreenOptions - ${routerName}`, () => {
test('title is configurable using navigationOptions and getScreenOptions', () => {
class FooView extends React.Component {
render() {
return <div />;
@@ -59,132 +58,38 @@ Object.keys(ROUTERS).forEach(routerName => {
];
expect(
router.getScreenOptions(
{
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
).title
).toEqual(undefined);
expect(
router.getScreenOptions(
{
addNavigationHelpers({
state: routes[1],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
).title
).toEqual('BarTitle');
expect(
router.getScreenOptions(
{
addNavigationHelpers({
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
).title
).toEqual('Baz-123');
});
test(`set params works in ${routerName}`, () => {
class FooView extends React.Component {
render() {
return <div />;
}
}
const router = Router({
Foo: { screen: FooView },
Bar: { screen: FooView },
});
const initState = router.getStateForAction(NavigationActions.init());
const initRoute = initState.routes[initState.index];
expect(initRoute.params).toEqual(undefined);
const state0 = router.getStateForAction(
NavigationActions.setParams({
params: { foo: 42 },
key: initRoute.key,
}),
initState
);
expect(state0.routes[state0.index].params.foo).toEqual(42);
});
});
});
test('Nested navigate behavior test', () => {
const Leaf = () => <div />;
const First = () => <div />;
First.router = StackRouter({
First1: Leaf,
First2: Leaf,
});
const Second = () => <div />;
Second.router = StackRouter({
Second1: Leaf,
Second2: Leaf,
});
const Main = () => <div />;
Main.router = StackRouter({
First,
Second,
});
const TestRouter = SwitchRouter({
Login: Leaf,
Main,
});
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const state2 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'First' },
state1
);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(0);
expect(state2.routes[1].routes[0].index).toEqual(0);
const state3 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Second2' },
state2
);
expect(state3.index).toEqual(1);
expect(state3.routes[1].index).toEqual(1); // second
expect(state3.routes[1].routes[1].index).toEqual(1); //second.second2
const state4 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'First',
action: { type: NavigationActions.NAVIGATE, routeName: 'First2' },
},
state3,
true
);
expect(state4.index).toEqual(1); // main
expect(state4.routes[1].index).toEqual(0); // first
expect(state4.routes[1].routes[0].index).toEqual(1); // first2
const state5 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'First',
action: { type: NavigationActions.NAVIGATE, routeName: 'First1' },
},
state3 // second.second2 is active on state3
);
expect(state5.index).toEqual(1); // main
expect(state5.routes[1].index).toEqual(0); // first
expect(state5.routes[1].routes[0].index).toEqual(0); // first.first1
});
test('Handles no-op actions with tabs within stack router', () => {
const BarView = () => <div />;
const FooTabNavigator = () => <div />;
@@ -230,7 +135,7 @@ test('Handles deep action', () => {
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const expectedState = {
index: 0,
isTransitioning: false,
transitioningFromKey: false,
key: 'StackRouterRoot',
routes: [
{
@@ -253,70 +158,6 @@ 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 />;
@@ -409,62 +250,3 @@ 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)
);
});

View File

@@ -3,10 +3,11 @@
import React from 'react';
import StackRouter from '../StackRouter';
import StackActions from '../StackActions';
import NavigationActions from '../../NavigationActions';
import TabRouter from '../TabRouter';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
import NavigationActions from '../../NavigationActions';
beforeEach(() => {
_TESTING_ONLY_normalize_keys();
});
@@ -91,7 +92,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -102,7 +103,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 1,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -126,7 +127,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -137,7 +138,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 1,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -352,7 +353,7 @@ describe('StackRouter', () => {
const initState = TestRouter.getStateForAction(NavigationActions.init());
expect(initState).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [{ key: 'id-0', routeName: 'foo' }],
});
@@ -365,38 +366,7 @@ describe('StackRouter', () => {
expect(pushedState.routes[1].routes[1].routeName).toEqual('qux');
});
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', () => {
test('pop does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
@@ -419,54 +389,51 @@ describe('StackRouter', () => {
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: StackActions.POP,
type: NavigationActions.POP,
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('Handle navigation to nested navigator', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.INIT,
});
const action = TestStackRouter.getActionForPathAndParams('fo/22/b/hello');
/* $FlowFixMe */
const state2 = TestStackRouter.getStateForAction(action);
expect(state2).toEqual({
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [
{
index: 0,
key: 'id-4',
isTransitioning: false,
routeName: 'foo',
params: {
fooThing: '22',
},
routes: [
{
routeName: 'bar',
key: 'id-3',
params: {
barThing: 'hello',
},
},
],
},
],
});
});
test('popToTop bubbles up', () => {
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', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
@@ -482,11 +449,12 @@ describe('StackRouter', () => {
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: StackActions.POP_TO_TOP,
type: NavigationActions.POP_TO_TOP,
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('popToTop targets StackRouter by key if specified', () => {
@@ -510,7 +478,7 @@ describe('StackRouter', () => {
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: StackActions.POP_TO_TOP,
type: NavigationActions.POP_TO_TOP,
key: state2.key,
},
state2
@@ -518,44 +486,6 @@ 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 /> },
@@ -564,7 +494,7 @@ describe('StackRouter', () => {
const state = {
index: 2,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'A', routeName: 'foo' },
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
@@ -572,79 +502,27 @@ describe('StackRouter', () => {
],
};
const poppedState = TestRouter.getStateForAction(
StackActions.popToTop(),
NavigationActions.popToTop(),
state
);
expect(poppedState.routes.length).toBe(1);
expect(poppedState.index).toBe(0);
expect(poppedState.isTransitioning).toBe(true);
expect(poppedState.transitioningFromKey).toBe('C');
const poppedState2 = TestRouter.getStateForAction(
StackActions.popToTop(),
NavigationActions.popToTop(),
poppedState
);
expect(poppedState).toEqual(poppedState2);
const poppedImmediatelyState = TestRouter.getStateForAction(
StackActions.popToTop({ immediate: true }),
NavigationActions.popToTop({ immediate: true }),
state
);
expect(poppedImmediatelyState.routes.length).toBe(1);
expect(poppedImmediatelyState.index).toBe(0);
expect(poppedImmediatelyState.isTransitioning).toBe(false);
expect(poppedImmediatelyState.transitioningFromKey).toBe(null);
});
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', () => {
test('Navigate Pushes duplicate routeName', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
@@ -657,7 +535,7 @@ describe('StackRouter', () => {
expect(pushedState.index).toEqual(1);
expect(pushedState.routes[1].routeName).toEqual('bar');
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'new-unique-key!' }),
NavigationActions.navigate({ routeName: 'bar' }),
pushedState
);
expect(pushedTwiceState.index).toEqual(2);
@@ -711,90 +589,28 @@ describe('StackRouter', () => {
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
initState
);
expect(pushedState).toEqual(null);
expect(pushedState.index).toEqual(0);
expect(pushedState.routes[0].routeName).toEqual('foo');
});
test('Navigate with key and without it is idempotent', () => {
test('Navigate with key is idempotent', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
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' }),
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
initState
);
expect(navigatedState).toBe(null);
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');
});
test('Push behaves like navigate, except for key', () => {
@@ -804,39 +620,19 @@ describe('StackRouter', () => {
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
StackActions.push({ routeName: 'bar' }),
NavigationActions.push({ routeName: 'bar' }),
initState
);
expect(pushedState.index).toEqual(1);
expect(pushedState.routes[1].routeName).toEqual('bar');
expect(() => {
TestRouter.getStateForAction(
{ type: StackActions.PUSH, routeName: 'bar', key: 'a' },
{ type: NavigationActions.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 /> },
@@ -882,7 +678,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -910,7 +706,7 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -930,7 +726,7 @@ describe('StackRouter', () => {
NavigationActions.navigate({ routeName: 'foo' })
);
const replacedState = TestRouter.getStateForAction(
StackActions.replace({
NavigationActions.replace({
routeName: 'bar',
params: { meaning: 42 },
key: initState.routes[0].key,
@@ -943,7 +739,7 @@ describe('StackRouter', () => {
expect(replacedState.routes[0].routeName).toEqual('bar');
expect(replacedState.routes[0].params.meaning).toEqual(42);
const replacedState2 = TestRouter.getStateForAction(
StackActions.replace({
NavigationActions.replace({
routeName: 'bar',
key: initState.routes[0].key,
newKey: 'wow',
@@ -977,15 +773,15 @@ describe('StackRouter', () => {
state
);
expect(state2 && state2.index).toEqual(1);
expect(state2 && state2.isTransitioning).toEqual(true);
expect(state2 && state2.transitioningFromKey).toEqual(state.routes[0].key);
const state3 = router.getStateForAction(
{
type: StackActions.COMPLETE_TRANSITION,
type: NavigationActions.COMPLETE_TRANSITION,
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.isTransitioning).toEqual(false);
expect(state3 && state3.transitioningFromKey).toEqual(null);
});
test('Handle basic stack logic for components with router', () => {
@@ -1007,7 +803,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1035,7 +831,7 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1109,7 +905,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1133,7 +929,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1202,51 +998,15 @@ 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 />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
const GrandChildNavigator = () => <div />;
GrandChildNavigator.router = StackRouter({
Quux: { screen: () => <div /> },
Corge: { screen: () => <div /> },
});
ChildNavigator.router = TabRouter({
Baz: { screen: GrandChildNavigator },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
@@ -1263,10 +1023,10 @@ describe('StackRouter', () => {
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes).toEqual([
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
{
key: 'id-0',
routeName: 'Baz',
routeName: 'Quux',
params: { name: 'foobar' },
},
]);
@@ -1284,7 +1044,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: StackActions.RESET,
type: NavigationActions.RESET,
actions: [
{
type: NavigationActions.NAVIGATE,
@@ -1319,7 +1079,7 @@ describe('StackRouter', () => {
});
const state1 = router.getStateForAction({ type: NavigationActions.INIT });
const resetAction = {
type: StackActions.RESET,
type: NavigationActions.RESET,
key: 'Bad Key',
actions: [
{
@@ -1352,7 +1112,7 @@ describe('StackRouter', () => {
});
test('Handles the reset action with nested Router', () => {
const ChildRouter = StackRouter({
const ChildRouter = TabRouter({
baz: {
screen: () => <div />,
},
@@ -1372,8 +1132,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: StackActions.RESET,
key: null,
type: NavigationActions.RESET,
actions: [
{
type: NavigationActions.NAVIGATE,
@@ -1425,7 +1184,7 @@ describe('StackRouter', () => {
);
const state3 = router.getStateForAction(
{
type: StackActions.RESET,
type: NavigationActions.RESET,
key: 'Init',
actions: [
{
@@ -1440,7 +1199,7 @@ describe('StackRouter', () => {
);
const state4 = router.getStateForAction(
{
type: StackActions.RESET,
type: NavigationActions.RESET,
key: null,
actions: [
{
@@ -1485,34 +1244,6 @@ describe('StackRouter', () => {
]);
});
test('Navigate action to previous nested StackRouter causes isTransitioning start', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
});
const router = StackRouter({
Bar: { screen: ChildNavigator },
Foo: { screen: () => <div /> },
});
const state = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Foo',
},
router.getStateForAction({ type: NavigationActions.INIT })
);
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state
);
expect(state2.index).toEqual(0);
expect(state2.isTransitioning).toEqual(true);
});
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
@@ -1543,19 +1274,19 @@ describe('StackRouter', () => {
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-2',
params: { code: 'test', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-1',
params: { code: 'test', foo: 'bar', id: '4' },
routeName: 'profile',
@@ -1602,19 +1333,19 @@ describe('StackRouter', () => {
expect(state2).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-5',
params: { code: '', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-4',
params: { code: '', foo: 'bar', id: '4' },
routeName: 'profile',
@@ -1633,6 +1364,42 @@ 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(
{
@@ -1681,7 +1448,7 @@ describe('StackRouter', () => {
const state = {
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{
index: 1,
@@ -1897,136 +1664,21 @@ test('Handles deep navigate completion action', () => {
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
expect(state2 && state2.routes[0].index).toEqual(1);
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(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(!!key).toEqual(true);
const state3 = router.getStateForAction(
{
type: StackActions.COMPLETE_TRANSITION,
type: NavigationActions.COMPLETE_TRANSITION,
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
expect(state3 && state3.routes[0].index).toEqual(1);
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
});
test('order of handling navigate action is correct for nested stackrouters', () => {
const Screen = () => <div />;
const NestedStack = () => <div />;
let nestedRouter = StackRouter({
Foo: Screen,
Bar: Screen,
});
NestedStack.router = nestedRouter;
let router = StackRouter(
{
NestedStack,
Bar: Screen,
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');
expect(state3.index).toEqual(0);
expect(state3.transitioningFromKey).toEqual(null);
expect(state3.routes[0].index).toEqual(1);
expect(state3.routes[0].transitioningFromKey).toEqual(null);
});

View File

@@ -1,252 +0,0 @@
/* 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;
};

View File

@@ -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,46 +140,6 @@ 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 },
@@ -221,7 +181,6 @@ describe('TabRouter', () => {
const navAction = {
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
params: { foo: '42' },
action: {
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
@@ -392,7 +351,7 @@ describe('TabRouter', () => {
});
const MidNavigator = () => <div />;
MidNavigator.router = TabRouter({
Fee: { screen: ChildNavigator0 },
Foo: { screen: ChildNavigator0 },
Bar: { screen: ChildNavigator1 },
});
const router = TabRouter({
@@ -412,8 +371,8 @@ describe('TabRouter', () => {
routes: [
{
index: 0,
key: 'Fee',
routeName: 'Fee',
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
@@ -451,8 +410,8 @@ describe('TabRouter', () => {
routes: [
{
index: 0,
key: 'Fee',
routeName: 'Fee',
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
@@ -485,10 +444,7 @@ describe('TabRouter', () => {
action: {
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
action: {
type: NavigationActions.NAVIGATE,
routeName: 'Zap',
},
action: { type: NavigationActions.NAVIGATE, routeName: 'Zap' },
},
});
expect(state4).toEqual({
@@ -503,8 +459,8 @@ describe('TabRouter', () => {
routes: [
{
index: 0,
key: 'Fee',
routeName: 'Fee',
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
@@ -734,15 +690,56 @@ 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 = TabRouter({
ScreenB.router = StackRouter({
Baz: { screen: PlainScreen },
Zoo: { screen: PlainScreen },
});
ScreenA.router = TabRouter({
ScreenA.router = StackRouter({
Bar: { screen: PlainScreen },
Boo: { screen: ScreenB },
});
@@ -751,10 +748,10 @@ describe('TabRouter', () => {
});
const screenApreState = {
index: 0,
key: 'Foo',
key: 'Init',
isTransitioning: false,
routeName: 'Foo',
routes: [{ key: 'Bar', routeName: 'Bar' }],
routes: [{ key: 'Init', routeName: 'Bar' }],
};
const preState = {
index: 0,
@@ -780,6 +777,7 @@ describe('TabRouter', () => {
routeName: 'Boo',
action: NavigationActions.navigate({ routeName: 'Zoo' }),
});
const expectedState = ScreenA.router.getStateForAction(
action,
screenApreState
@@ -787,25 +785,8 @@ 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)
);
});
});

View File

@@ -1,5 +1,6 @@
import { Component } from 'react';
import createConfigGetter from '../createConfigGetter';
import addNavigationHelpers from '../../addNavigationHelpers';
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
remove: () => {},
@@ -66,81 +67,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);
@@ -163,11 +164,11 @@ test('should throw if the route does not exist', () => {
expect(() =>
getScreenOptions(
{
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
)
).toThrowError(

View File

@@ -38,8 +38,7 @@ export default (routeConfigs, navigatorScreenConfig) => (
const routeConfig = routeConfigs[route.routeName];
const routeScreenConfig =
routeConfig === Component ? null : routeConfig.navigationOptions;
const routeScreenConfig = routeConfig.navigationOptions;
const componentScreenConfig = Component.navigationOptions;
const configOptions = { navigation, screenProps: screenProps || {} };

View File

@@ -1,46 +0,0 @@
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;

View File

@@ -1,3 +0,0 @@
export default function docsUrl(path) {
return `https://v2.reactnavigation.org/docs/${path}`;
}

View File

@@ -1,8 +0,0 @@
export default (obj, key, defaultValue) => {
if (obj.hasOwnProperty(key)) {
return obj;
}
obj[key] = defaultValue;
return obj;
};

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { View, Text, Platform, StyleSheet } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TouchableItem from '../TouchableItem';
/**
* Component that renders the navigation list in the drawer.
*/
const DrawerNavigatorItems = ({
navigation: { state, navigate },
items,
activeItemKey,
activeTintColor,
activeBackgroundColor,
inactiveTintColor,
inactiveBackgroundColor,
getLabel,
renderIcon,
onItemPress,
itemsContainerStyle,
itemStyle,
labelStyle,
activeLabelStyle,
inactiveLabelStyle,
iconContainerStyle,
drawerPosition,
}) => (
<View style={[styles.container, itemsContainerStyle]}>
{items.map((route, index) => {
const focused = activeItemKey === route.key;
const color = focused ? activeTintColor : inactiveTintColor;
const backgroundColor = focused
? activeBackgroundColor
: inactiveBackgroundColor;
const scene = { route, index, focused, tintColor: color };
const icon = renderIcon(scene);
const label = getLabel(scene);
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
return (
<TouchableItem
key={route.key}
onPress={() => {
onItemPress({ route, focused });
}}
delayPressIn={0}
>
<SafeAreaView
style={{ backgroundColor }}
forceInset={{
[drawerPosition]: 'always',
[drawerPosition === 'left' ? 'right' : 'left']: 'never',
vertical: 'never',
}}
>
<View style={[styles.item, itemStyle]}>
{icon ? (
<View
style={[
styles.icon,
focused ? null : styles.inactiveIcon,
iconContainerStyle,
]}
>
{icon}
</View>
) : null}
{typeof label === 'string' ? (
<Text
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
>
{label}
</Text>
) : (
label
)}
</View>
</SafeAreaView>
</TouchableItem>
);
})}
</View>
);
/* Material design specs - https://material.io/guidelines/patterns/navigation-drawer.html#navigation-drawer-specs */
DrawerNavigatorItems.defaultProps = {
activeTintColor: '#2196f3',
activeBackgroundColor: 'rgba(0, 0, 0, .04)',
inactiveTintColor: 'rgba(0, 0, 0, .87)',
inactiveBackgroundColor: 'transparent',
};
const styles = StyleSheet.create({
container: {
paddingVertical: 4,
},
item: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginHorizontal: 16,
width: 24,
alignItems: 'center',
},
inactiveIcon: {
/*
* Icons have 0.54 opacity according to guidelines
* 100/87 * 54 ~= 62
*/
opacity: 0.62,
},
label: {
margin: 16,
fontWeight: 'bold',
},
});
export default DrawerNavigatorItems;

View File

@@ -0,0 +1,24 @@
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;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import NavigationActions from '../../NavigationActions';
import invariant from '../../utils/invariant';
/**
* Component that renders the sidebar screen of the drawer.
*/
class DrawerSidebar extends React.PureComponent {
_getScreenOptions = routeKey => {
const descriptor = this.props.descriptors[routeKey];
invariant(
descriptor.options,
'Cannot access screen descriptor options from drawer sidebar'
);
return descriptor.options;
};
_getLabel = ({ focused, tintColor, route }) => {
const { drawerLabel, title } = this._getScreenOptions(route.key);
if (drawerLabel) {
return typeof drawerLabel === 'function'
? drawerLabel({ tintColor, focused })
: drawerLabel;
}
if (typeof title === 'string') {
return title;
}
return route.routeName;
};
_renderIcon = ({ focused, tintColor, route }) => {
const { drawerIcon } = this._getScreenOptions(route.key);
if (drawerIcon) {
return typeof drawerIcon === 'function'
? drawerIcon({ tintColor, focused })
: drawerIcon;
}
return null;
};
_onItemPress = ({ route, focused }) => {
if (!focused) {
let subAction;
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
if (route.index !== undefined && route.index !== 0) {
subAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName: route.routes[0].routeName,
}),
],
});
}
this.props.navigation.navigate(route.routeName, undefined, subAction);
}
};
render() {
const ContentComponent = this.props.contentComponent;
if (!ContentComponent) {
return null;
}
const { state } = this.props.navigation;
invariant(typeof state.index === 'number', 'should be set');
return (
<View style={[styles.container, this.props.style]}>
<ContentComponent
{...this.props.contentOptions}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
items={state.routes}
activeItemKey={
state.routes[state.index] ? state.routes[state.index].key : null
}
screenProps={this.props.screenProps}
getLabel={this._getLabel}
renderIcon={this._renderIcon}
onItemPress={this._onItemPress}
drawerPosition={this.props.drawerPosition}
/>
</View>
);
}
}
export default DrawerSidebar;
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -0,0 +1,113 @@
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';
/**
* Component that renders the drawer.
*/
export default class DrawerView extends React.PureComponent {
state = {
drawerWidth:
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
: this.props.navigationConfig.drawerWidth,
};
componentWillMount() {
Dimensions.addEventListener('change', this._updateWidth);
}
componentWillUnmount() {
Dimensions.removeEventListener('change', this._updateWidth);
}
componentWillReceiveProps(nextProps) {
const { isDrawerOpen } = nextProps.navigation.state;
const wasDrawerOpen = this.props.navigation.state.isDrawerOpen;
if (isDrawerOpen && !wasDrawerOpen) {
this._drawer.openDrawer();
} else if (wasDrawerOpen && !isDrawerOpen) {
this._drawer.closeDrawer();
}
}
_handleDrawerOpen = () => {
const { navigation } = this.props;
navigation.dispatch({ type: NavigationActions.OPEN_DRAWER });
};
_handleDrawerClose = () => {
const { navigation } = this.props;
navigation.dispatch({ type: NavigationActions.CLOSE_DRAWER });
};
_updateWidth = () => {
const drawerWidth =
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
: this.props.navigationConfig.drawerWidth;
if (this.state.drawerWidth !== drawerWidth) {
this.setState({ drawerWidth });
}
};
_renderNavigationView = () => {
return (
<DrawerSidebar
screenProps={this.props.screenProps}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
contentComponent={this.props.navigationConfig.contentComponent}
contentOptions={this.props.navigationConfig.contentOptions}
drawerPosition={this.props.navigationConfig.drawerPosition}
style={this.props.navigationConfig.style}
{...this.props.navigationConfig}
/>
);
};
render() {
const { state } = this.props.navigation;
const activeKey = state.routes[state.index].key;
const descriptor = this.props.descriptors[activeKey];
const DrawerScreen = descriptor.getComponent();
const { drawerLockMode } = descriptor.options;
return (
<DrawerLayout
ref={c => {
this._drawer = c;
}}
drawerLockMode={
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
this.props.navigationConfig.drawerLockMode
}
drawerBackgroundColor={
this.props.navigationConfig.drawerBackgroundColor
}
drawerWidth={this.state.drawerWidth}
onDrawerOpen={this._handleDrawerOpen}
onDrawerClose={this._handleDrawerClose}
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
renderNavigationView={this._renderNavigationView}
drawerPosition={
this.props.navigationConfig.drawerPosition === 'right'
? DrawerLayout.positions.Right
: DrawerLayout.positions.Left
}
>
<DrawerScreen
screenProps={this.props.screenProps}
navigation={descriptor.navigation}
/>
</DrawerLayout>
);
}
}

View File

@@ -7,7 +7,6 @@ import {
Platform,
StyleSheet,
View,
I18nManager,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
@@ -25,15 +24,12 @@ 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,
@@ -147,7 +143,7 @@ class Header extends React.PureComponent {
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
props.scene.descriptor.navigation.goBack(props.scene.descriptor.key);
this.props.navigation.goBack(props.scene.descriptor.key);
});
};
return (
@@ -155,7 +151,7 @@ class Header extends React.PureComponent {
onPress={goBack}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
backImage={options.headerBackImage}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
@@ -169,7 +165,7 @@ class Header extends React.PureComponent {
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options, navigation } = props.scene.descriptor;
const { options } = props.scene.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
@@ -178,21 +174,14 @@ 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={goBack}
onPress={this._navigateBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
backImage={options.headerBackImage}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
@@ -374,10 +363,6 @@ 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, {
@@ -386,6 +371,7 @@ class Header extends React.PureComponent {
});
const { isLandscape, transitionPreset } = this.props;
const { options } = props.scene.descriptor;
const wrapperProps = {
style: styles.header,
@@ -491,14 +477,10 @@ class Header extends React.PureComponent {
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
return (
<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>
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
<View style={{ flex: 1 }}>{appBar}</View>
</SafeAreaView>
);
}
}
@@ -566,7 +548,6 @@ 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,
@@ -594,9 +575,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
},
flexOne: {
flex: 1,
},
});
export default withOrientation(Header);

594
src/views/Header/Header2.js Normal file
View File

@@ -0,0 +1,594 @@
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);

View File

@@ -10,8 +10,6 @@ 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)',
@@ -19,6 +17,8 @@ class HeaderBackButton extends React.PureComponent {
ios: '#037aff',
}),
truncatedTitle: 'Back',
// eslint-disable-next-line global-require
buttonImage: require('../assets/back-icon.png'),
};
state = {};
@@ -32,37 +32,9 @@ 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,
@@ -92,7 +64,14 @@ class HeaderBackButton extends React.PureComponent {
borderless
>
<View style={styles.container}>
{this._renderBackImage()}
<Image
style={[
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
]}
source={buttonImage}
/>
{Platform.OS === 'ios' &&
typeof backButtonTitle === 'string' && (
<Text

View File

@@ -1,37 +1,13 @@
import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
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,
],
const crossFadeInterpolation = (first, index, last) => ({
inputRange: [first, index - 0.9, index - 0.2, index, last],
outputRange: [0, 0, 0.3, 1, 0],
});
/**
* Utilities that build the style for the navigation header.
* Utility that builds the style for the navigation header.
*
* +-------------+-------------+-------------+
* | | | |
@@ -41,51 +17,6 @@ const crossFadeInterpolation = (scenes, 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);
@@ -96,14 +27,12 @@ function forLeft(props) {
const index = scene.index;
return {
opacity: position.interpolate(
crossFadeInterpolation(scenes, first, index, last)
),
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
function forCenter(props) {
const { position, scene, scenes } = props;
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
@@ -112,14 +41,12 @@ function forCenter(props) {
const index = scene.index;
return {
opacity: position.interpolate(
crossFadeInterpolation(scenes, first, index, last)
),
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
function forRight(props) {
const { position, scene, scenes } = props;
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
@@ -127,9 +54,7 @@ function forRight(props) {
const index = scene.index;
return {
opacity: position.interpolate(
crossFadeInterpolation(scenes, first, index, last)
),
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -146,42 +71,25 @@ 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,
outputRange,
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 an approximation that gives us
/*
* 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)
*/
@@ -197,71 +105,41 @@ 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,
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,
],
inputRange: [first, index - 0.35, index, index + 0.5, last],
outputRange: [0, 0, 1, 0.5, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, first + 0.001, index, last - 0.001, last],
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [
-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,
],
? [-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, scenes } = props;
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
@@ -273,44 +151,16 @@ function forCenterFromLeft(props) {
return {
opacity: position.interpolate({
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,
],
inputRange: [first, index - 0.5, index, index + 0.7, last],
outputRange: [0, 0, 1, 0, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, first + 0.001, index, last - 0.001, last],
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [
-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,
],
? [-offset, 0, offset]
: [offset, 0, -offset],
}),
},
],
@@ -318,7 +168,6 @@ function forCenterFromLeft(props) {
}
export default {
forLayout,
forLeft,
forLeftButton,
forLeftLabel,

View File

@@ -0,0 +1,176 @@
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,
};

View File

@@ -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,37 +22,9 @@ 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,
@@ -89,7 +61,14 @@ class ModularHeaderBackButton extends React.PureComponent {
>
<View style={styles.container}>
<ButtonContainerComponent>
{this._renderBackImage()}
<Image
style={[
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
]}
source={buttonImage}
/>
</ButtonContainerComponent>
{typeof backButtonTitle === 'string' && (
<LabelContainerComponent>

View File

@@ -1,8 +0,0 @@
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;

Some files were not shown because too many files have changed in this diff Show More