Compare commits

..

48 Commits

Author SHA1 Message Date
Brent Vatne
371a714b57 Release 1.1.2 2018-02-20 16:00:44 -08:00
Ron Arts
1d33b95c5f Bump react-native-safe-area-view dep to 0.7.0 for react-native-web support (#3553) 2018-02-20 15:44:20 -08:00
Mike Grabowski
593bc8a648 Improve Header performance a bit (#3556)
* Improve `Header` performance a bit

I have been investigating `<Header />` component performance once again today with `render` cycles in particular. I have observed that during `push` phase, a typical Header in NavigationPlayground re-renders 3 times on iOS and 2 times on Android.

The first time is obvious on both platforms since that's when we add a new scene to an array. Second on iOS was also self-explanatory - we measure title width and other layout parameters in order to provide a better animation. 

What got me thinking was the last render cycle that didn't have a clear origin. After digging around I've found it is caused by a `scenes` array changing when transition finishes. What is surprising, it's just the reference to the object that changes, the content (items inside) stay the same.

I have found out that this is caused by `Transitioner` looping through an array of scenes attempting to remove stale scenes. Since scene becomes stale when you `pop` from it, this is obviously a noop when `pushing` a new route. That, obviously, causes an extra render cycle since `filter` produces same, but with a different pointer, object.

* Update Transitioner.js

* Update Transitioner.js
2018-02-20 15:44:10 -08:00
Brent Vatne
174a6e4175 Release 1.1.1 2018-02-19 18:18:38 -08:00
Brent Vatne
af991e5512 Release 1.1.0 2018-02-19 18:16:23 -08:00
Brent Vatne
0b0e9e9df5 Fix TabRouter to support shorthand route config 2018-02-19 18:16:23 -08:00
Brent Vatne
42b0ccca79 Release 1.1.0-rc.5 2018-02-19 18:16:23 -08:00
Brent Vatne
da6a960bb1 Fix regression in modular back button 2018-02-19 18:16:23 -08:00
Ron Arts
3ca47ec778 Fix react-native-web support for #3526 (#3546)
* MaskedViewIOS use broke react-native-web support, this fixes it.

* Fallback more gracefully.

* Actually return the value ...
2018-02-19 13:48:12 +01:00
Marcin Raburski
14ee56a20d getComponentForState made more generic (#2498) 2018-02-19 04:14:13 -08:00
Matthieu Lemoine
9d36daf48e Pass prevTransitionsProps to transitionConfig (#3304)
* Pass prevTransitionsProps to transitionConfig

* Remove flow type
2018-02-19 02:29:46 -08:00
Brent Vatne
055fdb2ffc Release 1.1.0-rc.4 2018-02-17 12:50:26 -08:00
Brent Vatne
b3bf80634d Move the back button handler subscription to the NavigationContainer constructor (#3542)
* Move the back button handler subscription to the constructor to fix precedence
- See 07d92947a1/src/Navigation.js (L190-L193)

* Return the boolean result of dispatch in global back handler
2018-02-17 12:50:04 -08:00
Brent Vatne
fafd0e8702 Cleanup around StackRouter (#3487)
* Make StackRouter a bit easier to read, update and add some comments, add an explicit test around stack and navigate

* Address feedback for clarity
2018-02-17 11:14:03 -08:00
Brent Vatne
d94045817c Release 1.1.0-rc.3 2018-02-17 09:42:45 -08:00
Brent Vatne
f0acaddf05 Fix headerLeft on first screen regression 2018-02-17 09:40:59 -08:00
Brent Vatne
9beb32ecac Update snapshots 2018-02-16 18:14:07 -08:00
Brent Vatne
29a29936bf Remove withNavigationFocus example until it's fixed 2018-02-16 18:10:33 -08:00
Brent Vatne
8961fef00b Bump version 2018-02-16 18:09:40 -08:00
Brent Vatne
021c7e54be Do not use hide views when swipe or animations are enabled. Improve withNavigationFocus example 2018-02-16 18:05:39 -08:00
Brent Vatne
27538cad94 Properly thread through childNavigation 2018-02-16 17:26:01 -08:00
Brent Vatne
9a8a50ef1d Release 1.1.0-rc.0 2018-02-16 17:07:28 -08:00
Brent Vatne
2c6dc35723 Revert "Revert "Add getParam navigation helper (#3510)" (#3532)"
This reverts commit 5febb81a1c.
2018-02-16 17:06:16 -08:00
Brent Vatne
3600ecbd9b Add back support for lazy tabs and use removeClippedSubviews (#3538)
* Lazy initialization of tabs and move contents off-screen when not active

* Make subview clipping and lazy both configurable

* Record snapshots again

* Update type definition

* Remove unused log
2018-02-16 16:59:19 -08:00
Sébastien Lorber
c74f001b1c add withNavigationFocus HOC (#3512)
* add withNavigationFocus HOC

See:
- https://github.com/react-navigation/react-navigation/issues/51#issuecomment-276003658
- https://github.com/react-navigation/react-navigation/issues/51#issuecomment-278761705
- https://github.com/react-navigation/react-navigation/pull/3345#issuecomment-360260067

* typos

* remove unused import

* Add withNavigationFocus export

* add example TabsWithNavigationFocus

* add example TabsWithNavigationFocus

* withNavigationFocus: get navigation from context or props

* subs => subscriptions

* fix flow issues
2018-02-16 16:57:33 -08:00
Brent Vatne
7f4706e4cc Update snapshots 2018-02-16 12:45:57 -08:00
Brent Vatne
d0ef33b12f Several small changes related to header style commit 2018-02-16 12:42:44 -08:00
Brent Vatne
3c3668c952 Header transition presets with support for standard iOS transition style (#3526)
Header transition presets with approximate support for UIKit transition style
2018-02-16 12:41:59 -08:00
Brent Vatne
5febb81a1c Revert "Add getParam navigation helper (#3510)" (#3532)
This reverts commit 50dcb37cd7.
2018-02-15 13:24:40 -08:00
Peter Piekarczyk
50dcb37cd7 Add getParam navigation helper (#3510)
* add getParam helper

* passing tests

* check for existence of param instead of just or

* fix spacing

* use in instead of checking for null

* add test for null
2018-02-15 10:14:08 -08:00
Ron Arts
69bca191a7 The Linking and BackHandler components are now both supported by react-native-web (#3494)
so there's no need to supply dummy replacements.
2018-02-13 12:23:26 -08:00
Sébastien Lorber
4df9198979 fix doc link (#3513) 2018-02-13 10:30:20 -08:00
Ashoat Tevosyan
0fc7f87173 Run Prettier on Flow libdef on precommit (#3495) 2018-02-12 14:45:50 -08:00
Brent Vatne
5b4d11ab5d Better orientation layout on playground 2018-02-12 11:53:23 -08:00
Brent Vatne
4d1cc285b9 Slightly improved margins on the iOS header back button 2018-02-09 21:19:02 -08:00
Brent Vatne
41725afa8a Use the correct iOS back icon 2018-02-09 20:08:56 -08:00
Brent Vatne
58b77d44ae Make Router({ RouteName: Component }) a valid way to configure a router (#3486)
* Make Router({routeName: Component}) a valid way to instantiate a route

* Update App.js in NavigationPlayground

* Fix route config flow type
2018-02-09 18:20:01 -08:00
Kazato Sugimoto
1d49b6e3fe Decode URI encoded string of deep link (#3455) 2018-02-09 16:26:20 -08:00
Brent Vatne
58b8a08af6 Update snapshots for TabView/TabNavigator 2018-02-09 14:22:31 -08:00
Maxime Florent Fankam
b11ea8a1d5 Allow swipeEnabled be a function (#3378)
* Allow swipeEnabled be a function with state param
2018-02-09 14:04:17 -08:00
Brent Vatne
3c2b977eca Number of lines in label for TabBarBottom is always 1 2018-02-09 13:51:04 -08:00
Brent Vatne
95565487ec Make it possible to go back to menu from key example and improve banner on menu screen (#3479)
* Make it possible to go back to menu from key example and improve banner on menu screen

* Fix NavigationPlayground flow error
2018-02-08 19:10:54 -08:00
Brent Vatne
a26d2acfca Bump to patch version 1.0.3 2018-02-08 18:24:20 -08:00
Brent Vatne
a1b3d2213d No need to have a conditional around slicing the routes array 2018-02-08 18:22:02 -08:00
Brent Vatne
eb39df507e Prevent navigation from getting in bad state when navigating back to route by key (#3478) 2018-02-08 18:20:14 -08:00
Brent Vatne
cca06bb530 Do not use contentInsetAdjustmentBehavior on iOS in ModalStack example 2018-02-08 16:14:45 -08:00
Eric Vicenti
2187d8fe66 Consistent treatment of route keys (#3474)
This problem was found and fixed by @matthargett and @jayphelps in #3397. I’m just rebasing and cleaning a few things up
2018-02-08 15:28:27 -08:00
Eric Vicenti
67f98b69eb Simpler implementation of withNavigation (#3476)
This will allow for refs with onRef (fixes #3461), and will avoid all these warnings from throwing during our tests
2018-02-08 14:20:52 -08:00
52 changed files with 2017 additions and 701 deletions

View File

@@ -1,11 +1,13 @@
/* @flow */
import React from 'react';
import { Constants, ScreenOrientation } from 'expo';
import { Asset, Constants, ScreenOrientation } from 'expo';
ScreenOrientation.allow(ScreenOrientation.Orientation.ALL);
import {
Animated,
Image,
Platform,
ScrollView,
StyleSheet,
@@ -16,7 +18,6 @@ import {
} from 'react-native';
import { SafeAreaView, StackNavigator } from 'react-navigation';
import Banner from './Banner';
import CustomTabs from './CustomTabs';
import CustomTransitioner from './CustomTransitioner';
import Drawer from './Drawer';
@@ -25,9 +26,12 @@ import TabsInDrawer from './TabsInDrawer';
import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs';
import StacksOverTabs from './StacksOverTabs';
import StacksWithKeys from './StacksWithKeys';
import SimpleStack from './SimpleStack';
import StackWithHeaderPreset from './StackWithHeaderPreset';
import SimpleTabs from './SimpleTabs';
import TabAnimations from './TabAnimations';
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
const ExampleInfo = {
SimpleStack: {
@@ -42,6 +46,10 @@ const ExampleInfo = {
name: 'Drawer Example',
description: 'Android-style drawer navigation',
},
StackWithHeaderPreset: {
name: 'UIKit-style Header Transitions',
description: 'Masked back button and sliding header items. iOS only.',
},
// MultipleDrawer: {
// name: 'Multiple Drawer Example',
// description: 'Add any drawer you need',
@@ -76,6 +84,10 @@ const ExampleInfo = {
name: 'Stacks over Tabs',
description: 'Nested stack navigation that pushes on top of tabs',
},
StacksWithKeys: {
name: 'Link in Stack with keys',
description: 'Use keys to link between screens',
},
LinkStack: {
name: 'Link in Stack',
description: 'Deep linking into a route in stack',
@@ -88,38 +100,27 @@ 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',
// },
};
const ExampleRoutes = {
SimpleStack: {
screen: SimpleStack,
},
SimpleTabs: {
screen: SimpleTabs,
},
Drawer: {
screen: Drawer,
},
SimpleStack: SimpleStack,
SimpleTabs: SimpleTabs,
Drawer: Drawer,
// MultipleDrawer: {
// screen: MultipleDrawer,
// },
TabsInDrawer: {
screen: TabsInDrawer,
},
CustomTabs: {
screen: CustomTabs,
},
CustomTransitioner: {
screen: CustomTransitioner,
},
ModalStack: {
screen: ModalStack,
},
StacksInTabs: {
screen: StacksInTabs,
},
StacksOverTabs: {
screen: StacksOverTabs,
},
StackWithHeaderPreset: StackWithHeaderPreset,
TabsInDrawer: TabsInDrawer,
CustomTabs: CustomTabs,
CustomTransitioner: CustomTransitioner,
ModalStack: ModalStack,
StacksWithKeys: StacksWithKeys,
StacksInTabs: StacksInTabs,
StacksOverTabs: StacksOverTabs,
LinkStack: {
screen: SimpleStack,
path: 'people/Jordan',
@@ -128,48 +129,148 @@ const ExampleRoutes = {
screen: SimpleTabs,
path: 'settings',
},
TabAnimations: {
screen: TabAnimations,
},
TabAnimations: TabAnimations,
// TabsWithNavigationFocus: TabsWithNavigationFocus,
};
class MainScreen extends React.Component<*> {
type State = {
scrollY: Animated.Value,
};
class MainScreen extends React.Component<any, State> {
state = {
scrollY: new Animated.Value(0),
};
componentWillMount() {
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon-mask.png')
).downloadAsync();
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon.png')
).downloadAsync();
}
render() {
const { navigation } = this.props;
const scale = this.state.scrollY.interpolate({
inputRange: [-450, 0, 100],
outputRange: [2, 1, 0.8],
extrapolate: 'clamp',
});
const translateY = this.state.scrollY.interpolate({
inputRange: [-450, 0, 100],
outputRange: [-150, 0, 40],
});
const opacity = this.state.scrollY.interpolate({
inputRange: [0, 50],
outputRange: [1, 0],
extrapolate: 'clamp',
});
const underlayOpacity = this.state.scrollY.interpolate({
inputRange: [0, 50],
outputRange: [0, 1],
extrapolate: 'clamp',
});
const backgroundScale = this.state.scrollY.interpolate({
inputRange: [-450, 0],
outputRange: [3, 1],
extrapolate: 'clamp',
});
const backgroundTranslateY = this.state.scrollY.interpolate({
inputRange: [-450, 0],
outputRange: [0, 0],
});
return (
<View style={{ flex: 1 }}>
<ScrollView style={{ flex: 1 }}>
<Banner />
{Object.keys(ExampleRoutes).map((routeName: string) => (
<TouchableOpacity
key={routeName}
onPress={() => {
const { path, params, screen } = ExampleRoutes[routeName];
const { router } = screen;
const action =
path && router.getActionForPathAndParams(path, params);
navigation.navigate(routeName, {}, action);
}}
<Animated.ScrollView
style={{ flex: 1 }}
scrollEventThrottle={1}
onScroll={Animated.event(
[
{
nativeEvent: { contentOffset: { y: this.state.scrollY } },
},
],
{ useNativeDriver: true }
)}
>
<Animated.View
style={[
styles.backgroundUnderlay,
{
transform: [
{ scale: backgroundScale },
{ translateY: backgroundTranslateY },
],
},
]}
/>
<Animated.View
style={{ opacity, transform: [{ scale }, { translateY }] }}
>
<SafeAreaView
style={styles.bannerContainer}
forceInset={{ top: 'always', bottom: 'never' }}
>
<SafeAreaView
style={styles.itemContainer}
forceInset={{ vertical: 'never' }}
>
<View style={styles.item}>
<Text style={styles.title}>
{ExampleInfo[routeName].name}
</Text>
<Text style={styles.description}>
{ExampleInfo[routeName].description}
</Text>
</View>
</SafeAreaView>
</TouchableOpacity>
))}
</ScrollView>
<View style={styles.banner}>
<Image
source={require('./assets/NavLogo.png')}
style={styles.bannerImage}
/>
<Text style={styles.bannerTitle}>
React Navigation Examples
</Text>
</View>
</SafeAreaView>
</Animated.View>
<SafeAreaView forceInset={{ bottom: 'always', horizontal: 'never' }}>
<View style={{ backgroundColor: '#fff' }}>
{Object.keys(ExampleRoutes).map((routeName: string) => (
<TouchableOpacity
key={routeName}
onPress={() => {
let route = ExampleRoutes[routeName];
if (route.screen || route.path || route.params) {
const { path, params, screen } = route;
const { router } = screen;
const action =
path && router.getActionForPathAndParams(path, params);
navigation.navigate(routeName, {}, action);
} else {
navigation.navigate(routeName);
}
}}
>
<SafeAreaView
style={styles.itemContainer}
forceInset={{ veritcal: 'never', bottom: 'never' }}
>
<View style={styles.item}>
<Text style={styles.title}>
{ExampleInfo[routeName].name}
</Text>
<Text style={styles.description}>
{ExampleInfo[routeName].description}
</Text>
</View>
</SafeAreaView>
</TouchableOpacity>
))}
</View>
</SafeAreaView>
</Animated.ScrollView>
<StatusBar barStyle="light-content" />
<View style={styles.statusBarUnderlay} />
<Animated.View
style={[styles.statusBarUnderlay, { opacity: underlayOpacity }]}
/>
</View>
);
}
@@ -230,4 +331,35 @@ const styles = StyleSheet.create({
fontSize: 13,
color: '#999',
},
backgroundUnderlay: {
backgroundColor: '#673ab7',
position: 'absolute',
top: -100,
height: 300,
left: 0,
right: 0,
},
bannerContainer: {
// backgroundColor: '#673ab7',
alignItems: 'center',
},
banner: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
bannerImage: {
width: 36,
height: 36,
resizeMode: 'contain',
tintColor: '#fff',
margin: 8,
},
bannerTitle: {
fontSize: 18,
fontWeight: '200',
color: '#fff',
marginVertical: 8,
marginRight: 5,
},
});

View File

@@ -8,7 +8,7 @@ import { SafeAreaView, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<ScrollView>
<SafeAreaView
forceInset={{
top: navigation.state.routeName === 'HeaderTest' ? 'always' : 'never',

View File

@@ -19,14 +19,12 @@ type MyNavScreenProps = {
class MyBackButton extends React.Component<any, any> {
render() {
return (
<Button onPress={this._navigateBack} title="Custom Back" />
);
return <Button onPress={this._navigateBack} title="Custom Back" />;
}
_navigateBack = () => {
this.props.navigation.goBack(null);
}
};
}
const MyBackButtonWithNavigation = withNavigation(MyBackButton);
@@ -108,7 +106,7 @@ type MyPhotosScreenProps = {
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
headerLeft: <MyBackButtonWithNavigation />
headerLeft: <MyBackButtonWithNavigation />,
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;
@@ -180,18 +178,20 @@ MyProfileScreen.navigationOptions = props => {
};
};
const SimpleStack = StackNavigator({
Home: {
screen: MyHomeScreen,
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
});
const SimpleStack = StackNavigator(
{
Home: {
screen: MyHomeScreen,
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
}
);
export default SimpleStack;

View File

@@ -163,6 +163,8 @@ const SimpleTabs = TabNavigator(
},
},
{
lazy: true,
removeClippedSubviews: true,
tabBarOptions: {
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
},

View File

@@ -0,0 +1,68 @@
/**
* @flow
*/
import type { NavigationScreenProp } from 'react-navigation';
import * as React from 'react';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
type NavScreenProps = {
navigation: NavigationScreenProp<*>,
};
class HomeScreen extends React.Component<NavScreenProps> {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
class OtherScreen extends React.Component<NavScreenProps> {
static navigationOptions = {
title: 'Your title here',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
const StackWithHeaderPreset = StackNavigator(
{
Home: HomeScreen,
Other: OtherScreen,
},
{
headerTransitionPreset: 'uikit',
}
);
export default StackWithHeaderPreset;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Button, StatusBar, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
class HomeScreen extends React.Component<any, any> {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home</Text>
<Button
title="Navigate to 'Profile' with key 'A'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Profile',
key: 'A',
params: { homeKey: this.props.navigation.state.key },
})
}
/>
<Button
title="Go back to other examples"
onPress={() => this.props.navigation.goBack(null)}
/>
<StatusBar barStyle="default" />
</View>
);
}
}
class ProfileScreen extends React.Component<any, any> {
render() {
const { homeKey } = this.props.navigation.state.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile</Text>
<Button
title="Navigate to 'Settings' with key 'B'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Settings',
key: 'B',
params: { homeKey },
})
}
/>
<Button
title={`Navigate back to 'Home' with key ${homeKey}`}
onPress={() =>
this.props.navigation.navigate({ routeName: 'Home', key: homeKey })
}
/>
</View>
);
}
}
class SettingsScreen extends React.Component<any, any> {
render() {
const { homeKey } = this.props.navigation.state.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Settings</Text>
<Button
title={`Navigate back to 'Home' with key ${homeKey}`}
onPress={() =>
this.props.navigation.navigate({ routeName: 'Home', key: homeKey })
}
/>
<Button
title="Navigate back to 'Profile' with key 'A'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Profile',
key: 'A',
})
}
/>
</View>
);
}
}
const Stack = StackNavigator(
{
Home: {
screen: HomeScreen,
},
Profile: {
screen: ProfileScreen,
},
Settings: {
screen: SettingsScreen,
},
},
{
headerMode: 'none',
}
);
export default Stack;

View File

@@ -0,0 +1,66 @@
/**
* @flow
*/
import React from 'react';
import { SafeAreaView, Text } from 'react-native';
import { TabNavigator, 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()}
</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' }}
/>
),
};
return withNavigationFocus(TabScreen);
};
const TabsWithNavigationFocus = TabNavigator(
{
One: {
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
},
Two: {
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
},
Three: {
screen: createTabScreen(
'Three',
'numeric-3-box-outline',
'numeric-3-box'
),
},
},
{
tabBarPosition: 'bottom',
animationEnabled: true,
swipeEnabled: true,
}
);
export default TabsWithNavigationFocus;

View File

@@ -568,9 +568,9 @@ babel-jest@^21.0.0, babel-jest@^21.2.0:
babel-plugin-istanbul "^4.0.0"
babel-preset-jest "^21.2.0"
babel-jest@^22.1.0, babel-jest@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.2.0.tgz#2d04087f5d149585e14f641d529551963fc9b4f8"
babel-jest@^22.1.0, babel-jest@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.2.2.tgz#eda38dca284e32cc5257f96a9b51351975de4e04"
dependencies:
babel-plugin-istanbul "^4.1.5"
babel-preset-jest "^22.2.0"
@@ -1217,8 +1217,8 @@ bplist-parser@0.1.1:
big-integer "^1.6.7"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -2093,9 +2093,9 @@ expect@^21.2.1:
jest-message-util "^21.2.1"
jest-regex-util "^21.2.0"
expect@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.2.0.tgz#dddcaab2e22ccc9f51e7c1732e0aa723f2f1f2b8"
expect@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.2.2.tgz#6cb6ae2eeb651a4187b9096de70333a018fab63f"
dependencies:
ansi-styles "^3.2.0"
jest-diff "^22.1.0"
@@ -3212,9 +3212,9 @@ jest-cli@^21.2.1:
worker-farm "^1.3.1"
yargs "^9.0.0"
jest-cli@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.2.1.tgz#f1df6675cd719e0773be77d6314c5f4b1d8af47f"
jest-cli@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.2.2.tgz#4431a93a29549da5dcb6d4a41dd03503c9198cd6"
dependencies:
ansi-escapes "^3.0.0"
chalk "^2.0.1"
@@ -3228,18 +3228,18 @@ jest-cli@^22.2.1:
istanbul-lib-instrument "^1.8.0"
istanbul-lib-source-maps "^1.2.1"
jest-changed-files "^22.2.0"
jest-config "^22.2.1"
jest-environment-jsdom "^22.2.0"
jest-config "^22.2.2"
jest-environment-jsdom "^22.2.2"
jest-get-type "^22.1.0"
jest-haste-map "^22.2.0"
jest-haste-map "^22.2.2"
jest-message-util "^22.2.0"
jest-regex-util "^22.1.0"
jest-resolve-dependencies "^22.1.0"
jest-runner "^22.2.1"
jest-runtime "^22.2.1"
jest-runner "^22.2.2"
jest-runtime "^22.2.2"
jest-snapshot "^22.2.0"
jest-util "^22.2.0"
jest-worker "^22.2.0"
jest-util "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
node-notifier "^5.2.1"
realpath-native "^1.0.0"
@@ -3266,20 +3266,20 @@ jest-config@^21.2.1:
jest-validate "^21.2.1"
pretty-format "^21.2.1"
jest-config@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.2.1.tgz#8617e99cff0544f0a5f254a5dde37f43b5383934"
jest-config@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.2.2.tgz#6b8ed615bc51239847d15460086f174dad4a7015"
dependencies:
chalk "^2.0.1"
glob "^7.1.1"
jest-environment-jsdom "^22.2.0"
jest-environment-node "^22.2.0"
jest-environment-jsdom "^22.2.2"
jest-environment-node "^22.2.2"
jest-get-type "^22.1.0"
jest-jasmine2 "^22.2.1"
jest-jasmine2 "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.2.0"
jest-util "^22.2.0"
jest-validate "^22.2.0"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
jest-validate "^22.2.2"
pretty-format "^22.1.0"
jest-diff@^21.2.1:
@@ -3310,9 +3310,9 @@ jest-docblock@^21.2.0:
version "21.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
jest-docblock@^22.1.0, jest-docblock@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.2.0.tgz#4d054eac354751e94a43a0ea2e2fe5c04cc61bbb"
jest-docblock@^22.1.0, jest-docblock@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.2.2.tgz#617f13edb16ec64202002b3c336cd14ae36c0631"
dependencies:
detect-newline "^2.1.0"
@@ -3324,12 +3324,12 @@ jest-environment-jsdom@^21.2.1:
jest-util "^21.2.1"
jsdom "^9.12.0"
jest-environment-jsdom@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.2.0.tgz#e9537400cbdef2d1e61d7196f8afa40e826fe9d8"
jest-environment-jsdom@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.2.2.tgz#3513ccdccc2bc41daf9cdee199b7069b0d9feebc"
dependencies:
jest-mock "^22.2.0"
jest-util "^22.2.0"
jest-util "^22.2.2"
jsdom "^11.5.1"
jest-environment-node@^21.2.1:
@@ -3339,12 +3339,12 @@ jest-environment-node@^21.2.1:
jest-mock "^21.2.0"
jest-util "^21.2.1"
jest-environment-node@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.2.0.tgz#ba7d0183fac076d34867367a4ac53ced69e3d3a9"
jest-environment-node@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.2.2.tgz#570896eef2dd0f939c71bd5712ef4321958c1270"
dependencies:
jest-mock "^22.2.0"
jest-util "^22.2.0"
jest-util "^22.2.2"
jest-expo@^25.1.0:
version "25.1.0"
@@ -3385,14 +3385,14 @@ jest-haste-map@^21.2.0:
sane "^2.0.0"
worker-farm "^1.3.1"
jest-haste-map@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.2.0.tgz#c9f508b8f63322490339ba02343dd688474d9ad5"
jest-haste-map@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.2.2.tgz#9d3d5a14bd5e05ab9176979f2a5fbb4ddc80eb20"
dependencies:
fb-watchman "^2.0.0"
graceful-fs "^4.1.11"
jest-docblock "^22.2.0"
jest-worker "^22.2.0"
jest-docblock "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
sane "^2.0.0"
@@ -3409,14 +3409,14 @@ jest-jasmine2@^21.2.1:
jest-snapshot "^21.2.1"
p-cancelable "^0.3.0"
jest-jasmine2@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.2.1.tgz#58d115f3f4a0a186b5e90c5db8ac68aecfc41051"
jest-jasmine2@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.2.2.tgz#9065255c8f635ae9dfa33fc66068f59adf53c9aa"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
co "^4.6.0"
expect "^22.2.0"
expect "^22.2.2"
graceful-fs "^4.1.11"
is-generator-fn "^1.0.0"
jest-diff "^22.1.0"
@@ -3501,9 +3501,9 @@ jest-resolve@^21.2.0:
chalk "^2.0.1"
is-builtin-module "^1.0.0"
jest-resolve@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.2.0.tgz#25aa8b887b31ab8c79763503e209d7c136f74ab1"
jest-resolve@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.2.2.tgz#6f49d91e3680c86a4d5e5f72ccdab3996d1cbc19"
dependencies:
browser-resolve "^1.11.2"
chalk "^2.0.1"
@@ -3523,20 +3523,20 @@ jest-runner@^21.2.1:
throat "^4.0.0"
worker-farm "^1.3.1"
jest-runner@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.2.1.tgz#539b2b7eb0ceb34e63a1ca78a1eda46ace70b940"
jest-runner@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.2.2.tgz#17fff27a61b63b58cf104c9cdcc0fdfccd3878ce"
dependencies:
exit "^0.1.2"
jest-config "^22.2.1"
jest-docblock "^22.2.0"
jest-haste-map "^22.2.0"
jest-jasmine2 "^22.2.1"
jest-config "^22.2.2"
jest-docblock "^22.2.2"
jest-haste-map "^22.2.2"
jest-jasmine2 "^22.2.2"
jest-leak-detector "^22.1.0"
jest-message-util "^22.2.0"
jest-runtime "^22.2.1"
jest-util "^22.2.0"
jest-worker "^22.2.0"
jest-runtime "^22.2.2"
jest-util "^22.2.2"
jest-worker "^22.2.2"
throat "^4.0.0"
jest-runtime@^21.2.1:
@@ -3561,22 +3561,22 @@ jest-runtime@^21.2.1:
write-file-atomic "^2.1.0"
yargs "^9.0.0"
jest-runtime@^22.2.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.2.1.tgz#c5b0173a7f5274b28da30019cf7bb7b8cda68040"
jest-runtime@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.2.2.tgz#256d0efb65deae1c23b819d88cec5ab43d7a4ed6"
dependencies:
babel-core "^6.0.0"
babel-jest "^22.2.0"
babel-jest "^22.2.2"
babel-plugin-istanbul "^4.1.5"
chalk "^2.0.1"
convert-source-map "^1.4.0"
exit "^0.1.2"
graceful-fs "^4.1.11"
jest-config "^22.2.1"
jest-haste-map "^22.2.0"
jest-config "^22.2.2"
jest-haste-map "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.2.0"
jest-util "^22.2.0"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
json-stable-stringify "^1.0.1"
micromatch "^2.3.11"
realpath-native "^1.0.0"
@@ -3619,16 +3619,16 @@ jest-util@^21.2.1:
jest-validate "^21.2.1"
mkdirp "^0.5.1"
jest-util@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.2.0.tgz#afad693641447858bec7b37f32952516bf887262"
jest-util@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.2.2.tgz#335484b6aeae0c5a1ae498401630324977fe3465"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
graceful-fs "^4.1.11"
is-ci "^1.0.10"
jest-message-util "^22.2.0"
jest-validate "^22.2.0"
jest-validate "^22.2.2"
mkdirp "^0.5.1"
jest-validate@^21.2.1:
@@ -3640,9 +3640,9 @@ jest-validate@^21.2.1:
leven "^2.1.0"
pretty-format "^21.2.1"
jest-validate@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.2.0.tgz#f7ce459105a8210825e5e57279f868ab080063fa"
jest-validate@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.2.2.tgz#9cdce422c93cc28395e907ac6bbc929158d9a6ba"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
@@ -3655,9 +3655,9 @@ jest-worker@22.1.0:
dependencies:
merge-stream "^1.0.1"
jest-worker@^22.1.0, jest-worker@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.2.0.tgz#d88d6ee176d6409f206cbbf7b485250793264262"
jest-worker@^22.1.0, jest-worker@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.2.2.tgz#c1f5dc39976884b81f68ec50cb8532b2cbab3390"
dependencies:
merge-stream "^1.0.1"
@@ -3668,11 +3668,11 @@ jest@^21.0.1:
jest-cli "^21.2.1"
jest@^22.1.1:
version "22.2.1"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.2.1.tgz#fb6524d35bd02968afe3b17f330d6f7207846147"
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.2.2.tgz#26aca0f5e4eaa76d52f2792b14033a3d1e7be2bd"
dependencies:
import-local "^1.0.0"
jest-cli "^22.2.1"
jest-cli "^22.2.2"
joi@^10.0.2:
version "10.6.0"
@@ -4156,10 +4156,6 @@ lru-memoizer@^1.11.1:
lru-cache "~4.0.0"
very-fast-args "^1.1.0"
lsmod@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b"
macos-release@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb"
@@ -4986,9 +4982,9 @@ probe-image-size@^3.1.0:
next-tick "^1.0.0"
stream-parser "~0.3.1"
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
process@~0.5.1:
version "0.5.2"
@@ -5099,11 +5095,10 @@ raven-js@^3.17.0:
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.2.tgz#85785928ebd664049e54efd0db8ff28da0cbb374"
raven@^2.1.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.0.tgz#49b7d5f838e5893f31dd72f82d05a35e42203f60"
version "2.4.1"
resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.1.tgz#7a6a6ff1c42d0a3892308f44c94273e7f88677fd"
dependencies:
cookie "0.3.1"
lsmod "1.0.0"
md5 "^2.2.1"
stack-trace "0.0.9"
timed-out "4.0.1"
@@ -5398,13 +5393,13 @@ readable-stream@1.1.x, readable-stream@^1.1.8, readable-stream@~1.1.8, readable-
string_decoder "~0.10.x"
readable-stream@2, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"

View File

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

View File

@@ -332,12 +332,15 @@ declare module 'react-navigation' {
navigationOptions?: ?NavigationScreenConfig<Options>,
};
declare export type NavigationRouteConfig = {
navigationOptions?: NavigationScreenConfig<*>,
path?: string,
} & NavigationScreenRouteConfig;
declare export type NavigationRouteConfig =
| NavigationComponent
| ({
navigationOptions?: NavigationScreenConfig<*>,
path?: string,
} & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig =
| NavigationComponent
| {
screen: NavigationComponent,
}
@@ -405,6 +408,7 @@ declare module 'react-navigation' {
declare export type NavigationStackViewConfig = {|
mode?: 'card' | 'modal',
headerMode?: HeaderMode,
headerTransitionPreset?: 'fade-in-place' | 'uikit',
cardStyle?: ViewStyleProp,
transitionConfig?: () => TransitionConfig,
onTransitionStart?: () => void,
@@ -829,6 +833,8 @@ declare module 'react-navigation' {
declare type _TabNavigatorConfig = {|
...NavigationTabRouterConfig,
..._TabViewConfig,
lazy?: boolean,
removeClippedSubviews?: boolean,
containerOptions?: void,
|};
declare export function TabNavigator(
@@ -1099,4 +1105,7 @@ declare module 'react-navigation' {
declare export function withNavigation<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps>
): React$ComponentType<T>;
declare export function withNavigationFocus<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps>
): React$ComponentType<T>;
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "1.0.2",
"version": "1.1.2",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -33,7 +33,7 @@
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10",
"react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.6.0",
"react-native-safe-area-view": "^0.7.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {
@@ -72,6 +72,10 @@
"modulePathIgnorePatterns": ["examples"]
},
"lint-staged": {
"*.js": ["eslint --fix", "git add"]
"*.js": [
"eslint --fix",
"prettier --write flow/react-navigation.js",
"git add"
]
}
}

View File

@@ -1,9 +1,9 @@
import {
BackAndroid as DeprecatedBackAndroid,
BackHandler as ModernBackHandler,
Linking,
MaskedViewIOS,
} from 'react-native';
const BackHandler = ModernBackHandler || DeprecatedBackAndroid;
export { BackHandler, Linking };
export { BackHandler, MaskedViewIOS };

View File

@@ -1,9 +1,6 @@
export const Linking = {
addEventListener: () => {},
removeEventListener: () => {},
getInitialURL: () => Promise.reject('Unsupported platform'),
};
import React from 'react';
import { BackHandler, View } from 'react-native';
export const BackHandler = {
addEventListener: () => {},
};
const MaskedViewIOS = () => <View>{this.props.children}</View>;
export { BackHandler, MaskedViewIOS };

View File

@@ -76,4 +76,43 @@ describe('addNavigationHelpers', () => {
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles GetParams action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings', params: { name: 'Peter' } },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name', 'Brent')
).toEqual('Peter');
});
it('handles GetParams action with default param', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name', 'Brent')
).toEqual('Brent');
});
it('handles GetParams action with param value as null', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings', params: { name: null } },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name')
).toEqual(null);
});
});

View File

@@ -61,6 +61,16 @@ export default function(navigation) {
return navigation.dispatch(NavigationActions.setParams({ params, key }));
},
getParam: (paramName, defaultValue) => {
const params = navigation.state.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
},
push: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.push({ routeName, params, action })

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { BackHandler, Linking } from './PlatformHelpers';
import { Linking } from 'react-native';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
import addNavigationHelpers from './addNavigationHelpers';
import invariant from './utils/invariant';
@@ -25,6 +26,20 @@ export default function createNavigationContainer(Component) {
this._validateProps(props);
this._initialAction = NavigationActions.init();
if (this._isStateful()) {
this.subs = BackHandler.addEventListener('hardwareBackPress', () => {
if (!this._isMounted) {
this.subs && this.subs.remove();
} else {
// dispatch returns true if the action results in a state change,
// and false otherwise. This maps well to what BackHandler expects
// from a callback -- true if handled, false if not handled
return this.dispatch(NavigationActions.back());
}
});
}
this.state = {
nav: this._isStateful()
? Component.router.getStateForAction(this._initialAction)
@@ -124,14 +139,11 @@ export default function createNavigationContainer(Component) {
}
componentDidMount() {
this._isMounted = true;
if (!this._isStateful()) {
return;
}
this.subs = BackHandler.addEventListener('hardwareBackPress', () =>
this.dispatch(NavigationActions.back())
);
Linking.addEventListener('url', this._handleOpenURL);
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
@@ -147,6 +159,7 @@ export default function createNavigationContainer(Component) {
}
componentWillUnmount() {
this._isMounted = false;
Linking.removeEventListener('url', this._handleOpenURL);
this.subs && this.subs.remove();
}

View File

@@ -15,6 +15,7 @@ export default (routeConfigMap, stackConfig = {}) => {
initialRouteParams,
paths,
headerMode,
headerTransitionPreset,
mode,
cardStyle,
transitionConfig,
@@ -38,6 +39,7 @@ export default (routeConfigMap, stackConfig = {}) => {
<CardStackTransitioner
{...props}
headerMode={headerMode}
headerTransitionPreset={headerTransitionPreset}
mode={mode}
cardStyle={cardStyle}
transitionConfig={transitionConfig}

View File

@@ -19,6 +19,8 @@ const TabNavigator = (routeConfigs, config = {}) => {
tabBarComponent,
tabBarPosition,
tabBarOptions,
lazy,
removeClippedSubviews,
swipeEnabled,
animationEnabled,
configureTransition,
@@ -31,6 +33,8 @@ const TabNavigator = (routeConfigs, config = {}) => {
const navigator = createNavigator(router, routeConfigs, config)(props => (
<TabView
{...props}
lazy={lazy}
removeClippedSubviews={removeClippedSubviews}
tabBarComponent={tabBarComponent}
tabBarPosition={tabBarPosition}
tabBarOptions={tabBarOptions}

View File

@@ -79,6 +79,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
headerTransitionPreset={undefined}
index={0}
layout={
Object {
@@ -89,12 +90,15 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"width": 0,
}
}
leftButtonInterpolator={[Function]}
leftInterpolator={[Function]}
leftLabelInterpolator={[Function]}
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"getParam": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
@@ -108,7 +112,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-1",
"key": "id-0-1",
"routeName": "Home",
},
],
@@ -127,8 +131,10 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"getStateForAction": [Function],
}
}
titleFromLeftInterpolator={[Function]}
titleInterpolator={[Function]}
transitionConfig={undefined}
transitionPreset="fade-in-place"
>
<View
collapsable={undefined}
@@ -184,11 +190,6 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"position": "absolute",
"right": 70,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
@@ -204,7 +205,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "600",
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
@@ -318,6 +319,7 @@ exports[`StackNavigator renders successfully 1`] = `
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
headerTransitionPreset={undefined}
index={0}
layout={
Object {
@@ -328,12 +330,15 @@ exports[`StackNavigator renders successfully 1`] = `
"width": 0,
}
}
leftButtonInterpolator={[Function]}
leftInterpolator={[Function]}
leftLabelInterpolator={[Function]}
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"getParam": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
@@ -347,7 +352,7 @@ exports[`StackNavigator renders successfully 1`] = `
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-0",
"key": "id-0-0",
"routeName": "Home",
},
],
@@ -366,8 +371,10 @@ exports[`StackNavigator renders successfully 1`] = `
"getStateForAction": [Function],
}
}
titleFromLeftInterpolator={[Function]}
titleInterpolator={[Function]}
transitionConfig={undefined}
transitionPreset="fade-in-place"
>
<View
collapsable={undefined}
@@ -423,11 +430,6 @@ exports[`StackNavigator renders successfully 1`] = `
"position": "absolute",
"right": 0,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
@@ -443,7 +445,7 @@ exports[`StackNavigator renders successfully 1`] = `
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "600",
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}

View File

@@ -55,13 +55,23 @@ exports[`TabNavigator renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
/>
>
<View
style={
Object {
"flex": 1,
}
}
/>
</View>
</View>
</View>
<View
@@ -168,6 +178,7 @@ exports[`TabNavigator renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",

View File

@@ -88,4 +88,7 @@ module.exports = {
get withNavigation() {
return require('./views/withNavigation').default;
},
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
};

View File

@@ -32,4 +32,7 @@ module.exports = {
get withNavigation() {
return require('./views/withNavigation').default;
},
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
};

View File

@@ -51,6 +51,61 @@ export default (routeConfigs, stackConfig = {}) => {
const pathsByRouteNames = { ...stackConfig.paths } || {};
let paths = [];
function getInitialState(action) {
let route = {};
const childRouter = childRouters[action.routeName];
// This is a push-like action, and childRouter will be a router or null if we are responsible for this routeName
if (behavesLikePushAction(action) && childRouter !== undefined) {
let childState = {};
// The router is null for normal leaf routes
if (childRouter !== null) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [
{
params: action.params,
...childState,
key: action.key || generateKey(),
routeName: action.routeName,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params || action.params || initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
route = {
...route,
...(params ? { params } : {}),
routeName: initialRouteName,
key: action.key || generateKey(),
};
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [route],
};
}
// Build paths for each route
routeNames.forEach(routeName => {
let pathPattern =
@@ -100,55 +155,11 @@ export default (routeConfigs, stackConfig = {}) => {
getStateForAction(action, state) {
// Set up the initial state if needed
if (!state) {
let route = {};
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [
{
routeName: action.routeName,
params: action.params,
key: `Init-${generateKey()}`,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params ||
action.params ||
initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
route = {
...route,
routeName: initialRouteName,
key: `Init-${generateKey()}`,
...(params ? { params } : {}),
};
// eslint-disable-next-line no-param-reassign
state = {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [route],
};
return getInitialState(action);
}
// Check if a child scene wants to handle the action as long as it is not a reset to the root stack
// Check if the focused child scene wants to handle the action, as long as
// it is not a reset to the root stack
if (action.type !== NavigationActions.RESET || action.key !== null) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
@@ -171,8 +182,131 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
// Handle explicit push navigation action. This must happen after the
// focused child router has had a chance to handle the action.
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
const childRouter = childRouters[action.routeName];
let route;
invariant(
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
if (action.key) {
const lastRouteIndex = state.routes.findIndex(
r => r.key === action.key
);
if (lastRouteIndex !== -1) {
// If index is unchanged and params are not being set, leave state identity intact
if (state.index === lastRouteIndex && !action.params) {
return state;
}
// Remove the now unused routes at the tail of the routes array
const routes = state.routes.slice(0, lastRouteIndex + 1);
// Apply params if provided, otherwise leave route identity intact
if (action.params) {
const route = state.routes.find(r => r.key === action.key);
routes[lastRouteIndex] = {
...route,
params: {
...route.params,
...action.params,
},
};
}
// Return state with new index. Change isTransitioning only if index has changed
return {
...state,
isTransitioning:
state.index !== lastRouteIndex
? action.immediate !== true
: undefined,
index: lastRouteIndex,
routes,
};
}
}
if (childRouter) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
routeName: action.routeName,
key: action.key || generateKey(),
};
} else {
route = {
params: action.params,
routeName: action.routeName,
key: action.key || generateKey(),
};
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
// If we've made it this far with a push action, we return the
// state with a new identity to prevent the action from bubbling
// back up.
return {
...state,
};
}
// Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) {
const childRouterNames = Object.keys(childRouters);
for (let i = 0; i < childRouterNames.length; i++) {
const childRouterName = childRouterNames[i];
const childRouter = childRouters[childRouterName];
if (childRouter) {
// For each child router, start with a blank state
const initChildRoute = childRouter.getStateForAction(
NavigationActions.init()
);
// Then check to see if the router handles our navigate action
const navigatedChildRoute = childRouter.getStateForAction(
action,
initChildRoute
);
let routeToPush = null;
if (navigatedChildRoute === null) {
// Push the route if the router has 'handled' the action and returned null
routeToPush = initChildRoute;
} else if (navigatedChildRoute !== initChildRoute) {
// Push the route if the state has changed in response to this navigation
routeToPush = navigatedChildRoute;
}
if (routeToPush) {
const route = {
...routeToPush,
routeName: childRouterName,
key: action.key || generateKey(),
};
return StateUtils.push(state, route);
}
}
}
}
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
// If we're already at the top, then we return the state with a new
// identity so that the action is handled by this router.
if (state.index === 0) {
return {
...state,
@@ -206,90 +340,14 @@ export default (routeConfigs, stackConfig = {}) => {
params: action.params,
// merge the child state in this order to allow params override
...childState,
key: action.newKey || generateKey(),
routeName: action.routeName,
key: action.newKey || generateKey(),
};
return { ...state, routes };
}
}
// Handle explicit push navigation action. Make sure this happens after children have had a chance to handle the action
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
const childRouter = childRouters[action.routeName];
let route;
invariant(
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
if (action.key) {
const lastRouteIndex = state.routes.findIndex(
r => r.key === action.key
);
if (lastRouteIndex !== -1) {
// If index is unchanged and params are not being set, leave state identity intact
if (state.index === lastRouteIndex && !action.params) {
return state;
}
const routes = [...state.routes];
// Apply params if provided, otherwise leave route identity intact
if (action.params) {
const route = state.routes.find(r => r.key === action.key);
routes[lastRouteIndex] = {
...route,
params: {
...route.params,
...action.params,
},
};
}
// Return state with new index. Change isTransitioning only if index has changed
return {
...state,
isTransitioning:
state.index !== lastRouteIndex
? action.immediate !== true
: undefined,
index: lastRouteIndex,
routes,
};
}
}
const key = action.key || generateKey();
if (childRouter) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
key,
routeName: action.routeName,
};
} else {
route = {
params: action.params,
key,
routeName: action.routeName,
};
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
return {
...state,
};
}
// Update transitioning state
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
@@ -301,41 +359,6 @@ export default (routeConfigs, stackConfig = {}) => {
};
}
// Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) {
const childRouterNames = Object.keys(childRouters);
for (let i = 0; i < childRouterNames.length; i++) {
const childRouterName = childRouterNames[i];
const childRouter = childRouters[childRouterName];
if (childRouter) {
// For each child router, start with a blank state
const initChildRoute = childRouter.getStateForAction(
NavigationActions.init()
);
// Then check to see if the router handles our navigate action
const navigatedChildRoute = childRouter.getStateForAction(
action,
initChildRoute
);
let routeToPush = null;
if (navigatedChildRoute === null) {
// Push the route if the router has 'handled' the action and returned null
routeToPush = initChildRoute;
} else if (navigatedChildRoute !== initChildRoute) {
// Push the route if the state has changed in response to this navigation
routeToPush = navigatedChildRoute;
}
if (routeToPush) {
return StateUtils.push(state, {
...routeToPush,
key: generateKey(),
routeName: childRouterName,
});
}
}
}
}
if (action.type === NavigationActions.SET_PARAMS) {
const key = action.key;
const lastRoute = state.routes.find(route => route.key === key);
@@ -369,20 +392,16 @@ export default (routeConfigs, stackConfig = {}) => {
...state,
routes: resetAction.actions.map(childAction => {
const router = childRouters[childAction.routeName];
let childState = {};
if (router) {
return {
...childAction,
...router.getStateForAction(childAction),
routeName: childAction.routeName,
key: generateKey(),
};
childState = router.getStateForAction(childAction);
}
const route = {
...childAction,
key: generateKey(),
return {
params: childAction.params,
...childState,
routeName: childAction.routeName,
key: childAction.key || generateKey(),
};
delete route.type;
return route;
}),
index: action.index,
};
@@ -521,7 +540,15 @@ export default (routeConfigs, stackConfig = {}) => {
}
const nextResult = result || {};
const paramName = key.name;
nextResult[paramName] = matchResult;
let decodedMatchResult;
try {
decodedMatchResult = decodeURIComponent(matchResult);
} catch (e) {
// ignore `URIError: malformed URI`
}
nextResult[paramName] = decodedMatchResult || matchResult;
return nextResult;
}, queryParams);

View File

@@ -30,8 +30,9 @@ export default (routeConfigs, config = {}) => {
paths[routeName] =
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
tabRouters[routeName] = null;
if (routeConfig.screen && routeConfig.screen.router) {
tabRouters[routeName] = routeConfig.screen.router;
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen.router) {
tabRouters[routeName] = screen.router;
}
});
if (initialRouteIndex === -1) {
@@ -239,7 +240,7 @@ export default (routeConfigs, config = {}) => {
},
getComponentForState(state) {
const routeName = order[state.index];
const routeName = state.routes[state.index].routeName;
invariant(
routeName,
`There is no route defined for index ${state.index}. Check that

View File

@@ -7,7 +7,6 @@ import TabRouter from '../TabRouter';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
@@ -19,7 +18,7 @@ const ROUTERS = {
StackRouter,
};
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
const dummyEventSubscriber = (name, handler) => ({
remove: () => {},
});
@@ -111,8 +110,8 @@ test('Handles no-op actions with tabs within stack router', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
expect(state1.routes[0].key).toEqual('id-0');
expect(state2.routes[0].key).toEqual('id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -140,7 +139,7 @@ test('Handles deep action', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -180,8 +179,8 @@ test('Supports lazily-evaluated getScreen', () => {
immediate: true,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
expect(state1.routes[0].key).toEqual('id-0');
expect(state2.routes[0].key).toEqual('id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(

View File

@@ -355,7 +355,7 @@ describe('StackRouter', () => {
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [{ key: 'Init-id-0', routeName: 'foo' }],
routes: [{ key: 'id-0', routeName: 'foo' }],
});
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'qux' }),
@@ -513,6 +513,40 @@ describe('StackRouter', () => {
expect(pushedTwiceState.routes[2].routeName).toEqual('bar');
});
test('Navigate from top propagates to any arbitary depth of stacks', () => {
const GrandChildNavigator = () => <div />;
GrandChildNavigator.router = StackRouter({
Quux: { screen: () => <div /> },
Corge: { screen: () => <div /> },
});
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Woo: { screen: () => <div /> },
Qux: { screen: GrandChildNavigator },
});
const Parent = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = Parent.getStateForAction({ type: NavigationActions.INIT });
const state2 = Parent.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Corge',
},
state
);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge');
});
test('Navigate with key is idempotent', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -553,6 +587,37 @@ describe('StackRouter', () => {
}).toThrow();
});
test('Navigate backwards with key removes leading routes', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
initState
);
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'b`' }),
pushedState
);
const pushedThriceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo', key: 'c`' }),
pushedTwiceState
);
expect(pushedThriceState.routes.length).toEqual(4);
const navigatedBackToFirstRouteState = TestRouter.getStateForAction(
NavigationActions.navigate({
routeName: 'foo',
key: pushedThriceState.routes[0].key,
}),
pushedThriceState
);
expect(navigatedBackToFirstRouteState.index).toEqual(0);
expect(navigatedBackToFirstRouteState.routes.length).toEqual(1);
});
test('Handle basic stack logic for plain components', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -571,7 +636,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -599,7 +664,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -696,7 +761,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -724,7 +789,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -798,7 +863,7 @@ describe('StackRouter', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -907,14 +972,14 @@ describe('StackRouter', () => {
{
type: NavigationActions.SET_PARAMS,
params: { name: 'foobar' },
key: 'Init-id-0',
key: 'id-0',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Quux',
params: { name: 'foobar' },
},
@@ -1133,6 +1198,126 @@ describe('StackRouter', () => {
]);
});
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: 'test',
foo: 'bar',
},
},
},
});
expect(state).toEqual({
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-2',
params: { code: 'test', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-1',
params: { code: 'test', foo: 'bar', id: '4' },
routeName: 'profile',
routes: [
{
key: 'id-0',
params: { code: 'test', foo: 'bar', id: '10259959195' },
routeName: 'list',
type: undefined,
},
],
},
],
},
],
});
const state2 = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: '',
foo: 'bar',
},
},
},
});
expect(state2).toEqual({
index: 0,
isTransitioning: false,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-5',
params: { code: '', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
key: 'id-4',
params: { code: '', foo: 'bar', id: '4' },
routeName: 'profile',
routes: [
{
key: 'id-3',
params: { code: '', foo: 'bar', id: '10259959195' },
routeName: 'list',
type: undefined,
},
],
},
],
},
],
});
});
test('Handles the navigate action with params and nested TabRouter', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = TabRouter({
@@ -1355,6 +1540,28 @@ describe('StackRouter', () => {
);
});
test('URI encoded string get passed to deep link', () => {
const uri = 'people/2018%2F02%2F07';
const action = TestStackRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
routeName: 'person',
params: {
id: '2018/02/07',
},
type: NavigationActions.NAVIGATE,
});
const malformedUri = 'people/%E0%A4%A';
const action2 = TestStackRouter.getActionForPathAndParams(malformedUri);
expect(action2).toEqual({
routeName: 'person',
params: {
id: '%E0%A4%A',
},
type: NavigationActions.NAVIGATE,
});
});
test('Querystring params get passed to nested deep link', () => {
// uri with two non-empty query param values
const uri = 'main/p/4/list/10259959195?code=test&foo=bar';

View File

@@ -175,23 +175,3 @@ test('should throw if the route does not exist', () => {
"There is no route defined for key Settings.\nMust be one of: 'Home'"
);
});
test('should throw if the screen is not defined under the route config', () => {
/* eslint-disable react/no-multi-comp */
const getScreenOptions = createConfigGetter({
Home: {},
});
const routes = [{ key: 'B', routeName: 'Home' }];
expect(() =>
getScreenOptions(
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
})
)
).toThrowError('Route Home must define a screen or a getScreen.');
});

View File

@@ -39,9 +39,7 @@ describe('validateRouteConfigMap', () => {
Home: {
screen: ProfileNavigator,
},
Chat: {
screen: ListScreen,
},
Chat: ListScreen,
};
validateRouteConfigMap(invalidMap);
});

View File

@@ -32,5 +32,5 @@ export default function getScreenForRouteName(routeConfigs, routeName) {
return screen;
}
throw new Error(`Route ${routeName} must define a screen or a getScreen.`);
return routeConfig;
}

View File

@@ -14,44 +14,37 @@ function validateRouteConfigMap(routeConfigs) {
routeNames.forEach(routeName => {
const routeConfig = routeConfigs[routeName];
if (!routeConfig.screen && !routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen. ` +
'For example:\n\n' +
"import MyScreen from './MyScreen';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyScreen,\n' +
'}'
);
} else if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
);
}
const screenComponent = routeConfig.screen
? routeConfig.screen
: routeConfig;
if (
routeConfig.screen &&
typeof routeConfig.screen !== 'function' &&
typeof routeConfig.screen !== 'string'
screenComponent &&
typeof screenComponent !== 'function' &&
typeof screenComponent !== 'string' &&
!routeConfig.getScreen
) {
throw new Error(
`The component for route '${routeName}' must be a ` +
'React component. For example:\n\n' +
"import MyScreen from './MyScreen';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyScreen,\n' +
`${routeName}: MyScreen,\n` +
'}\n\n' +
'You can also use a navigator:\n\n' +
"import MyNavigator from './MyNavigator';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyNavigator,\n' +
`${routeName}: MyNavigator,\n` +
'}'
);
}
if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
);
}
});
}

View File

@@ -85,7 +85,7 @@ class CardStack extends React.Component {
if (props.screenProps !== this.props.screenProps) {
this._screenDetails = {};
}
props.scenes.forEach(newScene => {
props.transitionProps.scenes.forEach(newScene => {
if (
this._screenDetails[newScene.key] &&
this._screenDetails[newScene.key].state !== newScene.route
@@ -96,7 +96,7 @@ class CardStack extends React.Component {
}
_getScreenDetails = scene => {
const { screenProps, navigation, router } = this.props;
const { screenProps, transitionProps: { navigation }, router } = this.props;
let screenDetails = this._screenDetails[scene.key];
if (!screenDetails || screenDetails.state !== scene.route) {
const screenNavigation = addNavigationHelpers({
@@ -131,12 +131,19 @@ class CardStack extends React.Component {
headerRightInterpolator,
} = this._getTransitionConfig();
const { mode, ...passProps } = this.props;
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
getScreenDetails: this._getScreenDetails,
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
@@ -153,22 +160,22 @@ class CardStack extends React.Component {
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.layout.width);
animatedSubscribeValue(props.layout.height);
animatedSubscribeValue(props.position);
animatedSubscribeValue(props.transitionProps.layout.width);
animatedSubscribeValue(props.transitionProps.layout.height);
animatedSubscribeValue(props.transitionProps.position);
}
_reset(resetToIndex, duration) {
Animated.timing(this.props.position, {
Animated.timing(this.props.transitionProps.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.position.__isNative,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
}
_goBack(backFromIndex, duration) {
const { navigation, position, scenes } = this.props;
const { navigation, position, scenes } = this.props.transitionProps;
const toValue = Math.max(backFromIndex - 1, 0);
// set temporary index for gesture handler to respect until the action is
@@ -198,9 +205,15 @@ class CardStack extends React.Component {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
floatingHeader = this._renderHeader(this.props.scene, headerMode);
floatingHeader = this._renderHeader(
this.props.transitionProps.scene,
headerMode
);
}
const { navigation, position, layout, scene, scenes, mode } = this.props;
const {
transitionProps: { navigation, position, layout, scene, scenes },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = this._getScreenDetails(scene);
@@ -363,6 +376,21 @@ class CardStack extends React.Component {
return 'float';
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
_renderInnerScene(SceneComponent, scene) {
const { navigation } = this._getScreenDetails(scene);
const { screenProps } = this.props;
@@ -395,8 +423,8 @@ class CardStack extends React.Component {
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
{},
{},
this.props.transitionProps,
this.props.prevTransitionProps,
isModal
);
};
@@ -404,15 +432,19 @@ class CardStack extends React.Component {
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator && screenInterpolator({ ...this.props, scene });
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
const SceneComponent = this.props.router.getComponentForRouteName(
scene.route.routeName
);
const { transitionProps, ...props } = this.props;
return (
<Card
{...this.props}
{...props}
{...transitionProps}
key={`card_${scene.key}`}
style={[style, this.props.cardStyle]}
scene={scene}

View File

@@ -53,10 +53,11 @@ class CardStackTransitioner extends React.Component {
return transitionSpec;
};
_render = props => {
_render = (props, prevProps) => {
const {
screenProps,
headerMode,
headerTransitionPreset,
mode,
router,
cardStyle,
@@ -66,11 +67,13 @@ class CardStackTransitioner extends React.Component {
<CardStack
screenProps={screenProps}
headerMode={headerMode}
headerTransitionPreset={headerTransitionPreset}
mode={mode}
router={router}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
{...props}
transitionProps={props}
prevTransitionProps={prevProps}
/>
);
};

View File

@@ -3,15 +3,18 @@ import React from 'react';
import {
Animated,
Dimensions,
Image,
Platform,
StyleSheet,
View,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import withOrientation from '../withOrientation';
@@ -19,9 +22,18 @@ const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad ? 32 : 44
: 56;
};
class Header extends React.PureComponent {
static defaultProps = {
leftInterpolator: HeaderStyleInterpolator.forLeft,
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
titleInterpolator: HeaderStyleInterpolator.forCenter,
rightInterpolator: HeaderStyleInterpolator.forRight,
};
@@ -116,15 +128,18 @@ class Header extends React.PureComponent {
_renderLeftComponent = props => {
const { options } = this.props.getScreenDetails(props.scene);
if (
React.isValidElement(options.headerLeft) ||
options.headerLeft === null
) {
return options.headerLeft;
}
if (props.scene.index === 0) {
return null;
return;
}
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
@@ -147,6 +162,36 @@ class Header extends React.PureComponent {
);
};
_renderModularLeftComponent = (
props,
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options } = this.props.getScreenDetails(props.scene);
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
return (
<ModularHeaderBackButton
onPress={this._navigateBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderRightComponent = props => {
const details = this.props.getScreenDetails(props.scene);
const { headerRight } = details.options;
@@ -154,16 +199,38 @@ class Header extends React.PureComponent {
};
_renderLeft(props) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
const { options } = this.props.getScreenDetails(props.scene);
const { transitionPreset } = this.props;
// On Android, or if we have a custom header left, or if we have a custom back image, we
// do not use the modular header (which is the one that imitates UINavigationController)
if (
transitionPreset !== 'uikit' ||
options.headerBackImage ||
options.headerLeft ||
options.headerLeft === null
) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
} else {
return this._renderModularSubView(
props,
'left',
this._renderModularLeftComponent,
this.props.leftLabelInterpolator,
this.props.leftButtonInterpolator
);
}
}
_renderTitle(props, options) {
const style = {};
const { transitionPreset } = this.props;
if (Platform.OS === 'android') {
if (!options.hasLeftComponent) {
@@ -185,7 +252,9 @@ class Header extends React.PureComponent {
{ ...props, style },
'title',
this._renderTitleComponent,
this.props.titleInterpolator
transitionPreset === 'uikit'
? this.props.titleFromLeftInterpolator
: this.props.titleInterpolator
);
}
@@ -198,6 +267,64 @@ class Header extends React.PureComponent {
);
}
_renderModularSubView(
props,
name,
renderer,
labelStyleInterpolator,
buttonStyleInterpolator
) {
const { scene } = props;
const { index, isStale, key } = scene;
// Never render a modular back button on the first screen in a stack.
if (index === 0) {
return;
}
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const ButtonContainer = ({ children }) => (
<Animated.View
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const LabelContainer = ({ children }) => (
<Animated.View
style={[labelStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const subView = renderer(props, ButtonContainer, LabelContainer);
if (subView === null) {
return subView;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<View
key={`${name}_${key}`}
pointerEvents={pointerEvents}
style={[styles.item, styles[name], props.style]}
>
{subView}
</View>
);
}
_renderSubView(props, name, renderer, styleInterpolator) {
const { scene } = props;
const { index, isStale, key } = scene;
@@ -246,16 +373,47 @@ class Header extends React.PureComponent {
hasRightComponent: !!right,
});
return (
<View
style={[StyleSheet.absoluteFill, styles.header]}
key={`scene_${props.scene.key}`}
>
{title}
{left}
{right}
</View>
);
const wrapperProps = {
style: [StyleSheet.absoluteFill, styles.header],
key: `scene_${props.scene.key}`,
};
const { isLandscape, transitionPreset } = this.props;
const { options } = this.props.getScreenDetails(props.scene);
if (
options.headerLeft ||
options.headerBackImage ||
Platform.OS !== 'ios' ||
transitionPreset !== 'uikit'
) {
return (
<View {...wrapperProps}>
{title}
{left}
{right}
</View>
);
} else {
return (
<MaskedViewIOS
{...wrapperProps}
maskElement={
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
/>
<View style={styles.iconMaskFillerRect} />
</View>
}
>
{title}
{left}
{right}
</MaskedViewIOS>
);
}
}
render() {
@@ -293,8 +451,7 @@ class Header extends React.PureComponent {
const { options } = this.props.getScreenDetails(scene);
const { headerStyle } = options;
const appBarHeight =
Platform.OS === 'ios' ? (isLandscape && !Platform.isPad ? 32 : 44) : 56;
const appBarHeight = getAppBarHeight(isLandscape);
const containerStyles = [
styles.container,
{
@@ -350,6 +507,25 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: 'transparent',
},
iconMaskContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
iconMaskFillerRect: {
flex: 1,
backgroundColor: '#d8d8d8',
marginLeft: -3,
},
iconMask: {
// These are mostly the same as the icon in ModularHeaderBackButton
height: 21,
width: 12,
marginLeft: 9,
marginTop: -0.5, // resizes down to 20.5
alignSelf: 'center',
resizeMode: 'contain',
},
title: {
bottom: 0,
left: TITLE_OFFSET,

View File

@@ -107,7 +107,7 @@ const styles = StyleSheet.create({
? {
height: 21,
width: 13,
marginLeft: 10,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
@@ -123,7 +123,7 @@ const styles = StyleSheet.create({
iconWithTitle:
Platform.OS === 'ios'
? {
marginRight: 5,
marginRight: 6,
}
: {},
});

View File

@@ -1,7 +1,11 @@
import { I18nManager } from 'react-native';
import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
const crossFadeInterpolation = (first, index, last) => ({
inputRange: [first, index - 0.75, index - 0.5, index, index + 0.5, last],
outputRange: [0, 0, 0.2, 1, 0.5, 0],
});
/**
* Utility that builds the style for the navigation header.
*
@@ -23,16 +27,7 @@ function forLeft(props) {
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0, 1, 0, 0],
}),
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -44,21 +39,9 @@ function forCenter(props) {
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index, last];
return {
opacity: position.interpolate({
inputRange,
outputRange: [0, 1, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange,
outputRange: I18nManager.isRTL ? [-200, 0, 200] : [200, 0, -200],
}),
},
],
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -70,16 +53,125 @@ function forRight(props) {
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
/**
* iOS UINavigationController style interpolators
*/
function forLeftButton(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [first, index, last],
outputRange: [0, 1, 0],
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0.5, 1, 0.5, 0],
}),
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. See the comment on title for more information.
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
function forLeftLabel(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const offset = LEFT_LABEL_OFFSET;
return {
// For now we fade out the label before fading in the title, so the
// differences between the label and title position can be hopefully not so
// noticable to the user
opacity: position.interpolate({
inputRange: [first, index - 0.35, index, index + 0.5, last],
outputRange: [0, 0, 1, 0.5, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset * 1.5],
}),
},
],
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. We want the back button label to transition
* smoothly into the title text and to do this we need to understand
* where the title is positioned within the title container (since it is
* centered).
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
function forCenterFromLeft(props) {
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index - 0.5, index, index + 0.5, last];
const offset = TITLE_OFFSET_IOS;
return {
opacity: position.interpolate({
inputRange: [first, index - 0.5, index, index + 0.7, last],
outputRange: [0, 0, 1, 0, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset],
}),
},
],
};
}
export default {
forLeft,
forLeftButton,
forLeftLabel,
forCenterFromLeft,
forCenter,
forRight,
};

View File

@@ -15,7 +15,7 @@ const HeaderTitle = ({ style, ...rest }) => (
const styles = StyleSheet.create({
title: {
fontSize: Platform.OS === 'ios' ? 17 : 20,
fontWeight: Platform.OS === 'ios' ? '600' : '500',
fontWeight: Platform.OS === 'ios' ? '700' : '500',
color: 'rgba(0, 0, 0, .9)',
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
marginHorizontal: 16,

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { I18nManager, Image, Text, View, StyleSheet } from 'react-native';
import TouchableItem from '../TouchableItem';
class ModularHeaderBackButton extends React.PureComponent {
static defaultProps = {
tintColor: '#037aff',
truncatedTitle: 'Back',
// eslint-disable-next-line global-require
buttonImage: require('../assets/back-icon.png'),
};
state = {};
_onTextLayout = e => {
if (this.state.initialTextWidth) {
return;
}
this.setState({
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
});
};
render() {
const {
buttonImage,
onPress,
width,
title,
titleStyle,
tintColor,
truncatedTitle,
} = this.props;
const renderTruncated =
this.state.initialTextWidth && width
? this.state.initialTextWidth > width
: false;
let backButtonTitle = renderTruncated ? truncatedTitle : title;
// TODO: When we've sorted out measuring in the header, let's revisit this.
// This is clearly a bad workaround.
if (backButtonTitle && backButtonTitle.length > 8) {
backButtonTitle = truncatedTitle;
}
const { ButtonContainerComponent, LabelContainerComponent } = this.props;
return (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={backButtonTitle}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
style={styles.container}
borderless
>
<View style={styles.container}>
<ButtonContainerComponent>
<Image
style={[
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
]}
source={buttonImage}
/>
</ButtonContainerComponent>
{typeof backButtonTitle === 'string' && (
<LabelContainerComponent>
<Text
onLayout={this._onTextLayout}
style={[
styles.title,
!!tintColor && { color: tintColor },
titleStyle,
]}
numberOfLines={1}
>
{backButtonTitle}
</Text>
</LabelContainerComponent>
)}
</View>
</TouchableItem>
);
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
backgroundColor: 'transparent',
},
title: {
fontSize: 17,
paddingRight: 10,
},
icon: {
height: 21,
width: 12,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
iconWithTitle: {
marginRight: 3,
},
});
export default ModularHeaderBackButton;

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
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 {
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,
};
}
componentWillMount() {
this._actionListener = this.props.navigation.addListener(
'action',
this._onAction
);
}
componentWillUnmount() {
this._actionListener.remove();
}
render() {
const { awake, visible } = this.state;
const {
childNavigation,
navigation,
removeClippedSubviews,
lazy,
...rest
} = this.props;
return (
<View
style={styles.container}
collapsable={false}
removeClippedSubviews={
Platform.OS === 'android'
? removeClippedSubviews
: !visible && removeClippedSubviews
}
>
<View
style={
this._mustAlwaysBeVisible() || visible
? styles.innerAttached
: styles.innerDetached
}
>
{awake ? <SceneView {...rest} navigation={childNavigation} /> : null}
</View>
</View>
);
}
_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({
container: {
flex: 1,
overflow: 'hidden',
},
innerAttached: {
flex: 1,
},
innerDetached: {
flex: 1,
top: FAR_FAR_AWAY,
},
});

View File

@@ -63,6 +63,7 @@ class TabBarBottom extends React.PureComponent {
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color },

View File

@@ -3,11 +3,13 @@ import { View, StyleSheet, Platform } from 'react-native';
import { TabViewAnimated, TabViewPagerPan } from 'react-native-tab-view';
import SafeAreaView from 'react-native-safe-area-view';
import SceneView from '../SceneView';
import ResourceSavingSceneView from '../ResourceSavingSceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
class TabView extends React.PureComponent {
static defaultProps = {
lazy: true,
removedClippedSubviews: true,
// fix for https://github.com/react-native-community/react-native-tab-view/issues/312
initialLayout: Platform.select({
android: { width: 1, height: 0 },
@@ -25,14 +27,18 @@ class TabView extends React.PureComponent {
const TabComponent = this.props.router.getComponentForRouteName(
route.routeName
);
return (
<View style={styles.page}>
<SceneView
screenProps={screenProps}
component={TabComponent}
navigation={childNavigation}
/>
</View>
<ResourceSavingSceneView
lazy={this.props.lazy}
removeClippedSubViews={this.props.removeClippedSubviews}
animationEnabled={this.props.animationEnabled}
swipeEnabled={this.props.swipeEnabled}
screenProps={screenProps}
component={TabComponent}
navigation={this.props.navigation}
childNavigation={childNavigation}
/>
);
};
@@ -140,11 +146,15 @@ class TabView extends React.PureComponent {
const tabBarVisible =
options.tabBarVisible == null ? true : options.tabBarVisible;
const swipeEnabled =
let swipeEnabled =
options.swipeEnabled == null
? this.props.swipeEnabled
: options.swipeEnabled;
if (typeof swipeEnabled === 'function') {
swipeEnabled = swipeEnabled(state);
}
if (tabBarComponent !== undefined && tabBarVisible) {
if (tabBarPosition === 'bottom') {
renderFooter = this._renderTabBar;
@@ -185,9 +195,4 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
page: {
flex: 1,
overflow: 'hidden',
},
});

View File

@@ -179,9 +179,19 @@ class Transitioner extends React.Component {
const prevTransitionProps = this._prevTransitionProps;
this._prevTransitionProps = null;
const scenes = this.state.scenes.filter(isSceneNotStale);
const nextState = {
...this.state,
scenes: this.state.scenes.filter(isSceneNotStale),
/**
* Array.prototype.filter creates a new instance of an array
* even if there were no elements removed. There are cases when
* `this.state.scenes` will have no stale scenes (typically when
* pushing a new route). As a result, components that rely on this prop
* might enter an unnecessary render cycle.
*/
scenes:
this.state.scenes.length === scenes.length ? this.state.scenes : scenes,
};
this._transitionProps = buildTransitionProps(this.props, nextState);

View File

@@ -124,6 +124,7 @@ exports[`TabBarBottom renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",
@@ -224,6 +225,8 @@ exports[`TabBarBottom renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
@@ -232,25 +235,34 @@ exports[`TabBarBottom renders successfully 1`] = `
}
>
<View
navigation={
style={
Object {
"addListener": [Function],
"dispatch": undefined,
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
"routeName": "s1",
},
"flex": 1,
}
}
screenProps={undefined}
/>
>
<View
navigation={
Object {
"addListener": [Function],
"dispatch": undefined,
"getParam": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
"routeName": "s1",
},
}
}
screenProps={undefined}
/>
</View>
</View>
</View>
</RCTScrollContentView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -3,16 +3,25 @@ import propTypes from 'prop-types';
import hoistStatics from 'hoist-non-react-statics';
export default function withNavigation(Component) {
const componentWithNavigation = (props, { navigation }) => (
<Component {...props} navigation={navigation} />
);
class ComponentWithNavigation extends React.Component {
static displayName = `withNavigation(${Component.displayName ||
Component.name})`;
const displayName = Component.displayName || Component.name;
componentWithNavigation.displayName = `withNavigation(${displayName})`;
static contextTypes = {
navigation: propTypes.object.isRequired,
};
componentWithNavigation.contextTypes = {
navigation: propTypes.object.isRequired,
};
render() {
const { navigation } = this.context;
return (
<Component
{...this.props}
navigation={navigation}
ref={this.props.onRef}
/>
);
}
}
return hoistStatics(componentWithNavigation, Component);
return hoistStatics(ComponentWithNavigation, Component);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import propTypes from 'prop-types';
import hoistStatics from 'hoist-non-react-statics';
import invariant from '../utils/invariant';
export default function withNavigationFocus(Component) {
class ComponentWithNavigationFocus extends React.Component {
static displayName = `withNavigationFocus(${Component.displayName ||
Component.name})`;
static contextTypes = {
navigation: propTypes.object.isRequired,
};
state = {
isFocused: false,
};
componentDidMount() {
const navigation = this.getNavigation();
this.subscriptions = [
navigation.addListener('didFocus', () =>
this.setState({ isFocused: true })
),
navigation.addListener('willBlur', () =>
this.setState({ isFocused: false })
),
];
}
componentWillUnmount() {
this.subscriptions.forEach(sub => sub.remove());
}
getNavigation = () => {
const navigation = this.props.navigation || this.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.'
);
return navigation;
};
render() {
return (
<Component
{...this.props}
isFocused={this.state.isFocused}
ref={this.props.onRef}
/>
);
}
}
return hoistStatics(ComponentWithNavigationFocus, Component);
}

312
yarn.lock
View File

@@ -52,7 +52,7 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
acorn@^5.0.0, acorn@^5.3.0, acorn@^5.4.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
@@ -363,8 +363,8 @@ babel-eslint@^7.2.3:
babylon "^6.17.0"
babel-generator@^6.18.0, babel-generator@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
version "6.26.1"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
dependencies:
babel-messages "^6.23.0"
babel-runtime "^6.26.0"
@@ -372,7 +372,7 @@ babel-generator@^6.18.0, babel-generator@^6.26.0:
detect-indent "^4.0.0"
jsesc "^1.3.0"
lodash "^4.17.4"
source-map "^0.5.6"
source-map "^0.5.7"
trim-right "^1.0.1"
babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
@@ -492,12 +492,12 @@ babel-jest@^20.0.3:
babel-plugin-istanbul "^4.0.0"
babel-preset-jest "^20.0.3"
babel-jest@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.1.0.tgz#7fae6f655fffe77e818a8c2868c754a42463fdfd"
babel-jest@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.2.2.tgz#eda38dca284e32cc5257f96a9b51351975de4e04"
dependencies:
babel-plugin-istanbul "^4.1.5"
babel-preset-jest "^22.1.0"
babel-preset-jest "^22.2.0"
babel-messages@^6.23.0:
version "6.23.0"
@@ -529,9 +529,9 @@ babel-plugin-jest-hoist@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767"
babel-plugin-jest-hoist@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.1.0.tgz#c1281dd7887d77a1711dc760468c3b8285dde9ee"
babel-plugin-jest-hoist@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.2.0.tgz#bd34f39d652406669713b8c89e23ef25c890b993"
babel-plugin-react-transform@2.0.2:
version "2.0.2"
@@ -860,11 +860,11 @@ babel-preset-jest@^20.0.3:
dependencies:
babel-plugin-jest-hoist "^20.0.3"
babel-preset-jest@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.1.0.tgz#ff4e704102f9642765e2254226050561d8942ec9"
babel-preset-jest@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.2.0.tgz#f77b43f06ef4d8547214b2e206cc76a25c3ba0e2"
dependencies:
babel-plugin-jest-hoist "^22.1.0"
babel-plugin-jest-hoist "^22.2.0"
babel-plugin-syntax-object-rest-spread "^6.13.0"
babel-preset-react-native@^2.1.0:
@@ -1095,8 +1095,8 @@ bplist-parser@0.1.1:
big-integer "^1.6.7"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -1325,9 +1325,9 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
commander@^2.11.0, commander@^2.9.0, commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
commander@^2.11.0, commander@^2.9.0, commander@~2.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
common-tags@^1.4.0:
version "1.7.2"
@@ -1852,8 +1852,8 @@ eslint-visitor-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
eslint@^4.2.0, eslint@^4.5.0:
version "4.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1"
version "4.17.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.17.0.tgz#dc24bb51ede48df629be7031c71d9dc0ee4f3ddf"
dependencies:
ajv "^5.3.0"
babel-code-frame "^6.22.0"
@@ -1894,10 +1894,10 @@ eslint@^4.2.0, eslint@^4.5.0:
text-table "~0.2.0"
espree@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
version "3.5.3"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6"
dependencies:
acorn "^5.2.1"
acorn "^5.4.0"
acorn-jsx "^3.0.0"
esprima@^3.1.3:
@@ -1938,8 +1938,8 @@ event-target-shim@^1.0.5:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-1.1.1.tgz#a86e5ee6bdaa16054475da797ccddf0c55698491"
eventemitter3@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.0.0.tgz#fc29ecf233bd19fbd527bb4089bbf665dc90c1e3"
version "3.0.1"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.0.1.tgz#4ce66c3fc5b5a6b9f2245e359e1938f1ab10f960"
exec-sh@^0.2.0:
version "0.2.1"
@@ -1991,15 +1991,15 @@ expand-range@^1.8.1:
dependencies:
fill-range "^2.1.0"
expect@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.1.0.tgz#f8f9b019ab275d859cbefed531fbaefe8972431d"
expect@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-22.2.2.tgz#6cb6ae2eeb651a4187b9096de70333a018fab63f"
dependencies:
ansi-styles "^3.2.0"
jest-diff "^22.1.0"
jest-get-type "^22.1.0"
jest-matcher-utils "^22.1.0"
jest-message-util "^22.1.0"
jest-matcher-utils "^22.2.0"
jest-message-util "^22.2.0"
jest-regex-util "^22.1.0"
express-session@~1.11.3:
@@ -2916,15 +2916,15 @@ istanbul-reports@^1.1.3:
dependencies:
handlebars "^4.0.3"
jest-changed-files@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.1.4.tgz#1f7844bcb739dec07e5899a633c0cb6d5069834e"
jest-changed-files@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.2.0.tgz#517610c4a8ca0925bdc88b0ca53bd678aa8d019e"
dependencies:
throat "^4.0.0"
jest-cli@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.1.4.tgz#0fe9f3ac881b0cdc00227114c58583a2ebefcc04"
jest-cli@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.2.2.tgz#4431a93a29549da5dcb6d4a41dd03503c9198cd6"
dependencies:
ansi-escapes "^3.0.0"
chalk "^2.0.1"
@@ -2937,21 +2937,21 @@ jest-cli@^22.1.4:
istanbul-lib-coverage "^1.1.1"
istanbul-lib-instrument "^1.8.0"
istanbul-lib-source-maps "^1.2.1"
jest-changed-files "^22.1.4"
jest-config "^22.1.4"
jest-environment-jsdom "^22.1.4"
jest-changed-files "^22.2.0"
jest-config "^22.2.2"
jest-environment-jsdom "^22.2.2"
jest-get-type "^22.1.0"
jest-haste-map "^22.1.0"
jest-message-util "^22.1.0"
jest-haste-map "^22.2.2"
jest-message-util "^22.2.0"
jest-regex-util "^22.1.0"
jest-resolve-dependencies "^22.1.0"
jest-runner "^22.1.4"
jest-runtime "^22.1.4"
jest-snapshot "^22.1.2"
jest-util "^22.1.4"
jest-worker "^22.1.0"
jest-runner "^22.2.2"
jest-runtime "^22.2.2"
jest-snapshot "^22.2.0"
jest-util "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
node-notifier "^5.1.2"
node-notifier "^5.2.1"
realpath-native "^1.0.0"
rimraf "^2.5.4"
slash "^1.0.0"
@@ -2960,20 +2960,20 @@ jest-cli@^22.1.4:
which "^1.2.12"
yargs "^10.0.3"
jest-config@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.1.4.tgz#075ffacce83c3e38cf85b1b9ba0d21bd3ee27ad0"
jest-config@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.2.2.tgz#6b8ed615bc51239847d15460086f174dad4a7015"
dependencies:
chalk "^2.0.1"
glob "^7.1.1"
jest-environment-jsdom "^22.1.4"
jest-environment-node "^22.1.4"
jest-environment-jsdom "^22.2.2"
jest-environment-node "^22.2.2"
jest-get-type "^22.1.0"
jest-jasmine2 "^22.1.4"
jest-jasmine2 "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.1.4"
jest-util "^22.1.4"
jest-validate "^22.1.2"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
jest-validate "^22.2.2"
pretty-format "^22.1.0"
jest-diff@^22.1.0:
@@ -2985,7 +2985,7 @@ jest-diff@^22.1.0:
jest-get-type "^22.1.0"
pretty-format "^22.1.0"
jest-docblock@22.1.0, jest-docblock@^22.1.0:
jest-docblock@22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.1.0.tgz#3fe5986d5444cbcb149746eb4b07c57c5a464dfd"
dependencies:
@@ -2995,20 +2995,26 @@ jest-docblock@^21.0.0:
version "21.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
jest-environment-jsdom@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.1.4.tgz#704518ce8375f7ec5de048d1e9c4268b08a03e00"
jest-docblock@^22.1.0, jest-docblock@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.2.2.tgz#617f13edb16ec64202002b3c336cd14ae36c0631"
dependencies:
jest-mock "^22.1.0"
jest-util "^22.1.4"
detect-newline "^2.1.0"
jest-environment-jsdom@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.2.2.tgz#3513ccdccc2bc41daf9cdee199b7069b0d9feebc"
dependencies:
jest-mock "^22.2.0"
jest-util "^22.2.2"
jsdom "^11.5.1"
jest-environment-node@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.1.4.tgz#0f2946e8f8686ce6c5d8fa280ce1cd8d58e869eb"
jest-environment-node@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.2.2.tgz#570896eef2dd0f939c71bd5712ef4321958c1270"
dependencies:
jest-mock "^22.1.0"
jest-util "^22.1.4"
jest-mock "^22.2.0"
jest-util "^22.2.2"
jest-get-type@^21.2.0:
version "21.2.0"
@@ -3018,7 +3024,7 @@ jest-get-type@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.1.0.tgz#4e90af298ed6181edc85d2da500dbd2753e0d5a9"
jest-haste-map@22.1.0, jest-haste-map@^22.1.0:
jest-haste-map@22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.1.0.tgz#1174c6ff393f9818ebf1163710d8868b5370da2a"
dependencies:
@@ -3029,20 +3035,31 @@ jest-haste-map@22.1.0, jest-haste-map@^22.1.0:
micromatch "^2.3.11"
sane "^2.0.0"
jest-jasmine2@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.1.4.tgz#cada0baf50a220c616a9575728b80d4ddedebe8b"
jest-haste-map@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.2.2.tgz#9d3d5a14bd5e05ab9176979f2a5fbb4ddc80eb20"
dependencies:
fb-watchman "^2.0.0"
graceful-fs "^4.1.11"
jest-docblock "^22.2.2"
jest-worker "^22.2.2"
micromatch "^2.3.11"
sane "^2.0.0"
jest-jasmine2@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.2.2.tgz#9065255c8f635ae9dfa33fc66068f59adf53c9aa"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
co "^4.6.0"
expect "^22.1.0"
expect "^22.2.2"
graceful-fs "^4.1.11"
is-generator-fn "^1.0.0"
jest-diff "^22.1.0"
jest-matcher-utils "^22.1.0"
jest-message-util "^22.1.0"
jest-snapshot "^22.1.2"
jest-matcher-utils "^22.2.0"
jest-message-util "^22.2.0"
jest-snapshot "^22.2.0"
source-map-support "^0.5.0"
jest-leak-detector@^22.1.0:
@@ -3051,17 +3068,17 @@ jest-leak-detector@^22.1.0:
dependencies:
pretty-format "^22.1.0"
jest-matcher-utils@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.1.0.tgz#e164665b5d313636ac29f7f6fe9ef0a6ce04febc"
jest-matcher-utils@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.2.0.tgz#5390f823c18c748543d463825aa8e4df0db253ca"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
pretty-format "^22.1.0"
jest-message-util@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.1.0.tgz#51ba0794cb6e579bfc4e9adfac452f9f1a0293fc"
jest-message-util@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.2.0.tgz#84a6bb34186d8b9af7e0732fabbef63f7355f7b2"
dependencies:
"@babel/code-frame" "^7.0.0-beta.35"
chalk "^2.0.1"
@@ -3069,9 +3086,9 @@ jest-message-util@^22.1.0:
slash "^1.0.0"
stack-utils "^1.0.1"
jest-mock@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.1.0.tgz#87ec21c0599325671c9a23ad0e05c86fb5879b61"
jest-mock@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.2.0.tgz#444b3f9488a7473adae09bc8a77294afded397a7"
jest-regex-util@^22.1.0:
version "22.1.0"
@@ -3083,45 +3100,45 @@ jest-resolve-dependencies@^22.1.0:
dependencies:
jest-regex-util "^22.1.0"
jest-resolve@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.1.4.tgz#72b9b371eaac48f84aad4ad732222ffe37692602"
jest-resolve@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.2.2.tgz#6f49d91e3680c86a4d5e5f72ccdab3996d1cbc19"
dependencies:
browser-resolve "^1.11.2"
chalk "^2.0.1"
jest-runner@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.1.4.tgz#e039039110cb1b31febc0f99e349bf7c94304a2f"
jest-runner@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.2.2.tgz#17fff27a61b63b58cf104c9cdcc0fdfccd3878ce"
dependencies:
exit "^0.1.2"
jest-config "^22.1.4"
jest-docblock "^22.1.0"
jest-haste-map "^22.1.0"
jest-jasmine2 "^22.1.4"
jest-config "^22.2.2"
jest-docblock "^22.2.2"
jest-haste-map "^22.2.2"
jest-jasmine2 "^22.2.2"
jest-leak-detector "^22.1.0"
jest-message-util "^22.1.0"
jest-runtime "^22.1.4"
jest-util "^22.1.4"
jest-worker "^22.1.0"
jest-message-util "^22.2.0"
jest-runtime "^22.2.2"
jest-util "^22.2.2"
jest-worker "^22.2.2"
throat "^4.0.0"
jest-runtime@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.1.4.tgz#1474d9f5cda518b702e0b25a17d4ef3fc563a20c"
jest-runtime@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.2.2.tgz#256d0efb65deae1c23b819d88cec5ab43d7a4ed6"
dependencies:
babel-core "^6.0.0"
babel-jest "^22.1.0"
babel-jest "^22.2.2"
babel-plugin-istanbul "^4.1.5"
chalk "^2.0.1"
convert-source-map "^1.4.0"
exit "^0.1.2"
graceful-fs "^4.1.11"
jest-config "^22.1.4"
jest-haste-map "^22.1.0"
jest-config "^22.2.2"
jest-haste-map "^22.2.2"
jest-regex-util "^22.1.0"
jest-resolve "^22.1.4"
jest-util "^22.1.4"
jest-resolve "^22.2.2"
jest-util "^22.2.2"
json-stable-stringify "^1.0.1"
micromatch "^2.3.11"
realpath-native "^1.0.0"
@@ -3130,27 +3147,27 @@ jest-runtime@^22.1.4:
write-file-atomic "^2.1.0"
yargs "^10.0.3"
jest-snapshot@^22.1.2:
version "22.1.2"
resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.1.2.tgz#b270cf6e3098f33aceeafda02b13eb0933dc6139"
jest-snapshot@^22.2.0:
version "22.2.0"
resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.2.0.tgz#0c0ba152d296ef70fa198cc84977a2cc269ee4cf"
dependencies:
chalk "^2.0.1"
jest-diff "^22.1.0"
jest-matcher-utils "^22.1.0"
jest-matcher-utils "^22.2.0"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
pretty-format "^22.1.0"
jest-util@^22.1.4:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.1.4.tgz#ac8cbd43ee654102f1941f3f0e9d1d789a8b6a9b"
jest-util@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.2.2.tgz#335484b6aeae0c5a1ae498401630324977fe3465"
dependencies:
callsites "^2.0.0"
chalk "^2.0.1"
graceful-fs "^4.1.11"
is-ci "^1.0.10"
jest-message-util "^22.1.0"
jest-validate "^22.1.2"
jest-message-util "^22.2.0"
jest-validate "^22.2.2"
mkdirp "^0.5.1"
jest-validate@^21.1.0:
@@ -3162,26 +3179,33 @@ jest-validate@^21.1.0:
leven "^2.1.0"
pretty-format "^21.2.1"
jest-validate@^22.1.2:
version "22.1.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.1.2.tgz#c3b06bcba7bd9a850919fe336b5f2a8c3a239404"
jest-validate@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.2.2.tgz#9cdce422c93cc28395e907ac6bbc929158d9a6ba"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
leven "^2.1.0"
pretty-format "^22.1.0"
jest-worker@22.1.0, jest-worker@^22.1.0:
jest-worker@22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.1.0.tgz#0987832fe58fbdc205357f4c19b992446368cafb"
dependencies:
merge-stream "^1.0.1"
jest@^22.1.3:
version "22.1.4"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.1.4.tgz#9ec71373a38f40ff92a3e5e96ae85687c181bb72"
jest-worker@^22.1.0, jest-worker@^22.2.2:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.2.2.tgz#c1f5dc39976884b81f68ec50cb8532b2cbab3390"
dependencies:
jest-cli "^22.1.4"
merge-stream "^1.0.1"
jest@^22.1.3:
version "22.2.2"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.2.2.tgz#26aca0f5e4eaa76d52f2792b14033a3d1e7be2bd"
dependencies:
import-local "^1.0.0"
jest-cli "^22.2.2"
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
@@ -3495,8 +3519,8 @@ lodash.keys@^3.0.0:
lodash.isarray "^3.0.0"
lodash.merge@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
lodash.pad@^4.1.0:
version "4.5.1"
@@ -3548,8 +3572,8 @@ lodash@^3.5.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.6.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
log-symbols@^1.0.2:
version "1.0.2"
@@ -3853,7 +3877,7 @@ node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
node-notifier@^5.1.2:
node-notifier@^5.1.2, node-notifier@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
dependencies:
@@ -4299,9 +4323,9 @@ private@^0.1.6, private@^0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
process@~0.5.1:
version "0.5.2"
@@ -4561,13 +4585,13 @@ read-pkg@^2.0.0:
path-type "^2.0.0"
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
@@ -4874,8 +4898,8 @@ safe-buffer@~5.0.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
sane@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.3.0.tgz#3f3df584abf69e63d4bb74f0f8c42468e4d7d46b"
version "2.4.1"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.4.1.tgz#29f991208cf28636720efdc584293e7fd66663a5"
dependencies:
anymatch "^1.3.0"
exec-sh "^0.2.0"
@@ -5044,7 +5068,7 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6:
source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -5163,8 +5187,8 @@ string_decoder@~1.0.3:
safe-buffer "~5.1.0"
stringify-object@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
version "3.2.2"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd"
dependencies:
get-own-enumerable-property-symbols "^2.0.1"
is-obj "^1.0.1"
@@ -5374,10 +5398,10 @@ ua-parser-js@^0.7.9:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
uglify-es@^3.1.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
version "3.3.10"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.10.tgz#8b0b7992cebe20edc26de1bf325cef797b8f3fa5"
dependencies:
commander "~2.13.0"
commander "~2.14.1"
source-map "~0.6.1"
uglify-js@^2.6: