mirror of
https://github.com/status-im/react-navigation.git
synced 2025-02-24 17:18:09 +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 StacksInTabs from './StacksInTabs';
|
||||||
import StacksOverTabs from './StacksOverTabs';
|
import StacksOverTabs from './StacksOverTabs';
|
||||||
import StacksWithKeys from './StacksWithKeys';
|
import StacksWithKeys from './StacksWithKeys';
|
||||||
|
import InactiveStack from './InactiveStack';
|
||||||
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
|
import StackWithCustomHeaderBackImage from './StackWithCustomHeaderBackImage';
|
||||||
import SimpleStack from './SimpleStack';
|
import SimpleStack from './SimpleStack';
|
||||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||||
@ -45,6 +46,11 @@ const ExampleInfo = {
|
|||||||
name: 'Switch between routes',
|
name: 'Switch between routes',
|
||||||
description: 'Jump 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: {
|
StackWithCustomHeaderBackImage: {
|
||||||
name: 'Custom header back image',
|
name: 'Custom header back image',
|
||||||
description: 'Stack with custom header back image',
|
description: 'Stack with custom header back image',
|
||||||
@ -150,6 +156,8 @@ const ExampleRoutes = {
|
|||||||
},
|
},
|
||||||
TabsWithNavigationFocus,
|
TabsWithNavigationFocus,
|
||||||
KeyboardHandlingExample,
|
KeyboardHandlingExample,
|
||||||
|
// This is commented out because it's rarely useful
|
||||||
|
// InactiveStack,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
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.
|
* Replace a route by a key.
|
||||||
* Note that this moves the index to the positon to where the new route in the
|
* Note that this moves the index to the position to where the new route in the
|
||||||
* stack is at.
|
* 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) {
|
replaceAt(state, key, route) {
|
||||||
const index = StateUtils.indexOf(state, key);
|
const index = StateUtils.indexOf(state, key);
|
||||||
@ -137,7 +152,7 @@ const StateUtils = {
|
|||||||
route.key
|
route.key
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.routes[index] === route) {
|
if (state.routes[index] === route && index === state.index) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +168,7 @@ const StateUtils = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all routes.
|
* 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.
|
* stack is at if the param `index` isn't provided.
|
||||||
*/
|
*/
|
||||||
reset(state, routes, index) {
|
reset(state, routes, index) {
|
||||||
|
@ -202,7 +202,7 @@ describe('StateUtils', () => {
|
|||||||
).toEqual(newState);
|
).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 = {
|
const state = {
|
||||||
index: 0,
|
index: 0,
|
||||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||||
@ -210,7 +210,7 @@ describe('StateUtils', () => {
|
|||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
||||||
).toEqual(state);
|
).toEqual({ ...state, index: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
|
@ -143,7 +143,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
|||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
Object {
|
Object {
|
||||||
"flexGrow": 1,
|
"flex": 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -156,6 +156,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
|||||||
"alignSelf": "center",
|
"alignSelf": "center",
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
|
"minWidth": 30,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
@ -170,6 +171,7 @@ exports[`TabNavigator renders successfully 1`] = `
|
|||||||
"alignSelf": "center",
|
"alignSelf": "center",
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
|
"minWidth": 30,
|
||||||
"opacity": 0,
|
"opacity": 0,
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
|
@ -27,6 +27,10 @@ function behavesLikePushAction(action) {
|
|||||||
|
|
||||||
const defaultActionCreators = (route, navStateKey) => ({});
|
const defaultActionCreators = (route, navStateKey) => ({});
|
||||||
|
|
||||||
|
function isResetToRootStack(action) {
|
||||||
|
return action.type === StackActions.RESET && action.key === null;
|
||||||
|
}
|
||||||
|
|
||||||
export default (routeConfigs, stackConfig = {}) => {
|
export default (routeConfigs, stackConfig = {}) => {
|
||||||
// Fail fast on invalid route definitions
|
// Fail fast on invalid route definitions
|
||||||
validateRouteConfigMap(routeConfigs);
|
validateRouteConfigMap(routeConfigs);
|
||||||
@ -223,7 +227,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
|||||||
|
|
||||||
// Check if the focused child scene wants to handle the action, as long as
|
// Check if the focused child scene wants to handle the action, as long as
|
||||||
// it is not a reset to the root stack
|
// 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
|
const keyIndex = action.key
|
||||||
? StateUtils.indexOf(state, action.key)
|
? StateUtils.indexOf(state, action.key)
|
||||||
: -1;
|
: -1;
|
||||||
@ -243,6 +250,28 @@ export default (routeConfigs, stackConfig = {}) => {
|
|||||||
return StateUtils.replaceAt(state, childRoute.key, route);
|
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
|
// Handle explicit push navigation action. This must happen after the
|
||||||
|
@ -679,12 +679,13 @@ describe('StackRouter', () => {
|
|||||||
expect(pushedState).toEqual(null);
|
expect(pushedState).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Navigate with key is idempotent', () => {
|
test('Navigate with key and without it is idempotent', () => {
|
||||||
const TestRouter = StackRouter({
|
const TestRouter = StackRouter({
|
||||||
foo: { screen: () => <div /> },
|
foo: { screen: () => <div /> },
|
||||||
bar: { screen: () => <div /> },
|
bar: { screen: () => <div /> },
|
||||||
});
|
});
|
||||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||||
|
for (key of ['a', null]) {
|
||||||
const pushedState = TestRouter.getStateForAction(
|
const pushedState = TestRouter.getStateForAction(
|
||||||
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
|
||||||
initState
|
initState
|
||||||
@ -696,6 +697,56 @@ describe('StackRouter', () => {
|
|||||||
pushedState
|
pushedState
|
||||||
);
|
);
|
||||||
expect(pushedTwiceState).toEqual(null);
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
const second = router.getStateForAction(
|
||||||
|
NavigationActions.navigate({ routeName: 'Second2' }),
|
||||||
|
first
|
||||||
|
);
|
||||||
|
|
||||||
|
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', () => {
|
test('Navigate to current routeName returns null to indicate handled action', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user