Compare commits

...

38 Commits

Author SHA1 Message Date
Brent Vatne
ee40dd7d24 Release 1.5.6 2018-03-14 21:50:20 -07:00
Brent Vatne
18a48105c2 Missed a case where we should not have flexGrow 2018-03-14 21:50:06 -07:00
Brent Vatne
fbac47b696 Release 1.5.5 2018-03-14 21:35:26 -07:00
Brent Vatne
9aab47dac2 Apply horizontal icon style whenever we should use horizontal tabs in icon 2018-03-14 21:35:07 -07:00
Brent Vatne
67309c00a6 Release 1.5.4 2018-03-14 11:21:15 -07:00
Moti Zilberman
86a724cfe3 Swap addListener out for isFocused prop on ResourceSavingSceneView (#3718)
This is a backport of 81a86fa091 from master.
2018-03-14 11:20:30 -07:00
Brent Vatne
eb78128439 Release 1.5.3 2018-03-14 11:16:19 -07:00
Brent Vatne
c39ec7a10c Use arrow function instead of bind 2018-03-14 11:16:01 -07:00
Chris
0ff3347e97 Update CardStack.js (#3749)
fix for https://github.com/react-navigation/react-navigation/issues/3729 , https://github.com/react-navigation/react-navigation/issues/3746
2018-03-14 11:08:26 -07:00
Brent Vatne
b9d55a6330 Release 1.5.2 2018-03-12 11:12:31 -07:00
Brent Vatne
315e43701b Fix tab icon height on horizontal / ipad 2018-03-12 11:12:22 -07:00
Hugo Dozois
1d573bc246 Fix isFocused from withNavigationFocus is a function (#3710)
Fixes #3709
2018-03-11 22:26:33 +01:00
Eric Vicenti
3bfb0b90d0 Fix onTransitionEnd props passthrough
Fixes #3647
2018-03-10 00:09:02 -08:00
Brent Vatne
8a129afe13 Release 1.5.1 2018-03-09 11:33:06 -08:00
Brent Vatne
ab2a63fe92 Update snapshot for tab icon changes 2018-03-09 11:32:53 -08:00
Brandon Smith
c411210ecc Pass initialRouteKey into StackRouter (#3540) (#3701) 2018-03-09 11:31:41 -08:00
Brent Vatne
01e7296520 Release 1.5.0 2018-03-07 17:42:38 -08:00
corupta
8f3e0997c5 Add activeLabelStyle and inactiveLabelStyle for DrawerItem (#3559)
* Add activeLabelStyle and inactiveLabelStyle

Adding activeLabelStyle, so that active items can be customized more.
My use case: Change font of the active item to bold.
Also, added inactiveLabelStyle which can be used for a similar purpose.

* prettier fix

* Update react-navigation.js

* prettier fix

* Update jest snapshot for DrawerNavigator - for adding a new style property to the styles array
2018-03-07 17:38:02 -08:00
Nicolas Beck
3f3ef6485c Add initialRouteKey for StackRouter (#3540)
* use initialRouteName as key when initializing StackRouter

* fix null headerLeft

* merge back

* fixed tests

* use config flag

* fixed snapshots

* implemented requested changes
2018-03-07 17:38:02 -08:00
Vishwesh Jainkuniya
b12abb553f Fix: tabBar icons are not visible. (#3650)
* Fix: tabBar icons are not visible.

* Fix: tests.
2018-03-07 17:38:02 -08:00
Ashoat Tevosyan
e02841a979 [Flow] Some updates, mostly from flow-typed (#3682)
1. Remove `NavigationComponent` from `NavigationScreenRouteConfig`. The only context `NavigationScreenRouteConfig` is used in is as an intersection with an object, and as such the only relevant portions of `NavigationScreenRouteConfig` are the object parts.
2. Add static `HEIGHT` variable to `Header` type.
3. In `NavigationContainerProps`, make `onNavigationStateChange` property value nullable.

PS: if in the future you guys would prefer that I separate these sort of PRs into their constituent parts, let me know.
2018-03-07 18:21:15 -05:00
Ben Styles
e147f34555 Allow passing null to onNavigationStateChange prop (#3683)
As seen here: react-navigation/react-navigation#360, we need to be able to pass null to turn off logging.
2018-03-07 18:20:58 -05:00
Sirui Li
81e0ce136e Flow type: SafeAreaView doesn't require children (#3670)
`children` prop should be optional.
2018-03-07 18:20:44 -05:00
Edward Drapkin
8ba727c2cf More specific injected Navigation props (#3645) 2018-03-07 18:19:37 -05:00
Yordis Prieto
9a86ef8362 Fix typespec of back action creator (#3659) 2018-03-07 18:19:26 -05:00
Ashoat Tevosyan
4fe7c92847 Fix NavigationEventPayload.lastState type (#3664)
Should be nullable, since it is initially called as `null` in `src/createNavigationContainer.js` (and in `react-navigation-redux-helpers`, where it is causing a Flow error)
2018-03-07 18:19:10 -05:00
Leon Miller-Out
afecaaed7f Call react-navigation-redux-helpers's initializeListeners() when mounting app. (#3666)
This makes the first navigation click work correctly, per
https://github.com/react-navigation/react-navigation-redux-helpers/issues/7
2018-03-05 15:02:34 -05:00
Brent Vatne
6373b802dd Release 1.4.0 2018-03-03 09:59:22 -08:00
Brent Vatne
138151433d Add isFocused helper to navigation and fix withNavigationFocus (1.x branch) (#3651)
* Add isFocused helper to navigation and fix withNavigationFocus accordingly

* Fix snapshots

* Make flow pass on TabsWithNavigationFocus example
2018-03-03 09:58:19 -08:00
Brent Vatne
2744cb32b7 Cache child event subscriptions (1.x branch) (#3649)
* Cache child event subscriptions on StackNavigator on 1.x branch

* Cache child event subscribers on DrawerView and withCachedChildNavigation
2018-03-03 09:01:24 -08:00
Brent Vatne
439b4222ce Release 1.3.2 2018-03-02 13:36:47 -08:00
Brent Vatne
ba0b1861e5 Use SceneView with SwitchNavigator to thread context through. Fixes #3646 2018-03-02 13:35:56 -08:00
Brent Vatne
318788ca60 Release 1.3.1 2018-03-02 12:19:05 -08:00
Brent Vatne
498a39c200 Fix regression in error message for route config validation 2018-03-02 12:18:57 -08:00
Brent Vatne
b31ebef5b0 Release 1.3.0 2018-03-01 13:44:22 -08:00
Arseny Yankovsky
f4fe588e08 Allow modification of SafeAreaView props (#3496)
* SafeAreaView fix

* Updated to only allow modification of forceInset property of SafeAreaView
2018-03-01 13:44:07 -08:00
Brent Vatne
403af82c3f Add SwitchNavigator (#3634)
* Add SwitchNavigator

* Fix type definition

* Update snapshot

* Add SwitchNavigator test coverage
2018-03-01 13:43:47 -08:00
Brent Vatne
0c2360dc36 Clarify that people should not report Redux or MobX related integration issues here 2018-02-26 15:52:31 -08:00
38 changed files with 1073 additions and 170 deletions

View File

@@ -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) - [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. - 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 ### Current Behavior

View File

@@ -25,6 +25,7 @@ import MultipleDrawer from './MultipleDrawer';
import TabsInDrawer from './TabsInDrawer'; import TabsInDrawer from './TabsInDrawer';
import ModalStack from './ModalStack'; import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs'; import StacksInTabs from './StacksInTabs';
import SwitchWithStacks from './SwitchWithStacks';
import StacksOverTabs from './StacksOverTabs'; import StacksOverTabs from './StacksOverTabs';
import StacksWithKeys from './StacksWithKeys'; import StacksWithKeys from './StacksWithKeys';
import SimpleStack from './SimpleStack'; import SimpleStack from './SimpleStack';
@@ -39,6 +40,10 @@ const ExampleInfo = {
name: 'Stack Example', name: 'Stack Example',
description: 'A card stack', description: 'A card stack',
}, },
SwitchWithStacks: {
name: 'Switch Example',
description: 'A switch with stacks inside',
},
SimpleTabs: { SimpleTabs: {
name: 'Tabs Example', name: 'Tabs Example',
description: 'Tabs following platform conventions', description: 'Tabs following platform conventions',
@@ -109,28 +114,29 @@ const ExampleInfo = {
name: 'Animated Tabs Example', name: 'Animated Tabs Example',
description: 'Tab transitions have custom animations', description: 'Tab transitions have custom animations',
}, },
// TabsWithNavigationFocus: { TabsWithNavigationFocus: {
// name: 'withNavigationFocus', name: 'withNavigationFocus',
// description: 'Receive the focus prop to know when a screen is focused', description: 'Receive the focus prop to know when a screen is focused',
// }, },
}; };
const ExampleRoutes = { const ExampleRoutes = {
SimpleStack: SimpleStack, SimpleStack,
SimpleTabs: SimpleTabs, SwitchWithStacks,
Drawer: Drawer, SimpleTabs,
Drawer,
// MultipleDrawer: { // MultipleDrawer: {
// screen: MultipleDrawer, // screen: MultipleDrawer,
// }, // },
StackWithHeaderPreset: StackWithHeaderPreset, StackWithHeaderPreset,
StackWithTranslucentHeader: StackWithTranslucentHeader, StackWithTranslucentHeader,
TabsInDrawer: TabsInDrawer, TabsInDrawer,
CustomTabs: CustomTabs, CustomTabs,
CustomTransitioner: CustomTransitioner, CustomTransitioner,
ModalStack: ModalStack, ModalStack,
StacksWithKeys: StacksWithKeys, StacksWithKeys,
StacksInTabs: StacksInTabs, StacksInTabs,
StacksOverTabs: StacksOverTabs, StacksOverTabs,
LinkStack: { LinkStack: {
screen: SimpleStack, screen: SimpleStack,
path: 'people/Jordan', path: 'people/Jordan',
@@ -139,8 +145,8 @@ const ExampleRoutes = {
screen: SimpleTabs, screen: SimpleTabs,
path: 'settings', path: 'settings',
}, },
TabAnimations: TabAnimations, TabAnimations,
// TabsWithNavigationFocus: TabsWithNavigationFocus, TabsWithNavigationFocus,
}; };
type State = { type State = {

View File

@@ -119,6 +119,9 @@ const StacksInTabs = TabNavigator(
tabBarPosition: 'bottom', tabBarPosition: 'bottom',
animationEnabled: false, animationEnabled: false,
swipeEnabled: false, swipeEnabled: false,
tabBarOptions: {
showLabel: false,
},
} }
); );

View 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,
});

View File

@@ -3,40 +3,75 @@
*/ */
import React from 'react'; import React from 'react';
import { SafeAreaView, Text } from 'react-native'; import { Button, SafeAreaView, Text } from 'react-native';
import { TabNavigator, withNavigationFocus } from 'react-navigation'; import { TabNavigator, withNavigationFocus } from 'react-navigation';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import SampleText from './SampleText'; import SampleText from './SampleText';
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => { class Child extends React.Component<any, any> {
const TabScreen = ({ isFocused }) => ( render() {
<SafeAreaView return (
forceInset={{ horizontal: 'always', top: 'always' }} <Text style={{ color: this.props.isFocused ? 'green' : 'maroon' }}>
style={{ {this.props.isFocused
flex: 1, ? 'I know that my parent is focused!'
alignItems: 'center', : 'My parent is not focused! :O'}
justifyContent: 'center',
}}
>
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
{'Tab ' + name.toLowerCase()}
</Text> </Text>
<Text>{'props.isFocused: ' + (isFocused ? ' true' : 'false')}</Text> );
</SafeAreaView> }
); }
TabScreen.navigationOptions = { const ChildWithNavigationFocus = withNavigationFocus(Child);
tabBarLabel: name,
tabBarIcon: ({ tintColor, focused }) => (
<MaterialCommunityIcons
name={focused ? focusedIcon : icon}
size={26}
style={{ color: focused ? tintColor : '#ccc' }}
/>
),
};
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); return withNavigationFocus(TabScreen);
}; };

View File

@@ -26,7 +26,7 @@
"react": "16.2.0", "react": "16.2.0",
"react-native": "^0.52.0", "react-native": "^0.52.0",
"react-navigation": "link:../..", "react-navigation": "link:../..",
"react-navigation-redux-helpers": "^1.0.0", "react-navigation-redux-helpers": "^1.0.3",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"redux": "^3.7.2" "redux": "^3.7.2"
}, },

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { addNavigationHelpers, StackNavigator } from 'react-navigation'; import { addNavigationHelpers, StackNavigator } from 'react-navigation';
import { initializeListeners } from 'react-navigation-redux-helpers';
import LoginScreen from '../components/LoginScreen'; import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen'; import MainScreen from '../components/MainScreen';
@@ -20,6 +21,10 @@ class AppWithNavigationState extends React.Component {
nav: PropTypes.object.isRequired, nav: PropTypes.object.isRequired,
}; };
componentDidMount() {
initializeListeners('root', this.props.nav);
}
render() { render() {
const { dispatch, nav } = this.props; const { dispatch, nav } = this.props;
return ( return (

View File

@@ -48,6 +48,15 @@ declare module 'react-navigation' {
// react-native/Libraries/Animated/src/nodes/AnimatedValue.js // react-native/Libraries/Animated/src/nodes/AnimatedValue.js
declare type AnimatedValue = Object; declare type AnimatedValue = Object;
declare type HeaderForceInset = {
horizontal?: string,
vertical?: string,
left?: string,
right?: string,
top?: string,
bottom?: string,
};
/** /**
* Next, all the type declarations * Next, all the type declarations
*/ */
@@ -286,7 +295,6 @@ declare module 'react-navigation' {
} & NavigationScreenRouteConfig); } & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig = declare export type NavigationScreenRouteConfig =
| NavigationComponent
| { | {
screen: NavigationComponent, screen: NavigationComponent,
} }
@@ -340,6 +348,7 @@ declare module 'react-navigation' {
headerPressColorAndroid?: string, headerPressColorAndroid?: string,
headerRight?: React$Node, headerRight?: React$Node,
headerStyle?: ViewStyleProp, headerStyle?: ViewStyleProp,
headerForceInset?: HeaderForceInset,
headerBackground?: React$Node | React$ElementType, headerBackground?: React$Node | React$ElementType,
gesturesEnabled?: boolean, gesturesEnabled?: boolean,
gestureResponseDistance?: { vertical?: number, horizontal?: number }, gestureResponseDistance?: { vertical?: number, horizontal?: number },
@@ -351,6 +360,7 @@ declare module 'react-navigation' {
initialRouteParams?: NavigationParams, initialRouteParams?: NavigationParams,
paths?: NavigationPathsConfig, paths?: NavigationPathsConfig,
navigationOptions?: NavigationScreenConfig<*>, navigationOptions?: NavigationScreenConfig<*>,
initialRouteKey?: string,
|}; |};
declare export type NavigationStackViewConfig = {| declare export type NavigationStackViewConfig = {|
@@ -368,6 +378,20 @@ declare module 'react-navigation' {
...NavigationStackRouterConfig, ...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 * Tab Navigator
*/ */
@@ -379,7 +403,6 @@ declare module 'react-navigation' {
navigationOptions?: NavigationScreenConfig<*>, navigationOptions?: NavigationScreenConfig<*>,
// todo: type these as the real route names rather than 'string' // todo: type these as the real route names rather than 'string'
order?: Array<string>, order?: Array<string>,
// Does the back button cause the router to switch to the initial tab // Does the back button cause the router to switch to the initial tab
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute` backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|}; |};
@@ -447,7 +470,7 @@ declare module 'react-navigation' {
type: EventType, type: EventType,
action: NavigationAction, action: NavigationAction,
state: NavigationState, state: NavigationState,
lastState: NavigationState, lastState: ?NavigationState,
}; };
declare export type NavigationEventCallback = ( declare export type NavigationEventCallback = (
@@ -530,7 +553,7 @@ declare module 'react-navigation' {
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{ declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
uriPrefix?: string | RegExp, uriPrefix?: string | RegExp,
onNavigationStateChange?: ( onNavigationStateChange?: ?(
NavigationState, NavigationState,
NavigationState, NavigationState,
NavigationAction NavigationAction
@@ -701,7 +724,7 @@ declare module 'react-navigation' {
SET_PARAMS: 'Navigation/SET_PARAMS', SET_PARAMS: 'Navigation/SET_PARAMS',
URI: 'Navigation/URI', URI: 'Navigation/URI',
back: { back: {
(payload: { key?: ?string }): NavigationBackAction, (payload?: { key?: ?string }): NavigationBackAction,
toString: () => string, toString: () => string,
}, },
init: { init: {
@@ -786,6 +809,13 @@ declare module 'react-navigation' {
routeConfigs: NavigationRouteConfigMap, routeConfigs: NavigationRouteConfigMap,
config?: _TabNavigatorConfig config?: _TabNavigatorConfig
): NavigationContainer<*, *, *>; ): NavigationContainer<*, *, *>;
declare type _SwitchNavigatorConfig = {|
...NavigationSwitchRouterConfig,
|};
declare export function SwitchNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _SwitchNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _DrawerViewConfig = {| declare type _DrawerViewConfig = {|
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open', drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
@@ -895,12 +925,14 @@ declare module 'react-navigation' {
vertical?: _SafeAreaViewForceInsetValue, vertical?: _SafeAreaViewForceInsetValue,
horizontal?: _SafeAreaViewForceInsetValue, horizontal?: _SafeAreaViewForceInsetValue,
}, },
children: React$Node, children?: React$Node,
style?: AnimatedViewStyleProp, style?: AnimatedViewStyleProp,
}; };
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>; 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 = { declare type _HeaderTitleProps = {
children: React$Node, children: React$Node,
@@ -965,6 +997,8 @@ declare module 'react-navigation' {
itemsContainerStyle?: ViewStyleProp, itemsContainerStyle?: ViewStyleProp,
itemStyle?: ViewStyleProp, itemStyle?: ViewStyleProp,
labelStyle?: TextStyleProp, labelStyle?: TextStyleProp,
activeLabelStyle?: TextStyleProp,
inactiveLabelStyle?: TextStyleProp,
iconContainerStyle?: ViewStyleProp, iconContainerStyle?: ViewStyleProp,
drawerPosition: 'left' | 'right', drawerPosition: 'left' | 'right',
}; };
@@ -1045,7 +1079,7 @@ declare module 'react-navigation' {
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>; declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
declare type _NavigationInjectedProps = { declare type _NavigationInjectedProps = {
navigation: NavigationScreenProp<NavigationState>, navigation: NavigationScreenProp<NavigationStateRoute>,
}; };
declare export function withNavigation<T: {}>( declare export function withNavigation<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps> Component: React$ComponentType<T & _NavigationInjectedProps>

View File

@@ -1,6 +1,6 @@
{ {
"name": "react-navigation", "name": "react-navigation",
"version": "1.2.1", "version": "1.5.6",
"description": "Routing and navigation for your React Native apps", "description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js", "main": "src/react-navigation.js",
"repository": { "repository": {
@@ -32,6 +32,7 @@
"hoist-non-react-statics": "^2.2.0", "hoist-non-react-statics": "^2.2.0",
"path-to-regexp": "^1.7.0", "path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react-lifecycles-compat": "^1.0.2",
"react-native-drawer-layout-polyfill": "^1.3.2", "react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.7.0", "react-native-safe-area-view": "^0.7.0",
"react-native-tab-view": "^0.0.74" "react-native-tab-view": "^0.0.74"

View File

@@ -4,6 +4,7 @@
* Based on the 'action' events that get fired for this navigation state, this utility will fire * Based on the 'action' events that get fired for this navigation state, this utility will fire
* focus and blur events for this child * focus and blur events for this child
*/ */
export default function getChildEventSubscriber(addListener, key) { export default function getChildEventSubscriber(addListener, key) {
const actionSubscribers = new Set(); const actionSubscribers = new Set();
const willFocusSubscribers = new Set(); const willFocusSubscribers = new Set();

View File

@@ -11,6 +11,7 @@ import NavigationActions from '../NavigationActions';
export default (routeConfigMap, stackConfig = {}) => { export default (routeConfigMap, stackConfig = {}) => {
const { const {
initialRouteKey,
initialRouteName, initialRouteName,
initialRouteParams, initialRouteParams,
paths, paths,
@@ -25,6 +26,7 @@ export default (routeConfigMap, stackConfig = {}) => {
} = stackConfig; } = stackConfig;
const stackRouterConfig = { const stackRouterConfig = {
initialRouteKey,
initialRouteName, initialRouteName,
initialRouteParams, initialRouteParams,
paths, paths,
@@ -47,7 +49,7 @@ export default (routeConfigMap, stackConfig = {}) => {
onTransitionEnd={(lastTransition, transition) => { onTransitionEnd={(lastTransition, transition) => {
const { state, dispatch } = props.navigation; const { state, dispatch } = props.navigation;
dispatch(NavigationActions.completeTransition({ key: state.key })); dispatch(NavigationActions.completeTransition({ key: state.key }));
onTransitionEnd && onTransitionEnd(); onTransitionEnd && onTransitionEnd(lastTransition, transition);
}} }}
/> />
) )

View 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);
};

View 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();
});
});

View File

@@ -224,6 +224,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
"color": "#2196f3", "color": "#2196f3",
}, },
undefined, undefined,
undefined,
] ]
} }
> >

View File

@@ -80,10 +80,11 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
pointerEvents="box-none" pointerEvents="box-none"
style={ style={
Object { Object {
"backgroundColor": "#F7F7F7", "backgroundColor": "red",
"borderBottomColor": "#A7A7AA", "borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5, "borderBottomWidth": 0.5,
"height": 64, "height": 64,
"opacity": 0.5,
"paddingBottom": 0, "paddingBottom": 0,
"paddingLeft": 0, "paddingLeft": 0,
"paddingRight": 0, "paddingRight": 0,
@@ -265,10 +266,11 @@ exports[`StackNavigator renders successfully 1`] = `
pointerEvents="box-none" pointerEvents="box-none"
style={ style={
Object { Object {
"backgroundColor": "#F7F7F7", "backgroundColor": "red",
"borderBottomColor": "#A7A7AA", "borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5, "borderBottomWidth": 0.5,
"height": 64, "height": 64,
"opacity": 0.5,
"paddingBottom": 0, "paddingBottom": 0,
"paddingLeft": 0, "paddingLeft": 0,
"paddingRight": 0, "paddingRight": 0,

View File

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

View File

@@ -137,9 +137,12 @@ exports[`TabNavigator renders successfully 1`] = `
> >
<View <View
style={ style={
Object { Array [
"flexGrow": 1, false,
} Object {
"flexGrow": 1,
},
]
} }
> >
<View <View
@@ -147,13 +150,12 @@ exports[`TabNavigator renders successfully 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"bottom": 0, "alignSelf": "center",
"height": "100%",
"justifyContent": "center", "justifyContent": "center",
"left": 0,
"opacity": 1, "opacity": 1,
"position": "absolute", "position": "absolute",
"right": 0, "width": "100%",
"top": 0,
} }
} }
/> />
@@ -162,13 +164,12 @@ exports[`TabNavigator renders successfully 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"bottom": 0, "alignSelf": "center",
"height": "100%",
"justifyContent": "center", "justifyContent": "center",
"left": 0,
"opacity": 0, "opacity": 0,
"position": "absolute", "position": "absolute",
"right": 0, "width": "100%",
"top": 0,
} }
} }
/> />

View File

@@ -22,6 +22,9 @@ module.exports = {
get StackNavigator() { get StackNavigator() {
return require('./navigators/StackNavigator').default; return require('./navigators/StackNavigator').default;
}, },
get SwitchNavigator() {
return require('./navigators/SwitchNavigator').default;
},
get TabNavigator() { get TabNavigator() {
return require('./navigators/TabNavigator').default; return require('./navigators/TabNavigator').default;
}, },
@@ -36,6 +39,9 @@ module.exports = {
get TabRouter() { get TabRouter() {
return require('./routers/TabRouter').default; return require('./routers/TabRouter').default;
}, },
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// Views // Views
get Transitioner() { get Transitioner() {
@@ -84,6 +90,11 @@ module.exports = {
return require('./views/TabView/TabBarBottom').default; return require('./views/TabView/TabBarBottom').default;
}, },
// SwitchView
get SwitchView() {
return require('./views/SwitchView/SwitchView').default;
},
// HOCs // HOCs
get withNavigation() { get withNavigation() {
return require('./views/withNavigation').default; return require('./views/withNavigation').default;

View File

@@ -92,11 +92,12 @@ export default (routeConfigs, stackConfig = {}) => {
...(action.params || {}), ...(action.params || {}),
...(initialRouteParams || {}), ...(initialRouteParams || {}),
}; };
const { initialRouteKey } = stackConfig;
route = { route = {
...route, ...route,
...(params ? { params } : {}), ...(params ? { params } : {}),
routeName: initialRouteName, routeName: initialRouteName,
key: action.key || generateKey(), key: action.key || (initialRouteKey || generateKey()),
}; };
return { return {
key: 'StackRouterRoot', key: 'StackRouterRoot',

360
src/routers/SwitchRouter.js Normal file
View File

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

View File

@@ -576,6 +576,23 @@ describe('StackRouter', () => {
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge'); 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', () => { test('Navigate with key is idempotent', () => {
const TestRouter = StackRouter({ const TestRouter = StackRouter({
foo: { screen: () => <div /> }, foo: { screen: () => <div /> },

View File

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

View File

@@ -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."`;

View File

@@ -13,9 +13,19 @@ ProfileNavigator.router = StackRouter({
}); });
describe('validateRouteConfigMap', () => { describe('validateRouteConfigMap', () => {
test('Fails on empty bare screen', () => {
const invalidMap = {
Home: undefined,
};
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails on empty config', () => { test('Fails on empty config', () => {
const invalidMap = {}; const invalidMap = {};
expect(() => validateRouteConfigMap(invalidMap)).toThrow(); expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
}); });
test('Fails on bad object', () => { test('Fails on bad object', () => {
const invalidMap = { const invalidMap = {
@@ -23,7 +33,9 @@ describe('validateRouteConfigMap', () => {
foo: 'bar', foo: 'bar',
}, },
}; };
expect(() => validateRouteConfigMap(invalidMap)).toThrow(); expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
}); });
test('Fails if both screen and getScreen are defined', () => { test('Fails if both screen and getScreen are defined', () => {
const invalidMap = { const invalidMap = {
@@ -32,15 +44,17 @@ describe('validateRouteConfigMap', () => {
getScreen: () => ListScreen, getScreen: () => ListScreen,
}, },
}; };
expect(() => validateRouteConfigMap(invalidMap)).toThrow(); expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
}); });
test('Succeeds on a valid config', () => { test('Succeeds on a valid config', () => {
const invalidMap = { const validMap = {
Home: { Home: {
screen: ProfileNavigator, screen: ProfileNavigator,
}, },
Chat: ListScreen, Chat: ListScreen,
}; };
validateRouteConfigMap(invalidMap); validateRouteConfigMap(validMap);
}); });
}); });

View File

@@ -13,16 +13,13 @@ function validateRouteConfigMap(routeConfigs) {
routeNames.forEach(routeName => { routeNames.forEach(routeName => {
const routeConfig = routeConfigs[routeName]; const routeConfig = routeConfigs[routeName];
const screenComponent = getScreenComponent(routeConfig);
const screenComponent = routeConfig.screen
? routeConfig.screen
: routeConfig;
if ( if (
screenComponent && !screenComponent ||
typeof screenComponent !== 'function' && (typeof screenComponent !== 'function' &&
typeof screenComponent !== 'string' && typeof screenComponent !== 'string' &&
!routeConfig.getScreen !routeConfig.getScreen)
) { ) {
throw new Error( throw new Error(
`The component for route '${routeName}' must be a ` + `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; export default validateRouteConfigMap;

View File

@@ -82,6 +82,8 @@ class CardStack extends React.Component {
_screenDetails = {}; _screenDetails = {};
_childEventSubscribers = {};
componentWillReceiveProps(props) { componentWillReceiveProps(props) {
if (props.screenProps !== this.props.screenProps) { if (props.screenProps !== this.props.screenProps) {
this._screenDetails = {}; 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 => { _getScreenDetails = scene => {
const { screenProps, transitionProps: { navigation }, router } = this.props; const { screenProps, transitionProps: { navigation }, router } = this.props;
let screenDetails = this._screenDetails[scene.key]; let screenDetails = this._screenDetails[scene.key];
if (!screenDetails || screenDetails.state !== scene.route) { 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({ const screenNavigation = addNavigationHelpers({
dispatch: navigation.dispatch, dispatch: navigation.dispatch,
state: scene.route, state: scene.route,
addListener: getChildEventSubscriber( isFocused: () => this._isRouteFocused(scene.route),
navigation.addListener, addListener: this._childEventSubscribers[scene.route.key],
scene.route.key
),
}); });
screenDetails = { screenDetails = {
state: scene.route, state: scene.route,

View File

@@ -21,6 +21,8 @@ const DrawerNavigatorItems = ({
itemsContainerStyle, itemsContainerStyle,
itemStyle, itemStyle,
labelStyle, labelStyle,
activeLabelStyle,
inactiveLabelStyle,
iconContainerStyle, iconContainerStyle,
drawerPosition, drawerPosition,
}) => ( }) => (
@@ -34,6 +36,7 @@ const DrawerNavigatorItems = ({
const scene = { route, index, focused, tintColor: color }; const scene = { route, index, focused, tintColor: color };
const icon = renderIcon(scene); const icon = renderIcon(scene);
const label = getLabel(scene); const label = getLabel(scene);
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
return ( return (
<TouchableItem <TouchableItem
key={route.key} key={route.key}
@@ -63,7 +66,9 @@ const DrawerNavigatorItems = ({
</View> </View>
) : null} ) : null}
{typeof label === 'string' ? ( {typeof label === 'string' ? (
<Text style={[styles.label, { color }, labelStyle]}> <Text
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
>
{label} {label}
</Text> </Text>
) : ( ) : (

View File

@@ -17,6 +17,8 @@ export default class DrawerView extends React.PureComponent {
: this.props.drawerWidth, : this.props.drawerWidth,
}; };
_childEventSubscribers = {};
componentWillMount() { componentWillMount() {
this._updateScreenNavigation(this.props.navigation); this._updateScreenNavigation(this.props.navigation);
@@ -27,6 +29,17 @@ export default class DrawerView extends React.PureComponent {
Dimensions.removeEventListener('change', this._updateWidth); 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) { componentWillReceiveProps(nextProps) {
if ( if (
this.props.navigation.state.index !== nextProps.navigation.state.index 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 => { _updateScreenNavigation = navigation => {
const { drawerCloseRoute } = this.props; const { drawerCloseRoute } = this.props;
const navigationState = navigation.state.routes.find( const navigationState = navigation.state.routes.find(
@@ -79,13 +98,18 @@ export default class DrawerView extends React.PureComponent {
) { ) {
return; return;
} }
if (!this._childEventSubscribers[navigationState.key]) {
this._childEventSubscribers[
navigationState.key
] = getChildEventSubscriber(navigation.addListener, navigationState.key);
}
this._screenNavigationProp = addNavigationHelpers({ this._screenNavigationProp = addNavigationHelpers({
dispatch: navigation.dispatch, dispatch: navigation.dispatch,
state: navigationState, state: navigationState,
addListener: getChildEventSubscriber( isFocused: () => this._isRouteFocused(navigationState),
navigation.addListener, addListener: this._childEventSubscribers[navigationState.key],
navigationState.key
),
}); });
}; };

View File

@@ -476,11 +476,11 @@ class Header extends React.PureComponent {
safeHeaderStyle, safeHeaderStyle,
]; ];
const { headerForceInset } = options;
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
return ( return (
<SafeAreaView <SafeAreaView forceInset={forceInset} style={containerStyles}>
forceInset={{ top: 'always', bottom: 'never' }}
style={containerStyles}
>
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View> <View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
<View style={{ flex: 1 }}>{appBar}</View> <View style={{ flex: 1 }}>{appBar}</View>
</SafeAreaView> </SafeAreaView>

View File

@@ -1,40 +1,33 @@
import React from 'react'; import React from 'react';
import { Platform, StyleSheet, View } from 'react-native'; import { Platform, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import withLifecyclePolyfill from 'react-lifecycles-compat';
import SceneView from './SceneView'; import SceneView from './SceneView';
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container 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) { constructor(props) {
super(); 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 = { this.state = {
awake: props.lazy ? isFocused : true, awake: props.lazy ? props.isFocused : true,
visible: isFocused,
}; };
} }
componentWillMount() { static getDerivedStateFromProps(nextProps, prevState) {
this._actionListener = this.props.navigation.addListener( if (nextProps.isFocused && !prevState.awake) {
'action', return { awake: true };
this._onAction }
);
}
componentWillUnmount() { return null;
this._actionListener.remove();
} }
render() { render() {
const { awake, visible } = this.state; const { awake } = this.state;
const { const {
isFocused,
childNavigation, childNavigation,
navigation, navigation,
removeClippedSubviews, removeClippedSubviews,
@@ -49,12 +42,12 @@ export default class ResourceSavingSceneView extends React.PureComponent {
removeClippedSubviews={ removeClippedSubviews={
Platform.OS === 'android' Platform.OS === 'android'
? removeClippedSubviews ? removeClippedSubviews
: !visible && removeClippedSubviews : !isFocused && removeClippedSubviews
} }
> >
<View <View
style={ style={
this._mustAlwaysBeVisible() || visible this._mustAlwaysBeVisible() || isFocused
? styles.innerAttached ? styles.innerAttached
: styles.innerDetached : styles.innerDetached
} }
@@ -68,33 +61,6 @@ export default class ResourceSavingSceneView extends React.PureComponent {
_mustAlwaysBeVisible = () => { _mustAlwaysBeVisible = () => {
return this.props.animationEnabled || this.props.swipeEnabled; 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({ const styles = StyleSheet.create({
@@ -110,3 +76,5 @@ const styles = StyleSheet.create({
top: FAR_FAR_AWAY, top: FAR_FAR_AWAY,
}, },
}); });
export default withLifecyclePolyfill(ResourceSavingSceneView);

View 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);

View File

@@ -100,6 +100,9 @@ class TabBarBottom extends React.PureComponent {
if (showIcon === false) { if (showIcon === false) {
return null; return null;
} }
const horizontal = this._shouldUseHorizontalTabs();
return ( return (
<TabBarIcon <TabBarIcon
position={position} position={position}
@@ -108,7 +111,10 @@ class TabBarBottom extends React.PureComponent {
inactiveTintColor={inactiveTintColor} inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon} renderIcon={renderIcon}
scene={scene} scene={scene}
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon} style={[
horizontal && styles.horizontalIcon,
showLabel !== false && !horizontal && styles.icon,
]}
/> />
); );
}; };
@@ -286,6 +292,9 @@ class TabBarBottom extends React.PureComponent {
} }
} }
const DEFAULT_HEIGHT = 49;
const COMPACT_HEIGHT = 29;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
tabBar: { tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10 backgroundColor: '#F7F7F7', // Default background color in iOS 10
@@ -294,10 +303,10 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
}, },
tabBarCompact: { tabBarCompact: {
height: 29, height: COMPACT_HEIGHT,
}, },
tabBarRegular: { tabBarRegular: {
height: 49, height: DEFAULT_HEIGHT,
}, },
tab: { tab: {
flex: 1, flex: 1,
@@ -314,6 +323,9 @@ const styles = StyleSheet.create({
icon: { icon: {
flexGrow: 1, flexGrow: 1,
}, },
horizontalIcon: {
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
},
label: { label: {
textAlign: 'center', textAlign: 'center',
backgroundColor: 'transparent', backgroundColor: 'transparent',

View File

@@ -23,6 +23,7 @@ export default class TabBarIcon extends React.PureComponent {
inputRange, inputRange,
outputRange: inputRange.map(i => (i === index ? 0 : 1)), outputRange: inputRange.map(i => (i === index ? 0 : 1)),
}); });
// We render the icon twice at the same position on top of each other: // We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them. // active and inactive one, so we can fade between them.
return ( return (
@@ -53,12 +54,11 @@ const styles = StyleSheet.create({
// We render the icon twice at the same position on top of each other: // We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them: // active and inactive one, so we can fade between them:
// Cover the whole iconContainer: // Cover the whole iconContainer:
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center', alignItems: 'center',
alignSelf: 'center',
height: '100%',
justifyContent: 'center', justifyContent: 'center',
position: 'absolute',
width: '100%',
}, },
}); });

View File

@@ -22,7 +22,10 @@ class TabView extends React.PureComponent {
}; };
_renderScene = ({ route }) => { _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 childNavigation = this.props.childNavigationProps[route.key];
const TabComponent = this.props.router.getComponentForRouteName( const TabComponent = this.props.router.getComponentForRouteName(
route.routeName route.routeName
@@ -31,6 +34,7 @@ class TabView extends React.PureComponent {
return ( return (
<ResourceSavingSceneView <ResourceSavingSceneView
lazy={this.props.lazy} lazy={this.props.lazy}
isFocused={focusedKey === key}
removeClippedSubViews={this.props.removeClippedSubviews} removeClippedSubViews={this.props.removeClippedSubviews}
animationEnabled={this.props.animationEnabled} animationEnabled={this.props.animationEnabled}
swipeEnabled={this.props.swipeEnabled} swipeEnabled={this.props.swipeEnabled}

View File

@@ -83,9 +83,12 @@ exports[`TabBarBottom renders successfully 1`] = `
> >
<View <View
style={ style={
Object { Array [
"flexGrow": 1, false,
} Object {
"flexGrow": 1,
},
]
} }
> >
<View <View
@@ -93,13 +96,12 @@ exports[`TabBarBottom renders successfully 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"bottom": 0, "alignSelf": "center",
"height": "100%",
"justifyContent": "center", "justifyContent": "center",
"left": 0,
"opacity": 1, "opacity": 1,
"position": "absolute", "position": "absolute",
"right": 0, "width": "100%",
"top": 0,
} }
} }
/> />
@@ -108,13 +110,12 @@ exports[`TabBarBottom renders successfully 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"bottom": 0, "alignSelf": "center",
"height": "100%",
"justifyContent": "center", "justifyContent": "center",
"left": 0,
"opacity": 0, "opacity": 0,
"position": "absolute", "position": "absolute",
"right": 0, "width": "100%",
"top": 0,
} }
} }
/> />
@@ -248,6 +249,7 @@ exports[`TabBarBottom renders successfully 1`] = `
"dispatch": undefined, "dispatch": undefined,
"getParam": [Function], "getParam": [Function],
"goBack": [Function], "goBack": [Function],
"isFocused": [Function],
"navigate": [Function], "navigate": [Function],
"pop": [Function], "pop": [Function],
"popToTop": [Function], "popToTop": [Function],

View File

@@ -12,9 +12,13 @@ export default function withNavigationFocus(Component) {
navigation: propTypes.object.isRequired, navigation: propTypes.object.isRequired,
}; };
state = { constructor(props, context) {
isFocused: false, super();
};
this.state = {
isFocused: this.getNavigation(props, context).isFocused(),
};
}
componentDidMount() { componentDidMount() {
const navigation = this.getNavigation(); const navigation = this.getNavigation();
@@ -32,8 +36,8 @@ export default function withNavigationFocus(Component) {
this.subscriptions.forEach(sub => sub.remove()); this.subscriptions.forEach(sub => sub.remove());
} }
getNavigation = () => { getNavigation = (props = this.props, context = this.context) => {
const navigation = this.props.navigation || this.context.navigation; const navigation = props.navigation || context.navigation;
invariant( invariant(
!!navigation, !!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.' '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.'

View File

@@ -10,6 +10,8 @@ export default function withCachedChildNavigation(Comp) {
return class extends React.PureComponent { return class extends React.PureComponent {
static displayName = `withCachedChildNavigation(${displayName})`; static displayName = `withCachedChildNavigation(${displayName})`;
_childEventSubscribers = {};
componentWillMount() { componentWillMount() {
this._updateNavigationProps(this.props.navigation); this._updateNavigationProps(this.props.navigation);
} }
@@ -18,6 +20,23 @@ export default function withCachedChildNavigation(Comp) {
this._updateNavigationProps(nextProps.navigation); 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 => { _updateNavigationProps = navigation => {
// Update props for each child route // Update props for each child route
if (!this._childNavigationProps) { if (!this._childNavigationProps) {
@@ -28,13 +47,19 @@ export default function withCachedChildNavigation(Comp) {
if (childNavigation && childNavigation.state === route) { if (childNavigation && childNavigation.state === route) {
return; return;
} }
if (!this._childEventSubscribers[route.key]) {
this._childEventSubscribers[route.key] = getChildEventSubscriber(
navigation.addListener,
route.key
);
}
this._childNavigationProps[route.key] = addNavigationHelpers({ this._childNavigationProps[route.key] = addNavigationHelpers({
dispatch: navigation.dispatch, dispatch: navigation.dispatch,
state: route, state: route,
addListener: getChildEventSubscriber( isFocused: () => this._isRouteFocused(route),
navigation.addListener, addListener: this._childEventSubscribers[route.key],
route.key
),
}); });
}); });
}; };

View File

@@ -4424,6 +4424,10 @@ react-devtools-core@3.0.0:
shell-quote "^1.6.1" shell-quote "^1.6.1"
ws "^2.0.3" ws "^2.0.3"
react-lifecycles-compat@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-1.0.2.tgz#551d8b1d156346e5fcf30ffac9b32ce3f78b8850"
react-native-dismiss-keyboard@1.0.0: react-native-dismiss-keyboard@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz#32886242b3f2317e121f3aeb9b0a585e2b879b49" resolved "https://registry.yarnpkg.com/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz#32886242b3f2317e121f3aeb9b0a585e2b879b49"