mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(react-router): isolate tab history to prevent cross-tab back navigation (#30854)
Issue number: resolves internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? When switching between tabs using the tab bar in a React Router 6 application, clicking IonBackButton incorrectly navigates back to the previous tab. Each tab should have its own isolated navigation history stack, and the back button should only navigate within the current tab's history. ## What is the new behavior? Tab navigation history is now properly isolated. When switching tabs via the tab bar: - The back button only navigates within the current tab's history - Pressing back on a tab root (with no navigation history) does nothing instead of navigating to the previous tab or default route - Within-tab navigation continues to work correctly (e.g., navigating to a details page and back) - Tab history is preserved when switching away and returning to a tab ## Does this introduce a breaking change? - [ ] Yes - [X] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Current dev build: ``` 8.7.12-dev.11765377112.16762e5b ```
This commit is contained in:
@@ -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') {
|
||||
/**
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user