diff --git a/packages/react-navigation/src/routers/TabRouter.js b/packages/react-navigation/src/routers/TabRouter.js index b27496d6..21f1025c 100644 --- a/packages/react-navigation/src/routers/TabRouter.js +++ b/packages/react-navigation/src/routers/TabRouter.js @@ -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 { diff --git a/packages/react-navigation/src/routers/__tests__/Routers-test.js b/packages/react-navigation/src/routers/__tests__/Routers-test.js index fd14be12..30b6fc82 100644 --- a/packages/react-navigation/src/routers/__tests__/Routers-test.js +++ b/packages/react-navigation/src/routers/__tests__/Routers-test.js @@ -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 = () =>
; + const BarView = () =>
; + 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); +});