Compare commits

...

16 Commits

Author SHA1 Message Date
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
23 changed files with 435 additions and 391 deletions

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

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

View File

@@ -1,7 +1,7 @@
{
"name": "react-navigation",
"version": "1.0.0-beta.31",
"description": "React Navigation",
"version": "1.0.1",
"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';
@@ -44,6 +45,9 @@ const navigate = createAction(NAVIGATE, payload => {
if (payload.key) {
action.key = payload.key;
}
if (payload.immediate) {
action.immediate = payload.immediate;
}
return action;
});
@@ -69,6 +73,9 @@ const push = createAction(PUSH, payload => {
if (payload.action) {
action.action = payload.action;
}
if (payload.immediate) {
action.immediate = payload.immediate;
}
return action;
});
@@ -79,6 +86,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 +174,7 @@ export default {
POP_TO_TOP,
PUSH,
RESET,
REPLACE,
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
@@ -169,6 +187,7 @@ export default {
popToTop,
push,
reset,
replace,
setParams,
uri,
completeTransition,

View File

@@ -109,7 +109,7 @@ describe('NavigationContainer', () => {
// First dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'bar' })
NavigationActions.navigate({ routeName: 'bar', immediate: true })
)
).toEqual(true);
@@ -119,7 +119,7 @@ describe('NavigationContainer', () => {
// Second dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'baz' })
NavigationActions.navigate({ routeName: 'baz', immediate: true })
)
).toEqual(true);
@@ -147,7 +147,7 @@ describe('NavigationContainer', () => {
// First dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'bar' })
NavigationActions.navigate({ routeName: 'bar', immediate: true })
)
).toEqual(true);
@@ -157,28 +157,28 @@ describe('NavigationContainer', () => {
// Second dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'baz' })
NavigationActions.navigate({ routeName: 'baz', immediate: true })
)
).toEqual(true);
// Third dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'car' })
NavigationActions.navigate({ routeName: 'car', immediate: true })
)
).toEqual(true);
// Fourth dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'dog' })
NavigationActions.navigate({ routeName: 'dog', immediate: true })
)
).toEqual(true);
// Fifth dispatch
expect(
navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'elk' })
NavigationActions.navigate({ routeName: 'elk', immediate: true })
)
).toEqual(true);

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

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

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

@@ -148,6 +148,17 @@ export default (routeConfigs, stackConfig = {}) => {
};
}
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
) {
return {
...state,
isTransitioning: false,
};
}
// Check if a 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
@@ -171,10 +182,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]],
@@ -183,11 +199,40 @@ 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) &&
childRouters[action.routeName] !== undefined
) {
if (state.isTransitioning) {
return { ...state };
}
const childRouter = childRouters[action.routeName];
let route;
@@ -235,6 +280,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,
@@ -250,16 +296,12 @@ export default (routeConfigs, stackConfig = {}) => {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
}
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
return {
...state,
isTransitioning: false,
};
}
@@ -364,6 +406,7 @@ export default (routeConfigs, stackConfig = {}) => {
const backRoute = state.routes.find(route => route.key === key);
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex > 0) {
return {
...state,
@@ -371,6 +414,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 /> },
@@ -402,6 +493,23 @@ describe('StackRouter', () => {
expect(poppedImmediatelyState.isTransitioning).toBe(false);
});
test('Navigate does not happen while transitioning', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = {
...TestRouter.getStateForAction(NavigationActions.init()),
isTransitioning: true,
};
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar' }),
initState
);
expect(pushedState.index).toEqual(0);
expect(pushedState.routes.length).toEqual(1);
});
test('Navigate Pushes duplicate routeName', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -409,13 +517,13 @@ describe('StackRouter', () => {
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar' }),
NavigationActions.navigate({ routeName: 'bar', immediate: true }),
initState
);
expect(pushedState.index).toEqual(1);
expect(pushedState.routes[1].routeName).toEqual('bar');
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar' }),
NavigationActions.navigate({ routeName: 'bar', immediate: true }),
pushedState
);
expect(pushedTwiceState.index).toEqual(2);
@@ -449,7 +557,7 @@ describe('StackRouter', () => {
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.push({ routeName: 'bar' }),
NavigationActions.push({ routeName: 'bar', immediate: true }),
initState
);
expect(pushedState.index).toEqual(1);
@@ -515,6 +623,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 />;

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

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

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",
@@ -240,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"