Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions packages/vue-router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ export const createIonRouter = (
router.push(routerLink);
};

const resetTab = (tab: string) => {
const resetTab = (tab: string, originalHref?: string) => {
/**
* Resetting the tab should go back
* to the initial view in the tab stack.
Expand All @@ -570,7 +570,29 @@ export const createIonRouter = (
*/
const routeInfo = locationHistory.getFirstRouteInfoForTab(tab);
if (routeInfo) {
router.go(routeInfo.position - currentHistoryPosition);
const delta = routeInfo.position - currentHistoryPosition;
if (delta !== 0) {
router.go(delta);
return;
}
/**
* The first history entry for this tab is the current entry,
* so there's nothing earlier to traverse back to. Happens
* after a deep load onto a tab child or an external navigation
* that reset the SPA history. Replace with `originalHref` so
* no stale child entry stays in browser history.
*/
if (originalHref && routeInfo.pathname !== originalHref) {
handleNavigate(originalHref, "pop", "back", undefined, tab);
}
return;
}
/**
* No routeInfo for this tab yet. Replace the current entry
* with `originalHref` so the tab has a root to reset to.
*/
if (originalHref) {
handleNavigate(originalHref, "pop", "back", undefined, tab);
}
};

Expand Down
23 changes: 13 additions & 10 deletions packages/vue/src/components/IonTabBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,29 +182,31 @@ export const IonTabBar = defineComponent({
}
});

if (activeTab && prevActiveTab) {
const prevHref = this.$data.tabState.tabs[prevActiveTab].currentHref;
if (activeTab && currentRoute?.pathname) {
const prevHref = prevActiveTab
? this.$data.tabState.tabs[prevActiveTab].currentHref
: undefined;
/**
* If the tabs change or the url changes,
* update the currentHref for the active tab.
* Ex: url changes from /tabs/tab1 --> /tabs/tab1/child
* Ex: url changes from /tabs/tab1 to /tabs/tab1/child.
* If we went to tab2 then back to tab1, we should
* land on /tabs/tab1/child instead of /tabs/tab1.
*
* Also runs on initial setup so a deep-loaded tab child
* records its real pathname instead of `originalHref`.
*/
if (
activeTab !== prevActiveTab ||
prevHref !== currentRoute?.pathname
) {
if (activeTab !== prevActiveTab || prevHref !== currentRoute.pathname) {
/**
* By default the search is `undefined` in Ionic Vue,
* but Vue Router can set the search to the empty string.
* We check for truthy here because empty string is falsy
* and currentRoute.search cannot ever be a boolean.
*/
const search = currentRoute?.search ? `?${currentRoute.search}` : "";
const search = currentRoute.search ? `?${currentRoute.search}` : "";
tabs[activeTab] = {
...tabs[activeTab],
currentHref: currentRoute?.pathname + search,
currentHref: currentRoute.pathname + search,
};
}

Expand All @@ -213,7 +215,8 @@ export const IonTabBar = defineComponent({
* set the previous tab back to its original href.
*/
if (
currentRoute?.routerAction === "pop" &&
prevActiveTab &&
currentRoute.routerAction === "pop" &&
activeTab !== prevActiveTab
) {
tabs[prevActiveTab] = {
Expand Down
2 changes: 1 addition & 1 deletion packages/vue/src/components/IonTabButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const IonTabButton = /*@__PURE__*/ defineComponent({
if (ionRouter !== null) {
if (prevActiveTab === tab) {
if (originalHref !== currentHref) {
ionRouter.resetTab(tab);
ionRouter.resetTab(tab, originalHref);
}
} else {
ionRouter.changeTab(tab, currentHref);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { test, expect } from './utils/test-base';
import { ionPageVisible, tabClick } from './utils/test-utils';

/**
* Tab navigation when the user lands directly on a tab child route
* (e.g. browser refresh or back from an external URL). The tab's
* locationHistory only contains the deep-loaded entry, so resetting or
* pushing within the tab has to fall back to the tab's originalHref
* instead of router.go on a non-existent prior entry.
*
* @see https://github.com/ionic-team/ionic-framework/issues/29705
*/
test.describe('Tabs: deep-loaded child', () => {
test('child-to-child push from deep-loaded tab child renders the next page', async ({ page }) => {
await page.goto('/tabs/tab1/childone');
await ionPageVisible(page, 'tab1childone');

await page.locator('.ion-page[data-pageid="tab1childone"] #child-two').click();

await expect.poll(() => new URL(page.url()).pathname).toBe('/tabs/tab1/childtwo');
await ionPageVisible(page, 'tab1childtwo');
});

test('clicking tab button on deep-loaded tab child returns to tab root', async ({ page }) => {
await page.goto('/tabs/tab1/childone');
await ionPageVisible(page, 'tab1childone');

await tabClick(page, 'tab1');

await expect.poll(() => new URL(page.url()).pathname).toBe('/tabs/tab1');
await ionPageVisible(page, 'tab1');

/**
* The fallback uses replace semantics, so the deep-loaded child
* is removed from history rather than stacked on top of the tab
* root. A push fallback would leave an extra entry behind.
*/
const historyLength = await page.evaluate(() => window.history.length);
expect(historyLength).toBeLessThanOrEqual(2);
});

test('deep-load works on tabs other than tab1', async ({ page }) => {
await page.goto('/tabs/tab2/childone');
await ionPageVisible(page, 'tab2childone');

await tabClick(page, 'tab2');

await expect.poll(() => new URL(page.url()).pathname).toBe('/tabs/tab2');
await ionPageVisible(page, 'tab2');
});
});
Loading