Compare commits

...

28 Commits

Author SHA1 Message Date
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
Brent Vatne
8bd25cf372 Bump to 1.0.0 2018-02-06 17:51:55 -08:00
Brent Vatne
20af8c688e Prevent push from bubbling up (#3454) 2018-02-06 17:49:52 -08:00
Brent Vatne
b0dccd7e88 Prevent pop and popToTop from bubbling up to parent stack (#3453) 2018-02-06 17:35:32 -08:00
Brent Vatne
c69a22f10e Bump version 2018-02-06 15:59:44 -08:00
Eric Vicenti
43a1c5ddbd Fix issue with StackRouter popToTop (#3451)
Previously the state was getting squashed, in this case it would destroy the routeName of the state, which was a route for the parent navigator, who could no longer render properly.
2018-02-06 15:56:39 -08:00
Brent Vatne
e97d41cccf Bump version and update description 2018-02-06 14:53:08 -08:00
Brent Vatne
3e4ddc685a Remove Header.HEIGHT deprecation warning, no good alternative solution available yet 2018-02-06 14:48:44 -08:00
Brent Vatne
f62c728593 Bump version 2018-02-06 14:13:41 -08:00
Eric Vicenti
616f9a56f2 Fix StackRouter Replace Key Behavior (#3450)
Replace should actually provide new keys on the replaced route, use ‘newKey’ on the action if you want to define the new route key
2018-02-06 14:12:59 -08:00
Brent Vatne
3b93faa0fc Bump version 2018-02-06 13:01:46 -08:00
Eric Vicenti
333b2e4b68 StackNavigator Replace Action (#3440)
* Navigation replace action

The long awaited action to replace the a route in StackNavigator

* Fix flow maybe
2018-02-06 12:59:16 -08:00
Brent Vatne
7f50173cf1 Bump version 2018-02-06 12:49:43 -08:00
Brent Vatne
8432f67529 Add adaptive to TabBarBottom flow interface 2018-02-06 12:49:37 -08:00
Eric Vicenti
c72b44ce10 Fix focus event for initial screen (#3448) 2018-02-06 12:49:23 -08:00
Peter Steffey
149f5f5c66 Fix #2795 - Not reducing header height in landscape on iPad (#3251)
* Adding tablet check for small header

* Switching to Platform.isPad

* Moving fully to Platform.isPad, no isHeightConstrained
2018-02-06 10:40:03 -08:00
Peter Steffey
85f77fe903 Issue #2794 - Tab bar fix - Continued from #3041 (#3267)
* Fixing iPad iOS 11 Tab Bar Bottom Behavior with changing widths via Multitasking

* Adding width constrained check

* Moving to only Platform.isPad (no more layout-based tests)

* Remove type import
2018-02-06 10:39:41 -08:00
Jay Phelps
d4b759606d remove $FlowFixMe comments left over, as flow is no longer used (#3439) 2018-02-06 14:59:38 +01:00
Brent Vatne
4d2609f4a8 Use a stack inside of drawer 2018-02-05 14:48:30 -08:00
28 changed files with 1198 additions and 886 deletions

View File

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

View File

@@ -4,7 +4,7 @@
import React from 'react';
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
import { DrawerNavigator, SafeAreaView } from 'react-navigation';
import { StackNavigator, DrawerNavigator, SafeAreaView } from 'react-navigation';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import SampleText from './SampleText';
@@ -16,6 +16,10 @@ const MyNavScreen = ({ navigation, banner }) => (
onPress={() => navigation.navigate('DrawerOpen')}
title="Open drawer"
/>
<Button
onPress={() => navigation.navigate('Email')}
title="Open other screen"
/>
<Button onPress={() => navigation.goBack(null)} title="Go back" />
</SafeAreaView>
<StatusBar barStyle="default" />
@@ -36,6 +40,10 @@ InboxScreen.navigationOptions = {
),
};
const EmailScreen = ({ navigation }) => (
<MyNavScreen banner={'Email Screen'} navigation={navigation} />
);
const DraftsScreen = ({ navigation }) => (
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
);
@@ -46,15 +54,25 @@ DraftsScreen.navigationOptions = {
),
};
const InboxStack = StackNavigator({
Inbox: { screen: InboxScreen },
Email: { screen: EmailScreen },
});
const DraftsStack = StackNavigator({
Drafts: { screen: DraftsScreen },
Email: { screen: EmailScreen },
});
const DrawerExample = DrawerNavigator(
{
Inbox: {
path: '/',
screen: InboxScreen,
screen: InboxStack,
},
Drafts: {
path: '/sent',
screen: DraftsScreen,
screen: DraftsStack,
},
},
{

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,20 @@ 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;
@@ -24,22 +38,19 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
<SafeAreaView>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
title="Go to a profile screen"
onPress={() => navigation.push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Go to a photos screen"
title="Navigate to a photos screen"
/>
<Button
onPress={() =>
navigation.navigate('Profile', {
name: 'Dog',
headerBackImage: require('./assets/dog-back.png'),
})
}
title="Custom back button"
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
@@ -97,6 +108,7 @@ type MyPhotosScreenProps = {
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
headerLeft: <MyBackButtonWithNavigation />
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;

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

@@ -1,7 +1,6 @@
// @flow
declare module 'react-navigation' {
/**
* First, a bunch of things we would love to import but instead must
* reconstruct (mostly copy-pasted).
@@ -68,6 +67,8 @@ declare module 'react-navigation' {
// The action to run inside the sub-router
action?: NavigationNavigateAction,
key?: string,
|};
declare type DeprecatedNavigationNavigateAction = {|
@@ -130,8 +131,9 @@ declare module 'react-navigation' {
type: 'Reset',
index: number,
key?: ?string,
actions:
Array<NavigationNavigateAction | DeprecatedNavigationNavigateAction>,
actions: Array<
NavigationNavigateAction | DeprecatedNavigationNavigateAction
>,
|};
declare export type NavigationUriAction = {|
@@ -144,9 +146,37 @@ declare module 'react-navigation' {
uri: string,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationPopAction = {|
+type: 'Navigation/POP',
+n?: number,
+immediate?: boolean,
|};
declare export type NavigationPopToTopAction = {|
+type: 'Navigation/POP_TO_TOP',
+immediate?: boolean,
|};
declare export type NavigationPushAction = {|
+type: 'Navigation/PUSH',
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
+key?: string,
|};
declare export type NavigationAction =
| NavigationInitAction
| NavigationNavigateAction
| NavigationReplaceAction
| NavigationPopAction
| NavigationPopToTopAction
| NavigationPushAction
| NavigationBackAction
| NavigationSetParamsAction
| NavigationResetAction;
@@ -209,9 +239,8 @@ declare module 'react-navigation' {
params?: NavigationParams,
};
declare export type NavigationStateRoute =
& NavigationLeafRoute
& NavigationState;
declare export type NavigationStateRoute = NavigationLeafRoute &
NavigationState;
/**
* Router
@@ -291,12 +320,8 @@ declare module 'react-navigation' {
Route: NavigationRoute,
Options: {},
Props: {}
> =
& React$ComponentType<NavigationNavigatorProps<Options, Route> & Props>
& (
| {}
| { navigationOptions: NavigationScreenConfig<Options> }
);
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
({} | { navigationOptions: NavigationScreenConfig<Options> });
declare export type NavigationNavigator<
State: NavigationState,
@@ -334,16 +359,18 @@ declare module 'react-navigation' {
declare export type HeaderMode = 'float' | 'screen' | 'none';
declare export type HeaderProps = $Shape<NavigationSceneRendererProps & {
mode: HeaderMode,
router: NavigationRouter<NavigationState, NavigationStackScreenOptions>,
getScreenDetails: NavigationScene => NavigationScreenDetails<
NavigationStackScreenOptions
>,
leftInterpolator: (props: NavigationSceneRendererProps) => {},
titleInterpolator: (props: NavigationSceneRendererProps) => {},
rightInterpolator: (props: NavigationSceneRendererProps) => {},
}>;
declare export type HeaderProps = $Shape<
NavigationSceneRendererProps & {
mode: HeaderMode,
router: NavigationRouter<NavigationState, NavigationStackScreenOptions>,
getScreenDetails: NavigationScene => NavigationScreenDetails<
NavigationStackScreenOptions
>,
leftInterpolator: (props: NavigationSceneRendererProps) => {},
titleInterpolator: (props: NavigationSceneRendererProps) => {},
rightInterpolator: (props: NavigationSceneRendererProps) => {},
}
>;
/**
* Stack Navigator
@@ -493,6 +520,18 @@ declare module 'react-navigation' {
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
push: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
};
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
@@ -767,7 +806,7 @@ declare module 'react-navigation' {
>(
router: NavigationRouter<S, O>,
routeConfigs?: NavigationRouteConfigMap,
navigatorConfig?: NavigatorConfig,
navigatorConfig?: NavigatorConfig
): _NavigatorCreator<NavigationViewProps, S, O>;
declare export function StackNavigator(
@@ -1026,6 +1065,7 @@ declare module 'react-navigation' {
declare type _TabBarBottomProps = {
activeTintColor: string,
activeBackgroundColor: string,
adaptive?: boolean,
inactiveTintColor: string,
inactiveBackgroundColor: string,
showLabel: boolean,

View File

@@ -1,7 +1,7 @@
{
"name": "react-navigation",
"version": "1.0.0-beta.30",
"description": "React Navigation",
"version": "1.0.2",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
"url": "git@github.com:react-navigation/react-navigation.git",
@@ -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.6.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {

View File

@@ -5,6 +5,7 @@ const POP = 'Navigation/POP';
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
const PUSH = 'Navigation/PUSH';
const RESET = 'Navigation/RESET';
const REPLACE = 'Navigation/REPLACE';
const SET_PARAMS = 'Navigation/SET_PARAMS';
const URI = 'Navigation/URI';
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
@@ -79,6 +80,16 @@ const reset = createAction(RESET, payload => ({
actions: payload.actions,
}));
const replace = createAction(REPLACE, payload => ({
type: REPLACE,
key: payload.key,
newKey: payload.newKey,
params: payload.params,
action: payload.action,
routeName: payload.routeName,
immediate: payload.immediate,
}));
const setParams = createAction(SET_PARAMS, payload => ({
type: SET_PARAMS,
key: payload.key,
@@ -157,6 +168,7 @@ export default {
POP_TO_TOP,
PUSH,
RESET,
REPLACE,
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
@@ -169,6 +181,7 @@ export default {
popToTop,
push,
reset,
replace,
setParams,
uri,
completeTransition,

View File

@@ -65,5 +65,15 @@ export default function(navigation) {
navigation.dispatch(
NavigationActions.push({ routeName, params, action })
),
replace: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.replace({
routeName,
params,
action,
key: navigation.state.key,
})
),
};
}

View File

@@ -24,9 +24,10 @@ export default function createNavigationContainer(Component) {
this._validateProps(props);
this._initialAction = NavigationActions.init();
this.state = {
nav: this._isStateful()
? Component.router.getStateForAction(NavigationActions.init())
? Component.router.getStateForAction(this._initialAction)
: null,
};
}
@@ -134,6 +135,15 @@ export default function createNavigationContainer(Component) {
Linking.addEventListener('url', this._handleOpenURL);
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action: this._initialAction,
state: this.state.nav,
lastState: null,
})
);
}
componentWillUnmount() {
@@ -154,7 +164,6 @@ export default function createNavigationContainer(Component) {
const nav = Component.router.getStateForAction(action, oldNav);
const dispatchActionEvents = () => {
this._actionEventSubscribers.forEach(subscriber =>
// $FlowFixMe - Payload should probably understand generic state type
subscriber({
type: 'action',
action,

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

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

@@ -100,6 +100,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
@@ -132,6 +133,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -337,6 +339,7 @@ exports[`StackNavigator renders successfully 1`] = `
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
@@ -369,6 +372,7 @@ exports[`StackNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",

View File

@@ -71,6 +71,7 @@ exports[`TabNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -105,68 +106,81 @@ exports[`TabNavigator renders successfully 1`] = `
"alignItems": "center",
"backgroundColor": "rgba(0, 0, 0, 0)",
"flex": 1,
"justifyContent": "flex-end",
}
}
testID={undefined}
>
<View
style={
Object {
"flexGrow": 1,
}
Array [
Object {
"alignItems": "center",
"flex": 1,
},
Object {
"flexDirection": "column",
"justifyContent": "flex-end",
},
undefined,
]
}
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"flexGrow": 1,
}
}
/>
<View
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"textAlign": "center",
}
}
/>
>
Welcome anonymous
</Text>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"marginLeft": 0,
"marginTop": 0,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>

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

View File

@@ -81,7 +81,6 @@ export default (routeConfigs, stackConfig = {}) => {
});
paths = Object.entries(pathsByRouteNames);
/* $FlowFixMe */
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
return {
@@ -172,10 +171,15 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
//Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
// 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 (state.index !== 0) {
if (state.index === 0) {
return {
...state,
};
} else {
return {
...state,
isTransitioning: action.immediate !== true,
index: 0,
routes: [state.routes[0]],
@@ -184,6 +188,31 @@ export default (routeConfigs, stackConfig = {}) => {
return state;
}
// Handle replace action
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
let childState = {};
if (childRouter) {
const childAction =
action.action ||
NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
const routes = [...state.routes];
routes[routeIndex] = {
params: action.params,
// merge the child state in this order to allow params override
...childState,
key: action.newKey || generateKey(),
routeName: action.routeName,
};
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) &&
@@ -236,6 +265,7 @@ export default (routeConfigs, stackConfig = {}) => {
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,
@@ -251,6 +281,13 @@ export default (routeConfigs, stackConfig = {}) => {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
return {
...state,
};
}
if (
@@ -365,6 +402,7 @@ export default (routeConfigs, stackConfig = {}) => {
const backRoute = state.routes.find(route => route.key === key);
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex > 0) {
return {
...state,
@@ -372,6 +410,13 @@ export default (routeConfigs, stackConfig = {}) => {
index: backRouteIndex - 1,
isTransitioning: immediate !== true,
};
} else if (
backRouteIndex === 0 &&
action.type === NavigationActions.POP
) {
return {
...state,
};
}
}
return state;

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);
@@ -213,9 +220,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 {

View File

@@ -8,6 +8,12 @@ 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,
@@ -105,8 +111,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('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -134,7 +140,7 @@ test('Handles deep action', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0-2',
key: 'Init-id-0',
routeName: 'Bar',
},
],
@@ -174,8 +180,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('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -188,3 +194,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

@@ -366,6 +366,97 @@ describe('StackRouter', () => {
expect(pushedState.routes[1].routes[1].routeName).toEqual('qux');
});
test('pop does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
key: 'StackRouterRoot',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.POP,
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('push does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
Bad: { screen: () => <div /> },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.PUSH,
routeName: 'Bad',
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes.length).toEqual(2);
});
test('popToTop does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.POP_TO_TOP,
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('popToTop works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -515,6 +606,41 @@ describe('StackRouter', () => {
});
});
test('Replace action works', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo' })
);
const replacedState = TestRouter.getStateForAction(
NavigationActions.replace({
routeName: 'bar',
params: { meaning: 42 },
key: initState.routes[0].key,
}),
initState
);
expect(replacedState.index).toEqual(0);
expect(replacedState.routes.length).toEqual(1);
expect(replacedState.routes[0].key).not.toEqual(initState.routes[0].key);
expect(replacedState.routes[0].routeName).toEqual('bar');
expect(replacedState.routes[0].params.meaning).toEqual(42);
const replacedState2 = TestRouter.getStateForAction(
NavigationActions.replace({
routeName: 'bar',
key: initState.routes[0].key,
newKey: 'wow',
}),
initState
);
expect(replacedState2.index).toEqual(0);
expect(replacedState2.routes.length).toEqual(1);
expect(replacedState2.routes[0].key).toEqual('wow');
expect(replacedState2.routes[0].routeName).toEqual('bar');
});
test('Handles push transition logic with completion action', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -1161,9 +1287,7 @@ describe('StackRouter', () => {
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('baz/321');
/* $FlowFixMe: params.id has to exist */
expect(params.id).toEqual('123');
/* $FlowFixMe: params.bazId has to exist */
expect(params.bazId).toEqual('321');
}
@@ -1323,9 +1447,7 @@ test('Handles deep navigate completion action', () => {
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
/* $FlowFixMe */
expect(state2 && state2.routes[0].index).toEqual(1);
/* $FlowFixMe */
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(!!key).toEqual(true);
const state3 = router.getStateForAction(
@@ -1336,8 +1458,6 @@ test('Handles deep navigate completion action', () => {
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
/* $FlowFixMe */
expect(state3 && state3.routes[0].index).toEqual(1);
/* $FlowFixMe */
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
});

View File

@@ -206,132 +206,137 @@ class CardStack extends React.Component {
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,

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

@@ -8,11 +8,11 @@ import {
View,
ViewPropTypes,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
@@ -27,9 +27,6 @@ class Header extends React.PureComponent {
};
static get HEIGHT() {
console.warn(
'Header.HEIGHT is deprecated and will be removed before react-navigation comes out of beta.'
);
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
}
@@ -265,7 +262,11 @@ class Header extends React.PureComponent {
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,
@@ -292,7 +293,8 @@ class Header extends React.PureComponent {
const { options } = this.props.getScreenDetails(scene);
const { headerStyle } = options;
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
const appBarHeight =
Platform.OS === 'ios' ? (isLandscape && !Platform.isPad ? 32 : 44) : 56;
const containerStyles = [
styles.container,
{

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,13 +7,15 @@ 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);
const isIos = Platform.OS === 'ios';
const useHorizontalTabs = majorVersion >= 11 && isIos;
const isIOS11 = majorVersion >= 11 && isIos;
const defaultMaxTabBarItemWidth = 125;
class TabBarBottom extends React.PureComponent {
// See https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/UITabBar.html
@@ -25,6 +27,7 @@ class TabBarBottom extends React.PureComponent {
showLabel: true,
showIcon: true,
allowFontScaling: true,
adaptive: isIOS11,
};
_renderLabel = scene => {
@@ -56,19 +59,18 @@ class TabBarBottom extends React.PureComponent {
const tintColor = scene.focused ? activeTintColor : inactiveTintColor;
const label = this.props.getLabel({ ...scene, tintColor });
let marginLeft = 0;
if (isLandscape && showIcon && useHorizontalTabs) {
marginLeft = LABEL_LEFT_MARGIN;
}
let marginTop = 0;
if (!isLandscape && showIcon && useHorizontalTabs) {
marginTop = LABEL_TOP_MARGIN;
}
if (typeof label === 'string') {
return (
<Animated.Text
style={[styles.label, { color, marginLeft, marginTop }, labelStyle]}
style={[
styles.label,
{ color },
showIcon && this._shouldUseHorizontalTabs()
? styles.labelBeside
: styles.labelBeneath,
labelStyle,
]}
allowFontScaling={allowFontScaling}
>
{label}
@@ -104,7 +106,7 @@ class TabBarBottom extends React.PureComponent {
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
scene={scene}
style={showLabel && useHorizontalTabs ? {} : styles.icon}
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
/>
);
};
@@ -115,6 +117,64 @@ class TabBarBottom extends React.PureComponent {
return testIDProps;
};
_tabItemMaxWidth() {
const { tabStyle, layout } = this.props;
let maxTabBarItemWidth;
const flattenedTabStyle = StyleSheet.flatten(tabStyle);
if (flattenedTabStyle) {
if (typeof flattenedTabStyle.width === 'number') {
maxTabBarItemWidth = flattenedTabStyle.width;
} else if (
typeof flattenedTabStyle.width === 'string' &&
flattenedTabStyle.endsWith('%')
) {
const width = parseFloat(flattenedTabStyle.width);
if (Number.isFinite(width)) {
maxTabBarItemWidth = layout.width * (width / 100);
}
} else if (typeof flattenedTabStyle.maxWidth === 'number') {
maxTabBarItemWidth = flattenedTabStyle.maxWidth;
} else if (
typeof flattenedTabStyle.maxWidth === 'string' &&
flattenedTabStyle.endsWith('%')
) {
const width = parseFloat(flattenedTabStyle.maxWidth);
if (Number.isFinite(width)) {
maxTabBarItemWidth = layout.width * (width / 100);
}
}
}
if (!maxTabBarItemWidth) {
maxTabBarItemWidth = defaultMaxTabBarItemWidth;
}
return maxTabBarItemWidth;
}
_shouldUseHorizontalTabs() {
const { routes } = this.props.navigation.state;
const { isLandscape, layout, adaptive, tabStyle } = this.props;
if (!adaptive) {
return false;
}
let tabBarWidth = layout.width;
if (tabBarWidth === 0) {
return Platform.isPad;
}
if (!Platform.isPad) {
return isLandscape;
} else {
const maxTabBarItemWidth = this._tabItemMaxWidth();
return routes.length * maxTabBarItemWidth <= tabBarWidth;
}
}
render() {
const {
position,
@@ -136,9 +196,9 @@ class TabBarBottom extends React.PureComponent {
const tabBarStyle = [
styles.tabBar,
isLandscape && useHorizontalTabs
? styles.tabBarLandscape
: styles.tabBarPortrait,
this._shouldUseHorizontalTabs() && !Platform.isPad
? styles.tabBarCompact
: styles.tabBarRegular,
style,
];
@@ -178,17 +238,19 @@ class TabBarBottom extends React.PureComponent {
: jumpToIndex(index)
}
>
<Animated.View
style={[
styles.tab,
isLandscape && useHorizontalTabs && styles.tabLandscape,
!isLandscape && useHorizontalTabs && styles.tabPortrait,
{ backgroundColor },
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
<Animated.View style={[styles.tab, { backgroundColor }]}>
<View
style={[
styles.tab,
this._shouldUseHorizontalTabs()
? styles.tabLandscape
: styles.tabPortrait,
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
</View>
</Animated.View>
</TouchableWithoutFeedback>
);
@@ -199,8 +261,6 @@ class TabBarBottom extends React.PureComponent {
}
}
const LABEL_LEFT_MARGIN = 20;
const LABEL_TOP_MARGIN = 15;
const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10
@@ -208,16 +268,15 @@ const styles = StyleSheet.create({
borderTopColor: 'rgba(0, 0, 0, .3)',
flexDirection: 'row',
},
tabBarLandscape: {
tabBarCompact: {
height: 29,
},
tabBarPortrait: {
tabBarRegular: {
height: 49,
},
tab: {
flex: 1,
alignItems: isIos ? 'center' : 'stretch',
justifyContent: 'flex-end',
},
tabPortrait: {
justifyContent: 'flex-end',
@@ -232,9 +291,15 @@ const styles = StyleSheet.create({
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',
},
labelBeneath: {
fontSize: 10,
marginBottom: 1.5,
backgroundColor: 'transparent',
},
labelBeside: {
fontSize: 13,
marginLeft: 20,
},
});

View File

@@ -1,10 +1,10 @@
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 withCachedChildNavigation from '../../withCachedChildNavigation';
import SafeAreaView from '../SafeAreaView';
class TabView extends React.PureComponent {
static defaultProps = {

View File

@@ -27,6 +27,7 @@ exports[`TabBarBottom renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -61,68 +62,81 @@ exports[`TabBarBottom renders successfully 1`] = `
"alignItems": "center",
"backgroundColor": "rgba(0, 0, 0, 0)",
"flex": 1,
"justifyContent": "flex-end",
}
}
testID={undefined}
>
<View
style={
Object {
"flexGrow": 1,
}
Array [
Object {
"alignItems": "center",
"flex": 1,
},
Object {
"flexDirection": "column",
"justifyContent": "flex-end",
},
undefined,
]
}
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"flexGrow": 1,
}
}
/>
<View
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"textAlign": "center",
}
}
/>
>
s1
</Text>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"marginLeft": 0,
"marginTop": 0,
"textAlign": "center",
}
}
>
s1
</Text>
</View>
</View>
</View>
@@ -227,6 +241,7 @@ exports[`TabBarBottom renders successfully 1`] = `
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",

View File

@@ -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"
@@ -4416,6 +4416,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"