mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(react-router): correct tab and nested outlet navigation
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 `<Redirect />` 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);
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,11 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
|
||||
private waitingForIonPage = false;
|
||||
private ionPageWaitTimeout?: ReturnType<typeof setTimeout>;
|
||||
private outOfScopeUnmountTimeout?: ReturnType<typeof setTimeout>;
|
||||
/**
|
||||
* 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<StackManagerProps, StackMa
|
||||
* route or on an initial page load (i.e. refreshing). In cases when loading
|
||||
* /tabs/tab-1, we need to transition the /tabs page element into the view.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if we've already started a transition for the same entering/leaving pair.
|
||||
* This can happen during rapid navigation (e.g., Navigate redirects) where
|
||||
* multiple handlePageTransition calls occur before the first transition completes.
|
||||
*
|
||||
* Only skip if there's an actual leaving view involved - we don't want to skip
|
||||
* transitions where leaving is undefined as those could be legitimate initial loads
|
||||
* or transitions to new views.
|
||||
*/
|
||||
const currentTransition = {
|
||||
enteringId: enteringViewItem.id,
|
||||
leavingId: leavingViewItem?.id,
|
||||
};
|
||||
|
||||
if (
|
||||
leavingViewItem &&
|
||||
this.lastTransition &&
|
||||
this.lastTransition.leavingId &&
|
||||
this.lastTransition.enteringId === currentTransition.enteringId &&
|
||||
this.lastTransition.leavingId === currentTransition.leavingId
|
||||
) {
|
||||
console.log(
|
||||
`[StackManager] Skipping duplicate transition: entering=${currentTransition.enteringId}, leaving=${currentTransition.leavingId}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTransition = currentTransition;
|
||||
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
|
||||
|
||||
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
|
||||
@@ -495,11 +529,23 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
|
||||
// For replace actions, remove actual pages (with ionPageElement) from the stack entirely
|
||||
// Don't remove utility components like Navigate that don't have ionPageElement
|
||||
if (routeInfo.routeAction === 'replace' && leavingViewItem.ionPageElement) {
|
||||
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);
|
||||
// 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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user