Skip to content

Commit edbc304

Browse files
authored
fix(vue-router): reset tab with memory history (#31511)
Issue number: resolves #29785 --------- ## What is the current behavior? Currently, with `createMemoryHistory`, tapping the active tab from a child page leaves the child on screen. Memory history doesn't store a position in `history.state`, so the offset `resetTab()` computes is `NaN` and `router.go(NaN)` goes nowhere. On 9.x it also throws `Cannot read properties of undefined (reading '0')`. ## What is the new behavior? When there's no position to traverse, `resetTab()` now replaces the current entry with the tab's `href`, the same fallback it already uses after a deep load. It uses the tab's `href` rather than the first route recorded for the tab, because after a deep load onto a child that first route is the child itself. Web and hash history always have a numeric position, so they're unaffected. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The new tests are in `memory.spec.ts`, since the Vue test app uses web history and can't reproduce this. They cover resetting from a child page, resetting after a deep load onto a child, and replacing the child entry rather than pushing over it.
1 parent 643584b commit edbc304

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

‎packages/vue-router/src/router.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,16 @@ export const createIonRouter = (
827827
const routeInfo = locationHistory.getFirstRouteInfoForTab(tab);
828828
if (routeInfo) {
829829
const delta = routeInfo.position! - currentHistoryPosition;
830+
/**
831+
* Memory history doesn't store a position in `history.state`, so
832+
* `delta` is NaN and there's nothing to traverse. Replace instead.
833+
*/
834+
if (Number.isNaN(delta)) {
835+
if (originalHref) {
836+
handleNavigate(originalHref, "pop", "back", undefined, tab);
837+
}
838+
return;
839+
}
830840
if (delta !== 0) {
831841
router.go(delta);
832842
return;

‎packages/vue/test/base/tests/unit/memory.spec.ts‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
IonApp,
1111
IonRouterOutlet,
1212
IonPage,
13+
IonTabs,
14+
IonTabBar,
15+
IonTabButton,
1316
} from '@ionic/vue';
1417
import { waitForRouter } from './utils';
1518

@@ -112,4 +115,115 @@ describe('createMemoryHistory', () => {
112115
await waitForRouter();
113116
expect(router.currentRoute.value.path).toBe('/page3');
114117
});
118+
119+
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/29785
120+
describe('tapping the active tab', () => {
121+
const Tabs = {
122+
components: { IonPage, IonTabs, IonTabBar, IonTabButton, IonRouterOutlet },
123+
template: `
124+
<ion-page>
125+
<ion-tabs>
126+
<ion-router-outlet></ion-router-outlet>
127+
<ion-tab-bar slot="bottom">
128+
<ion-tab-button tab="tab1" href="/tabs/tab1">Tab 1</ion-tab-button>
129+
<ion-tab-button tab="tab2" href="/tabs/tab2">Tab 2</ion-tab-button>
130+
</ion-tab-bar>
131+
</ion-tabs>
132+
</ion-page>
133+
`,
134+
};
135+
const Tab1 = {
136+
components: { IonPage },
137+
template: '<ion-page>Tab 1</ion-page>',
138+
};
139+
const Tab1Child = {
140+
components: { IonPage },
141+
template: '<ion-page>Tab 1 Child</ion-page>',
142+
};
143+
const Tab2 = {
144+
components: { IonPage },
145+
template: '<ion-page>Tab 2</ion-page>',
146+
};
147+
148+
const createTabsRouter = () =>
149+
createRouter({
150+
history: createMemoryHistory(process.env.BASE_URL),
151+
routes: [
152+
{ path: '/', redirect: '/tabs/tab1' },
153+
{
154+
path: '/tabs/',
155+
component: Tabs,
156+
children: [
157+
{ path: 'tab1', component: Tab1 },
158+
{ path: 'tab1/child', component: Tab1Child },
159+
{ path: 'tab2', component: Tab2 },
160+
],
161+
},
162+
],
163+
});
164+
165+
const tapTab = async (wrapper: ReturnType<typeof mount>, tab: string) => {
166+
const button = wrapper
167+
.findAllComponents(IonTabButton)
168+
.find((b) => b.props('tab') === tab)!;
169+
await button.trigger('click');
170+
await waitForRouter();
171+
};
172+
173+
it('should return to the tab root from a child page', async () => {
174+
const router = createTabsRouter();
175+
176+
router.push('/tabs/tab1');
177+
await router.isReady();
178+
const wrapper = mount(App, {
179+
global: { plugins: [router, IonicVue] },
180+
});
181+
await waitForRouter();
182+
183+
router.push('/tabs/tab1/child');
184+
await waitForRouter();
185+
expect(router.currentRoute.value.path).toBe('/tabs/tab1/child');
186+
187+
await tapTab(wrapper, 'tab1');
188+
189+
expect(router.currentRoute.value.path).toBe('/tabs/tab1');
190+
});
191+
192+
it('should return to the tab root when the app started on a child page', async () => {
193+
const router = createTabsRouter();
194+
195+
router.push('/tabs/tab1/child');
196+
await router.isReady();
197+
const wrapper = mount(App, {
198+
global: { plugins: [router, IonicVue] },
199+
});
200+
await waitForRouter();
201+
202+
await tapTab(wrapper, 'tab1');
203+
204+
expect(router.currentRoute.value.path).toBe('/tabs/tab1');
205+
});
206+
207+
it('should replace the child page entry rather than push over it', async () => {
208+
const router = createTabsRouter();
209+
210+
router.push('/tabs/tab1');
211+
await router.isReady();
212+
const wrapper = mount(App, {
213+
global: { plugins: [router, IonicVue] },
214+
});
215+
await waitForRouter();
216+
217+
router.push('/tabs/tab1/child');
218+
await waitForRouter();
219+
220+
await tapTab(wrapper, 'tab1');
221+
expect(router.currentRoute.value.path).toBe('/tabs/tab1');
222+
223+
router.back();
224+
await waitForRouter();
225+
226+
expect(router.currentRoute.value.path).toBe('/tabs/tab1');
227+
});
228+
});
115229
})

0 commit comments

Comments
 (0)