mirror of
https://github.com/status-im/react-navigation.git
synced 2025-02-24 09:08:15 +00:00
Give inactive routes in stack opportunity to handle action (#4064)
This commit is contained in:
parent
42bb1cc317
commit
5fff7ef5c6
@ -27,6 +27,7 @@ import ModalStack from './ModalStack';
|
||||
import StacksInTabs from './StacksInTabs';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
import StacksWithKeys from './StacksWithKeys';
|
||||
import InactiveStack from './InactiveStack';
|
||||
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
|
||||
import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
@ -45,6 +46,11 @@ const ExampleInfo = {
|
||||
name: 'Switch between routes',
|
||||
description: 'Jump between routes',
|
||||
},
|
||||
InactiveStack: {
|
||||
name: 'Navigate idempotently to stacks in inactive routes',
|
||||
description:
|
||||
'An inactive route in a stack should be given the opportunity to handle actions',
|
||||
},
|
||||
StackWithCustomHeaderBackImage: {
|
||||
name: 'Custom header back image',
|
||||
description: 'Stack with custom header back image',
|
||||
@ -150,6 +156,8 @@ const ExampleRoutes = {
|
||||
},
|
||||
TabsWithNavigationFocus,
|
||||
KeyboardHandlingExample,
|
||||
// This is commented out because it's rarely useful
|
||||
// InactiveStack,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
96
examples/NavigationPlayground/js/InactiveStack.js
Normal file
96
examples/NavigationPlayground/js/InactiveStack.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { Button, Text, StatusBar, View, StyleSheet } from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createSwitchNavigator,
|
||||
NavigationActions,
|
||||
} from 'react-navigation';
|
||||
|
||||
const runSubRoutes = navigation => {
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'Second2' }));
|
||||
navigation.dispatch(NavigationActions.navigate({ routeName: 'First2' }));
|
||||
};
|
||||
|
||||
const runSubRoutesWithIntermediate = navigation => {
|
||||
navigation.dispatch(toFirst1);
|
||||
navigation.dispatch(toSecond2);
|
||||
navigation.dispatch(toFirst);
|
||||
navigation.dispatch(toFirst2);
|
||||
};
|
||||
|
||||
const runSubAction = navigation => {
|
||||
navigation.dispatch(toFirst2);
|
||||
navigation.dispatch(toSecond2);
|
||||
navigation.dispatch(toFirstChild1);
|
||||
};
|
||||
|
||||
const DummyScreen = ({ routeName, navigation, style }) => {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
StyleSheet.absoluteFill,
|
||||
{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontWeight: '800' }}>
|
||||
{routeName}({navigation.state.key})
|
||||
</Text>
|
||||
<View>
|
||||
<Button title="back" onPress={() => navigation.goBack()} />
|
||||
<Button title="dismiss" onPress={() => navigation.dismiss()} />
|
||||
<Button
|
||||
title="between sub-routes"
|
||||
onPress={() => runSubRoutes(navigation)}
|
||||
/>
|
||||
<Button
|
||||
title="between sub-routes (with intermediate)"
|
||||
onPress={() => runSubRoutesWithIntermediate(navigation)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="with sub-action"
|
||||
onPress={() => runSubAction(navigation)}
|
||||
/>
|
||||
</View>
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const createDummyScreen = routeName => {
|
||||
const BoundDummyScreen = props => DummyScreen({ ...props, routeName });
|
||||
return BoundDummyScreen;
|
||||
};
|
||||
|
||||
const toFirst = NavigationActions.navigate({ routeName: 'First' });
|
||||
const toFirst1 = NavigationActions.navigate({ routeName: 'First1' });
|
||||
const toFirst2 = NavigationActions.navigate({ routeName: 'First2' });
|
||||
const toSecond2 = NavigationActions.navigate({ routeName: 'Second2' });
|
||||
const toFirstChild1 = NavigationActions.navigate({
|
||||
routeName: 'First',
|
||||
action: NavigationActions.navigate({ routeName: 'First1' }),
|
||||
});
|
||||
|
||||
export default createStackNavigator(
|
||||
{
|
||||
Other: createDummyScreen('Leaf'),
|
||||
First: createStackNavigator({
|
||||
First1: createDummyScreen('First1'),
|
||||
First2: createDummyScreen('First2'),
|
||||
}),
|
||||
Second: createStackNavigator({
|
||||
Second1: createDummyScreen('Second1'),
|
||||
Second2: createDummyScreen('Second2'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
headerMode: 'none',
|
||||
}
|
||||
);
|
@ -116,8 +116,23 @@ const StateUtils = {
|
||||
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the positon to where the new route in the
|
||||
* stack is at.
|
||||
* Note that this moves the index to the position to where the new route in the
|
||||
* stack is at and updates the routes array accordingly.
|
||||
*/
|
||||
replaceAndPrune(state, key, route) {
|
||||
const index = StateUtils.indexOf(state, key);
|
||||
const replaced = StateUtils.replaceAtIndex(state, index, route);
|
||||
|
||||
return {
|
||||
...replaced,
|
||||
routes: replaced.routes.slice(0, index + 1),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the position to where the new route in the
|
||||
* stack is at. Does not prune the routes.
|
||||
*/
|
||||
replaceAt(state, key, route) {
|
||||
const index = StateUtils.indexOf(state, key);
|
||||
@ -137,7 +152,7 @@ const StateUtils = {
|
||||
route.key
|
||||
);
|
||||
|
||||
if (state.routes[index] === route) {
|
||||
if (state.routes[index] === route && index === state.index) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -153,7 +168,7 @@ const StateUtils = {
|
||||
|
||||
/**
|
||||
* Resets all routes.
|
||||
* Note that this moves the index to the positon to where the last route in the
|
||||
* Note that this moves the index to the position to where the last route in the
|
||||
* stack is at if the param `index` isn't provided.
|
||||
*/
|
||||
reset(state, routes, index) {
|
||||
|
@ -202,7 +202,7 @@ describe('StateUtils', () => {
|
||||
).toEqual(newState);
|
||||
});
|
||||
|
||||
it('Returns the state if index matches the route', () => {
|
||||
it('Returns the state with updated index if route is unchanged but index changes', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
@ -210,7 +210,7 @@ describe('StateUtils', () => {
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
||||
).toEqual(state);
|
||||
).toEqual({ ...state, index: 1 });
|
||||
});
|
||||
|
||||
// Reset
|
||||
|
@ -143,7 +143,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -156,6 +156,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"minWidth": 30,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
@ -170,6 +171,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"minWidth": 30,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
|
@ -27,6 +27,10 @@ function behavesLikePushAction(action) {
|
||||
|
||||
const defaultActionCreators = (route, navStateKey) => ({});
|
||||
|
||||
function isResetToRootStack(action) {
|
||||
return action.type === StackActions.RESET && action.key === null;
|
||||
}
|
||||
|
||||
export default (routeConfigs, stackConfig = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
@ -223,7 +227,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
|
||||
// Check if the focused child scene wants to handle the action, as long as
|
||||
// it is not a reset to the root stack
|
||||
if (action.type !== StackActions.RESET || action.key !== null) {
|
||||
if (
|
||||
!isResetToRootStack(action) &&
|
||||
action.type !== NavigationActions.NAVIGATE
|
||||
) {
|
||||
const keyIndex = action.key
|
||||
? StateUtils.indexOf(state, action.key)
|
||||
: -1;
|
||||
@ -243,6 +250,28 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
return StateUtils.replaceAt(state, childRoute.key, route);
|
||||
}
|
||||
}
|
||||
} else if (action.type === NavigationActions.NAVIGATE) {
|
||||
// Traverse routes from the top of the stack to the bottom, so the
|
||||
// active route has the first opportunity, then the one before it, etc.
|
||||
for (let childRoute of state.routes.slice().reverse()) {
|
||||
let childRouter = childRouters[childRoute.routeName];
|
||||
let debug = action.params && action.params.debug;
|
||||
|
||||
if (childRouter) {
|
||||
const nextRouteState = childRouter.getStateForAction(
|
||||
action,
|
||||
childRoute
|
||||
);
|
||||
|
||||
if (nextRouteState === null || nextRouteState !== childRoute) {
|
||||
return StateUtils.replaceAndPrune(
|
||||
state,
|
||||
nextRouteState ? nextRouteState.key : childRoute.key,
|
||||
nextRouteState ? nextRouteState : childRoute
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle explicit push navigation action. This must happen after the
|
||||
|
@ -679,23 +679,74 @@ describe('StackRouter', () => {
|
||||
expect(pushedState).toEqual(null);
|
||||
});
|
||||
|
||||
test('Navigate with key is idempotent', () => {
|
||||
test('Navigate with key and without it is idempotent', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
});
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
initState
|
||||
for (key of ['a', null]) {
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
);
|
||||
expect(pushedTwiceState).toEqual(null);
|
||||
}
|
||||
});
|
||||
|
||||
// https://github.com/react-navigation/react-navigation/issues/4063
|
||||
test('Navigate on inactive stackrouter is idempotent', () => {
|
||||
const FirstChildNavigator = () => <div />;
|
||||
FirstChildNavigator.router = StackRouter({
|
||||
First1: () => <div />,
|
||||
First2: () => <div />,
|
||||
});
|
||||
|
||||
const SecondChildNavigator = () => <div />;
|
||||
SecondChildNavigator.router = StackRouter({
|
||||
Second1: () => <div />,
|
||||
Second2: () => <div />,
|
||||
});
|
||||
|
||||
const router = StackRouter({
|
||||
Leaf: () => <div />,
|
||||
First: FirstChildNavigator,
|
||||
Second: SecondChildNavigator,
|
||||
});
|
||||
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
|
||||
const first = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'First2' }),
|
||||
state
|
||||
);
|
||||
expect(pushedState.index).toEqual(1);
|
||||
expect(pushedState.routes[1].routeName).toEqual('bar');
|
||||
const pushedTwiceState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||
pushedState
|
||||
|
||||
const second = router.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'Second2' }),
|
||||
first
|
||||
);
|
||||
expect(pushedTwiceState).toEqual(null);
|
||||
|
||||
const firstAgain = router.getStateForAction(
|
||||
NavigationActions.navigate({
|
||||
routeName: 'First2',
|
||||
params: { debug: true },
|
||||
}),
|
||||
second
|
||||
);
|
||||
|
||||
expect(first.routes.length).toEqual(2);
|
||||
expect(first.index).toEqual(1);
|
||||
expect(second.routes.length).toEqual(3);
|
||||
expect(second.index).toEqual(2);
|
||||
|
||||
expect(firstAgain.index).toEqual(1);
|
||||
expect(firstAgain.routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('Navigate to current routeName returns null to indicate handled action', () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user