diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index a59278af..246e3e0d 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -16,6 +16,10 @@ export type NavigationState = { * List of valid route names as defined in the screen components. */ routeNames: string[]; + /** + * Alternative entries for history. + */ + history?: unknown[]; /** * List of rendered routes. */ diff --git a/packages/drawer/src/views/DrawerView.tsx b/packages/drawer/src/views/DrawerView.tsx index 2f637708..c8683e51 100644 --- a/packages/drawer/src/views/DrawerView.tsx +++ b/packages/drawer/src/views/DrawerView.tsx @@ -168,7 +168,7 @@ export default function DrawerView({ it.type === 'drawer'))} gestureEnabled={gestureEnabled !== false} onOpen={handleDrawerOpen} onClose={handleDrawerClose} diff --git a/packages/routers/__tests__/DrawerRouter.test.tsx b/packages/routers/__tests__/DrawerRouter.test.tsx index e9c7f918..9ceecd5e 100644 --- a/packages/routers/__tests__/DrawerRouter.test.tsx +++ b/packages/routers/__tests__/DrawerRouter.test.tsx @@ -1,5 +1,5 @@ import { CommonActions } from '@react-navigation/core'; -import { DrawerRouter, DrawerActions } from '../src'; +import { DrawerRouter, DrawerActions, DrawerNavigationState } from '../src'; jest.mock('shortid', () => () => 'test'); @@ -17,14 +17,13 @@ it('gets initial state from route names and params with initialRouteName', () => ).toEqual({ index: 1, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'baz-test' }], stale: false, type: 'drawer', }); @@ -44,14 +43,13 @@ it('gets initial state from route names and params without initialRouteName', () ).toEqual({ index: 0, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-test' }], stale: false, type: 'drawer', }); @@ -81,14 +79,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 0, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-1', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-0' }], stale: false, type: 'drawer', }); @@ -103,14 +100,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 1, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-0', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'baz-0' }], stale: false, type: 'drawer', }); @@ -130,14 +126,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 2, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-1', name: 'baz', params: { answer: 42 } }, { key: 'qux-2', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'qux-2' }], stale: false, type: 'drawer', }); @@ -153,14 +148,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 2, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'qux-test' }], stale: false, type: 'drawer', }); @@ -169,8 +163,12 @@ it('gets rehydrated state from partial state', () => { router.getRehydratedState( { index: 1, - isDrawerOpen: true, - routeKeyHistory: ['bar-test', 'qux-test', 'foo-test'], + history: [ + { type: 'route', key: 'bar-test' }, + { type: 'route', key: 'qux-test' }, + { type: 'route', key: 'foo-test' }, + { type: 'drawer' }, + ], routes: [], }, options @@ -178,14 +176,17 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 1, key: 'drawer-test', - isDrawerOpen: true, - routeKeyHistory: ['bar-test', 'qux-test'], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [ + { type: 'route', key: 'bar-test' }, + { type: 'route', key: 'qux-test' }, + { type: 'drawer' }, + ], stale: false, type: 'drawer', }); @@ -194,17 +195,16 @@ it('gets rehydrated state from partial state', () => { it("doesn't rehydrate state if it's not stale", () => { const router = DrawerRouter({}); - const state = { + const state: DrawerNavigationState = { index: 0, key: 'drawer-test', - isDrawerOpen: true, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-test' }, { type: 'drawer' }], stale: false as const, type: 'drawer' as const, }; @@ -232,12 +232,11 @@ it('handles navigate action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], - isDrawerOpen: false, routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }, CommonActions.navigate('baz', { answer: 42 }), options @@ -248,12 +247,14 @@ it('handles navigate action', () => { key: 'root', index: 0, routeNames: ['baz', 'bar'], - isDrawerOpen: false, - routeKeyHistory: ['bar'], routes: [ { key: 'baz', name: 'baz', params: { answer: 42 } }, { key: 'bar', name: 'bar' }, ], + history: [ + { type: 'route', key: 'bar' }, + { type: 'route', key: 'baz' }, + ], }); }); @@ -272,12 +273,11 @@ it('handles navigate action with open drawer', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], - isDrawerOpen: true, routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], }, CommonActions.navigate('baz', { answer: 42 }), options @@ -288,12 +288,14 @@ it('handles navigate action with open drawer', () => { key: 'root', index: 0, routeNames: ['baz', 'bar'], - isDrawerOpen: false, - routeKeyHistory: ['bar'], routes: [ { key: 'baz', name: 'baz', params: { answer: 42 } }, { key: 'bar', name: 'bar' }, ], + history: [ + { type: 'route', key: 'bar' }, + { type: 'route', key: 'baz' }, + ], }); }); @@ -312,12 +314,11 @@ it('handles open drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], - isDrawerOpen: false, routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }, DrawerActions.openDrawer(), options @@ -328,26 +329,24 @@ it('handles open drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - isDrawerOpen: true, - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], }); - const state = { + const state: DrawerNavigationState = { stale: false as const, type: 'drawer' as const, key: 'root', index: 1, routeNames: ['baz', 'bar'], - isDrawerOpen: true, - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], }; expect( @@ -370,12 +369,11 @@ it('handles close drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], - isDrawerOpen: true, routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], }, DrawerActions.closeDrawer(), options @@ -386,26 +384,27 @@ it('handles close drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - isDrawerOpen: false, - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }); - const state = { + const state: DrawerNavigationState = { stale: false as const, type: 'drawer' as const, key: 'root', index: 1, routeNames: ['baz', 'bar'], - isDrawerOpen: false, - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [ + { type: 'route', key: 'bar' }, + { type: 'route', key: 'baz' }, + ], }; expect( @@ -428,12 +427,11 @@ it('handles toggle drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], - isDrawerOpen: true, routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], }, DrawerActions.toggleDrawer(), options @@ -444,12 +442,11 @@ it('handles toggle drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - isDrawerOpen: false, - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }); expect( @@ -460,12 +457,11 @@ it('handles toggle drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], - isDrawerOpen: false, routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }, DrawerActions.toggleDrawer(), options @@ -476,39 +472,38 @@ it('handles toggle drawer action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - isDrawerOpen: true, - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], }); }); -it('updates route key history on focus change', () => { +it('updates history on focus change', () => { const router = DrawerRouter({ backBehavior: 'history' }); - const state = { + const state: DrawerNavigationState = { index: 0, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], - routeNames: ['bar', 'baz', 'qux'], + routeNames: ['baz', 'bar'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz', params: { answer: 42 } }, { key: 'qux-0', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-0' }], stale: false as const, type: 'drawer' as const, }; - expect(router.getStateForRouteFocus(state, 'bar-0').routeKeyHistory).toEqual( - [] - ); + expect(router.getStateForRouteFocus(state, 'bar-0').history).toEqual([ + { type: 'route', key: 'bar-0' }, + ]); - expect(router.getStateForRouteFocus(state, 'baz-0').routeKeyHistory).toEqual([ - 'bar-0', + expect(router.getStateForRouteFocus(state, 'baz-0').history).toEqual([ + { type: 'route', key: 'bar-0' }, + { type: 'route', key: 'baz-0' }, ]); }); @@ -520,14 +515,13 @@ it('closes drawer on focus change', () => { { index: 0, key: 'drawer-test', - isDrawerOpen: false, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz' }, { key: 'qux-0', name: 'qux' }, ], + history: [{ type: 'route', key: 'bar-0' }], stale: false, type: 'drawer', }, @@ -535,15 +529,17 @@ it('closes drawer on focus change', () => { ) ).toEqual({ index: 1, - isDrawerOpen: false, key: 'drawer-test', - routeKeyHistory: ['bar-0'], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz' }, { key: 'qux-0', name: 'qux' }, ], + history: [ + { type: 'route', key: 'bar-0' }, + { type: 'route', key: 'baz-0' }, + ], stale: false, type: 'drawer', }); @@ -553,14 +549,13 @@ it('closes drawer on focus change', () => { { index: 0, key: 'drawer-test', - isDrawerOpen: true, - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz' }, { key: 'qux-0', name: 'qux' }, ], + history: [{ type: 'route', key: 'bar-0' }, { type: 'drawer' }], stale: false, type: 'drawer', }, @@ -568,15 +563,17 @@ it('closes drawer on focus change', () => { ) ).toEqual({ index: 1, - isDrawerOpen: false, key: 'drawer-test', - routeKeyHistory: ['bar-0'], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz' }, { key: 'qux-0', name: 'qux' }, ], + history: [ + { type: 'route', key: 'bar-0' }, + { type: 'route', key: 'baz-0' }, + ], stale: false, type: 'drawer', }); diff --git a/packages/routers/__tests__/TabRouter.test.tsx b/packages/routers/__tests__/TabRouter.test.tsx index 4ee91cec..94a91b2e 100644 --- a/packages/routers/__tests__/TabRouter.test.tsx +++ b/packages/routers/__tests__/TabRouter.test.tsx @@ -17,13 +17,13 @@ it('gets initial state from route names and params with initialRouteName', () => ).toEqual({ index: 1, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'baz-test' }], stale: false, type: 'tab', }); @@ -43,13 +43,13 @@ it('gets initial state from route names and params without initialRouteName', () ).toEqual({ index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-test' }], stale: false, type: 'tab', }); @@ -79,13 +79,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-1', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-0' }], stale: false, type: 'tab', }); @@ -100,13 +100,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 1, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-0', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'baz-0' }], stale: false, type: 'tab', }); @@ -126,13 +126,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 2, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-1', name: 'baz', params: { answer: 42 } }, { key: 'qux-2', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'qux-2' }], stale: false, type: 'tab', }); @@ -148,13 +148,13 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 2, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'qux-test' }], stale: false, type: 'tab', }); @@ -163,7 +163,11 @@ it('gets rehydrated state from partial state', () => { router.getRehydratedState( { index: 1, - routeKeyHistory: ['bar-test', 'qux-test', 'foo-test'], + history: [ + { type: 'route', key: 'bar-test' }, + { type: 'route', key: 'qux-test' }, + { type: 'route', key: 'foo-test' }, + ], routes: [], }, options @@ -171,13 +175,16 @@ it('gets rehydrated state from partial state', () => { ).toEqual({ index: 1, key: 'tab-test', - routeKeyHistory: ['bar-test', 'qux-test'], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [ + { type: 'route', key: 'bar-test' }, + { type: 'route', key: 'qux-test' }, + ], stale: false, type: 'tab', }); @@ -186,18 +193,18 @@ it('gets rehydrated state from partial state', () => { it("doesn't rehydrate state if it's not stale", () => { const router = TabRouter({}); - const state = { + const state: TabNavigationState = { index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], - stale: false as const, - type: 'tab' as const, + history: [{ type: 'route', key: 'bar-test' }], + stale: false, + type: 'tab', }; expect( @@ -216,13 +223,13 @@ it('gets state on route names change', () => { { index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'bar-test' }], stale: false, type: 'tab', }, @@ -237,7 +244,6 @@ it('gets state on route names change', () => { ).toEqual({ index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['qux', 'baz', 'foo', 'fiz'], routes: [ { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, @@ -245,6 +251,7 @@ it('gets state on route names change', () => { { key: 'foo-test', name: 'foo' }, { key: 'fiz-test', name: 'fiz', params: { fruit: 'apple' } }, ], + history: [{ type: 'route', key: 'qux-test' }], stale: false, type: 'tab', }); @@ -258,13 +265,13 @@ it('preserves focused route on route names change', () => { { index: 1, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'baz-test' }], stale: false, type: 'tab', }, @@ -279,7 +286,6 @@ it('preserves focused route on route names change', () => { ).toEqual({ index: 3, key: 'tab-test', - routeKeyHistory: [], routeNames: ['qux', 'foo', 'fiz', 'baz'], routes: [ { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, @@ -287,6 +293,7 @@ it('preserves focused route on route names change', () => { { key: 'fiz-test', name: 'fiz', params: { fruit: 'apple' } }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, ], + history: [{ type: 'route', key: 'baz-test' }], stale: false, type: 'tab', }); @@ -300,13 +307,13 @@ it('falls back to first route if route is removed on route names change', () => { index: 1, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-test', name: 'bar' }, { key: 'baz-test', name: 'baz', params: { answer: 42 } }, { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route', key: 'baz-test' }], stale: false, type: 'tab', }, @@ -321,13 +328,13 @@ it('falls back to first route if route is removed on route names change', () => ).toEqual({ index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['qux', 'foo', 'fiz'], routes: [ { key: 'qux-test', name: 'qux', params: { name: 'Jane' } }, { key: 'foo-test', name: 'foo' }, { key: 'fiz-test', name: 'fiz', params: { fruit: 'apple' } }, ], + history: [{ type: 'route', key: 'qux-test' }], stale: false, type: 'tab', }); @@ -348,11 +355,11 @@ it('handles navigate action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], routes: [ { key: 'baz-1', name: 'baz' }, { key: 'bar-1', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar-1' }], }, CommonActions.navigate({ key: 'bar-1', params: { answer: 42 } }), options @@ -363,11 +370,11 @@ it('handles navigate action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: ['bar-1'], routes: [ { key: 'baz-1', name: 'baz' }, { key: 'bar-1', name: 'bar', params: { answer: 42 } }, ], + history: [{ type: 'route', key: 'bar-1' }], }); expect( @@ -378,11 +385,11 @@ it('handles navigate action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }, CommonActions.navigate('baz', { answer: 42 }), options @@ -393,11 +400,14 @@ it('handles navigate action', () => { key: 'root', index: 0, routeNames: ['baz', 'bar'], - routeKeyHistory: ['bar'], routes: [ { key: 'baz', name: 'baz', params: { answer: 42 } }, { key: 'bar', name: 'bar' }, ], + history: [ + { type: 'route', key: 'bar' }, + { type: 'route', key: 'baz' }, + ], }); expect( @@ -408,11 +418,11 @@ it('handles navigate action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'bar' }], }, CommonActions.navigate('non-existent'), options @@ -435,11 +445,11 @@ it('handles jump to action', () => { key: 'root', index: 0, routeNames: ['baz', 'bar'], - routeKeyHistory: [], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [{ type: 'route', key: 'baz' }], }, TabActions.jumpTo('bar'), options @@ -450,213 +460,259 @@ it('handles jump to action', () => { key: 'root', index: 1, routeNames: ['baz', 'bar'], - routeKeyHistory: ['baz'], routes: [ { key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }, ], + history: [ + { type: 'route', key: 'baz' }, + { type: 'route', key: 'bar' }, + ], }); }); it('handles back action with backBehavior: history', () => { const router = TabRouter({ backBehavior: 'history' }); const options = { - routeNames: ['bar', 'baz'], + routeNames: ['bar', 'baz', 'qux'], routeParamList: {}, }; + let state = router.getInitialState(options); + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 0, - routeNames: ['baz', 'bar'], - routeKeyHistory: ['bar'], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual(null); + + state = router.getStateForAction( + state, + TabActions.jumpTo('qux'), + options + ) as TabNavigationState; + + expect( + router.getStateForAction(state, CommonActions.goBack(), options) ).toEqual({ stale: false, type: 'tab', - key: 'root', - index: 1, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], + key: 'tab-test', + index: 0, + routeNames: ['bar', 'baz', 'qux'], routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, + ], + history: [{ type: 'route', key: 'bar-test' }], + }); + + state = router.getStateForAction( + state, + TabActions.jumpTo('baz'), + options + ) as TabNavigationState; + + expect( + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual({ + stale: false, + type: 'tab', + key: 'tab-test', + index: 2, + routeNames: ['bar', 'baz', 'qux'], + routes: [ + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, + ], + history: [ + { type: 'route', key: 'bar-test' }, + { type: 'route', key: 'qux-test' }, ], }); + state = router.getStateForAction( + state, + TabActions.jumpTo('bar'), + options + ) as TabNavigationState; + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 0, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) - ).toBe(null); + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual({ + stale: false, + type: 'tab', + key: 'tab-test', + index: 1, + routeNames: ['bar', 'baz', 'qux'], + routes: [ + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, + ], + history: [ + { type: 'route', key: 'qux-test' }, + { type: 'route', key: 'baz-test' }, + ], + }); }); it('handles back action with backBehavior: order', () => { const router = TabRouter({ backBehavior: 'order' }); const options = { - routeNames: ['bar', 'baz'], + routeNames: ['bar', 'baz', 'qux'], routeParamList: {}, }; + let state = router.getInitialState(options); + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 1, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual(null); + + state = router.getStateForAction( + state, + TabActions.jumpTo('qux'), + options + ) as TabNavigationState; + + expect( + router.getStateForAction(state, CommonActions.goBack(), options) ).toEqual({ stale: false, type: 'tab', - key: 'root', - index: 0, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], + key: 'tab-test', + index: 1, + routeNames: ['bar', 'baz', 'qux'], routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, + ], + history: [ + { type: 'route', key: 'bar-test' }, + { type: 'route', key: 'baz-test' }, ], }); + state = router.getStateForAction( + state, + TabActions.jumpTo('baz'), + options + ) as TabNavigationState; + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 0, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) - ).toBe(null); + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual({ + stale: false, + type: 'tab', + key: 'tab-test', + index: 0, + routeNames: ['bar', 'baz', 'qux'], + routes: [ + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, + ], + history: [{ type: 'route', key: 'bar-test' }], + }); + + state = router.getStateForAction( + state, + TabActions.jumpTo('bar'), + options + ) as TabNavigationState; + + expect( + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual(null); }); it('handles back action with backBehavior: initialRoute', () => { - const router = TabRouter({ - backBehavior: 'initialRoute', - initialRouteName: 'bar', - }); - + const router = TabRouter({ backBehavior: 'initialRoute' }); const options = { - routeNames: ['bar', 'baz'], + routeNames: ['bar', 'baz', 'qux'], routeParamList: {}, }; + let state = router.getInitialState(options); + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 0, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual(null); + + state = router.getStateForAction( + state, + TabActions.jumpTo('qux'), + options + ) as TabNavigationState; + + expect( + router.getStateForAction(state, CommonActions.goBack(), options) ).toEqual({ stale: false, type: 'tab', - key: 'root', - index: 1, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], + key: 'tab-test', + index: 0, + routeNames: ['bar', 'baz', 'qux'], routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, ], + history: [{ type: 'route', key: 'bar-test' }], }); + state = router.getStateForAction( + state, + TabActions.jumpTo('baz'), + options + ) as TabNavigationState; + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 1, - routeNames: ['baz', 'bar'], - routeKeyHistory: [], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) - ).toBe(null); + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual({ + stale: false, + type: 'tab', + key: 'tab-test', + index: 0, + routeNames: ['bar', 'baz', 'qux'], + routes: [ + { key: 'bar-test', name: 'bar' }, + { key: 'baz-test', name: 'baz' }, + { key: 'qux-test', name: 'qux' }, + ], + history: [{ type: 'route', key: 'bar-test' }], + }); + + state = router.getStateForAction( + state, + TabActions.jumpTo('bar'), + options + ) as TabNavigationState; + + expect( + router.getStateForAction(state, CommonActions.goBack(), options) + ).toEqual(null); }); it('handles back action with backBehavior: none', () => { const router = TabRouter({ backBehavior: 'none' }); const options = { - routeNames: ['bar', 'baz'], + routeNames: ['bar', 'baz', 'qux'], routeParamList: {}, }; + let state = router.getInitialState(options); + + state = router.getStateForAction( + state, + TabActions.jumpTo('baz'), + options + ) as TabNavigationState; + expect( - router.getStateForAction( - { - stale: false, - type: 'tab', - key: 'root', - index: 0, - routeNames: ['bar', 'baz'], - routeKeyHistory: [], - routes: [ - { key: 'baz', name: 'baz' }, - { key: 'bar', name: 'bar' }, - ], - }, - CommonActions.goBack(), - options - ) + router.getStateForAction(state, CommonActions.goBack(), options) ).toEqual(null); }); @@ -670,8 +726,8 @@ it('updates route key history on navigate and jump to', () => { let state: TabNavigationState = { index: 1, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], + history: [{ type: 'route', key: 'baz-0' }], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz', params: { answer: 42 } }, @@ -681,15 +737,16 @@ it('updates route key history on navigate and jump to', () => { type: 'tab', }; - expect(state.routeKeyHistory).toEqual([]); - state = router.getStateForAction( state, TabActions.jumpTo('qux'), options ) as TabNavigationState; - expect(state.routeKeyHistory).toEqual(['baz-0']); + expect(state.history).toEqual([ + { type: 'route', key: 'baz-0' }, + { type: 'route', key: 'qux-0' }, + ]); state = router.getStateForAction( state, @@ -697,7 +754,11 @@ it('updates route key history on navigate and jump to', () => { options ) as TabNavigationState; - expect(state.routeKeyHistory).toEqual(['baz-0', 'qux-0']); + expect(state.history).toEqual([ + { type: 'route', key: 'baz-0' }, + { type: 'route', key: 'qux-0' }, + { type: 'route', key: 'bar-0' }, + ]); state = router.getStateForAction( state, @@ -705,7 +766,11 @@ it('updates route key history on navigate and jump to', () => { options ) as TabNavigationState; - expect(state.routeKeyHistory).toEqual(['qux-0', 'bar-0']); + expect(state.history).toEqual([ + { type: 'route', key: 'qux-0' }, + { type: 'route', key: 'bar-0' }, + { type: 'route', key: 'baz-0' }, + ]); state = router.getStateForAction( state, @@ -713,7 +778,10 @@ it('updates route key history on navigate and jump to', () => { options ) as TabNavigationState; - expect(state.routeKeyHistory).toEqual(['qux-0']); + expect(state.history).toEqual([ + { type: 'route', key: 'qux-0' }, + { type: 'route', key: 'bar-0' }, + ]); state = router.getStateForAction( state, @@ -721,7 +789,7 @@ it('updates route key history on navigate and jump to', () => { options ) as TabNavigationState; - expect(state.routeKeyHistory).toEqual([]); + expect(state.history).toEqual([{ type: 'route', key: 'qux-0' }]); }); it('updates route key history on focus change', () => { @@ -730,22 +798,23 @@ it('updates route key history on focus change', () => { const state = { index: 0, key: 'tab-test', - routeKeyHistory: [], routeNames: ['bar', 'baz', 'qux'], routes: [ { key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz', params: { answer: 42 } }, { key: 'qux-0', name: 'qux', params: { name: 'Jane' } }, ], + history: [{ type: 'route' as const, key: 'bar-0' }], stale: false as const, type: 'tab' as const, }; - expect(router.getStateForRouteFocus(state, 'bar-0').routeKeyHistory).toEqual( - [] - ); + expect(router.getStateForRouteFocus(state, 'bar-0').history).toEqual([ + { type: 'route', key: 'bar-0' }, + ]); - expect(router.getStateForRouteFocus(state, 'baz-0').routeKeyHistory).toEqual([ - 'bar-0', + expect(router.getStateForRouteFocus(state, 'baz-0').history).toEqual([ + { type: 'route', key: 'bar-0' }, + { type: 'route', key: 'baz-0' }, ]); }); diff --git a/packages/routers/src/DrawerRouter.tsx b/packages/routers/src/DrawerRouter.tsx index 370f5ab0..46fc88ab 100644 --- a/packages/routers/src/DrawerRouter.tsx +++ b/packages/routers/src/DrawerRouter.tsx @@ -1,5 +1,5 @@ import shortid from 'shortid'; -import { CommonAction, Router } from '@react-navigation/core'; +import { CommonAction, Router, PartialState } from '@react-navigation/core'; import TabRouter, { TabActions, TabActionType, @@ -17,15 +17,18 @@ export type DrawerActionType = export type DrawerRouterOptions = TabRouterOptions; -export type DrawerNavigationState = Omit & { +export type DrawerNavigationState = Omit< + TabNavigationState, + 'type' | 'history' +> & { /** * Type of the router, in this case, it's drawer. */ type: 'drawer'; /** - * Whether the drawer is open or closed. + * List of previously visited route keys and drawer open status. */ - isDrawerOpen: boolean; + history: ({ type: 'route'; key: string } | { type: 'drawer' })[]; }; export const DrawerActions = { @@ -41,6 +44,32 @@ export const DrawerActions = { }, }; +const isDrawerOpen = ( + state: DrawerNavigationState | PartialState +) => Boolean(state.history?.find(it => it.type === 'drawer')); + +const openDrawer = (state: DrawerNavigationState): DrawerNavigationState => { + if (isDrawerOpen(state)) { + return state; + } + + return { + ...state, + history: [...state.history, { type: 'drawer' }], + }; +}; + +const closeDrawer = (state: DrawerNavigationState): DrawerNavigationState => { + if (!isDrawerOpen(state)) { + return state; + } + + return { + ...state, + history: state.history.filter(it => it.type !== 'drawer'), + }; +}; + export default function DrawerRouter( options: DrawerRouterOptions ): Router { @@ -55,24 +84,13 @@ export default function DrawerRouter( type: 'drawer', getInitialState({ routeNames, routeParamList }) { - const index = - options.initialRouteName === undefined - ? 0 - : routeNames.indexOf(options.initialRouteName); + const state = router.getInitialState({ routeNames, routeParamList }); return { + ...state, stale: false, type: 'drawer', key: `drawer-${shortid()}`, - index, - routeNames, - routeKeyHistory: [], - routes: routeNames.map(name => ({ - name, - key: `${name}-${shortid()}`, - params: routeParamList[name], - })), - isDrawerOpen: false, }; }, @@ -81,84 +99,46 @@ export default function DrawerRouter( return partialState; } - const state = router.getRehydratedState(partialState, { + let state = router.getRehydratedState(partialState, { routeNames, routeParamList, }); + if (isDrawerOpen(partialState)) { + state = openDrawer(state); + } + return { ...state, type: 'drawer', key: `drawer-${shortid()}`, - isDrawerOpen: - typeof partialState.isDrawerOpen === 'boolean' - ? partialState.isDrawerOpen - : false, }; }, getStateForRouteFocus(state, key) { - const index = state.routes.findIndex(r => r.key === key); + const result = router.getStateForRouteFocus(state, key); - const result = - index === -1 || index === state.index - ? state - : router.getStateForRouteFocus(state, key); - - if (result.isDrawerOpen) { - return { - ...result, - isDrawerOpen: false, - }; - } - - return result; + return closeDrawer(result); }, getStateForAction(state, action, options) { switch (action.type) { case 'OPEN_DRAWER': - if (state.isDrawerOpen) { - return state; - } - - return { - ...state, - isDrawerOpen: true, - }; + return openDrawer(state); case 'CLOSE_DRAWER': - if (!state.isDrawerOpen) { - return state; - } - - return { - ...state, - isDrawerOpen: false, - }; + return closeDrawer(state); case 'TOGGLE_DRAWER': - return { - ...state, - isDrawerOpen: !state.isDrawerOpen, - }; + if (isDrawerOpen(state)) { + return closeDrawer(state); + } - case 'NAVIGATE': - return router.getStateForAction( - { - ...state, - isDrawerOpen: false, - }, - action, - options - ); + return openDrawer(state); case 'GO_BACK': - if (state.isDrawerOpen) { - return { - ...state, - isDrawerOpen: false, - }; + if (isDrawerOpen(state)) { + return closeDrawer(state); } return router.getStateForAction(state, action, options); diff --git a/packages/routers/src/TabRouter.tsx b/packages/routers/src/TabRouter.tsx index 9e4d38d0..57a6ce79 100644 --- a/packages/routers/src/TabRouter.tsx +++ b/packages/routers/src/TabRouter.tsx @@ -16,11 +16,13 @@ export type TabActionType = { target?: string; }; +export type BackBehavior = 'initialRoute' | 'order' | 'history' | 'none'; + export type TabRouterOptions = DefaultRouterOptions & { - backBehavior?: 'initialRoute' | 'order' | 'history' | 'none'; + backBehavior?: BackBehavior; }; -export type TabNavigationState = NavigationState & { +export type TabNavigationState = Omit & { /** * Type of the router, in this case, it's tab. */ @@ -28,26 +30,64 @@ export type TabNavigationState = NavigationState & { /** * List of previously visited route keys. */ - routeKeyHistory: string[]; + history: { type: 'route'; key: string }[]; }; +const TYPE_ROUTE = 'route' as const; + export const TabActions = { jumpTo(name: string, params?: object): TabActionType { return { type: 'JUMP_TO', payload: { name, params } }; }, }; -const changeIndex = (state: TabNavigationState, index: number) => { - const previousKey = state.routes[state.index].key; - const currentKey = state.routes[index].key; - const routeKeyHistory = state.routeKeyHistory - .filter(key => key !== currentKey && key !== previousKey) - .concat(previousKey); +const getRouteHistory = ( + routes: Route[], + index: number, + backBehavior: BackBehavior +) => { + const history = [{ type: TYPE_ROUTE, key: routes[index].key }]; + + switch (backBehavior) { + case 'initialRoute': + if (index !== 0) { + history.unshift({ type: TYPE_ROUTE, key: routes[0].key }); + } + break; + case 'order': + for (let i = index; i > 0; i--) { + history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key }); + } + break; + case 'history': + // The history will fill up on navigation + break; + } + + return history; +}; + +const changeIndex = ( + state: TabNavigationState, + index: number, + backBehavior: BackBehavior +) => { + let history; + + if (backBehavior === 'history') { + const currentKey = state.routes[index].key; + + history = state.history + .filter(it => (it.type === 'route' ? it.key !== currentKey : false)) + .concat({ type: TYPE_ROUTE, key: currentKey }); + } else { + history = getRouteHistory(state.routes, index, backBehavior); + } return { ...state, index, - routeKeyHistory, + history, }; }; @@ -66,18 +106,22 @@ export default function TabRouter({ ? routeNames.indexOf(initialRouteName) : 0; + const routes = routeNames.map(name => ({ + name, + key: `${name}-${shortid()}`, + params: routeParamList[name], + })); + + const history = getRouteHistory(routes, index, backBehavior); + return { stale: false, type: 'tab', key: `tab-${shortid()}`, index, routeNames, - routeKeyHistory: [], - routes: routeNames.map(name => ({ - name, - key: `${name}-${shortid()}`, - params: routeParamList[name], - })), + history, + routes, }; }, @@ -122,9 +166,13 @@ export default function TabRouter({ routes.length - 1 ); - const routeKeyHistory = state.routeKeyHistory - ? state.routeKeyHistory.filter(key => routes.find(r => r.key === key)) - : []; + let history = state.history?.filter(it => + routes.find(r => r.key === it.key) + ); + + if (!history?.length) { + history = getRouteHistory(routes, index, backBehavior); + } return { stale: false, @@ -132,7 +180,7 @@ export default function TabRouter({ key: `tab-${shortid()}`, index, routeNames, - routeKeyHistory, + history, routes, }; }, @@ -152,8 +200,17 @@ export default function TabRouter({ routeNames.indexOf(state.routes[state.index].name) ); + let history = state.history.filter(it => + routes.find(r => r.key === it.key) + ); + + if (!history.length) { + history = getRouteHistory(routes, index, backBehavior); + } + return { ...state, + history, routeNames, routes, index, @@ -167,7 +224,7 @@ export default function TabRouter({ return state; } - return changeIndex(state, index); + return changeIndex(state, index, backBehavior); }, getStateForAction(state, action) { @@ -208,56 +265,32 @@ export default function TabRouter({ ) : state.routes, }, - index + index, + backBehavior ); } - case 'GO_BACK': - switch (backBehavior) { - case 'initialRoute': { - const index = initialRouteName - ? state.routeNames.indexOf(initialRouteName) - : 0; - - if (index === -1 || index === state.index) { - return null; - } - - return { ...state, index }; - } - - case 'order': - if (state.index === 0) { - return null; - } - - return { - ...state, - index: state.index - 1, - }; - - case 'history': { - const previousKey = - state.routeKeyHistory[state.routeKeyHistory.length - 1]; - const index = state.routes.findIndex( - route => route.key === previousKey - ); - - if (index === -1) { - return null; - } - - return { - ...state, - routeKeyHistory: state.routeKeyHistory.slice(0, -1), - index, - }; - } - - default: - return null; + case 'GO_BACK': { + if (state.history.length === 1) { + return null; } + const previousKey = state.history[state.history.length - 2].key; + const index = state.routes.findIndex( + route => route.key === previousKey + ); + + if (index === -1) { + return null; + } + + return { + ...state, + history: state.history.slice(0, -1), + index, + }; + } + default: return BaseRouter.getStateForAction(state, action); }