diff --git a/packages/react-router/scripts/test_runner.sh b/packages/react-router/scripts/test_runner.sh
index a57ad589cd..9e616448a8 100755
--- a/packages/react-router/scripts/test_runner.sh
+++ b/packages/react-router/scripts/test_runner.sh
@@ -64,6 +64,5 @@ fi
echo "Server is healthy."
echo "Running Cypress tests..."
-# Run specific failing tests first
-npm run cypress -- --spec "tests/e2e/specs/swipe-to-go-back.cy.js"
+npm run cypress
diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx
index 74a9bd400e..b921eb86e4 100644
--- a/packages/react-router/src/ReactRouter/IonRouter.tsx
+++ b/packages/react-router/src/ReactRouter/IonRouter.tsx
@@ -233,7 +233,9 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
};
// It's a linear navigation.
if (isPushed) {
- routeInfo.tab = leavingLocationInfo.tab;
+ // Only inherit tab from leaving route if we don't already have one.
+ // This preserves tab context for same-tab navigation while allowing cross-tab navigation.
+ routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
// Triggered by a browser back button or handleNavigateBack.
} else if (routeInfo.routeAction === 'pop') {
@@ -248,7 +250,10 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
*/
const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
// This helps maintain correct back stack behavior within tabs.
- routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
+ // 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;
+ console.log('[IonRouter TAB SWITCH] pathname=' + routeInfo.pathname + ' tab=' + routeInfo.tab + ' leavingTab=' + leavingLocationInfo.tab + ' leavingPathname=' + leavingLocationInfo.pathname + ' lastRoutePushedBy=' + (lastRoute?.pushedByRoute || 'undefined') + ' FINAL_pushedByRoute=' + routeInfo.pushedByRoute);
// Triggered by `history.replace()` or a `` component, etc.
} else if (routeInfo.routeAction === 'replace') {
/**
@@ -403,9 +408,11 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
const config = getConfig();
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
const routeInfo = locationHistory.current.current();
+ console.log('[IonRouter BACK] START currentPath=' + (routeInfo?.pathname || 'undefined') + ' currentTab=' + (routeInfo?.tab || 'undefined') + ' pushedByRoute=' + (routeInfo?.pushedByRoute || 'undefined'));
// It's a linear navigation.
if (routeInfo && routeInfo.pushedByRoute) {
const prevInfo = locationHistory.current.findLastLocation(routeInfo);
+ console.log('[IonRouter BACK] findLastLocation result: prevPath=' + (prevInfo?.pathname || 'undefined') + ' prevTab=' + (prevInfo?.tab || 'undefined'));
if (prevInfo) {
/**
* This needs to be passed to handleNavigate
@@ -423,22 +430,18 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
* Check if it's a simple linear back navigation (not tabbed).
* e.g., `/home` → `/settings` → back to `/home`
*/
- if (
- routeInfo.lastPathname === routeInfo.pushedByRoute ||
- /**
- * We need to exclude tab switches/tab
- * context changes here because tabbed
- * navigation is not linear, but router.back()
- * will go back in a linear fashion.
- */
- (prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')
- ) {
+ const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
+ const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '';
+ console.log('[IonRouter BACK] Decision: condition1=' + condition1 + ' (lastPathname=' + routeInfo.lastPathname + ' == pushedByRoute=' + routeInfo.pushedByRoute + ') condition2=' + condition2 + ' (prevPath=' + prevInfo.pathname + ' == pushedByRoute=' + routeInfo.pushedByRoute + ' && currentTab=' + routeInfo.tab + ' && prevTab=' + prevInfo.tab + ')');
+ if (condition1 || condition2) {
+ console.log('[IonRouter BACK] Using navigate(-1) - LINEAR navigation');
navigate(-1);
} else {
/**
* It's a non-linear back navigation.
* e.g., direct link or tab switch or nested navigation with redirects
*/
+ console.log('[IonRouter BACK] Using handleNavigate - NON-LINEAR navigation to: ' + prevInfo.pathname);
handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
}
/**
@@ -446,6 +449,7 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
* the history stack.
*/
} else {
+ console.log('[IonRouter BACK] No prevInfo found! Using defaultHref: ' + defaultHref);
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
/**
diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx
index 09161bb358..c52152eb50 100644
--- a/packages/react-router/src/ReactRouter/StackManager.tsx
+++ b/packages/react-router/src/ReactRouter/StackManager.tsx
@@ -63,6 +63,11 @@ export class StackManager extends React.PureComponent;
private outOfScopeUnmountTimeout?: ReturnType;
+ /**
+ * Track the last transition's entering and leaving view IDs to prevent
+ * duplicate transitions during rapid navigation (e.g., Navigate redirects)
+ */
+ private lastTransition?: { enteringId: string; leavingId?: string };
constructor(props: StackManagerProps) {
super(props);
@@ -488,6 +493,35 @@ export class StackManager extends React.PureComponent {
- // Use a timeout to ensure the transition completes before removal
- this.context.unMountViewItem(leavingViewItem);
- }, 250);
+ // Check if the entering view contains a nested outlet that's responsible for the replace.
+ // If the entering view's route has a wildcard (e.g., /tabs/*), it means this outlet
+ // is showing a container view and the replace is happening inside the nested outlet.
+ // In this case, we should NOT remove the leaving view from this outlet's stack,
+ // as it may be needed for back navigation.
+ const enteringRoutePath = enteringViewItem.reactElement?.props?.path as string | undefined;
+ const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
+
+ if (isEnteringContainerRoute) {
+ console.log(`[StackManager] Skipping removal of ${leavingViewItem.id} - entering route ${enteringRoutePath} is a container with nested outlet`);
+ } else {
+ console.log(`[StackManager] Removing page view ${leavingViewItem.id} from stack after replace action`);
+ setTimeout(() => {
+ // Use a timeout to ensure the transition completes before removal
+ this.context.unMountViewItem(leavingViewItem);
+ }, 250);
+ }
}
}
} else if (enteringViewItem && !enteringViewItem.ionPageElement) {
diff --git a/packages/react/src/routing/LocationHistory.ts b/packages/react/src/routing/LocationHistory.ts
index e99e5a88cc..f2c185a723 100644
--- a/packages/react/src/routing/LocationHistory.ts
+++ b/packages/react/src/routing/LocationHistory.ts
@@ -90,8 +90,24 @@ export class LocationHistory {
private _replace(routeInfo: RouteInfo) {
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
+ const hadPreviousTabHistory = routeInfos && routeInfos.length > 0;
routeInfos && routeInfos.pop();
- this.locationHistory.pop();
+
+ // Get the current route that's being replaced
+ const currentRoute = this.locationHistory[this.locationHistory.length - 1];
+
+ console.log('[LocationHistory._replace] currentRoute:', currentRoute?.pathname, 'tab:', currentRoute?.tab);
+ console.log('[LocationHistory._replace] newRoute:', routeInfo.pathname, 'tab:', routeInfo.tab);
+
+ // Only pop from global history if we're replacing in the same outlet context.
+ // Don't pop if we're entering a nested outlet (current route has no tab, new route has a tab)
+ const isEnteringNestedOutlet = currentRoute && !currentRoute.tab && !!routeInfo.tab;
+ console.log('[LocationHistory._replace] isEnteringNestedOutlet:', isEnteringNestedOutlet);
+
+ if (!isEnteringNestedOutlet) {
+ this.locationHistory.pop();
+ }
+
this._add(routeInfo);
}