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
109 changed files with 6882 additions and 9290 deletions

View File

@@ -42,7 +42,9 @@
"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-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/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.

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

@@ -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": "26.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,38 +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,
} from 'react-navigation';
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 = () => {
@@ -65,8 +49,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack()} title="Go back" />
<Button onPress={() => navigation.dismiss()} title="Dismiss" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
@@ -74,7 +57,7 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
}
type MyHomeScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<*>,
};
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
@@ -118,7 +101,7 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
}
type MyPhotosScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<*>,
};
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
@@ -159,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}
/>
);
@@ -168,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}
/>
);
@@ -185,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,9 +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 { Button } from './commonComponents/ButtonWithMargin';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
type NavScreenProps = {
navigation: NavigationScreenProp<*>,
@@ -26,11 +25,8 @@ class HomeScreen extends React.Component<NavScreenProps> {
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button
onPress={() => navigation.push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<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>
);
@@ -42,51 +38,6 @@ class OtherScreen extends React.Component<NavScreenProps> {
title: 'Your title here',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('ScreenWithLongTitle')}
title="Push another screen"
/>
<Button
onPress={() => navigation.push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
class ScreenWithLongTitle extends React.Component<NavScreenProps> {
static navigationOptions = {
title: "Another title that's kind of long",
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button onPress={() => navigation.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;
@@ -104,12 +55,10 @@ class ScreenWithNoHeader extends React.Component<NavScreenProps> {
}
}
const StackWithHeaderPreset = createStackNavigator(
const StackWithHeaderPreset = StackNavigator(
{
Home: HomeScreen,
Other: OtherScreen,
ScreenWithNoHeader: ScreenWithNoHeader,
ScreenWithLongTitle: ScreenWithLongTitle,
},
{
headerTransitionPreset: 'uikit',

View File

@@ -12,16 +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 { Header, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
type MyNavScreenProps = {
navigation: NavigationScreenProp<*>,
@@ -194,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,7 +89,7 @@ const TabNav = createBottomTabNavigator(
}
);
const StacksOverTabs = createStackNavigator({
const StacksOverTabs = StackNavigator({
Root: {
screen: TabNav,
},

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,25 +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": "^26.0.0",
"react": "16.3.0-alpha.1",
"react-native": "^0.54.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.61.0",
"jest": "^22.1.3",
"jest-expo": "^26.0.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,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StackNavigator } from 'react-navigation';
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
@@ -24,11 +24,11 @@ class AppWithNavigationState extends React.Component {
const { dispatch, nav } = this.props;
return (
<AppNavigator
navigation={{
navigation={addNavigationHelpers({
dispatch,
state: nav,
addListener,
}}
})}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -296,6 +296,7 @@ declare module 'react-navigation' {
} & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig =
| NavigationComponent
| {
screen: NavigationComponent,
}
@@ -343,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,
@@ -379,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
*/
@@ -404,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`
|};
@@ -486,21 +474,12 @@ declare module 'react-navigation' {
+state: S,
dispatch: NavigationDispatch,
goBack: (routeKey?: ?string) => boolean,
dismiss: () => boolean,
navigate: (
routeName:
| string
| {
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction,
key?: string,
},
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
setParams: (newParams: NavigationParams) => boolean,
getParam: (paramName: string, fallback?: any) => any,
addListener: (
eventName: string,
callback: NavigationEventCallback
@@ -517,7 +496,6 @@ declare module 'react-navigation' {
) => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
isFocused: () => boolean,
};
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
@@ -541,14 +519,12 @@ declare module 'react-navigation' {
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,
}>;
@@ -772,10 +748,6 @@ declare module 'react-navigation' {
routeConfigMap: NavigationRouteConfigMap,
stackConfig?: StackNavigatorConfig
): NavigationContainer<*, *, *>;
declare export function createStackNavigator(
routeConfigMap: NavigationRouteConfigMap,
stackConfig?: StackNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _TabViewConfig = {|
tabBarComponent?: React$ElementType,
@@ -800,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',
@@ -845,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,
@@ -934,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,
@@ -1084,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.0.0",
"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-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.7.0",
"react-navigation-deprecated-tab-navigator": "1.2.0",
"react-navigation-tabs": "0.2.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, 'error').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

@@ -6,32 +6,6 @@ const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
});
describe('addNavigationHelpers', () => {
it('handles dismiss action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
const child = { key: 'A', routeName: 'Home' };
expect(
addNavigationHelpers({
state: child,
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
dangerouslyGetParent: () => ({
state: {
key: 'P',
routeName: 'Parent',
routes: [child],
},
}),
}).dismiss()
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
type: NavigationActions.BACK,
key: 'P',
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles Back action', () => {
const mockedDispatch = jest
.fn(() => false)

View File

@@ -2,10 +2,10 @@
import NavigationActions from './NavigationActions';
import invariant from './utils/invariant';
export default function(navigation) {
return {
...navigation,
// Go back from the given key, default to active key
goBack: key => {
let actualizedKey = key;
if (key === undefined && navigation.state.key) {
@@ -19,18 +19,6 @@ export default function(navigation) {
NavigationActions.back({ key: actualizedKey })
);
},
// Go back from the parent key. If this is a nested stack, the entire
// stack will be dismissed.
dismiss: () => {
let parent = navigation.dangerouslyGetParent();
if (parent && parent.state) {
return navigation.dispatch(
NavigationActions.back({ key: parent.state.key })
);
} else {
return false;
}
},
navigate: (navigateTo, params, action) => {
if (typeof navigateTo === 'string') {
return navigation.dispatch(

View File

@@ -1,54 +1,9 @@
import React from 'react';
import { Linking, AsyncStorage } from 'react-native';
import { polyfill } from 'react-lifecycles-compat';
import { Linking } from 'react-native';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
import addNavigationHelpers from './addNavigationHelpers';
import invariant from './utils/invariant';
import getNavigationActionCreators from './routers/getNavigationActionCreators';
import docsUrl from './utils/docsUrl';
function isStateful(props) {
return !props.navigation;
}
function validateProps(props) {
if (isStateful(props)) {
return;
}
const { navigation, screenProps, ...containerProps } = props;
const keys = Object.keys(containerProps);
if (keys.length !== 0) {
throw new Error(
'This navigator has both navigation and container props, so it is ' +
`unclear if it should own its own state. Remove props: "${keys.join(
', '
)}" ` +
'if the navigator should get its state from the navigation prop. If the ' +
'navigator should maintain its own state, do not pass a navigation prop.'
);
}
}
// Track the number of stateful container instances. Warn if >0 and not using the
// detached prop to explicitly acknowledge the behavior. We should deprecated implicit
// stateful navigation containers in a future release and require a provider style pattern
// instead in order to eliminate confusion entirely.
let _statefulContainerCount = 0;
export function _TESTING_ONLY_reset_container_count() {
_statefulContainerCount = 0;
}
// We keep a global flag to catch errors during the state persistence hydrating scenario.
// The innermost navigator who catches the error will dispatch a new init action.
let _reactNavigationIsHydratingState = false;
// Unfortunate to use global state here, but it seems necessesary for the time
// being. There seems to be some problems with cascading componentDidCatch
// handlers. Ideally the inner non-stateful navigator catches the error and
// re-throws it, to be caught by the top-level stateful navigator.
/**
* Create an HOC that injects the navigation and manages the navigation state
@@ -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,6 +127,10 @@ export default function createNavigationContainer(Component) {
}
}
componentWillReceiveProps(nextProps) {
this._validateProps(nextProps);
}
componentDidUpdate() {
// Clear cached _nav every tick
if (this._nav === this.state.nav) {
@@ -191,122 +138,37 @@ export default function createNavigationContainer(Component) {
}
}
async componentDidMount() {
componentDidMount() {
this._isMounted = true;
if (!this._isStateful()) {
return;
}
if (__DEV__ && !this.props.detached) {
if (_statefulContainerCount > 0) {
console.error(
`You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl(
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
)}`
);
}
}
_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());
}
}
_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;
}
this._nav = this._nav || this.state.nav;
const oldNav = this._nav;
@@ -328,7 +190,6 @@ export default function createNavigationContainer(Component) {
this.setState({ nav }, () => {
this._onNavigationStateChange(oldNav, nav, action);
dispatchActionEvents();
this._persistNavigationState(nav);
});
return true;
} else {
@@ -341,11 +202,9 @@ export default function createNavigationContainer(Component) {
let navigation = this.props.navigation;
if (this._isStateful()) {
const nav = this.state.nav;
if (!nav) {
return this._renderLoading();
}
invariant(nav, 'should be set in constructor if stateful');
if (!this._navigation || this._navigation.state !== nav) {
this._navigation = {
this._navigation = addNavigationHelpers({
dispatch: this.dispatch,
state: nav,
addListener: (eventName, handler) => {
@@ -359,11 +218,6 @@ export default function createNavigationContainer(Component) {
},
};
},
};
const actionCreators = getNavigationActionCreators(nav);
Object.keys(actionCreators).forEach(actionName => {
this._navigation[actionName] = (...args) =>
this.dispatch(actionCreators[actionName](...args));
});
}
navigation = this._navigation;
@@ -373,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

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import DrawerNavigator from '../createDrawerNavigator';
import DrawerNavigator from '../DrawerNavigator';
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({

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

@@ -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

@@ -137,15 +137,9 @@ exports[`TabNavigator renders successfully 1`] = `
>
<View
style={
Array [
Object {
"height": 29,
},
false,
Object {
"flex": 1,
},
]
Object {
"flexGrow": 1,
}
}
>
<View
@@ -156,7 +150,6 @@ exports[`TabNavigator renders successfully 1`] = `
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"minWidth": 30,
"opacity": 1,
"position": "absolute",
"width": "100%",
@@ -171,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,6 +1,7 @@
import React from 'react';
import getChildEventSubscriber from '../getChildEventSubscriber';
import addNavigationHelpers from '../addNavigationHelpers';
function createNavigator(NavigatorView, router, navigationConfig) {
class Navigator extends React.Component {
@@ -14,26 +15,23 @@ function createNavigator(NavigatorView, router, navigationConfig) {
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
Object.keys(this.childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
this.childEventSubscribers[key].removeAll();
delete this.childEventSubscribers[key];
}
});
}
// Remove all subscription references
// Remove all subscriptions
componentWillUnmount() {
this.childEventSubscribers = {};
Object.values(this.childEventSubscribers).map(s => s.removeAll());
}
_isRouteFocused = route => {
_isRouteFocused = route => () => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_dangerouslyGetParent = () => {
return this.props.navigation;
};
render() {
const { navigation, screenProps } = this.props;
const { dispatch, state, addListener } = navigation;
@@ -51,37 +49,12 @@ function createNavigator(NavigatorView, router, navigationConfig) {
);
}
const actionCreators = {
...navigation.actions,
...router.getActionCreators(route, state.key),
};
const actionHelpers = {};
Object.keys(actionCreators).forEach(actionName => {
actionHelpers[actionName] = (...args) => {
const actionCreator = actionCreators[actionName];
const action = actionCreator(...args);
dispatch(action);
};
});
const childNavigation = {
...actionHelpers,
actions: actionCreators,
const childNavigation = addNavigationHelpers({
dispatch,
state: route,
isFocused: () => this._isRouteFocused(route),
dangerouslyGetParent: this._dangerouslyGetParent,
addListener: this.childEventSubscribers[route.key].addListener,
getParam: (paramName, defaultValue) => {
const params = route.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
},
};
isFocused: this._isRouteFocused.bind(this, route),
});
const options = router.getScreenOptions(childNavigation, screenProps);
descriptors[route.key] = {
key: route.key,
@@ -94,7 +67,6 @@ function createNavigator(NavigatorView, router, navigationConfig) {
return (
<NavigatorView
{...this.props}
screenProps={screenProps}
navigation={navigation}
navigationConfig={navigationConfig}

View File

@@ -1,22 +1,18 @@
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,
} = stackConfig;
const stackRouterConfig = {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
@@ -26,10 +22,7 @@ function createStackNavigator(routeConfigMap, stackConfig = {}) {
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,68 +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('./navigators/createDrawerNavigator').default;
},
get DrawerNavigator() {
console.warn(
'The DrawerNavigator function name is deprecated, please use createDrawerNavigator instead'
);
return require('./navigators/createDrawerNavigator').default;
},
get createTabNavigator() {
console.warn(
'createTabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
);
return require('react-navigation-deprecated-tab-navigator')
.createTabNavigator;
},
get TabNavigator() {
console.warn(
'TabNavigator is deprecated. Please use the createBottomTabNavigator or createMaterialTopTabNavigator instead.'
);
return require('react-navigation-deprecated-tab-navigator')
.createTabNavigator;
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('./routers/DrawerActions').default;
get DrawerNavigator() {
return require('./navigators/DrawerNavigator').default;
},
// Routers
@@ -79,9 +36,6 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// Views
get Transitioner() {
@@ -96,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() {
@@ -124,27 +72,13 @@ module.exports = {
// 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
@@ -154,7 +88,4 @@ module.exports = {
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
get withOrientation() {
return require('./views/withOrientation').default;
},
};

View File

@@ -8,23 +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;
},
// Routers
get StackRouter() {
return require('./routers/StackRouter').default;
@@ -32,9 +27,6 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// HOCs
get withNavigation() {

View File

@@ -1,28 +0,0 @@
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
const openDrawer = payload => ({
type: OPEN_DRAWER,
...payload,
});
const closeDrawer = payload => ({
type: CLOSE_DRAWER,
...payload,
});
const toggleDrawer = payload => ({
type: TOGGLE_DRAWER,
...payload,
});
export default {
OPEN_DRAWER,
CLOSE_DRAWER,
TOGGLE_DRAWER,
openDrawer,
closeDrawer,
toggleDrawer,
};

View File

@@ -1,84 +1,54 @@
import SwitchRouter from './SwitchRouter';
import invariant from '../utils/invariant';
import TabRouter from './TabRouter';
import NavigationActions from '../NavigationActions';
import invariant from '../utils/invariant';
import withDefaultValue from '../utils/withDefaultValue';
import DrawerActions from './DrawerActions';
export default (routeConfigs, config = {}) => {
config = { ...config };
config = withDefaultValue(config, 'resetOnBlur', false);
config = withDefaultValue(config, 'backBehavior', 'initialRoute');
const switchRouter = SwitchRouter(routeConfigs, config);
const tabRouter = TabRouter(routeConfigs, config);
return {
...switchRouter,
getActionCreators(route, navStateKey) {
return {
openDrawer: () => DrawerActions.openDrawer({ key: navStateKey }),
closeDrawer: () => DrawerActions.closeDrawer({ key: navStateKey }),
toggleDrawer: () => DrawerActions.toggleDrawer({ key: navStateKey }),
...switchRouter.getActionCreators(route, navStateKey),
};
},
...tabRouter,
getStateForAction(action, lastState) {
const state = lastState || {
...switchRouter.getStateForAction(action, undefined),
...tabRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
const isRouterTargeted = action.key == null || action.key === state.key;
if (isRouterTargeted) {
// Only handle actions that are meant for this drawer, as specified by action.key.
if (action.type === DrawerActions.CLOSE_DRAWER && state.isDrawerOpen) {
return {
...state,
isDrawerOpen: false,
};
}
if (action.type === DrawerActions.OPEN_DRAWER && !state.isDrawerOpen) {
return {
...state,
isDrawerOpen: true,
};
}
if (action.type === DrawerActions.TOGGLE_DRAWER) {
return {
...state,
isDrawerOpen: !state.isDrawerOpen,
};
}
// 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 switch router for screen switching logic, and handling of child routers
const switchedState = switchRouter.getStateForAction(action, state);
if (switchedState === null) {
// The switch router or a child router is attempting to swallow this action. We return null to allow this.
return null;
// 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,
};
}
if (switchedState !== state) {
if (switchedState.index !== state.index) {
// If the tabs have changed, make sure to close the drawer
return {
...switchedState,
isDrawerOpen: false,
};
}
// Return the state new state, as returned by the switch router.
// The index hasn't changed, so this most likely means that a child router has returned a new state
return switchedState;
}
return state;
},
};

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,7 +136,7 @@ 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 {
getComponentForState(state) {
@@ -162,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;
@@ -250,31 +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) {
return StateUtils.replaceAndPrune(
state,
nextRouteState ? nextRouteState.key : childRoute.key,
nextRouteState ? nextRouteState : childRoute
);
}
}
}
}
// Handle explicit push navigation action. This must happen after the
@@ -287,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) {
@@ -352,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
@@ -399,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) {
@@ -408,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]],
};
@@ -420,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) {
@@ -446,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,
};
}
@@ -476,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
@@ -513,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);
@@ -531,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;
},
@@ -566,7 +482,6 @@ export default (routeConfigs, stackConfig = {}) => {
if (!pathToResolve) {
return NavigationActions.navigate({
routeName: initialRouteName,
params: inputParams,
});
}
@@ -637,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,388 +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 {
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

@@ -4,7 +4,6 @@ import React from 'react';
import DrawerRouter from '../DrawerRouter';
import NavigationActions from '../../NavigationActions';
import DrawerActions from '../../routers/DrawerActions';
const INIT_ACTION = { type: NavigationActions.INIT };
@@ -19,7 +18,7 @@ describe('DrawerRouter', () => {
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
@@ -33,7 +32,7 @@ describe('DrawerRouter', () => {
);
const expectedState2 = {
index: 1,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
@@ -55,85 +54,19 @@ describe('DrawerRouter', () => {
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER },
{ type: NavigationActions.OPEN_DRAWER },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction(
{ type: DrawerActions.CLOSE_DRAWER },
{ type: NavigationActions.CLOSE_DRAWER },
state2
);
expect(state3.isDrawerOpen).toEqual(false);
const state4 = router.getStateForAction(
{ type: DrawerActions.TOGGLE_DRAWER },
{ type: NavigationActions.TOGGLE_DRAWER },
state3
);
expect(state4.isDrawerOpen).toEqual(true);
});
test('Drawer opens closes with key targeted', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
state
);
expect(state2.isDrawerOpen).toEqual(false);
const state3 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
state2
);
expect(state3.isDrawerOpen).toEqual(true);
});
});
test('Nested routers bubble up blocked actions', () => {
const ScreenA = () => <div />;
ScreenA.router = {
getStateForAction(action, lastState) {
if (action.type === 'CHILD_ACTION') return null;
return lastState;
},
};
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const state2 = router.getStateForAction({ type: 'CHILD_ACTION' }, state);
expect(state2).toEqual(null);
});
test('Drawer stays open when child routers return new state', () => {
const ScreenA = () => <div />;
ScreenA.router = {
getStateForAction(action, lastState = { changed: false }) {
if (action.type === 'CHILD_ACTION')
return { ...lastState, changed: true };
return lastState;
},
};
const router = DrawerRouter({
Foo: { screen: ScreenA },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction({ type: 'CHILD_ACTION' }, state2);
expect(state3.isDrawerOpen).toEqual(true);
expect(state3.routes[0].changed).toEqual(true);
});

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(() => {
@@ -58,31 +58,31 @@ Object.keys(ROUTERS).forEach(routerName => {
];
expect(
router.getScreenOptions(
{
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
).title
).toEqual(undefined);
expect(
router.getScreenOptions(
{
addNavigationHelpers({
state: routes[1],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
).title
).toEqual('BarTitle');
expect(
router.getScreenOptions(
{
addNavigationHelpers({
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
},
}),
{}
).title
).toEqual('Baz-123');
@@ -90,75 +90,6 @@ Object.keys(ROUTERS).forEach(routerName => {
});
});
test('Nested navigate behavior test', () => {
const Leaf = () => <div />;
const First = () => <div />;
First.router = StackRouter({
First1: Leaf,
First2: Leaf,
});
const Second = () => <div />;
Second.router = StackRouter({
Second1: Leaf,
Second2: Leaf,
});
const Main = () => <div />;
Main.router = StackRouter({
First,
Second,
});
const TestRouter = SwitchRouter({
Login: Leaf,
Main,
});
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const state2 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'First' },
state1
);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(0);
expect(state2.routes[1].routes[0].index).toEqual(0);
const state3 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Second2' },
state2
);
expect(state3.index).toEqual(1);
expect(state3.routes[1].index).toEqual(1); // second
expect(state3.routes[1].routes[1].index).toEqual(1); //second.second2
const state4 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'First',
action: { type: NavigationActions.NAVIGATE, routeName: 'First2' },
},
state3,
true
);
expect(state4.index).toEqual(1); // main
expect(state4.routes[1].index).toEqual(0); // first
expect(state4.routes[1].routes[0].index).toEqual(1); // first2
const state5 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'First',
action: { type: NavigationActions.NAVIGATE, routeName: 'First1' },
},
state3 // second.second2 is active on state3
);
expect(state5.index).toEqual(1); // main
expect(state5.routes[1].index).toEqual(0); // first
expect(state5.routes[1].routes[0].index).toEqual(0); // first.first1
});
test('Handles no-op actions with tabs within stack router', () => {
const BarView = () => <div />;
const FooTabNavigator = () => <div />;
@@ -204,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: [
{
@@ -227,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 />;
@@ -383,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,14 +389,46 @@ 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('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 /> },
@@ -447,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', () => {
@@ -475,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
@@ -483,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 /> },
@@ -529,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' } },
@@ -537,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 /> },
@@ -622,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);
@@ -676,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', () => {
@@ -769,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 /> },
@@ -847,7 +678,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -875,7 +706,7 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -895,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,
@@ -908,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',
@@ -942,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', () => {
@@ -972,7 +803,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1000,7 +831,7 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1074,7 +905,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1098,7 +929,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1167,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({
@@ -1228,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' },
},
]);
@@ -1249,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,
@@ -1284,7 +1079,7 @@ describe('StackRouter', () => {
});
const state1 = router.getStateForAction({ type: NavigationActions.INIT });
const resetAction = {
type: StackActions.RESET,
type: NavigationActions.RESET,
key: 'Bad Key',
actions: [
{
@@ -1317,7 +1112,7 @@ describe('StackRouter', () => {
});
test('Handles the reset action with nested Router', () => {
const ChildRouter = StackRouter({
const ChildRouter = TabRouter({
baz: {
screen: () => <div />,
},
@@ -1337,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,
@@ -1390,7 +1184,7 @@ describe('StackRouter', () => {
);
const state3 = router.getStateForAction(
{
type: StackActions.RESET,
type: NavigationActions.RESET,
key: 'Init',
actions: [
{
@@ -1405,7 +1199,7 @@ describe('StackRouter', () => {
);
const state4 = router.getStateForAction(
{
type: StackActions.RESET,
type: NavigationActions.RESET,
key: null,
actions: [
{
@@ -1480,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',
@@ -1539,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',
@@ -1570,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(
{
@@ -1618,7 +1448,7 @@ describe('StackRouter', () => {
const state = {
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{
index: 1,
@@ -1834,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

@@ -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

@@ -1,8 +1,8 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import NavigationActions from '../../NavigationActions';
import StackActions from '../../routers/StackActions';
import invariant from '../../utils/invariant';
/**
@@ -47,10 +47,9 @@ class DrawerSidebar extends React.PureComponent {
_onItemPress = ({ route, focused }) => {
if (!focused) {
let subAction;
// TODO (v3): Revisit and repeal this behavior:
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
if (route.index != null && route.index !== 0) {
subAction = StackActions.reset({
if (route.index !== undefined && route.index !== 0) {
subAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
@@ -59,12 +58,7 @@ class DrawerSidebar extends React.PureComponent {
],
});
}
this.props.navigation.dispatch(
NavigationActions.navigate({
routeName: route.routeName,
action: subAction,
})
);
this.props.navigation.navigate(route.routeName, undefined, subAction);
}
};

View File

@@ -2,9 +2,9 @@ import React from 'react';
import { Dimensions } from 'react-native';
import DrawerLayout from 'react-native-drawer-layout-polyfill';
import addNavigationHelpers from '../../addNavigationHelpers';
import DrawerSidebar from './DrawerSidebar';
import NavigationActions from '../../NavigationActions';
import DrawerActions from '../../routers/DrawerActions';
/**
* Component that renders the drawer.
@@ -17,7 +17,7 @@ export default class DrawerView extends React.PureComponent {
: this.props.navigationConfig.drawerWidth,
};
componentDidMount() {
componentWillMount() {
Dimensions.addEventListener('change', this._updateWidth);
}
@@ -25,10 +25,9 @@ export default class DrawerView extends React.PureComponent {
Dimensions.removeEventListener('change', this._updateWidth);
}
componentDidUpdate(prevProps, prevState) {
const { isDrawerOpen } = this.props.navigation.state;
const wasDrawerOpen = prevProps.navigation.state.isDrawerOpen;
componentWillReceiveProps(nextProps) {
const { isDrawerOpen } = nextProps.navigation.state;
const wasDrawerOpen = this.props.navigation.state.isDrawerOpen;
if (isDrawerOpen && !wasDrawerOpen) {
this._drawer.openDrawer();
} else if (wasDrawerOpen && !isDrawerOpen) {
@@ -38,18 +37,12 @@ export default class DrawerView extends React.PureComponent {
_handleDrawerOpen = () => {
const { navigation } = this.props;
const { isDrawerOpen } = navigation.state;
if (!isDrawerOpen) {
navigation.dispatch({ type: DrawerActions.OPEN_DRAWER });
}
navigation.dispatch({ type: NavigationActions.OPEN_DRAWER });
};
_handleDrawerClose = () => {
const { navigation } = this.props;
const { isDrawerOpen } = navigation.state;
if (isDrawerOpen) {
navigation.dispatch({ type: DrawerActions.CLOSE_DRAWER });
}
navigation.dispatch({ type: NavigationActions.CLOSE_DRAWER });
};
_updateWidth = () => {

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;

View File

@@ -1,33 +1,40 @@
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { polyfill } from 'react-lifecycles-compat';
import SceneView from './SceneView';
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
class ResourceSavingSceneView extends React.PureComponent {
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.isFocused && !prevState.awake) {
return { awake: true };
} else {
return null;
}
}
export default class ResourceSavingSceneView extends React.PureComponent {
constructor(props) {
super();
const key = props.childNavigation.state.key;
const focusedIndex = props.navigation.state.index;
const focusedKey = props.navigation.state.routes[focusedIndex].key;
const isFocused = key === focusedKey;
this.state = {
awake: props.lazy ? props.isFocused : true,
awake: props.lazy ? isFocused : true,
visible: isFocused,
};
}
componentWillMount() {
this._actionListener = this.props.navigation.addListener(
'action',
this._onAction
);
}
componentWillUnmount() {
this._actionListener.remove();
}
render() {
const { awake } = this.state;
const { awake, visible } = this.state;
const {
isFocused,
childNavigation,
navigation,
removeClippedSubviews,
@@ -42,12 +49,12 @@ class ResourceSavingSceneView extends React.PureComponent {
removeClippedSubviews={
Platform.OS === 'android'
? removeClippedSubviews
: !isFocused && removeClippedSubviews
: !visible && removeClippedSubviews
}
>
<View
style={
this._mustAlwaysBeVisible() || isFocused
this._mustAlwaysBeVisible() || visible
? styles.innerAttached
: styles.innerDetached
}
@@ -61,6 +68,33 @@ class ResourceSavingSceneView extends React.PureComponent {
_mustAlwaysBeVisible = () => {
return this.props.animationEnabled || this.props.swipeEnabled;
};
_onAction = payload => {
// We do not care about transition complete events, they won't actually change the state
if (
payload.action.type == 'Navigation/COMPLETE_TRANSITION' ||
!payload.state
) {
return;
}
const { routes, index } = payload.state;
const key = this.props.childNavigation.state.key;
if (routes[index].key === key) {
if (!this.state.visible) {
let nextState = { visible: true };
if (!this.state.awake) {
nextState.awake = true;
}
this.setState(nextState);
}
} else {
if (this.state.visible) {
this.setState({ visible: false });
}
}
};
}
const styles = StyleSheet.create({
@@ -76,5 +110,3 @@ const styles = StyleSheet.create({
top: FAR_FAR_AWAY,
},
});
export default polyfill(ResourceSavingSceneView);

View File

@@ -1,14 +1,19 @@
import React from 'react';
import propTypes from 'prop-types';
import { NavigationProvider } from './NavigationContext';
export default class SceneView extends React.PureComponent {
static childContextTypes = {
navigation: propTypes.object.isRequired,
};
getChildContext() {
return {
navigation: this.props.navigation,
};
}
render() {
const { screenProps, component: Component, navigation } = this.props;
return (
<NavigationProvider value={navigation}>
<Component screenProps={screenProps} navigation={navigation} />
</NavigationProvider>
);
const { screenProps, navigation, component: Component } = this.props;
return <Component screenProps={screenProps} navigation={navigation} />;
}
}

View File

@@ -4,7 +4,6 @@ import { NativeModules } from 'react-native';
import StackViewLayout from './StackViewLayout';
import Transitioner from '../Transitioner';
import NavigationActions from '../../NavigationActions';
import StackActions from '../../routers/StackActions';
import TransitionConfigs from './StackViewTransitionConfigs';
const NativeAnimatedModule =
@@ -25,16 +24,14 @@ class StackView extends React.Component {
navigation={this.props.navigation}
descriptors={this.props.descriptors}
onTransitionStart={this.props.onTransitionStart}
onTransitionEnd={(transition, lastTransition) => {
onTransitionEnd={(lastTransition, transition) => {
const { onTransitionEnd, navigation } = this.props;
if (transition.navigation.state.isTransitioning) {
navigation.dispatch(
StackActions.completeTransition({
key: navigation.state.key,
})
);
}
onTransitionEnd && onTransitionEnd(transition, lastTransition);
navigation.dispatch(
NavigationActions.completeTransition({
key: navigation.state.key,
})
);
onTransitionEnd && onTransitionEnd(lastTransition, transition);
}}
/>
);
@@ -57,9 +54,6 @@ class StackView extends React.Component {
return (
<StackViewLayout
{...navigationConfig}
onGestureBegin={this.props.onGestureBegin}
onGestureCanceled={this.props.onGestureCanceled}
onGestureEnd={this.props.onGestureEnd}
screenProps={screenProps}
descriptors={this.props.descriptors}
transitionProps={transitionProps}

View File

@@ -0,0 +1,546 @@
import * as React from 'react';
import Transitioner from './Transitioner2';
import NavigationActions from '../../NavigationActions';
import Transitions from './StackViewTransitions';
const NativeAnimatedModule =
NativeModules && NativeModules.NativeAnimatedModule;
import clamp from 'clamp';
import {
Animated,
StyleSheet,
PanResponder,
Platform,
View,
I18nManager,
Easing,
NativeModules,
} from 'react-native';
import Card from './StackViewCard';
// import Header from '../Header/Header2'; // WIP.. interpolation reconfiguration, fun!
import SceneView from '../SceneView';
import invariant from '../../utils/invariant';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
const EaseInOut = Easing.inOut(Easing.ease);
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
* is always less then the full distance of the layout.
*/
const ANIMATION_DURATION = 500;
/**
* The gesture distance threshold to trigger the back behavior. For instance,
* `1/2` means that moving greater than 1/2 of the width of the screen will
* trigger a back action
*/
const POSITION_THRESHOLD = 1 / 2;
/**
* The threshold (in pixels) to start the gesture action.
*/
const RESPOND_THRESHOLD = 20;
/**
* The distance of touch start from the edge of the screen where the gesture will be recognized
*/
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const animatedSubscribeValue = animatedValue => {
if (!animatedValue.__isNative) {
return;
}
if (Object.keys(animatedValue._listeners).length === 0) {
animatedValue.addListener(emptyFunction);
}
};
class StackViewLayout extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
* be updated according to its relative position. This means that a card can effectively be
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
* corresponding location for the touch.
*/
_gestureStartValue = 0;
// tracks if a touch is currently happening
_isResponding = false;
/**
* immediateIndex is used to represent the expected index that we will be on after a
* transition. To achieve a smooth animation when swiping back, the action to go back
* doesn't actually fire until the transition completes. The immediateIndex is used during
* the transition so that gestures can be handled correctly. This is a work-around for
* cases when the user quickly swipes back several times.
*/
_immediateIndex = null;
// _panResponder = PanResponder.create({
// onPanResponderTerminate: () => {
// this._isResponding = false;
// this._reset(index, 0);
// },
// onPanResponderGrant: () => {
// position.stopAnimation((value: number) => {
// this._isResponding = true;
// this._gestureStartValue = value;
// });
// },
// onMoveShouldSetPanResponder: (event, gesture) => {
// if (index !== scene.index) {
// return false;
// }
// const immediateIndex =
// this._immediateIndex == null ? index : this._immediateIndex;
// const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
// const currentDragPosition =
// event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
// const axisLength = isVertical
// ? layout.height.__getValue()
// : layout.width.__getValue();
// const axisHasBeenMeasured = !!axisLength;
// // Measure the distance from the touch to the edge of the screen
// const screenEdgeDistance = gestureDirectionInverted
// ? axisLength - (currentDragPosition - currentDragDistance)
// : currentDragPosition - currentDragDistance;
// // Compare to the gesture distance relavant to card or modal
// const { options } = scene.descriptor;
// const {
// gestureResponseDistance: userGestureResponseDistance = {},
// } = options;
// const gestureResponseDistance = isVertical
// ? userGestureResponseDistance.vertical ||
// GESTURE_RESPONSE_DISTANCE_VERTICAL
// : userGestureResponseDistance.horizontal ||
// GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// // GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
// if (screenEdgeDistance > gestureResponseDistance) {
// // Reject touches that started in the middle of the screen
// return false;
// }
// const hasDraggedEnough =
// Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
// const isOnFirstCard = immediateIndex === 0;
// const shouldSetResponder =
// hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
// return shouldSetResponder;
// },
// onPanResponderMove: (event, gesture) => {
// // Handle the moving touches for our granted responder
// const startValue = this._gestureStartValue;
// const axis = isVertical ? 'dy' : 'dx';
// const axisDistance = isVertical
// ? layout.height.__getValue()
// : layout.width.__getValue();
// const currentValue =
// (I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
// ? startValue + gesture[axis] / axisDistance
// : startValue - gesture[axis] / axisDistance;
// const value = clamp(index - 1, currentValue, index);
// position.setValue(value);
// },
// onPanResponderTerminationRequest: () =>
// // Returning false will prevent other views from becoming responder while
// // the navigation view is the responder (mid-gesture)
// false,
// onPanResponderRelease: (event, gesture) => {
// if (!this._isResponding) {
// return;
// }
// this._isResponding = false;
// const immediateIndex =
// this._immediateIndex == null ? index : this._immediateIndex;
// // Calculate animate duration according to gesture speed and moved distance
// const axisDistance = isVertical
// ? layout.height.__getValue()
// : layout.width.__getValue();
// const movementDirection = gestureDirectionInverted ? -1 : 1;
// const movedDistance =
// movementDirection * gesture[isVertical ? 'dy' : 'dx'];
// const gestureVelocity =
// movementDirection * gesture[isVertical ? 'vy' : 'vx'];
// const defaultVelocity = axisDistance / ANIMATION_DURATION;
// const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
// const resetDuration = gestureDirectionInverted
// ? (axisDistance - movedDistance) / velocity
// : movedDistance / velocity;
// const goBackDuration = gestureDirectionInverted
// ? movedDistance / velocity
// : (axisDistance - movedDistance) / velocity;
// // To asyncronously get the current animated value, we need to run stopAnimation:
// position.stopAnimation(value => {
// // If the speed of the gesture release is significant, use that as the indication
// // of intent
// if (gestureVelocity < -0.5) {
// this._reset(immediateIndex, resetDuration);
// return;
// }
// if (gestureVelocity > 0.5) {
// this._goBack(immediateIndex, goBackDuration);
// return;
// }
// // Then filter based on the distance the screen was moved. Over a third of the way swiped,
// // and the back will happen.
// if (value <= index - POSITION_THRESHOLD) {
// this._goBack(immediateIndex, goBackDuration);
// } else {
// this._reset(immediateIndex, resetDuration);
// }
// });
// },
// });
_renderHeader(descriptor, headerMode) {
const { options } = descriptor;
const { header } = options;
if (typeof header !== 'undefined' && typeof header !== 'function') {
return header;
}
// const renderHeader = header || (props => <Header {...props} />);
const renderHeader = header || (props => null);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
descriptor,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
});
}
// eslint-disable-next-line class-methods-use-this
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.layout.width);
animatedSubscribeValue(props.layout.height);
animatedSubscribeValue(props.position);
}
// _reset(resetToIndex, duration) {
// if (
// Platform.OS === 'ios' &&
// ReactNativeFeatures.supportsImprovedSpringAnimation()
// ) {
// Animated.spring(this.props.transitionProps.position, {
// toValue: resetToIndex,
// stiffness: 5000,
// damping: 600,
// mass: 3,
// useNativeDriver: this.props.transitionProps.position.__isNative,
// }).start();
// } else {
// Animated.timing(this.props.transitionProps.position, {
// toValue: resetToIndex,
// duration,
// easing: EaseInOut,
// useNativeDriver: this.props.transitionProps.position.__isNative,
// }).start();
// }
// }
// _goBack(backFromIndex, duration) {
// const { navigation, position, scenes } = this.props.transitionProps;
// const toValue = Math.max(backFromIndex - 1, 0);
// // set temporary index for gesture handler to respect until the action is
// // dispatched at the end of the transition.
// this._immediateIndex = toValue;
// const onCompleteAnimation = () => {
// this._immediateIndex = null;
// const backFromScene = scenes.find(s => s.index === toValue + 1);
// if (!this._isResponding && backFromScene) {
// navigation.dispatch(
// NavigationActions.back({
// key: backFromScene.route.key,
// immediate: true,
// })
// );
// }
// };
// if (
// Platform.OS === 'ios' &&
// ReactNativeFeatures.supportsImprovedSpringAnimation()
// ) {
// Animated.spring(position, {
// toValue,
// stiffness: 5000,
// damping: 600,
// mass: 3,
// useNativeDriver: position.__isNative,
// }).start(onCompleteAnimation);
// } else {
// Animated.timing(position, {
// toValue,
// duration,
// easing: EaseInOut,
// useNativeDriver: position.__isNative,
// }).start(onCompleteAnimation);
// }
// }
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
const {
navigation,
transition,
descriptor,
descriptors,
layout,
mode,
} = this.props;
if (headerMode === 'float') {
floatingHeader = this._renderHeader(descriptor, headerMode);
}
const { index, routes } = navigation.state;
const isVertical = mode === 'modal';
const { options } = descriptor;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
// const handlers = gesturesEnabled ? this._panResponder.panHandlers : {};
const handlers = {};
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
let forwardScene = null;
let backwardScene = null;
if (transition) {
const { fromDescriptor, toDescriptor } = transition;
const fromKey = fromDescriptor.key;
const toKey = toDescriptor.key;
const toIndex = navigation.state.routes.findIndex(r => r.key === toKey);
invariant(
toIndex !== -1,
`Could not find toIndex in navigation state for ${fromKey}`
);
const fromIndex = navigation.state.routes.findIndex(
r => r.key === fromKey
);
if (fromIndex == -1) {
// we are coming from a screen that is no longer in the stack
backwardScene = fromDescriptor;
} else if (toIndex > fromIndex) {
// presumably we are going doing a push.
backwardScene = fromDescriptor;
} else {
// we are navigating back, and the forward scene is on top
forwardScene = fromDescriptor;
}
} else if (index > 0) {
// when we aren't transitioning, render the previous screen in case we swipe back.
const previousKey = routes[index - 1].key;
const previousDescriptor = descriptors[previousKey];
backwardScene = previousDescriptor;
}
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{backwardScene && this._renderScene(backwardScene, index - 1)}
{this._renderScene(descriptor, index)}
{forwardScene && this._renderScene(forwardScene, index + 1)}
</View>
{floatingHeader}
</View>
);
}
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
_renderInnerScene(descriptor) {
const { options, navigation, getComponent } = descriptor;
const SceneComponent = getComponent();
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={{ flex: 1 }}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(descriptor, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return Transitions.getTransitionConfig(
this.props.transitionConfig,
this.props,
isModal
);
};
_renderScene = (descriptor, index) => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator &&
screenInterpolator({ ...this.props, descriptor, index });
return (
<Card
{...this.props}
// providing this descriptor will override this.props.descriptor, to tell the card exactly which scene to render, instead of this.props.descriptor, which defines what scene is active
descriptor={descriptor}
index={index}
key={`card_${descriptor.key}`}
style={[style, this.props.cardStyle]}
>
{this._renderInnerScene(descriptor)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
class StackView extends React.Component {
static defaultProps = {
navigationConfig: {
mode: 'card',
},
};
render() {
return (
<Transitioner
render={this._render}
configureTransition={this._configureTransition}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
// onTransitionStart={this.props.onTransitionStart}
// onTransitionEnd={(lastTransition, transition) => {
// const { onTransitionEnd, navigation } = this.props;
// navigation.dispatch(
// NavigationActions.completeTransition({
// key: navigation.state.key,
// })
// );
// onTransitionEnd && onTransitionEnd(lastTransition, transition);
// }}
/>
);
}
_configureTransition = transitionProps => {
return {
...Transitions.getTransitionConfig(
this.props.navigationConfig.transitionConfig,
transitionProps,
this.props.navigationConfig.mode === 'modal'
).transitionSpec,
useNativeDriver: !!NativeAnimatedModule,
};
};
_render = transitionProps => {
const { screenProps } = this.props;
return <StackViewLayout {...transitionProps} screenProps={screenProps} />;
};
}
export default StackView;

View File

@@ -9,29 +9,18 @@ import {
View,
I18nManager,
Easing,
Dimensions,
} from 'react-native';
import Card from './StackViewCard';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import StackActions from '../../routers/StackActions';
import SceneView from '../SceneView';
import withOrientation from '../withOrientation';
import { NavigationProvider } from '../NavigationContext';
import TransitionConfigs from './StackViewTransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
const IS_IPHONE_X =
Platform.OS === 'ios' &&
!Platform.isPad &&
!Platform.isTVOS &&
(WINDOW_HEIGHT === 812 || WINDOW_WIDTH === 812);
const EaseInOut = Easing.inOut(Easing.ease);
/**
@@ -93,18 +82,11 @@ class StackViewLayout extends React.Component {
const { options } = scene.descriptor;
const { header } = options;
if (header === null && headerMode === 'screen') {
return null;
}
// check if it's a react element
if (React.isValidElement(header)) {
if (typeof header !== 'undefined' && typeof header !== 'function') {
return header;
}
// Handle the case where the header option is a function, and provide the default
const renderHeader = header || (props => <Header {...props} />);
const renderHeader = header || ((props: *) => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
@@ -184,7 +166,6 @@ class StackViewLayout extends React.Component {
immediate: true,
})
);
navigation.dispatch(StackActions.completeTransition());
}
};
@@ -213,11 +194,9 @@ class StackViewLayout extends React.Component {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
const { scene } = this.props.transitionProps;
floatingHeader = (
<NavigationProvider value={scene.descriptor.navigation}>
{this._renderHeader(scene, headerMode)}
</NavigationProvider>
floatingHeader = this._renderHeader(
this.props.transitionProps.scene,
headerMode
);
}
const {
@@ -227,12 +206,8 @@ class StackViewLayout extends React.Component {
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
@@ -245,14 +220,12 @@ class StackViewLayout extends React.Component {
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
this.props.onGestureCanceled && this.props.onGestureCanceled();
},
onPanResponderGrant: () => {
position.stopAnimation((value: number) => {
this._isResponding = true;
this._gestureStartValue = value;
});
this.props.onGestureBegin && this.props.onGestureBegin();
},
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
@@ -306,7 +279,7 @@ class StackViewLayout extends React.Component {
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
axis === 'dx' && gestureDirectionInverted
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
@@ -351,12 +324,10 @@ class StackViewLayout extends React.Component {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this.props.onGestureCanceled && this.props.onGestureCanceled();
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this.props.onGestureFinish && this.props.onGestureFinish();
this._goBack(immediateIndex, goBackDuration);
return;
}
@@ -364,10 +335,8 @@ class StackViewLayout extends React.Component {
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this.props.onGestureFinish && this.props.onGestureFinish();
this._goBack(immediateIndex, goBackDuration);
} else {
this.props.onGestureCanceled && this.props.onGestureCanceled();
this._reset(immediateIndex, resetDuration);
}
});
@@ -383,7 +352,7 @@ class StackViewLayout extends React.Component {
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map(s => this._renderCard(s))}
{scenes.map((s: *) => this._renderCard(s))}
</View>
{floatingHeader}
</View>
@@ -424,7 +393,7 @@ class StackViewLayout extends React.Component {
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={styles.scenes}>
<View style={{ flex: 1 }}>
<SceneView
screenProps={screenProps}
navigation={navigation}
@@ -461,34 +430,11 @@ class StackViewLayout extends React.Component {
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
// If this screen has "header" set to `null` in it's navigation options, but
// it exists in a stack with headerMode float, add a negative margin to
// compensate for the hidden header
const { options } = scene.descriptor;
const hasHeader = options.header !== null;
const headerMode = this._getHeaderMode();
let marginTop = 0;
if (!hasHeader && headerMode === 'float') {
const { isLandscape } = this.props;
let headerHeight;
if (Platform.OS === 'android') {
// TODO: Need to handle translucent status bar.
headerHeight = 56;
} else if (isLandscape && !Platform.isPad) {
headerHeight = 52;
} else if (IS_IPHONE_X) {
headerHeight = 88;
} else {
headerHeight = 64;
}
marginTop = -headerHeight;
}
return (
<Card
{...this.props.transitionProps}
key={`card_${scene.key}`}
style={[style, { marginTop }, this.props.cardStyle]}
style={[style, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(scene)}
@@ -511,4 +457,4 @@ const styles = StyleSheet.create({
},
});
export default withOrientation(StackViewLayout);
export default StackViewLayout;

View File

@@ -60,21 +60,19 @@ const FadeOutToBottomAndroid = {
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
function defaultTransitionConfig(
transitionProps,
prevTransitionProps,
isModal
) {
function defaultTransitionConfig(transitionProps, isModal) {
if (Platform.OS === 'android') {
// Use the default Android animation no matter if the screen is a modal.
// Android doesn't have full-screen modals like iOS does, it has dialogs.
if (
prevTransitionProps &&
transitionProps.index < prevTransitionProps.index
) {
// Navigating back to the previous screen
return FadeOutToBottomAndroid;
}
// todo, uncomment and fix, stop using prevTransitionProps
// // Use the default Android animation no matter if the screen is a modal.
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
// if (
// prevTransitionProps &&
// transitionProps.index < prevTransitionProps.index
// ) {
// // Navigating back to the previous screen
// return FadeOutToBottomAndroid;
// }
return FadeInFromBottomAndroid;
}
// iOS and other platforms
@@ -84,21 +82,12 @@ function defaultTransitionConfig(
return SlideFromRightIOS;
}
function getTransitionConfig(
transitionConfigurer,
transitionProps,
prevTransitionProps,
isModal
) {
const defaultConfig = defaultTransitionConfig(
transitionProps,
prevTransitionProps,
isModal
);
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
if (transitionConfigurer) {
return {
...defaultConfig,
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
...transitionConfigurer(transitionProps, isModal),
};
}
return defaultConfig;

View File

@@ -0,0 +1,268 @@
import { Animated, Easing, Platform } from 'react-native';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
import { I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
/**
* Utility that builds the style for the card in the cards stack.
*
* +------------+
* +-+ |
* +-+ | |
* | | | |
* | | | Focused |
* | | | Card |
* | | | |
* +-+ | |
* +-+ |
* +------------+
*/
/**
* Render the initial style when the initial layout isn't measured yet.
*/
function forInitial(props) {
const { navigation, descriptor } = props;
const { state } = navigation;
const activeKey = state.routes[state.index].key;
const focused = descriptor.key === activeKey;
const opacity = focused ? 1 : 0;
// If not focused, move the card far away.
const translate = focused ? 0 : 1000000;
return {
opacity,
transform: [{ translateX: translate }, { translateY: translate }],
};
}
/**
* Standard iOS-style slide in from the right.
*/
function forHorizontal(props) {
const { layout, transition, navigation, index } = props;
const { state } = navigation;
if (!layout.isMeasured) {
return forInitial(props);
}
const first = index - 1;
const last = index + 1;
const opacity = transition
? transition.progress.interpolate({
inputRange: [first, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
})
: 1;
const width = layout.initWidth;
const translateX = transition
? transition.progress.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-width, 0, width * 0.3]
: [width, 0, width * -0.3],
})
: 0;
return {
opacity,
transform: [{ translateX }],
};
}
/**
* Standard iOS-style slide in from the bottom (used for modals).
*/
function forVertical(props) {
const { layout, transition, descriptor } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = transition.progress.interpolate({
inputRange: [first, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
});
const height = layout.initHeight;
const translateY = transition.progress.interpolate({
inputRange: [first, index, last],
outputRange: [height, 0, 0],
});
const translateX = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* Standard Android-style fade in from the bottom.
*/
function forFadeFromBottomAndroid(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index, last - 0.01, last];
const opacity = position.interpolate({
inputRange,
outputRange: [0, 1, 1, 0],
});
const translateY = position.interpolate({
inputRange,
outputRange: [50, 0, 0, 0],
});
const translateX = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* fadeIn and fadeOut
*/
function forFade(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = position.interpolate({
inputRange: [first, index, last],
outputRange: [0, 1, 1],
});
return {
opacity,
};
}
const StyleInterpolator = {
forHorizontal,
forVertical,
forFadeFromBottomAndroid,
forFade,
};
let IOSTransitionSpec;
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
// These are the exact values from UINavigationController's animation configuration
IOSTransitionSpec = {
timing: Animated.spring,
stiffness: 1000,
damping: 500,
mass: 3,
};
} else {
// This is an approximation of the IOS spring animation using a derived bezier curve
IOSTransitionSpec = {
duration: 500,
easing: Easing.bezier(0.2833, 0.99, 0.31833, 0.99),
timing: Animated.timing,
};
}
// Standard iOS navigation transition
const SlideFromRightIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: StyleInterpolator.forHorizontal,
containerStyle: {
backgroundColor: '#000',
},
};
// Standard iOS navigation transition for modals
const ModalSlideFromBottomIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: StyleInterpolator.forVertical,
containerStyle: {
backgroundColor: '#000',
},
};
// Standard Android navigation transition when opening an Activity
const FadeInFromBottomAndroid = {
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
transitionSpec: {
duration: 350,
easing: Easing.out(Easing.poly(5)), // decelerate
timing: Animated.timing,
},
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
// Standard Android navigation transition when closing an Activity
const FadeOutToBottomAndroid = {
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
transitionSpec: {
duration: 230,
easing: Easing.in(Easing.poly(4)), // accelerate
timing: Animated.timing,
},
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
function defaultTransitionConfig(transitionProps, isModal) {
if (Platform.OS === 'android') {
// todo, uncomment and fix, stop using prevTransitionProps
// // Use the default Android animation no matter if the screen is a modal.
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
// if (
// prevTransitionProps &&
// transitionProps.index < prevTransitionProps.index
// ) {
// // Navigating back to the previous screen
// return FadeOutToBottomAndroid;
// }
return FadeInFromBottomAndroid;
}
// iOS and other platforms
if (isModal) {
return ModalSlideFromBottomIOS;
}
return SlideFromRightIOS;
}
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
if (transitionConfigurer) {
return {
...defaultConfig,
...transitionConfigurer(transitionProps, isModal),
};
}
return defaultConfig;
}
export default {
defaultTransitionConfig,
getTransitionConfig,
StyleInterpolator,
};

View File

@@ -0,0 +1,225 @@
import React from 'react';
import { Animated, Easing, StyleSheet, View } from 'react-native';
import invariant from '../../utils/invariant';
// Used for all animations unless overriden
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
timing: Animated.timing,
};
class Transitioner extends React.Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
static getDerivedStateFromProps = (props, lastState) => {
const { navigation, descriptors } = props;
const { state } = navigation;
const canGoBack = state.index > 0;
const activeKey = state.routes[state.index].key;
const descriptor = descriptors[activeKey];
if (!lastState) {
lastState = {
backProgress: canGoBack ? new Animaged.Value(1) : null,
descriptor,
descriptors,
navigation,
transition: null,
layout: {
height: new Animated.Value(0),
initHeight: 0,
initWidth: 0,
isMeasured: false,
width: new Animated.Value(0),
},
};
}
// const lastNavState = this.props.navigation.state;
const lastNavState = lastState.navigation.state;
const lastActiveKey = lastNavState.routes[lastNavState.index].key;
// const transitionFromKey =
// lastActiveKey !== activeKey ? lastActiveKey : null;
const transitionFromKey = state.transitioningFromKey;
const transitionFromDescriptor =
transitionFromKey &&
lastState.descriptor &&
lastState.descriptor.key === transitionFromKey;
// We can only perform a transition if we have been told to via state.transitioningFromKey, and if our previous descriptor matches, indicating that the transitioningFromKey is currently being presented.
if (transitionFromDescriptor) {
if (lastState.transition) {
// there is already a transition in progress.. Don't interrupt it!
// At the end of the transition, we will compare props and start again
return lastState;
}
return {
...lastState,
navigation,
descriptor,
backProgress: null,
transition: {
fromDescriptor: lastState.descriptor,
toDescriptor: descriptor,
progress: new Animated.Value(0),
},
};
}
// No transition is being performed. If the key has changed, present it immediately without transition
if (lastActiveKey !== activeKey) {
return {
...lastState,
backProgress: canGoBack ? new Animaged.Value(1) : null,
descriptor,
transition: null,
};
}
return lastState;
};
// React doesn't handle getDerivedStateFromProps yet, but the polyfill is simple..
state = Transitioner.getDerivedStateFromProps(this.props);
componentWillReceiveProps(nextProps) {
const nextState = Transitioner.getDerivedStateFromProps(
nextProps,
this.state
);
if (this.state !== nextState) {
this.setState(nextState);
}
}
_startTransition(transition) {
const { configureTransition } = this.props;
const { descriptors } = this.state;
const { progress, fromDescriptor, toDescriptor } = transition;
progress.setValue(0);
// get the transition spec.
// passing the new transitionProps format (this.state) into configureTransition is a breaking change that I haven't documented yet!
const transitionUserSpec =
(configureTransition && configureTransition(this.state)) || null;
const transitionSpec = {
...DefaultTransitionSpec,
...transitionUserSpec,
};
const { timing } = transitionSpec;
// mutating a prop, this is terrible!
// it was in the previous transitioner implementation, so I'm leaving it as-is for now:
delete transitionSpec.timing;
timing(progress, {
...transitionSpec,
toValue: 1,
}).start(didComplete => {
this._completeTransition(transition, didComplete);
});
}
_completeTransition(transition, didComplete) {
if (!this._isMounted) {
return;
}
const { progress, fromDescriptor, toDescriptor } = transition;
const { navigation, descriptors } = this.props;
const nextState = navigation.state;
const activeKey = nextState.routes[nextState.index].key;
const nextDescriptor =
descriptors[activeKey] || this.state.descriptors[activeKey];
if (activeKey !== toDescriptor.key) {
// The user has changed navigation states during the transition! This is known as a queued transition.
// Now we set state for a new transition to the current navigation state
this.setState({
navigation,
descriptors,
descriptor: nextDescriptor,
transition: {
fromDescriptor: toDescriptor,
toDescriptor: nextDescriptor,
progress: new Animated.Value(0),
},
backProgress: null,
});
return;
}
const canGoBack = navigation.state.index > 0;
// All transitions are complete. Reset to normal state:
this.setState({
navigation,
descriptors,
descriptor: nextDescriptor,
transition: null,
backProgress: canGoBack ? new Animated.Value(1) : null,
});
}
render() {
console.log('Rendering Transitioner', this.state);
return (
<View onLayout={this._onLayout} style={[styles.main]}>
{this.props.render(this.state)}
</View>
);
}
componentDidUpdate(lastProps, lastState) {
// start transition if it needs it
if (
this.state.transition &&
(!lastState.transition ||
lastState.transition.toDescriptor !==
this.state.transition.toDescriptor)
) {
this._startTransition(this.state.transition);
}
}
_onLayout = event => {
const lastLayout = this.state.layout;
const { height, width } = event.nativeEvent.layout;
if (lastLayout.initWidth === width && lastLayout.initHeight === height) {
return;
}
const layout = {
...lastLayout,
initHeight: height,
initWidth: width,
isMeasured: true,
};
layout.height.setValue(height);
layout.width.setValue(width);
this.setState({ layout });
};
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
export default Transitioner;

View File

@@ -11,66 +11,24 @@ const MIN_POSITION_OFFSET = 0.01;
*/
export default function createPointerEventsContainer(Component) {
class Container extends React.Component {
constructor(props, context) {
super(props, context);
this._pointerEvents = this._computePointerEvents();
}
componentWillUnmount() {
this._positionListener && this._positionListener.remove();
}
render() {
this._bindPosition();
this._pointerEvents = this._computePointerEvents();
return (
<Component
{...this.props}
pointerEvents={this._pointerEvents}
onComponentRef={this._onComponentRef}
/>
<Component {...this.props} pointerEvents={this._getPointerEvents()} />
);
}
_onComponentRef = component => {
this._component = component;
if (component) {
invariant(
typeof component.setNativeProps === 'function',
'component must implement method `setNativeProps`'
);
}
};
_bindPosition() {
this._positionListener && this._positionListener.remove();
this._positionListener = new AnimatedValueSubscription(
this.props.position,
this._onPositionChange
_getPointerEvents() {
const { navigation, descriptor, transition } = this.props;
const { state } = navigation;
const descriptorIndex = navigation.state.routes.findIndex(
r => r.key === descriptor.key
);
}
_onPositionChange = () => {
if (this._component) {
const pointerEvents = this._computePointerEvents();
if (this._pointerEvents !== pointerEvents) {
this._pointerEvents = pointerEvents;
this._component.setNativeProps({ pointerEvents });
}
}
};
_computePointerEvents() {
const { navigation, position, scene } = this.props;
if (scene.isStale || navigation.state.index !== scene.index) {
if (descriptorIndex !== state.index) {
// The scene isn't focused.
return scene.index > navigation.state.index ? 'box-only' : 'none';
return descriptorIndex > state.index ? 'box-only' : 'none';
}
const offset = position.__getAnimatedValue() - navigation.state.index;
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
if (transition) {
// The positon is still away from scene's index.
// Scene's children should not receive touches until the position
// is close enough to scene's index.

View File

@@ -1,19 +0,0 @@
import React from 'react';
import SceneView from '../SceneView';
export default class SwitchView extends React.Component {
render() {
const { state } = this.props.navigation;
const activeKey = state.routes[state.index].key;
const descriptor = this.props.descriptors[activeKey];
const ChildComponent = descriptor.getComponent();
return (
<SceneView
component={ChildComponent}
navigation={descriptor.navigation}
screenProps={this.props.screenProps}
/>
);
}
}

View File

@@ -0,0 +1,331 @@
import React from 'react';
import {
Animated,
TouchableWithoutFeedback,
StyleSheet,
View,
Platform,
Keyboard,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TabBarIcon from './TabBarIcon';
import NavigationActions from '../../NavigationActions';
import withOrientation from '../withOrientation';
const majorVersion = parseInt(Platform.Version, 10);
const isIos = Platform.OS === 'ios';
const isIOS11 = majorVersion >= 11 && isIos;
const defaultMaxTabBarItemWidth = 125;
class TabBarBottom extends React.PureComponent {
// See https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/UITabBar.html
static defaultProps = {
activeTintColor: '#3478f6', // Default active tint color in iOS 10
activeBackgroundColor: 'transparent',
inactiveTintColor: '#929292', // Default inactive tint color in iOS 10
inactiveBackgroundColor: 'transparent',
showLabel: true,
showIcon: true,
allowFontScaling: true,
adaptive: isIOS11,
};
_renderLabel = scene => {
const {
position,
navigation,
activeTintColor,
inactiveTintColor,
labelStyle,
showLabel,
showIcon,
isLandscape,
allowFontScaling,
} = this.props;
if (showLabel === false) {
return null;
}
const { index } = scene;
const { routes } = navigation.state;
// Prepend '-1', so there are always at least 2 items in inputRange
const inputRange = [-1, ...routes.map((x, i) => i)];
const outputRange = inputRange.map(
inputIndex => (inputIndex === index ? activeTintColor : inactiveTintColor)
);
const color = position.interpolate({
inputRange,
outputRange: outputRange,
});
const tintColor = scene.focused ? activeTintColor : inactiveTintColor;
const label = this.props.getLabel({ ...scene, tintColor });
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color },
showIcon && this._shouldUseHorizontalTabs()
? styles.labelBeside
: styles.labelBeneath,
labelStyle,
]}
allowFontScaling={allowFontScaling}
>
{label}
</Animated.Text>
);
}
if (typeof label === 'function') {
return label({ ...scene, tintColor });
}
return label;
};
_renderIcon = scene => {
const {
position,
navigation,
activeTintColor,
inactiveTintColor,
renderIcon,
showIcon,
showLabel,
} = this.props;
if (showIcon === false) {
return null;
}
return (
<TabBarIcon
position={position}
navigation={navigation}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
scene={scene}
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
/>
);
};
_renderTestIDProps = scene => {
const testIDProps =
this.props.getTestIDProps && this.props.getTestIDProps(scene);
return testIDProps;
};
_tabItemMaxWidth() {
const { tabStyle, layout } = this.props;
let maxTabBarItemWidth;
const flattenedTabStyle = StyleSheet.flatten(tabStyle);
if (flattenedTabStyle) {
if (typeof flattenedTabStyle.width === 'number') {
maxTabBarItemWidth = flattenedTabStyle.width;
} else if (
typeof flattenedTabStyle.width === 'string' &&
flattenedTabStyle.endsWith('%')
) {
const width = parseFloat(flattenedTabStyle.width);
if (Number.isFinite(width)) {
maxTabBarItemWidth = layout.width * (width / 100);
}
} else if (typeof flattenedTabStyle.maxWidth === 'number') {
maxTabBarItemWidth = flattenedTabStyle.maxWidth;
} else if (
typeof flattenedTabStyle.maxWidth === 'string' &&
flattenedTabStyle.endsWith('%')
) {
const width = parseFloat(flattenedTabStyle.maxWidth);
if (Number.isFinite(width)) {
maxTabBarItemWidth = layout.width * (width / 100);
}
}
}
if (!maxTabBarItemWidth) {
maxTabBarItemWidth = defaultMaxTabBarItemWidth;
}
return maxTabBarItemWidth;
}
_shouldUseHorizontalTabs() {
const { routes } = this.props.navigation.state;
const { isLandscape, layout, adaptive, tabStyle } = this.props;
if (!adaptive) {
return false;
}
let tabBarWidth = layout.width;
if (tabBarWidth === 0) {
return Platform.isPad;
}
if (!Platform.isPad) {
return isLandscape;
} else {
const maxTabBarItemWidth = this._tabItemMaxWidth();
return routes.length * maxTabBarItemWidth <= tabBarWidth;
}
}
_handleTabPress = index => {
const { jumpToIndex, navigation } = this.props;
const currentIndex = navigation.state.index;
if (currentIndex === index) {
let childRoute = navigation.state.routes[index];
if (childRoute.hasOwnProperty('index') && childRoute.index > 0) {
navigation.dispatch(
NavigationActions.popToTop({ key: childRoute.key })
);
} else {
// TODO: do something to scroll to top
}
} else {
jumpToIndex(index);
}
};
render() {
const {
position,
navigation,
jumpToIndex,
getOnPress,
getTestIDProps,
activeBackgroundColor,
inactiveBackgroundColor,
style,
animateStyle,
tabStyle,
isLandscape,
} = this.props;
const { routes } = navigation.state;
const previousScene = routes[navigation.state.index];
// Prepend '-1', so there are always at least 2 items in inputRange
const inputRange = [-1, ...routes.map((x, i) => i)];
const tabBarStyle = [
styles.tabBar,
this._shouldUseHorizontalTabs() && !Platform.isPad
? styles.tabBarCompact
: styles.tabBarRegular,
style,
];
return (
<Animated.View style={animateStyle}>
<SafeAreaView
style={tabBarStyle}
forceInset={{ bottom: 'always', top: 'never' }}
>
{routes.map((route, index) => {
const focused = index === navigation.state.index;
const scene = { route, index, focused };
const onPress = getOnPress(previousScene, scene);
const outputRange = inputRange.map(
inputIndex =>
inputIndex === index
? activeBackgroundColor
: inactiveBackgroundColor
);
const backgroundColor = position.interpolate({
inputRange,
outputRange: outputRange,
});
const justifyContent = this.props.showIcon ? 'flex-end' : 'center';
const extraProps = this._renderTestIDProps(scene) || {};
const { testID, accessibilityLabel } = extraProps;
return (
<TouchableWithoutFeedback
key={route.key}
testID={testID}
accessibilityLabel={accessibilityLabel}
onPress={() =>
onPress
? onPress({
previousScene,
scene,
jumpToIndex,
defaultHandler: this._handleTabPress,
})
: this._handleTabPress(index)
}
>
<Animated.View style={[styles.tab, { backgroundColor }]}>
<View
style={[
styles.tab,
this._shouldUseHorizontalTabs()
? styles.tabLandscape
: styles.tabPortrait,
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
</View>
</Animated.View>
</TouchableWithoutFeedback>
);
})}
</SafeAreaView>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0, 0, 0, .3)',
flexDirection: 'row',
},
tabBarCompact: {
height: 29,
},
tabBarRegular: {
height: 49,
},
tab: {
flex: 1,
alignItems: isIos ? 'center' : 'stretch',
},
tabPortrait: {
justifyContent: 'flex-end',
flexDirection: 'column',
},
tabLandscape: {
justifyContent: 'center',
flexDirection: 'row',
},
icon: {
flexGrow: 1,
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',
},
labelBeneath: {
fontSize: 10,
marginBottom: 1.5,
},
labelBeside: {
fontSize: 13,
marginLeft: 20,
},
});
export default withOrientation(TabBarBottom);

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