mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-20 19:08:15 +08:00
Compare commits
62 Commits
3.5.0-alph
...
1.5.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fe72f6627 | ||
|
|
2a7ce9fc8c | ||
|
|
c8b431773e | ||
|
|
66486cecad | ||
|
|
b065b31bb2 | ||
|
|
06d483677c | ||
|
|
d8bcbdd690 | ||
|
|
1a0ebfeb90 | ||
|
|
1ebd8f000e | ||
|
|
a322a6f6d5 | ||
|
|
efa53aab44 | ||
|
|
3eeea48fa3 | ||
|
|
3039ce1a5f | ||
|
|
cbe46d7303 | ||
|
|
11a99c8aaf | ||
|
|
6b5100c335 | ||
|
|
34dd1fe490 | ||
|
|
0cc9070dcd | ||
|
|
e369c89b81 | ||
|
|
99bf123ff2 | ||
|
|
feb93411bc | ||
|
|
86f07175fb | ||
|
|
8288853e3c | ||
|
|
c4bd2db542 | ||
|
|
ee40dd7d24 | ||
|
|
18a48105c2 | ||
|
|
fbac47b696 | ||
|
|
9aab47dac2 | ||
|
|
67309c00a6 | ||
|
|
86a724cfe3 | ||
|
|
eb78128439 | ||
|
|
c39ec7a10c | ||
|
|
0ff3347e97 | ||
|
|
b9d55a6330 | ||
|
|
315e43701b | ||
|
|
1d573bc246 | ||
|
|
3bfb0b90d0 | ||
|
|
8a129afe13 | ||
|
|
ab2a63fe92 | ||
|
|
c411210ecc | ||
|
|
01e7296520 | ||
|
|
8f3e0997c5 | ||
|
|
3f3ef6485c | ||
|
|
b12abb553f | ||
|
|
e02841a979 | ||
|
|
e147f34555 | ||
|
|
81e0ce136e | ||
|
|
8ba727c2cf | ||
|
|
9a86ef8362 | ||
|
|
4fe7c92847 | ||
|
|
afecaaed7f | ||
|
|
6373b802dd | ||
|
|
138151433d | ||
|
|
2744cb32b7 | ||
|
|
439b4222ce | ||
|
|
ba0b1861e5 | ||
|
|
318788ca60 | ||
|
|
498a39c200 | ||
|
|
b31ebef5b0 | ||
|
|
f4fe588e08 | ||
|
|
403af82c3f | ||
|
|
0c2360dc36 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -9,6 +9,8 @@ If you have a question, feature request, or an idea for improving the library or
|
||||
- [Get help on Discord chat (#react-navigation on Reactiflux)](https://discord.gg/4xEK3nD) or [on StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation)
|
||||
- Search for your issue - it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue.
|
||||
|
||||
Bugs with react-navigation must be reproducible *without any external libraries that operate on it*. This means that if you are attempting to use Redux or MobX with it and you think you have found a bug, you must be able to reproduce it without Redux or MobX in this report. Redux related issues belong in [react-navigation-redux-helpers](https://github.com/react-navigation/react-navigation-redux-helpers), and we do not have any first-class integration with MobX at the moment.
|
||||
|
||||
---
|
||||
|
||||
### Current Behavior
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "25.0.0",
|
||||
"sdkVersion": "26.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
|
||||
@@ -25,12 +25,14 @@ import MultipleDrawer from './MultipleDrawer';
|
||||
import TabsInDrawer from './TabsInDrawer';
|
||||
import ModalStack from './ModalStack';
|
||||
import StacksInTabs from './StacksInTabs';
|
||||
import SwitchWithStacks from './SwitchWithStacks';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
import StacksWithKeys from './StacksWithKeys';
|
||||
import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import CustomTabNavigator from './CustomTabNavigator';
|
||||
import TabAnimations from './TabAnimations';
|
||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||
|
||||
@@ -39,6 +41,10 @@ const ExampleInfo = {
|
||||
name: 'Stack Example',
|
||||
description: 'A card stack',
|
||||
},
|
||||
SwitchWithStacks: {
|
||||
name: 'Switch Example',
|
||||
description: 'A switch with stacks inside',
|
||||
},
|
||||
SimpleTabs: {
|
||||
name: 'Tabs Example',
|
||||
description: 'Tabs following platform conventions',
|
||||
@@ -67,9 +73,13 @@ const ExampleInfo = {
|
||||
name: 'Drawer + Tabs Example',
|
||||
description: 'A drawer combined with tabs',
|
||||
},
|
||||
CustomTabNavigator: {
|
||||
name: 'Custom Tab Navigator Example',
|
||||
description: 'Extending the tab navigator',
|
||||
},
|
||||
CustomTabs: {
|
||||
name: 'Custom Tabs',
|
||||
description: 'Custom tabs with tab router',
|
||||
name: 'Custom Tabs View',
|
||||
description: 'Custom tabs using TabRouter',
|
||||
},
|
||||
CustomTransitioner: {
|
||||
name: 'Custom Transitioner',
|
||||
@@ -109,28 +119,30 @@ const ExampleInfo = {
|
||||
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',
|
||||
// },
|
||||
TabsWithNavigationFocus: {
|
||||
name: 'withNavigationFocus',
|
||||
description: 'Receive the focus prop to know when a screen is focused',
|
||||
},
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
SimpleStack: SimpleStack,
|
||||
SimpleTabs: SimpleTabs,
|
||||
Drawer: Drawer,
|
||||
SimpleStack,
|
||||
SwitchWithStacks,
|
||||
SimpleTabs,
|
||||
Drawer,
|
||||
// MultipleDrawer: {
|
||||
// screen: MultipleDrawer,
|
||||
// },
|
||||
StackWithHeaderPreset: StackWithHeaderPreset,
|
||||
StackWithTranslucentHeader: StackWithTranslucentHeader,
|
||||
TabsInDrawer: TabsInDrawer,
|
||||
CustomTabs: CustomTabs,
|
||||
CustomTransitioner: CustomTransitioner,
|
||||
ModalStack: ModalStack,
|
||||
StacksWithKeys: StacksWithKeys,
|
||||
StacksInTabs: StacksInTabs,
|
||||
StacksOverTabs: StacksOverTabs,
|
||||
StackWithHeaderPreset,
|
||||
StackWithTranslucentHeader,
|
||||
TabsInDrawer,
|
||||
CustomTabs,
|
||||
CustomTransitioner,
|
||||
ModalStack,
|
||||
StacksWithKeys,
|
||||
StacksInTabs,
|
||||
StacksOverTabs,
|
||||
CustomTabNavigator,
|
||||
LinkStack: {
|
||||
screen: SimpleStack,
|
||||
path: 'people/Jordan',
|
||||
@@ -139,8 +151,8 @@ const ExampleRoutes = {
|
||||
screen: SimpleTabs,
|
||||
path: 'settings',
|
||||
},
|
||||
TabAnimations: TabAnimations,
|
||||
// TabsWithNavigationFocus: TabsWithNavigationFocus,
|
||||
TabAnimations,
|
||||
TabsWithNavigationFocus,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
201
examples/NavigationPlayground/js/CustomTabNavigator.js
Normal file
201
examples/NavigationPlayground/js/CustomTabNavigator.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {
|
||||
NavigationScreenProp,
|
||||
NavigationEventSubscription,
|
||||
} from 'react-navigation';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, TabNavigator } from 'react-navigation';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Home')}
|
||||
title="Go to home tab"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
title="Go to settings tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
const MyHomeScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Home Tab" navigation={navigation} />
|
||||
);
|
||||
|
||||
MyHomeScreen.navigationOptions = {
|
||||
tabBarTestIDProps: {
|
||||
testID: 'TEST_ID_HOME',
|
||||
accessibilityLabel: 'TEST_ID_HOME_ACLBL',
|
||||
},
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-home' : 'ios-home-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
type MyPeopleScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'People',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-people' : 'ios-people-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="People Tab" navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
type MyChatScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-chatboxes' : 'ios-chatboxes-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="Chat Tab" navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Tab" navigation={navigation} />
|
||||
);
|
||||
|
||||
MySettingsScreen.navigationOptions = {
|
||||
tabBarLabel: 'Settings',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-settings' : 'ios-settings-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const SimpleTabs = TabNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
People: {
|
||||
screen: MyPeopleScreen,
|
||||
},
|
||||
Chat: {
|
||||
screen: MyChatScreen,
|
||||
},
|
||||
Settings: {
|
||||
screen: MySettingsScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
tabBarVisible: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type SimpleTabsContainerProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
const TabBarButton = ({ name, navigation }) => {
|
||||
const currentName = navigation.state.routes[navigation.state.index].routeName;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate(name);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ color: currentName === name ? 'blue' : 'black', fontSize: 32 }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
class CustomTabBar extends React.Component<*> {
|
||||
render() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
}}
|
||||
>
|
||||
<TabBarButton navigation={this.props.navigation} name="Home" />
|
||||
<TabBarButton navigation={this.props.navigation} name="People" />
|
||||
<TabBarButton navigation={this.props.navigation} name="Chat" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTabsContainer extends React.Component<SimpleTabsContainerProps> {
|
||||
static router = {
|
||||
...SimpleTabs.router,
|
||||
getStateForAction(action, lastState) {
|
||||
// You can override the behavior navigation actions here, which are dispatched via navigation.dispatch, or via helpers like navigaiton.navigate.
|
||||
|
||||
// In this case we simply use the default behavior:
|
||||
const newState = SimpleTabs.router.getStateForAction(action, lastState);
|
||||
|
||||
console.log('Tab router action:', action, newState);
|
||||
return newState;
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SimpleTabs navigation={this.props.navigation} />
|
||||
<CustomTabBar navigation={this.props.navigation} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleTabsContainer;
|
||||
@@ -17,7 +17,11 @@ type MyNavScreenProps = {
|
||||
banner: React.Node,
|
||||
};
|
||||
|
||||
class MyBackButton extends React.Component<any, any> {
|
||||
type MyBackButtonProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
class MyBackButton extends React.Component<MyBackButtonProps, any> {
|
||||
render() {
|
||||
return <Button onPress={this._navigateBack} title="Custom Back" />;
|
||||
}
|
||||
@@ -142,7 +146,7 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.name}'s Photos`}
|
||||
banner={`${navigation.getParam('name')}'s Photos`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
@@ -151,9 +155,9 @@ class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
|
||||
|
||||
const MyProfileScreen = ({ navigation }) => (
|
||||
<MyNavScreen
|
||||
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
|
||||
navigation.state.params.name
|
||||
}'s Profile`}
|
||||
banner={`${
|
||||
navigation.getParam('mode') === 'edit' ? 'Now Editing ' : ''
|
||||
}${navigation.getParam('name')}'s Profile`}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -52,11 +52,6 @@ type MyPeopleScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
_s0: NavigationEventSubscription;
|
||||
_s1: NavigationEventSubscription;
|
||||
_s2: NavigationEventSubscription;
|
||||
_s3: NavigationEventSubscription;
|
||||
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'People',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
@@ -67,21 +62,6 @@ class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
/>
|
||||
),
|
||||
};
|
||||
componentDidMount() {
|
||||
this._s0 = this.props.navigation.addListener('willFocus', this._onEvent);
|
||||
this._s1 = this.props.navigation.addListener('didFocus', this._onEvent);
|
||||
this._s2 = this.props.navigation.addListener('willBlur', this._onEvent);
|
||||
this._s3 = this.props.navigation.addListener('didBlur', this._onEvent);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._s0.remove();
|
||||
this._s1.remove();
|
||||
this._s2.remove();
|
||||
this._s3.remove();
|
||||
}
|
||||
_onEvent = a => {
|
||||
console.log('EVENT ON PEOPLE TAB', a.type, a);
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="People Tab" navigation={navigation} />;
|
||||
@@ -92,11 +72,6 @@ type MyChatScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
_s0: NavigationEventSubscription;
|
||||
_s1: NavigationEventSubscription;
|
||||
_s2: NavigationEventSubscription;
|
||||
_s3: NavigationEventSubscription;
|
||||
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
@@ -107,21 +82,6 @@ class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
/>
|
||||
),
|
||||
};
|
||||
componentDidMount() {
|
||||
this._s0 = this.props.navigation.addListener('willFocus', this._onEvent);
|
||||
this._s1 = this.props.navigation.addListener('didFocus', this._onEvent);
|
||||
this._s2 = this.props.navigation.addListener('willBlur', this._onEvent);
|
||||
this._s3 = this.props.navigation.addListener('didBlur', this._onEvent);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._s0.remove();
|
||||
this._s1.remove();
|
||||
this._s2.remove();
|
||||
this._s3.remove();
|
||||
}
|
||||
_onEvent = a => {
|
||||
console.log('EVENT ON CHAT TAB', a.type, a);
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="Chat Tab" navigation={navigation} />;
|
||||
@@ -171,35 +131,4 @@ const SimpleTabs = TabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
type SimpleTabsContainerProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
class SimpleTabsContainer extends React.Component<SimpleTabsContainerProps> {
|
||||
static router = SimpleTabs.router;
|
||||
_s0: NavigationEventSubscription;
|
||||
_s1: NavigationEventSubscription;
|
||||
_s2: NavigationEventSubscription;
|
||||
_s3: NavigationEventSubscription;
|
||||
|
||||
componentDidMount() {
|
||||
this._s0 = this.props.navigation.addListener('willFocus', this._onAction);
|
||||
this._s1 = this.props.navigation.addListener('didFocus', this._onAction);
|
||||
this._s2 = this.props.navigation.addListener('willBlur', this._onAction);
|
||||
this._s3 = this.props.navigation.addListener('didBlur', this._onAction);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._s0.remove();
|
||||
this._s1.remove();
|
||||
this._s2.remove();
|
||||
this._s3.remove();
|
||||
}
|
||||
_onAction = a => {
|
||||
console.log('TABS EVENT', a.type, a);
|
||||
};
|
||||
render() {
|
||||
return <SimpleTabs navigation={this.props.navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleTabsContainer;
|
||||
export default SimpleTabs;
|
||||
|
||||
@@ -119,6 +119,9 @@ const StacksInTabs = TabNavigator(
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: false,
|
||||
swipeEnabled: false,
|
||||
tabBarOptions: {
|
||||
showLabel: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
Button,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { StackNavigator, SwitchNavigator } from 'react-navigation';
|
||||
|
||||
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('App');
|
||||
};
|
||||
}
|
||||
|
||||
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 = StackNavigator({ Home: HomeScreen, Other: OtherScreen });
|
||||
const AuthStack = StackNavigator({ SignIn: SignInScreen });
|
||||
|
||||
export default SwitchNavigator({
|
||||
Loading: LoadingScreen,
|
||||
App: AppStack,
|
||||
Auth: AuthStack,
|
||||
});
|
||||
@@ -3,40 +3,80 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SafeAreaView, Text } from 'react-native';
|
||||
import { TabNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import { Button, SafeAreaView, Text } from 'react-native';
|
||||
import {
|
||||
TabNavigator,
|
||||
TabBarTop,
|
||||
StackNavigator,
|
||||
withNavigationFocus,
|
||||
} from 'react-navigation';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
const TabScreen = ({ isFocused }) => (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
|
||||
{'Tab ' + name.toLowerCase()}
|
||||
class Child extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<Text style={{ color: this.props.isFocused ? 'green' : 'maroon' }}>
|
||||
{this.props.isFocused
|
||||
? 'I know that my parent is focused!'
|
||||
: 'My parent is not focused! :O'}
|
||||
</Text>
|
||||
<Text>{'props.isFocused: ' + (isFocused ? ' true' : 'false')}</Text>
|
||||
</SafeAreaView>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabScreen.navigationOptions = {
|
||||
tabBarLabel: name,
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? focusedIcon : icon}
|
||||
size={26}
|
||||
style={{ color: focused ? tintColor : '#ccc' }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
const ChildWithNavigationFocus = withNavigationFocus(Child);
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
class TabScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: name,
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? focusedIcon : icon}
|
||||
size={26}
|
||||
style={{ color: focused ? tintColor : '#ccc' }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
state = { showChild: false };
|
||||
|
||||
render() {
|
||||
const { isFocused } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
|
||||
{'Tab ' + name.toLowerCase()}
|
||||
</Text>
|
||||
<Text style={{ marginBottom: 20 }}>
|
||||
{'props.isFocused: ' + (isFocused ? ' true' : 'false')}
|
||||
</Text>
|
||||
{this.state.showChild ? (
|
||||
<ChildWithNavigationFocus />
|
||||
) : (
|
||||
<Button
|
||||
title="Press me"
|
||||
onPress={() => this.setState({ showChild: true })}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={() => this.props.navigation.goBack(null)}
|
||||
title="Back to other examples"
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
return withNavigationFocus(TabScreen);
|
||||
};
|
||||
|
||||
@@ -57,10 +97,35 @@ const TabsWithNavigationFocus = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
tabBarComponent: TabBarTop,
|
||||
tabBarPosition: 'top',
|
||||
animationEnabled: true,
|
||||
swipeEnabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default TabsWithNavigationFocus;
|
||||
const Stack = StackNavigator(
|
||||
{
|
||||
TabsWithNavigationFocus,
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
headerTitle: 'Navigation focus example',
|
||||
headerLeft: null,
|
||||
headerTitleStyle: {
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
headerTintColor: '#fff',
|
||||
headerStyle: {
|
||||
backgroundColor: '#2196f3',
|
||||
borderBottomWidth: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default Stack;
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"test": "node node_modules/jest/bin/jest.js && flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^25.0.0",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"expo": "^26.0.0",
|
||||
"react": "16.3.0-alpha.1",
|
||||
"react-native": "^0.54.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../.."
|
||||
},
|
||||
@@ -22,9 +22,9 @@
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"jest": "^21.0.1",
|
||||
"jest-expo": "^25.1.0",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
"react-test-renderer": "16.0.0"
|
||||
"react-test-renderer": "16.3.0-alpha.1"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-redux-helpers": "^1.0.0",
|
||||
"react-navigation-redux-helpers": "^1.0.3",
|
||||
"react-redux": "^5.0.6",
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
|
||||
import { initializeListeners } from 'react-navigation-redux-helpers';
|
||||
|
||||
import LoginScreen from '../components/LoginScreen';
|
||||
import MainScreen from '../components/MainScreen';
|
||||
@@ -20,6 +21,10 @@ class AppWithNavigationState extends React.Component {
|
||||
nav: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
initializeListeners('root', this.props.nav);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
|
||||
83
flow/react-navigation.js
vendored
83
flow/react-navigation.js
vendored
@@ -48,6 +48,15 @@ declare module 'react-navigation' {
|
||||
// react-native/Libraries/Animated/src/nodes/AnimatedValue.js
|
||||
declare type AnimatedValue = Object;
|
||||
|
||||
declare type HeaderForceInset = {
|
||||
horizontal?: string,
|
||||
vertical?: string,
|
||||
left?: string,
|
||||
right?: string,
|
||||
top?: string,
|
||||
bottom?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Next, all the type declarations
|
||||
*/
|
||||
@@ -286,7 +295,6 @@ declare module 'react-navigation' {
|
||||
} & NavigationScreenRouteConfig);
|
||||
|
||||
declare export type NavigationScreenRouteConfig =
|
||||
| NavigationComponent
|
||||
| {
|
||||
screen: NavigationComponent,
|
||||
}
|
||||
@@ -340,6 +348,7 @@ declare module 'react-navigation' {
|
||||
headerPressColorAndroid?: string,
|
||||
headerRight?: React$Node,
|
||||
headerStyle?: ViewStyleProp,
|
||||
headerForceInset?: HeaderForceInset,
|
||||
headerBackground?: React$Node | React$ElementType,
|
||||
gesturesEnabled?: boolean,
|
||||
gestureResponseDistance?: { vertical?: number, horizontal?: number },
|
||||
@@ -351,6 +360,7 @@ declare module 'react-navigation' {
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
initialRouteKey?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationStackViewConfig = {|
|
||||
@@ -368,6 +378,20 @@ declare module 'react-navigation' {
|
||||
...NavigationStackRouterConfig,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Switch Navigator
|
||||
*/
|
||||
|
||||
declare export type NavigationSwitchRouterConfig = {|
|
||||
initialRouteName?: string,
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
order?: Array<string>,
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults to `'none'`
|
||||
resetOnBlur?: boolean, // defaults to `true`
|
||||
|};
|
||||
|
||||
/**
|
||||
* Tab Navigator
|
||||
*/
|
||||
@@ -379,7 +403,6 @@ declare module 'react-navigation' {
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
// todo: type these as the real route names rather than 'string'
|
||||
order?: Array<string>,
|
||||
|
||||
// Does the back button cause the router to switch to the initial tab
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|
||||
|};
|
||||
@@ -447,7 +470,7 @@ declare module 'react-navigation' {
|
||||
type: EventType,
|
||||
action: NavigationAction,
|
||||
state: NavigationState,
|
||||
lastState: NavigationState,
|
||||
lastState: ?NavigationState,
|
||||
};
|
||||
|
||||
declare export type NavigationEventCallback = (
|
||||
@@ -468,6 +491,7 @@ declare module 'react-navigation' {
|
||||
action?: NavigationNavigateAction
|
||||
) => boolean,
|
||||
setParams: (newParams: NavigationParams) => boolean,
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
addListener: (
|
||||
eventName: string,
|
||||
callback: NavigationEventCallback
|
||||
@@ -530,7 +554,7 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
|
||||
uriPrefix?: string | RegExp,
|
||||
onNavigationStateChange?: (
|
||||
onNavigationStateChange?: ?(
|
||||
NavigationState,
|
||||
NavigationState,
|
||||
NavigationAction
|
||||
@@ -698,10 +722,11 @@ declare module 'react-navigation' {
|
||||
INIT: 'Navigation/INIT',
|
||||
NAVIGATE: 'Navigation/NAVIGATE',
|
||||
RESET: 'Navigation/RESET',
|
||||
REPLACE: 'Navigation/REPLACE',
|
||||
SET_PARAMS: 'Navigation/SET_PARAMS',
|
||||
URI: 'Navigation/URI',
|
||||
back: {
|
||||
(payload: { key?: ?string }): NavigationBackAction,
|
||||
(payload?: { key?: ?string }): NavigationBackAction,
|
||||
toString: () => string,
|
||||
},
|
||||
init: {
|
||||
@@ -724,6 +749,17 @@ declare module 'react-navigation' {
|
||||
}): NavigationResetAction,
|
||||
toString: () => string,
|
||||
},
|
||||
replace: {
|
||||
(payload: {
|
||||
routeName: string,
|
||||
key: string,
|
||||
newKey?: ?string,
|
||||
params?: ?NavigationParams,
|
||||
action?: ?NavigationNavigateAction,
|
||||
immediate?: ?boolean,
|
||||
}): NavigationReplaceAction,
|
||||
toString: () => string,
|
||||
},
|
||||
setParams: {
|
||||
(payload: {
|
||||
key: string,
|
||||
@@ -786,6 +822,13 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare type _SwitchNavigatorConfig = {|
|
||||
...NavigationSwitchRouterConfig,
|
||||
|};
|
||||
declare export function SwitchNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _SwitchNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare type _DrawerViewConfig = {|
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
@@ -895,12 +938,14 @@ declare module 'react-navigation' {
|
||||
vertical?: _SafeAreaViewForceInsetValue,
|
||||
horizontal?: _SafeAreaViewForceInsetValue,
|
||||
},
|
||||
children: React$Node,
|
||||
children?: React$Node,
|
||||
style?: AnimatedViewStyleProp,
|
||||
};
|
||||
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>;
|
||||
|
||||
declare export var Header: React$ComponentType<HeaderProps>;
|
||||
declare export var Header: React$ComponentType<HeaderProps> & {
|
||||
HEIGHT: number,
|
||||
};
|
||||
|
||||
declare type _HeaderTitleProps = {
|
||||
children: React$Node,
|
||||
@@ -965,6 +1010,8 @@ declare module 'react-navigation' {
|
||||
itemsContainerStyle?: ViewStyleProp,
|
||||
itemStyle?: ViewStyleProp,
|
||||
labelStyle?: TextStyleProp,
|
||||
activeLabelStyle?: TextStyleProp,
|
||||
inactiveLabelStyle?: TextStyleProp,
|
||||
iconContainerStyle?: ViewStyleProp,
|
||||
drawerPosition: 'left' | 'right',
|
||||
};
|
||||
@@ -1044,13 +1091,17 @@ declare module 'react-navigation' {
|
||||
};
|
||||
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
|
||||
|
||||
declare type _NavigationInjectedProps = {
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
};
|
||||
declare export function withNavigation<T: {}>(
|
||||
Component: React$ComponentType<T & _NavigationInjectedProps>
|
||||
): React$ComponentType<T>;
|
||||
declare export function withNavigationFocus<T: {}>(
|
||||
Component: React$ComponentType<T & _NavigationInjectedProps>
|
||||
): React$ComponentType<T>;
|
||||
declare export function withNavigation<Props: {}>(
|
||||
Component: React$ComponentType<Props>
|
||||
): React$ComponentType<
|
||||
$Diff<
|
||||
Props,
|
||||
{
|
||||
navigation: NavigationScreenProp<NavigationStateRoute> | void,
|
||||
}
|
||||
>
|
||||
>;
|
||||
declare export function withNavigationFocus<Props: {}>(
|
||||
Component: React$ComponentType<Props>
|
||||
): React$ComponentType<$Diff<Props, { isFocused: boolean | void }>>;
|
||||
}
|
||||
|
||||
34
package.json
34
package.json
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "1.2.1",
|
||||
"version": "1.5.13",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
"url": "git@github.com:react-navigation/react-navigation.git",
|
||||
"type": "git"
|
||||
},
|
||||
"author":
|
||||
"Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>",
|
||||
"author": "Adam Miskiewicz <adam@sk3vy.com>, Eric Vicenti <ericvicenti@gmail.com>",
|
||||
"license": "BSD-2-Clause",
|
||||
"scripts": {
|
||||
"start": "npm run ios",
|
||||
@@ -22,7 +21,9 @@
|
||||
"format": "eslint --fix .",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"files": ["src"],
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
@@ -32,9 +33,10 @@
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^3.0.2",
|
||||
"react-native-drawer-layout-polyfill": "^1.3.2",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"react-native-tab-view": "^0.0.74"
|
||||
"react-native-safe-area-view": "^0.9.0",
|
||||
"react-native-tab-view": "github:react-navigation/react-native-tab-view"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
@@ -63,13 +65,23 @@
|
||||
"notify": true,
|
||||
"preset": "react-native",
|
||||
"testRegex": "./src/.*\\-test\\.js$",
|
||||
"setupFiles": ["<rootDir>/jest-setup.js"],
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
],
|
||||
"coverageDirectory": "./coverage/",
|
||||
"collectCoverage": true,
|
||||
"coverageReporters": ["lcov"],
|
||||
"collectCoverageFrom": ["src/**/*.js"],
|
||||
"coveragePathIgnorePatterns": ["jest-setup.js"],
|
||||
"modulePathIgnorePatterns": ["examples"]
|
||||
"coverageReporters": [
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"jest-setup.js"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
"examples"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Based on the 'action' events that get fired for this navigation state, this utility will fire
|
||||
* focus and blur events for this child
|
||||
*/
|
||||
|
||||
export default function getChildEventSubscriber(addListener, key) {
|
||||
const actionSubscribers = new Set();
|
||||
const willFocusSubscribers = new Set();
|
||||
|
||||
@@ -11,6 +11,7 @@ import NavigationActions from '../NavigationActions';
|
||||
|
||||
export default (routeConfigMap, stackConfig = {}) => {
|
||||
const {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
@@ -25,6 +26,7 @@ export default (routeConfigMap, stackConfig = {}) => {
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
@@ -47,7 +49,7 @@ export default (routeConfigMap, stackConfig = {}) => {
|
||||
onTransitionEnd={(lastTransition, transition) => {
|
||||
const { state, dispatch } = props.navigation;
|
||||
dispatch(NavigationActions.completeTransition({ key: state.key }));
|
||||
onTransitionEnd && onTransitionEnd();
|
||||
onTransitionEnd && onTransitionEnd(lastTransition, transition);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
15
src/navigators/SwitchNavigator.js
Normal file
15
src/navigators/SwitchNavigator.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import SwitchRouter from '../routers/SwitchRouter';
|
||||
import SwitchView from '../views/SwitchView/SwitchView';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from '../navigators/createNavigator';
|
||||
|
||||
export default (routeConfigMap, switchConfig = {}) => {
|
||||
const router = SwitchRouter(routeConfigMap, switchConfig);
|
||||
|
||||
const navigator = createNavigator(router, routeConfigMap, switchConfig)(
|
||||
props => <SwitchView {...props} />
|
||||
);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import SwitchNavigator from '../SwitchNavigator';
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -224,6 +224,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
|
||||
"color": "#2196f3",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
@@ -80,10 +80,11 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
@@ -265,10 +266,11 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwitchNavigator renders successfully 1`] = `<View />`;
|
||||
@@ -137,9 +137,15 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"height": 29,
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -147,13 +153,13 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"minWidth": 30,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -162,13 +168,13 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"minWidth": 30,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
11
src/react-navigation.js
vendored
11
src/react-navigation.js
vendored
@@ -22,6 +22,9 @@ module.exports = {
|
||||
get StackNavigator() {
|
||||
return require('./navigators/StackNavigator').default;
|
||||
},
|
||||
get SwitchNavigator() {
|
||||
return require('./navigators/SwitchNavigator').default;
|
||||
},
|
||||
get TabNavigator() {
|
||||
return require('./navigators/TabNavigator').default;
|
||||
},
|
||||
@@ -36,6 +39,9 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
|
||||
// Views
|
||||
get Transitioner() {
|
||||
@@ -84,6 +90,11 @@ module.exports = {
|
||||
return require('./views/TabView/TabBarBottom').default;
|
||||
},
|
||||
|
||||
// SwitchView
|
||||
get SwitchView() {
|
||||
return require('./views/SwitchView/SwitchView').default;
|
||||
},
|
||||
|
||||
// HOCs
|
||||
get withNavigation() {
|
||||
return require('./views/withNavigation').default;
|
||||
|
||||
@@ -92,11 +92,12 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
...(action.params || {}),
|
||||
...(initialRouteParams || {}),
|
||||
};
|
||||
const { initialRouteKey } = stackConfig;
|
||||
route = {
|
||||
...route,
|
||||
...(params ? { params } : {}),
|
||||
routeName: initialRouteName,
|
||||
key: action.key || generateKey(),
|
||||
key: action.key || (initialRouteKey || generateKey()),
|
||||
};
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
|
||||
364
src/routers/SwitchRouter.js
Normal file
364
src/routers/SwitchRouter.js
Normal file
@@ -0,0 +1,364 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
NavigationActions.SET_PARAMS,
|
||||
NavigationActions.COMPLETE_TRANSITION,
|
||||
].includes(actionType);
|
||||
}
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
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];
|
||||
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;
|
||||
},
|
||||
|
||||
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
|
||||
if (backBehavior !== 'none') {
|
||||
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) {
|
||||
const navigateAction = action;
|
||||
didNavigate = !!order.find((childId, i) => {
|
||||
if (childId === navigateAction.routeName) {
|
||||
activeChildIndex = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (didNavigate) {
|
||||
const childState = state.routes[activeChildIndex];
|
||||
let newChildState;
|
||||
|
||||
const childRouter = childRouters[action.routeName];
|
||||
|
||||
if (action.action) {
|
||||
newChildState = childRouter
|
||||
? childRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 null;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return (
|
||||
order
|
||||
.map(childId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[childId];
|
||||
if (parts[0] === pathToTest) {
|
||||
const childRouter = childRouters[childId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: childId,
|
||||
});
|
||||
if (childRouter && childRouter.getActionForPathAndParams) {
|
||||
action.action = childRouter.getActionForPathAndParams(
|
||||
parts.slice(1).join('/'),
|
||||
params
|
||||
);
|
||||
} else 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
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
};
|
||||
@@ -576,6 +576,23 @@ describe('StackRouter', () => {
|
||||
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge');
|
||||
});
|
||||
|
||||
test('Navigate to initial screen is possible', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteKey: 'foo' }
|
||||
);
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(0);
|
||||
expect(pushedState.routes[0].routeName).toEqual('foo');
|
||||
});
|
||||
|
||||
test('Navigate with key is idempotent', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
|
||||
123
src/routers/__tests__/SwitchRouter-test.js
Normal file
123
src/routers/__tests__/SwitchRouter-test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/* 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('sets the next state even if no previous state is provided', () => {
|
||||
const router = getExampleRouter();
|
||||
const initialState = router.getStateForAction({
|
||||
type: NavigationActions.INIT,
|
||||
});
|
||||
const nextState = router.getStateForAction({
|
||||
type: NavigationActions.NAVIGATE,
|
||||
routeName: 'A2',
|
||||
});
|
||||
|
||||
expect(nextState.routes[0].index).toEqual(1);
|
||||
expect(nextState.routes[0].routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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: StackA,
|
||||
B: StackB,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'A',
|
||||
...config,
|
||||
}
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`validateRouteConfigMap Fails if both screen and getScreen are defined 1`] = `"Route 'Home' should declare a screen or a getScreen, not both."`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on bad object 1`] = `
|
||||
"The component for route 'Home' must be a React component. For example:
|
||||
|
||||
import MyScreen from './MyScreen';
|
||||
...
|
||||
Home: MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
import MyNavigator from './MyNavigator';
|
||||
...
|
||||
Home: MyNavigator,
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on empty bare screen 1`] = `
|
||||
"The component for route 'Home' must be a React component. For example:
|
||||
|
||||
import MyScreen from './MyScreen';
|
||||
...
|
||||
Home: MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
import MyNavigator from './MyNavigator';
|
||||
...
|
||||
Home: MyNavigator,
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on empty config 1`] = `"Please specify at least one route when configuring a navigator."`;
|
||||
@@ -13,9 +13,19 @@ ProfileNavigator.router = StackRouter({
|
||||
});
|
||||
|
||||
describe('validateRouteConfigMap', () => {
|
||||
test('Fails on empty bare screen', () => {
|
||||
const invalidMap = {
|
||||
Home: undefined,
|
||||
};
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails on empty config', () => {
|
||||
const invalidMap = {};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails on bad object', () => {
|
||||
const invalidMap = {
|
||||
@@ -23,7 +33,9 @@ describe('validateRouteConfigMap', () => {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails if both screen and getScreen are defined', () => {
|
||||
const invalidMap = {
|
||||
@@ -32,15 +44,17 @@ describe('validateRouteConfigMap', () => {
|
||||
getScreen: () => ListScreen,
|
||||
},
|
||||
};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Succeeds on a valid config', () => {
|
||||
const invalidMap = {
|
||||
const validMap = {
|
||||
Home: {
|
||||
screen: ProfileNavigator,
|
||||
},
|
||||
Chat: ListScreen,
|
||||
};
|
||||
validateRouteConfigMap(invalidMap);
|
||||
validateRouteConfigMap(validMap);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,16 +13,13 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
|
||||
routeNames.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
|
||||
const screenComponent = routeConfig.screen
|
||||
? routeConfig.screen
|
||||
: routeConfig;
|
||||
const screenComponent = getScreenComponent(routeConfig);
|
||||
|
||||
if (
|
||||
screenComponent &&
|
||||
typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen
|
||||
!screenComponent ||
|
||||
(typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen)
|
||||
) {
|
||||
throw new Error(
|
||||
`The component for route '${routeName}' must be a ` +
|
||||
@@ -48,4 +45,12 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
});
|
||||
}
|
||||
|
||||
function getScreenComponent(routeConfig) {
|
||||
if (!routeConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return routeConfig.screen ? routeConfig.screen : routeConfig;
|
||||
}
|
||||
|
||||
export default validateRouteConfigMap;
|
||||
|
||||
@@ -82,6 +82,8 @@ class CardStack extends React.Component {
|
||||
|
||||
_screenDetails = {};
|
||||
|
||||
_childEventSubscribers = {};
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.screenProps !== this.props.screenProps) {
|
||||
this._screenDetails = {};
|
||||
@@ -96,17 +98,39 @@ class CardStack extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.transitionProps.navigation.state.routes.map(
|
||||
route => route.key
|
||||
);
|
||||
Object.keys(this._childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this._childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isRouteFocused = route => {
|
||||
const { transitionProps: { navigation: { state } } } = this.props;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_getScreenDetails = scene => {
|
||||
const { screenProps, transitionProps: { navigation }, router } = this.props;
|
||||
let screenDetails = this._screenDetails[scene.key];
|
||||
if (!screenDetails || screenDetails.state !== scene.route) {
|
||||
if (!this._childEventSubscribers[scene.route.key]) {
|
||||
this._childEventSubscribers[scene.route.key] = getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
scene.route.key
|
||||
);
|
||||
}
|
||||
|
||||
const screenNavigation = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: scene.route,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
scene.route.key
|
||||
),
|
||||
isFocused: () => this._isRouteFocused(scene.route),
|
||||
addListener: this._childEventSubscribers[scene.route.key],
|
||||
});
|
||||
screenDetails = {
|
||||
state: scene.route,
|
||||
|
||||
@@ -21,6 +21,8 @@ const DrawerNavigatorItems = ({
|
||||
itemsContainerStyle,
|
||||
itemStyle,
|
||||
labelStyle,
|
||||
activeLabelStyle,
|
||||
inactiveLabelStyle,
|
||||
iconContainerStyle,
|
||||
drawerPosition,
|
||||
}) => (
|
||||
@@ -34,6 +36,7 @@ const DrawerNavigatorItems = ({
|
||||
const scene = { route, index, focused, tintColor: color };
|
||||
const icon = renderIcon(scene);
|
||||
const label = getLabel(scene);
|
||||
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
|
||||
return (
|
||||
<TouchableItem
|
||||
key={route.key}
|
||||
@@ -63,7 +66,9 @@ const DrawerNavigatorItems = ({
|
||||
</View>
|
||||
) : null}
|
||||
{typeof label === 'string' ? (
|
||||
<Text style={[styles.label, { color }, labelStyle]}>
|
||||
<Text
|
||||
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
|
||||
@@ -11,8 +11,9 @@ import invariant from '../../utils/invariant';
|
||||
*/
|
||||
class DrawerSidebar extends React.PureComponent {
|
||||
_getScreenOptions = routeKey => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
'DrawerClose'
|
||||
drawerCloseRoute
|
||||
);
|
||||
invariant(
|
||||
DrawerScreen.router,
|
||||
@@ -56,7 +57,8 @@ class DrawerSidebar extends React.PureComponent {
|
||||
};
|
||||
|
||||
_onItemPress = ({ route, focused }) => {
|
||||
this.props.navigation.navigate('DrawerClose');
|
||||
const { drawerCloseRoute } = this.props;
|
||||
this.props.navigation.navigate(drawerCloseRoute);
|
||||
if (!focused) {
|
||||
let subAction;
|
||||
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
|
||||
|
||||
@@ -17,6 +17,8 @@ export default class DrawerView extends React.PureComponent {
|
||||
: this.props.drawerWidth,
|
||||
};
|
||||
|
||||
_childEventSubscribers = {};
|
||||
|
||||
componentWillMount() {
|
||||
this._updateScreenNavigation(this.props.navigation);
|
||||
|
||||
@@ -27,6 +29,17 @@ export default class DrawerView extends React.PureComponent {
|
||||
Dimensions.removeEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(
|
||||
route => route.key
|
||||
);
|
||||
Object.keys(this._childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this._childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.navigation.state.index !== nextProps.navigation.state.index
|
||||
@@ -68,6 +81,12 @@ export default class DrawerView extends React.PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_isRouteFocused = route => () => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_updateScreenNavigation = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
@@ -79,13 +98,18 @@ export default class DrawerView extends React.PureComponent {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._childEventSubscribers[navigationState.key]) {
|
||||
this._childEventSubscribers[
|
||||
navigationState.key
|
||||
] = getChildEventSubscriber(navigation.addListener, navigationState.key);
|
||||
}
|
||||
|
||||
this._screenNavigationProp = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: navigationState,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
navigationState.key
|
||||
),
|
||||
isFocused: () => this._isRouteFocused(navigationState),
|
||||
addListener: this._childEventSubscribers[navigationState.key],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -108,17 +132,24 @@ export default class DrawerView extends React.PureComponent {
|
||||
return navigationState;
|
||||
};
|
||||
|
||||
_renderNavigationView = () => (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this._screenNavigationProp}
|
||||
router={this.props.router}
|
||||
contentComponent={this.props.contentComponent}
|
||||
contentOptions={this.props.contentOptions}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
style={this.props.style}
|
||||
/>
|
||||
);
|
||||
_renderNavigationView = () => {
|
||||
const { drawerOpenRoute, drawerCloseRoute, drawerToggleRoute } = this.props;
|
||||
|
||||
return (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this._screenNavigationProp}
|
||||
router={this.props.router}
|
||||
contentComponent={this.props.contentComponent}
|
||||
contentOptions={this.props.contentOptions}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
style={this.props.style}
|
||||
drawerOpenRoute={drawerOpenRoute}
|
||||
drawerCloseRoute={drawerCloseRoute}
|
||||
drawerToggleRoute={drawerToggleRoute}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
|
||||
@@ -476,11 +476,11 @@ class Header extends React.PureComponent {
|
||||
safeHeaderStyle,
|
||||
];
|
||||
|
||||
const { headerForceInset } = options;
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ top: 'always', bottom: 'never' }}
|
||||
style={containerStyles}
|
||||
>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
|
||||
<View style={{ flex: 1 }}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import SceneView from './SceneView';
|
||||
|
||||
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
|
||||
|
||||
export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
class ResourceSavingSceneView extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
const key = props.childNavigation.state.key;
|
||||
const focusedIndex = props.navigation.state.index;
|
||||
const focusedKey = props.navigation.state.routes[focusedIndex].key;
|
||||
const isFocused = key === focusedKey;
|
||||
|
||||
this.state = {
|
||||
awake: props.lazy ? isFocused : true,
|
||||
visible: isFocused,
|
||||
awake: props.lazy ? props.isFocused : true,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._actionListener = this.props.navigation.addListener(
|
||||
'action',
|
||||
this._onAction
|
||||
);
|
||||
}
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.isFocused && !prevState.awake) {
|
||||
return { awake: true };
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._actionListener.remove();
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { awake, visible } = this.state;
|
||||
const { awake } = this.state;
|
||||
const {
|
||||
isFocused,
|
||||
childNavigation,
|
||||
navigation,
|
||||
removeClippedSubviews,
|
||||
@@ -49,12 +42,12 @@ export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
removeClippedSubviews={
|
||||
Platform.OS === 'android'
|
||||
? removeClippedSubviews
|
||||
: !visible && removeClippedSubviews
|
||||
: !isFocused && removeClippedSubviews
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
this._mustAlwaysBeVisible() || visible
|
||||
this._mustAlwaysBeVisible() || isFocused
|
||||
? styles.innerAttached
|
||||
: styles.innerDetached
|
||||
}
|
||||
@@ -68,33 +61,6 @@ export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
_mustAlwaysBeVisible = () => {
|
||||
return this.props.animationEnabled || this.props.swipeEnabled;
|
||||
};
|
||||
|
||||
_onAction = payload => {
|
||||
// We do not care about transition complete events, they won't actually change the state
|
||||
if (
|
||||
payload.action.type == 'Navigation/COMPLETE_TRANSITION' ||
|
||||
!payload.state
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { routes, index } = payload.state;
|
||||
const key = this.props.childNavigation.state.key;
|
||||
|
||||
if (routes[index].key === key) {
|
||||
if (!this.state.visible) {
|
||||
let nextState = { visible: true };
|
||||
if (!this.state.awake) {
|
||||
nextState.awake = true;
|
||||
}
|
||||
this.setState(nextState);
|
||||
}
|
||||
} else {
|
||||
if (this.state.visible) {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -110,3 +76,5 @@ const styles = StyleSheet.create({
|
||||
top: FAR_FAR_AWAY,
|
||||
},
|
||||
});
|
||||
|
||||
export default polyfill(ResourceSavingSceneView);
|
||||
|
||||
27
src/views/SwitchView/SwitchView.js
Normal file
27
src/views/SwitchView/SwitchView.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import SceneView from '../SceneView';
|
||||
import withCachedChildNavigation from '../../withCachedChildNavigation';
|
||||
|
||||
class SwitchContainer extends React.Component {
|
||||
render() {
|
||||
const { screenProps } = this.props;
|
||||
|
||||
const route = this.props.navigation.state.routes[
|
||||
this.props.navigation.state.index
|
||||
];
|
||||
const childNavigation = this.props.childNavigationProps[route.key];
|
||||
const ChildComponent = this.props.router.getComponentForRouteName(
|
||||
route.routeName
|
||||
);
|
||||
|
||||
return (
|
||||
<SceneView
|
||||
component={ChildComponent}
|
||||
navigation={childNavigation}
|
||||
screenProps={screenProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withCachedChildNavigation(SwitchContainer);
|
||||
@@ -100,6 +100,9 @@ class TabBarBottom extends React.PureComponent {
|
||||
if (showIcon === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const horizontal = this._shouldUseHorizontalTabs();
|
||||
|
||||
return (
|
||||
<TabBarIcon
|
||||
position={position}
|
||||
@@ -108,7 +111,11 @@ class TabBarBottom extends React.PureComponent {
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
renderIcon={renderIcon}
|
||||
scene={scene}
|
||||
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
|
||||
style={[
|
||||
styles.iconWithExplicitHeight,
|
||||
showLabel === false && !horizontal && styles.iconWithoutLabel,
|
||||
showLabel !== false && !horizontal && styles.iconWithLabel,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -130,7 +137,7 @@ class TabBarBottom extends React.PureComponent {
|
||||
maxTabBarItemWidth = flattenedTabStyle.width;
|
||||
} else if (
|
||||
typeof flattenedTabStyle.width === 'string' &&
|
||||
flattenedTabStyle.endsWith('%')
|
||||
flattenedTabStyle.width.endsWith('%')
|
||||
) {
|
||||
const width = parseFloat(flattenedTabStyle.width);
|
||||
if (Number.isFinite(width)) {
|
||||
@@ -140,7 +147,7 @@ class TabBarBottom extends React.PureComponent {
|
||||
maxTabBarItemWidth = flattenedTabStyle.maxWidth;
|
||||
} else if (
|
||||
typeof flattenedTabStyle.maxWidth === 'string' &&
|
||||
flattenedTabStyle.endsWith('%')
|
||||
flattenedTabStyle.width.endsWith('%')
|
||||
) {
|
||||
const width = parseFloat(flattenedTabStyle.maxWidth);
|
||||
if (Number.isFinite(width)) {
|
||||
@@ -286,6 +293,9 @@ class TabBarBottom extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_HEIGHT = 49;
|
||||
const COMPACT_HEIGHT = 29;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
backgroundColor: '#F7F7F7', // Default background color in iOS 10
|
||||
@@ -294,10 +304,10 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tabBarCompact: {
|
||||
height: 29,
|
||||
height: COMPACT_HEIGHT,
|
||||
},
|
||||
tabBarRegular: {
|
||||
height: 49,
|
||||
height: DEFAULT_HEIGHT,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
@@ -311,8 +321,14 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
flexGrow: 1,
|
||||
iconWithoutLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
iconWithLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
iconWithExplicitHeight: {
|
||||
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
|
||||
},
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class TabBarIcon extends React.PureComponent {
|
||||
inputRange,
|
||||
outputRange: inputRange.map(i => (i === index ? 0 : 1)),
|
||||
});
|
||||
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them.
|
||||
return (
|
||||
@@ -54,11 +55,11 @@ const styles = StyleSheet.create({
|
||||
// active and inactive one, so we can fade between them:
|
||||
// Cover the whole iconContainer:
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minWidth: 30,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,7 +22,10 @@ class TabView extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderScene = ({ route }) => {
|
||||
const { screenProps } = this.props;
|
||||
const { screenProps, navigation } = this.props;
|
||||
const focusedIndex = navigation.state.index;
|
||||
const focusedKey = navigation.state.routes[focusedIndex].key;
|
||||
const key = route.key;
|
||||
const childNavigation = this.props.childNavigationProps[route.key];
|
||||
const TabComponent = this.props.router.getComponentForRouteName(
|
||||
route.routeName
|
||||
@@ -31,6 +34,7 @@ class TabView extends React.PureComponent {
|
||||
return (
|
||||
<ResourceSavingSceneView
|
||||
lazy={this.props.lazy}
|
||||
isFocused={focusedKey === key}
|
||||
removeClippedSubViews={this.props.removeClippedSubviews}
|
||||
animationEnabled={this.props.animationEnabled}
|
||||
swipeEnabled={this.props.swipeEnabled}
|
||||
|
||||
@@ -83,9 +83,15 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"height": 29,
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -93,13 +99,13 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"minWidth": 30,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -108,13 +114,13 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"minWidth": 30,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -248,6 +254,7 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
"dispatch": undefined,
|
||||
"getParam": [Function],
|
||||
"goBack": [Function],
|
||||
"isFocused": [Function],
|
||||
"navigate": [Function],
|
||||
"pop": [Function],
|
||||
"popToTop": [Function],
|
||||
|
||||
@@ -12,9 +12,13 @@ export default function withNavigationFocus(Component) {
|
||||
navigation: propTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isFocused: false,
|
||||
};
|
||||
constructor(props, context) {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isFocused: this.getNavigation(props, context).isFocused(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const navigation = this.getNavigation();
|
||||
@@ -32,8 +36,8 @@ export default function withNavigationFocus(Component) {
|
||||
this.subscriptions.forEach(sub => sub.remove());
|
||||
}
|
||||
|
||||
getNavigation = () => {
|
||||
const navigation = this.props.navigation || this.context.navigation;
|
||||
getNavigation = (props = this.props, context = this.context) => {
|
||||
const navigation = props.navigation || context.navigation;
|
||||
invariant(
|
||||
!!navigation,
|
||||
'withNavigationFocus can only be used on a view hierarchy of a navigator. The wrapped component is unable to get access to navigation from props or context.'
|
||||
|
||||
@@ -10,6 +10,8 @@ export default function withCachedChildNavigation(Comp) {
|
||||
return class extends React.PureComponent {
|
||||
static displayName = `withCachedChildNavigation(${displayName})`;
|
||||
|
||||
_childEventSubscribers = {};
|
||||
|
||||
componentWillMount() {
|
||||
this._updateNavigationProps(this.props.navigation);
|
||||
}
|
||||
@@ -18,6 +20,23 @@ export default function withCachedChildNavigation(Comp) {
|
||||
this._updateNavigationProps(nextProps.navigation);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(
|
||||
route => route.key
|
||||
);
|
||||
Object.keys(this._childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this._childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isRouteFocused = route => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_updateNavigationProps = navigation => {
|
||||
// Update props for each child route
|
||||
if (!this._childNavigationProps) {
|
||||
@@ -28,13 +47,19 @@ export default function withCachedChildNavigation(Comp) {
|
||||
if (childNavigation && childNavigation.state === route) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._childEventSubscribers[route.key]) {
|
||||
this._childEventSubscribers[route.key] = getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
route.key
|
||||
);
|
||||
}
|
||||
|
||||
this._childNavigationProps[route.key] = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: route,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
route.key
|
||||
),
|
||||
isFocused: () => this._isRouteFocused(route),
|
||||
addListener: this._childEventSubscribers[route.key],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user