From 06bd3c74c5b007aea722834d9f3b042674336a33 Mon Sep 17 00:00:00 2001 From: ShaneK Date: Wed, 30 Sep 2026 13:45:19 -0700 Subject: [PATCH] fix(vue-router): return to tab root on active tab tap with memory history --- packages/vue-router/src/router.ts | 10 ++ .../vue/test/base/tests/unit/memory.spec.ts | 114 ++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts index 7045dad5947..940f4c2aa42 100644 --- a/packages/vue-router/src/router.ts +++ b/packages/vue-router/src/router.ts @@ -776,6 +776,16 @@ export const createIonRouter = ( const routeInfo = locationHistory.getFirstRouteInfoForTab(tab); if (routeInfo) { const delta = routeInfo.position! - currentHistoryPosition; + /** + * Memory history doesn't store a position in `history.state`, so + * `delta` is NaN and there's nothing to traverse. Replace instead. + */ + if (Number.isNaN(delta)) { + if (originalHref) { + handleNavigate(originalHref, "pop", "back", undefined, tab); + } + return; + } if (delta !== 0) { router.go(delta); return; diff --git a/packages/vue/test/base/tests/unit/memory.spec.ts b/packages/vue/test/base/tests/unit/memory.spec.ts index 5a109446f8e..a1a482d482d 100644 --- a/packages/vue/test/base/tests/unit/memory.spec.ts +++ b/packages/vue/test/base/tests/unit/memory.spec.ts @@ -10,6 +10,9 @@ import { IonApp, IonRouterOutlet, IonPage, + IonTabs, + IonTabBar, + IonTabButton, } from '@ionic/vue'; import { waitForRouter } from './utils'; @@ -112,4 +115,115 @@ describe('createMemoryHistory', () => { await waitForRouter(); expect(router.currentRoute.value.path).toBe('/page3'); }); + + // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/29785 + describe('tapping the active tab', () => { + const Tabs = { + components: { IonPage, IonTabs, IonTabBar, IonTabButton, IonRouterOutlet }, + template: ` + + + + + Tab 1 + Tab 2 + + + + `, + }; + const Tab1 = { + components: { IonPage }, + template: 'Tab 1', + }; + const Tab1Child = { + components: { IonPage }, + template: 'Tab 1 Child', + }; + const Tab2 = { + components: { IonPage }, + template: 'Tab 2', + }; + + const createTabsRouter = () => + createRouter({ + history: createMemoryHistory(process.env.BASE_URL), + routes: [ + { path: '/', redirect: '/tabs/tab1' }, + { + path: '/tabs/', + component: Tabs, + children: [ + { path: 'tab1', component: Tab1 }, + { path: 'tab1/child', component: Tab1Child }, + { path: 'tab2', component: Tab2 }, + ], + }, + ], + }); + + const tapTab = async (wrapper: ReturnType, tab: string) => { + const button = wrapper + .findAllComponents(IonTabButton) + .find((b) => b.props('tab') === tab)!; + await button.trigger('click'); + await waitForRouter(); + }; + + it('should return to the tab root from a child page', async () => { + const router = createTabsRouter(); + + router.push('/tabs/tab1'); + await router.isReady(); + const wrapper = mount(App, { + global: { plugins: [router, IonicVue] }, + }); + await waitForRouter(); + + router.push('/tabs/tab1/child'); + await waitForRouter(); + expect(router.currentRoute.value.path).toBe('/tabs/tab1/child'); + + await tapTab(wrapper, 'tab1'); + + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + }); + + it('should return to the tab root when the app started on a child page', async () => { + const router = createTabsRouter(); + + router.push('/tabs/tab1/child'); + await router.isReady(); + const wrapper = mount(App, { + global: { plugins: [router, IonicVue] }, + }); + await waitForRouter(); + + await tapTab(wrapper, 'tab1'); + + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + }); + + it('should replace the child page entry rather than push over it', async () => { + const router = createTabsRouter(); + + router.push('/tabs/tab1'); + await router.isReady(); + const wrapper = mount(App, { + global: { plugins: [router, IonicVue] }, + }); + await waitForRouter(); + + router.push('/tabs/tab1/child'); + await waitForRouter(); + + await tapTab(wrapper, 'tab1'); + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + + router.back(); + await waitForRouter(); + + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + }); + }); })