Compare commits

..

58 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
Brent Vatne
c0c5c86f63 Bump to patch version 1.0.2 2018-02-08 12:49:04 -08:00
Brent Vatne
4388b6403c Remove console logs from published version of navigation playground 2018-02-08 12:48:28 -08:00
Brent Vatne
2cb740fbf6 Only initialize the CardStack PanResponder if gestures are enabled 2018-02-08 12:36:26 -08:00
Brent Vatne
ac741a703b Remove extra scene in floating header if it hasn't been evicted due to transition yet 2018-02-08 12:32:09 -08:00
Brent Vatne
5641b42975 Revert "StackRouter block actions while transitioning (#3469)"
This reverts commit 858a0d7a53.
2018-02-08 11:49:25 -08:00
Brent Vatne
ea19ceaa6a Bump to minor version 1.0.1 2018-02-08 10:47:48 -08:00
Brent Vatne
57ae6e4736 Make TabRouter handle COMPLETE_TRANSITION in a child router without switching active index (#3473) 2018-02-08 10:46:12 -08:00
Eric Vicenti
858a0d7a53 StackRouter block actions while transitioning (#3469)
The most straightforward fix for two issues is to block all navigation actions while mid-transition of a stack navigator. This will fix:

The double-navigate on double tap issue, because the first navigation will start the transition and the second action will be ignored.

Will fix the buggy header experience that you can see when going back and forward to a different route quickly. This happens because the next navigate action happens before the completion action. After the fix, the navigate action will be ignored, the user will tap again, and will see a good transition
2018-02-08 09:02:47 -08:00
Dave Pack
cf36f22e68 Sync and switch SafeAreaView with standalone (#3452)
* add react-native-safe-area-view npm package

* remove local SafeAreaView, import from package in views

* update to latest react-native-safe-area-view

* update snapshots
2018-02-07 17:32:06 -08:00
Brent Vatne
7385c244b7 Add custom back button example 2018-02-07 10:42:06 -08:00
59 changed files with 2609 additions and 1324 deletions

View File

@@ -6,6 +6,11 @@
"development": {
"plugins": [
"transform-react-jsx-source"
],
},
"production": {
"plugins": [
"transform-remove-console"
]
}
}

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

@@ -9,7 +9,7 @@ import type {
import * as React from 'react';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
import { StackNavigator, SafeAreaView, withNavigation } from 'react-navigation';
import SampleText from './SampleText';
type MyNavScreenProps = {
@@ -17,6 +17,18 @@ type MyNavScreenProps = {
banner: React.Node,
};
class MyBackButton extends React.Component<any, any> {
render() {
return <Button onPress={this._navigateBack} title="Custom Back" />;
}
_navigateBack = () => {
this.props.navigation.goBack(null);
};
}
const MyBackButtonWithNavigation = withNavigation(MyBackButton);
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
@@ -94,6 +106,7 @@ type MyPhotosScreenProps = {
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
headerLeft: <MyBackButtonWithNavigation />,
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;
@@ -165,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

@@ -17,6 +17,7 @@
"react-navigation": "link:../.."
},
"devDependencies": {
"babel-plugin-transform-remove-console": "^6.9.0",
"babel-jest": "^21.0.0",
"flow-bin": "^0.61.0",
"jest": "^21.0.1",

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,4 @@
## Usage
Please see the [Contributors Guide](https://reactnavigation.org/docs/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.0",
"version": "1.1.2",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -33,6 +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.7.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {
@@ -71,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

@@ -1,5 +1,6 @@
import React from 'react';
import { Dimensions, Platform, ScrollView } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
@@ -7,7 +8,6 @@ import TabRouter from '../routers/TabRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
import SafeAreaView from '../views/SafeAreaView';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)

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

@@ -140,6 +140,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"paddingBottom": 0,
@@ -187,6 +188,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "rgba(0, 0, 0, .04)",

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,12 +131,15 @@ 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}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -183,11 +190,6 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"position": "absolute",
"right": 70,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
@@ -203,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",
}
@@ -317,6 +319,7 @@ exports[`StackNavigator renders successfully 1`] = `
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
headerTransitionPreset={undefined}
index={0}
layout={
Object {
@@ -327,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],
@@ -346,7 +352,7 @@ exports[`StackNavigator renders successfully 1`] = `
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-0",
"key": "id-0-0",
"routeName": "Home",
},
],
@@ -365,12 +371,15 @@ exports[`StackNavigator renders successfully 1`] = `
"getStateForAction": [Function],
}
}
titleFromLeftInterpolator={[Function]}
titleInterpolator={[Function]}
transitionConfig={undefined}
transitionPreset="fade-in-place"
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -421,11 +430,6 @@ exports[`StackNavigator renders successfully 1`] = `
"position": "absolute",
"right": 0,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
@@ -441,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
@@ -71,6 +81,7 @@ exports[`TabNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -167,6 +178,7 @@ exports[`TabNavigator renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",

View File

@@ -51,7 +51,7 @@ module.exports = {
return require('./views/CardStack/Card').default;
},
get SafeAreaView() {
return require('./views/SafeAreaView').default;
return require('react-native-safe-area-view').default;
},
// Header
@@ -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

@@ -6,6 +6,13 @@ import NavigationActions from '../NavigationActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
function childrenUpdateWithoutSwitchingIndex(actionType) {
return [
NavigationActions.SET_PARAMS,
NavigationActions.COMPLETE_TRANSITION,
].includes(actionType);
}
export default (routeConfigs, config = {}) => {
// Fail fast on invalid route definitions
validateRouteConfigMap(routeConfigs);
@@ -23,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) {
@@ -213,9 +221,13 @@ export default (routeConfigs, config = {}) => {
});
// console.log(`${order.join('-')}: Processed other tabs:`, {lastIndex: state.index, index});
// keep active tab index if action type is SET_PARAMS
index =
action.type === NavigationActions.SET_PARAMS ? state.index : index;
// Nested routers can be updated after switching tabs with actions such as SET_PARAMS
// and COMPLETE_TRANSITION.
// NOTE: This may be problematic with custom routers because we whitelist the actions
// that can be handled by child routers without automatically changing index.
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
index = state.index;
}
if (index !== state.index || routes !== state.routes) {
return {
@@ -228,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,13 +7,18 @@ import TabRouter from '../TabRouter';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
_TESTING_ONLY_normalize_keys();
});
const ROUTERS = {
TabRouter,
StackRouter,
};
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
const dummyEventSubscriber = (name, handler) => ({
remove: () => {},
});
@@ -105,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-0');
expect(state2.routes[0].key).toEqual('Init-id-0-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(
@@ -134,7 +139,7 @@ test('Handles deep action', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0-2',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -174,8 +179,8 @@ test('Supports lazily-evaluated getScreen', () => {
immediate: true,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0-4');
expect(state2.routes[0].key).toEqual('Init-id-0-5');
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(
@@ -188,3 +193,60 @@ test('Supports lazily-evaluated getScreen', () => {
);
expect(state2).toEqual(state3);
});
test('Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION or SET_PARAMS', () => {
const FooStackNavigator = () => <div />;
const BarView = () => <div />;
FooStackNavigator.router = StackRouter({
Foo: {
screen: BarView,
},
Bar: {
screen: BarView,
},
});
const TestRouter = TabRouter({
Zap: { screen: FooStackNavigator },
Zoo: { screen: FooStackNavigator },
});
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
// Navigate to the second screen in the first tab
const state2 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state1
);
// Switch tabs
const state3 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Zoo',
},
state2
);
const stateAfterCompleteTransition = TestRouter.getStateForAction(
{
type: NavigationActions.COMPLETE_TRANSITION,
key: state2.routes[0].key,
},
state3
);
const stateAfterSetParams = TestRouter.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
key: state1.routes[0].routes[0].key,
params: { key: 'value' },
},
state3
);
expect(stateAfterCompleteTransition.index).toEqual(1);
expect(stateAfterSetParams.index).toEqual(1);
});

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,140 +205,151 @@ 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);
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const responder = PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = this._getScreenDetails(scene).options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
return false;
}
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
const isOnFirstCard = immediateIndex === 0;
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
onPanResponderMove: (event, gesture) => {
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
onPanResponderRelease: (event, gesture) => {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const movementDirection = gestureDirectionInverted ? -1 : 1;
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
const defaultVelocity = axisDistance / ANIMATION_DURATION;
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run stopAnimation:
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const responder = !gesturesEnabled
? null
: PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = this._getScreenDetails(scene).options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
return false;
}
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
const isOnFirstCard = immediateIndex === 0;
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
onPanResponderMove: (event, gesture) => {
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
onPanResponderRelease: (event, gesture) => {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const movementDirection = gestureDirectionInverted ? -1 : 1;
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
const defaultVelocity = axisDistance / ANIMATION_DURATION;
const velocity = Math.max(
Math.abs(gestureVelocity),
defaultVelocity
);
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run stopAnimation:
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
styles.container,
@@ -358,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;
@@ -390,8 +423,8 @@ class CardStack extends React.Component {
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
{},
{},
this.props.transitionProps,
this.props.prevTransitionProps,
isModal
);
};
@@ -399,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

@@ -1,7 +1,7 @@
import React from 'react';
import { View, Text, Platform, StyleSheet } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import SafeAreaView from '../SafeAreaView';
import TouchableItem from '../TouchableItem';
/**

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import withCachedChildNavigation from '../../withCachedChildNavigation';
import NavigationActions from '../../NavigationActions';
import invariant from '../../utils/invariant';
import SafeAreaView from '../SafeAreaView';
/**
* Component that renders the sidebar screen of the drawer.
*/

View File

@@ -3,25 +3,37 @@ 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 SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';
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,23 +373,58 @@ 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() {
let appBar;
if (this.props.mode === 'float') {
const scenesProps = this.props.scenes.map(scene => ({
const scenesByIndex = {};
this.props.scenes.forEach(scene => {
scenesByIndex[scene.index] = scene;
});
const scenesProps = Object.values(scenesByIndex).map(scene => ({
position: this.props.position,
progress: this.props.progress,
scene,
@@ -289,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,
{
@@ -346,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

@@ -1,316 +0,0 @@
import React, { Component } from 'react';
import {
DeviceInfo,
Dimensions,
InteractionManager,
NativeModules,
Platform,
StyleSheet,
Animated,
} from 'react-native';
import withOrientation from './withOrientation';
// See https://mydevice.io/devices/ for device dimensions
const X_WIDTH = 375;
const X_HEIGHT = 812;
const PAD_WIDTH = 768;
const PAD_HEIGHT = 1024;
const { height: D_HEIGHT, width: D_WIDTH } = Dimensions.get('window');
const { PlatformConstants = {} } = NativeModules;
const { minor = 0 } = PlatformConstants.reactNativeVersion || {};
const isIPhoneX = (() => {
if (Platform.OS === 'web') return false;
if (minor >= 50) {
return DeviceInfo.isIPhoneX_deprecated;
}
return (
Platform.OS === 'ios' &&
((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||
(D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT))
);
})();
const isIPad = (() => {
if (Platform.OS !== 'ios' || isIPhoneX) return false;
// if portrait and width is smaller than iPad width
if (D_HEIGHT > D_WIDTH && D_WIDTH < PAD_WIDTH) {
return false;
}
// if landscape and height is smaller that iPad height
if (D_WIDTH > D_HEIGHT && D_HEIGHT < PAD_WIDTH) {
return false;
}
return true;
})();
let _customStatusBarHeight = null;
const statusBarHeight = isLandscape => {
if (_customStatusBarHeight !== null) {
return _customStatusBarHeight;
}
/**
* This is a temporary workaround because we don't have a way to detect
* if the status bar is translucent or opaque. If opaque, we don't need to
* factor in the height here; if translucent (content renders under it) then
* we do.
*/
if (Platform.OS === 'android') {
if (global.Expo) {
return global.Expo.Constants.statusBarHeight;
} else {
return 0;
}
}
if (isIPhoneX) {
return isLandscape ? 0 : 44;
}
if (isIPad) {
return 20;
}
return isLandscape ? 0 : 20;
};
const doubleFromPercentString = percent => {
if (!percent.includes('%')) {
return 0;
}
const dbl = parseFloat(percent) / 100;
if (isNaN(dbl)) return 0;
return dbl;
};
class SafeView extends Component {
static setStatusBarHeight = height => {
_customStatusBarHeight = height;
};
state = {
touchesTop: true,
touchesBottom: true,
touchesLeft: true,
touchesRight: true,
orientation: null,
viewWidth: 0,
viewHeight: 0,
};
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this._onLayout();
});
}
componentWillReceiveProps() {
this._onLayout();
}
render() {
const { forceInset = false, isLandscape, children, style } = this.props;
const safeAreaStyle = this._getSafeAreaStyle();
return (
<Animated.View
ref={c => (this.view = c)}
onLayout={this._onLayout}
style={safeAreaStyle}
>
{this.props.children}
</Animated.View>
);
}
_onLayout = () => {
if (!this.view) return;
const { isLandscape } = this.props;
const { orientation } = this.state;
const newOrientation = isLandscape ? 'landscape' : 'portrait';
if (orientation && orientation === newOrientation) {
return;
}
const WIDTH = isLandscape ? X_HEIGHT : X_WIDTH;
const HEIGHT = isLandscape ? X_WIDTH : X_HEIGHT;
this.view._component.measureInWindow((winX, winY, winWidth, winHeight) => {
let realY = winY;
let realX = winX;
if (realY >= HEIGHT) {
realY = realY % HEIGHT;
} else if (realY < 0) {
realY = realY % HEIGHT + HEIGHT;
}
if (realX >= WIDTH) {
realX = realX % WIDTH;
} else if (realX < 0) {
realX = realX % WIDTH + WIDTH;
}
const touchesTop = realY === 0;
const touchesBottom = realY + winHeight >= HEIGHT;
const touchesLeft = realX === 0;
const touchesRight = realX + winWidth >= WIDTH;
this.setState({
touchesTop,
touchesBottom,
touchesLeft,
touchesRight,
orientation: newOrientation,
viewWidth: winWidth,
viewHeight: winHeight,
});
});
};
_getSafeAreaStyle = () => {
const { touchesTop, touchesBottom, touchesLeft, touchesRight } = this.state;
const { forceInset, isLandscape } = this.props;
const {
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
viewStyle,
} = this._getViewStyles();
const style = {
...viewStyle,
paddingTop: touchesTop ? this._getInset('top') : 0,
paddingBottom: touchesBottom ? this._getInset('bottom') : 0,
paddingLeft: touchesLeft ? this._getInset('left') : 0,
paddingRight: touchesRight ? this._getInset('right') : 0,
};
if (forceInset) {
Object.keys(forceInset).forEach(key => {
let inset = forceInset[key];
if (inset === 'always') {
inset = this._getInset(key);
}
if (inset === 'never') {
inset = 0;
}
switch (key) {
case 'horizontal': {
style.paddingLeft = inset;
style.paddingRight = inset;
break;
}
case 'vertical': {
style.paddingTop = inset;
style.paddingBottom = inset;
break;
}
case 'left':
case 'right':
case 'top':
case 'bottom': {
const padding = `padding${key[0].toUpperCase()}${key.slice(1)}`;
style[padding] = inset;
break;
}
}
});
}
// new height/width should only include padding from insets
// height/width should not be affected by padding from style obj
if (style.height && typeof style.height === 'number') {
style.height += style.paddingTop + style.paddingBottom;
}
if (style.width && typeof style.width === 'number') {
style.width += style.paddingLeft + style.paddingRight;
}
style.paddingTop = Math.max(style.paddingTop, paddingTop);
style.paddingBottom = Math.max(style.paddingBottom, paddingBottom);
style.paddingLeft = Math.max(style.paddingLeft, paddingLeft);
style.paddingRight = Math.max(style.paddingRight, paddingRight);
return style;
};
_getViewStyles = () => {
const { viewWidth } = this.state;
// get padding values from style to add back in after insets are determined
// default precedence: padding[Side] -> vertical | horizontal -> padding -> 0
let {
padding = 0,
paddingVertical = padding,
paddingHorizontal = padding,
paddingTop = paddingVertical,
paddingBottom = paddingVertical,
paddingLeft = paddingHorizontal,
paddingRight = paddingHorizontal,
...viewStyle
} = StyleSheet.flatten(this.props.style || {});
if (typeof paddingTop !== 'number') {
paddingTop = doubleFromPercentString(paddingTop) * viewWidth;
}
if (typeof paddingBottom !== 'number') {
paddingBottom = doubleFromPercentString(paddingBottom) * viewWidth;
}
if (typeof paddingLeft !== 'number') {
paddingLeft = doubleFromPercentString(paddingLeft) * viewWidth;
}
if (typeof paddingRight !== 'number') {
paddingRight = doubleFromPercentString(paddingRight) * viewWidth;
}
return {
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
viewStyle,
};
};
_getInset = key => {
const { isLandscape } = this.props;
switch (key) {
case 'horizontal':
case 'right':
case 'left': {
return isLandscape ? (isIPhoneX ? 44 : 0) : 0;
}
case 'vertical':
case 'top': {
return statusBarHeight(isLandscape);
}
case 'bottom': {
return isIPhoneX ? (isLandscape ? 24 : 34) : 0;
}
}
};
}
export default withOrientation(SafeView);

View File

@@ -7,8 +7,9 @@ import {
Platform,
Keyboard,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TabBarIcon from './TabBarIcon';
import SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';
const majorVersion = parseInt(Platform.Version, 10);
@@ -62,6 +63,7 @@ class TabBarBottom extends React.PureComponent {
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color },

View File

@@ -1,13 +1,15 @@
import React from 'react';
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';
import SafeAreaView from '../SafeAreaView';
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

@@ -27,6 +27,7 @@ exports[`TabBarBottom renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -123,6 +124,7 @@ exports[`TabBarBottom renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",
@@ -223,6 +225,8 @@ exports[`TabBarBottom renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
@@ -231,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);
}

320
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:
@@ -2512,7 +2512,7 @@ hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoist-non-react-statics@^2.2.0:
hoist-non-react-statics@^2.2.0, hoist-non-react-statics@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
@@ -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"
@@ -4416,6 +4440,12 @@ react-native-drawer-layout@1.3.2:
dependencies:
react-native-dismiss-keyboard "1.0.0"
react-native-safe-area-view@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.6.0.tgz#ce01eb27905a77780219537e0f53fe9c783a8b3d"
dependencies:
hoist-non-react-statics "^2.3.1"
react-native-tab-view@^0.0.74:
version "0.0.74"
resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-0.0.74.tgz#62c0c882d9232b461ce181d440d683b4f99d1bd8"
@@ -4555,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"
@@ -4868,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"
@@ -5038,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"
@@ -5157,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"
@@ -5368,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: