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
20 changes: 14 additions & 6 deletions packages/react-router/src/ReactRouter/IonRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,15 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
* tab and use its `pushedByRoute`.
*/
const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
// This helps maintain correct back stack behavior within tabs.
// If this is the first time entering this tab from a different context,
// use the leaving route's pathname as the pushedByRoute to maintain the back stack.
routeInfo.pushedByRoute = lastRoute?.pushedByRoute ?? leavingLocationInfo.pathname;
/**
* Tab bar switches (direction 'none') should not create cross-tab back
* navigation. Only inherit pushedByRoute from the tab's own history.
*/
if (routeInfo.routeDirection === 'none') {
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
} else {
routeInfo.pushedByRoute = lastRoute?.pushedByRoute ?? leavingLocationInfo.pathname;
}
// Triggered by `history.replace()` or a `<Redirect />` component, etc.
} else if (routeInfo.routeAction === 'replace') {
/**
Expand Down Expand Up @@ -465,10 +470,13 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
/**
* No `pushedByRoute`
* e.g., initial page load
* No `pushedByRoute` (e.g., initial page load or tab root).
* Tabs with no back history should not navigate.
*/
} else {
if (routeInfo && routeInfo.tab) {
return;
}
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
};
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router/test/base/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { SwipeToGoBack } from './pages/swipe-to-go-back/SwipToGoBack';
import TabsContext from './pages/tab-context/TabContext';
import Tabs from './pages/tabs/Tabs';
import TabsSecondary from './pages/tabs/TabsSecondary';
import TabHistoryIsolation from './pages/tab-history-isolation/TabHistoryIsolation';
import Overlays from './pages/overlays/Overlays';

setupIonicReact();
Expand All @@ -66,6 +67,7 @@ const App: React.FC = () => {
<Route path="/dynamic-ionpage-classnames" element={<DynamicIonpageClassnames />} />
<Route path="/tabs/*" element={<Tabs />} />
<Route path="/tabs-secondary/*" element={<TabsSecondary />} />
<Route path="/tab-history-isolation/*" element={<TabHistoryIsolation />} />
<Route path="/refs/*" element={<Refs />} />
<Route path="/overlays" element={<Overlays />} />
<Route path="/params/:id" element={<Params />} />
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/test/base/src/pages/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ const Main: React.FC = () => {
<IonItem routerLink="/tabs" id="go-to-tabs">
<IonLabel>Tabs</IonLabel>
</IonItem>
<IonItem routerLink="/tab-history-isolation">
<IonLabel>Tab History Isolation</IonLabel>
</IonItem>
<IonItem routerLink="/params/0">
<IonLabel>Params</IonLabel>
</IonItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
IonTabs,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonBackButton,
IonTitle,
IonContent,
IonButton,
} from '@ionic/react';
import { triangle, square, ellipse } from 'ionicons/icons';
import React from 'react';
import { Route, Navigate } from 'react-router';

const TabHistoryIsolation: React.FC = () => {
return (
<IonTabs>
<IonRouterOutlet id="tab-history-isolation">
<Route index element={<Navigate to="/tab-history-isolation/a" replace />} />
<Route path="a" element={<TabA />} />
<Route path="b" element={<TabB />} />
<Route path="c" element={<TabC />} />
<Route path="a/details" element={<TabADetails />} />
<Route path="b/details" element={<TabBDetails />} />
<Route path="c/details" element={<TabCDetails />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab-a" href="/tab-history-isolation/a">
<IonIcon icon={triangle} />
<IonLabel>Tab A</IonLabel>
</IonTabButton>
<IonTabButton tab="tab-b" href="/tab-history-isolation/b">
<IonIcon icon={square} />
<IonLabel>Tab B</IonLabel>
</IonTabButton>
<IonTabButton tab="tab-c" href="/tab-history-isolation/c">
<IonIcon icon={ellipse} />
<IonLabel>Tab C</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};

const TabA = () => {
return (
<IonPage data-pageid="tab-a">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab A</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab A
<IonButton routerLink="/tab-history-isolation/a/details" id="go-to-a-details">
Go to A Details
</IonButton>
</IonContent>
</IonPage>
);
};

const TabB = () => {
return (
<IonPage data-pageid="tab-b">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab B</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab B
<IonButton routerLink="/tab-history-isolation/b/details" id="go-to-b-details">
Go to B Details
</IonButton>
</IonContent>
</IonPage>
);
};

const TabC = () => {
return (
<IonPage data-pageid="tab-c">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab C</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab C
<IonButton routerLink="/tab-history-isolation/c/details" id="go-to-c-details">
Go to C Details
</IonButton>
</IonContent>
</IonPage>
);
};

const TabADetails = () => {
return (
<IonPage data-pageid="tab-a-details">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab A Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>Tab A Details</IonContent>
</IonPage>
);
};

const TabBDetails = () => {
return (
<IonPage data-pageid="tab-b-details">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab B Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>Tab B Details</IonContent>
</IonPage>
);
};

const TabCDetails = () => {
return (
<IonPage data-pageid="tab-c-details">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab C Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>Tab C Details</IonContent>
</IonPage>
);
};

export default TabHistoryIsolation;
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
const port = 3000;

describe('Tab History Isolation', () => {
it('should NOT navigate back to previous tab when using back button after tab bar switch', () => {
cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
cy.ionPageVisible('tab-a');

cy.ionTabClick('Tab B');
cy.ionPageHidden('tab-a');
cy.ionPageVisible('tab-b');

cy.get(`div.ion-page[data-pageid=tab-b]`)
.find('ion-back-button')
.click({ force: true });

cy.wait(500);

cy.ionPageVisible('tab-b');
cy.ionPageHidden('tab-a');
cy.url().should('include', '/tab-history-isolation/b');
});

it('should NOT allow back navigation through multiple tab switches', () => {
cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
cy.ionPageVisible('tab-a');

cy.ionTabClick('Tab B');
cy.ionPageHidden('tab-a');
cy.ionPageVisible('tab-b');

cy.ionTabClick('Tab C');
cy.ionPageHidden('tab-b');
cy.ionPageVisible('tab-c');

cy.get(`div.ion-page[data-pageid=tab-c]`)
.find('ion-back-button')
.click({ force: true });

cy.wait(500);

cy.ionPageVisible('tab-c');
cy.url().should('include', '/tab-history-isolation/c');
});

it('should navigate back within the same tab when using back button', () => {
cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
cy.ionPageVisible('tab-a');

cy.get('#go-to-a-details').click();
cy.ionPageHidden('tab-a');
cy.ionPageVisible('tab-a-details');

cy.ionBackClick('tab-a-details');
cy.ionPageDoesNotExist('tab-a-details');
cy.ionPageVisible('tab-a');

cy.url().should('include', '/tab-history-isolation/a');
cy.url().should('not.include', '/details');
});

it('should only navigate back within current tab after switching tabs and navigating', () => {
cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
cy.ionPageVisible('tab-a');

cy.ionTabClick('Tab B');
cy.ionPageHidden('tab-a');
cy.ionPageVisible('tab-b');

cy.get('#go-to-b-details').click();
cy.ionPageHidden('tab-b');
cy.ionPageVisible('tab-b-details');

cy.ionBackClick('tab-b-details');
cy.ionPageDoesNotExist('tab-b-details');
cy.ionPageVisible('tab-b');

cy.url().should('include', '/tab-history-isolation/b');
cy.url().should('not.include', '/details');

cy.get(`div.ion-page[data-pageid=tab-b]`)
.find('ion-back-button')
.click({ force: true });

cy.wait(500);

cy.ionPageVisible('tab-b');
cy.url().should('include', '/tab-history-isolation/b');
});

it('should preserve tab history when switching away and back', () => {
cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
cy.ionPageVisible('tab-a');

cy.get('#go-to-a-details').click();
cy.ionPageHidden('tab-a');
cy.ionPageVisible('tab-a-details');

cy.ionTabClick('Tab B');
cy.ionPageHidden('tab-a-details');
cy.ionPageVisible('tab-b');

cy.ionTabClick('Tab A');
cy.ionPageHidden('tab-b');
cy.ionPageVisible('tab-a-details');

cy.ionBackClick('tab-a-details');
cy.ionPageDoesNotExist('tab-a-details');
cy.ionPageVisible('tab-a');
});

it('should have no back navigation when first visiting a tab', () => {
cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
cy.ionPageVisible('tab-a');

cy.get(`div.ion-page[data-pageid=tab-a]`)
.find('ion-back-button')
.click({ force: true });

cy.wait(500);

cy.ionPageVisible('tab-a');
cy.url().should('include', '/tab-history-isolation/a');
});
});
Loading