fix(react-router): hide all views in outlet for container route transitions, allowing navigation to pages not properly wrapped in ion-pages

This commit is contained in:
ShaneK
2026-03-05 13:38:21 -08:00
parent 073c8ba809
commit a9f94ec0d1
2 changed files with 58 additions and 11 deletions

View File

@@ -232,8 +232,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
this.outOfScopeUnmountTimeout = undefined;
}
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
allViewsInOutlet.forEach((viewItem) => {
hideIonPageElement(viewItem.ionPageElement);
this.context.unMountViewItem(viewItem);
@@ -501,8 +500,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
return;
}
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
const areSiblingRoutes = (path1: string, path2: string): boolean => {
const path1IsRelative = !path1.startsWith('/');
const path2IsRelative = !path2.startsWith('/');
@@ -569,8 +567,15 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
}
this.pendingPageTransition = false;
// Hide the leaving view immediately for Navigate redirects
hideIonPageElement(leavingViewItem?.ionPageElement);
// Hide ALL other visible views in this outlet for Navigate redirects.
// Same rationale as the timeout path: intermediate redirects can shift
// the leaving view reference, leaving the original page visible.
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
allViewsInOutlet.forEach((viewItem) => {
if (viewItem.id !== enteringViewItem.id && viewItem.ionPageElement) {
hideIonPageElement(viewItem.ionPageElement);
}
});
// Don't unmount if entering and leaving are the same view item
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
@@ -617,11 +622,16 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
/**
* Timeout fired and entering view still has no ionPageElement.
* This happens for container routes that render nested outlets without a direct IonPage.
* Hide the leaving view since there's no entering IonPage to wait for.
* Hide ALL other visible views in this outlet, not just the computed leaving view.
* This handles cases where intermediate redirects (e.g., Navigate in nested routes)
* change the leaving view reference, leaving the original page still visible.
*/
if (latestLeavingView?.ionPageElement) {
hideIonPageElement(latestLeavingView.ionPageElement);
}
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
allViewsInOutlet.forEach((viewItem) => {
if (viewItem.id !== latestEnteringView.id && viewItem.ionPageElement) {
hideIonPageElement(viewItem.ionPageElement);
}
});
this.forceUpdate();
}
}, ION_PAGE_WAIT_TIMEOUT_MS);
@@ -688,7 +698,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
// the nested outlet's componentDidUpdate won't be called, so we must hide
// the ion-page elements here to prevent them from remaining visible on top
// of other content after navigation to a different route.
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
allViewsInOutlet.forEach((viewItem) => {
hideIonPageElement(viewItem.ionPageElement);
});

View File

@@ -100,6 +100,43 @@ describe('Nested Tabs with Relative Links', () => {
cy.get('[data-testid="page-a-content"]').should('exist');
});
/**
* This test navigates from the home page (/) to nested-tabs-relative-links
* via routerLink, rather than visiting the URL directly.
*
* This is a distinct scenario because NestedTabsRelativeLinks is a container
* route that does NOT wrap its content in IonPage — it renders IonRouterOutlet
* directly. When the root outlet transitions to a container without IonPage,
* the nested <Navigate to="tab1" replace /> redirect fires before the root
* outlet's timeout, causing the leaving view reference to shift. Without
* proper handling, the home page is never hidden.
*/
it('should navigate from home page to nested tabs', () => {
cy.visit(`http://localhost:${port}/`);
cy.ionPageVisible('home');
// Click the Nested Tabs Relative Links item
cy.get('ion-item[router-link="/nested-tabs-relative-links"]').click();
// Should navigate to tab1 (via index redirect)
cy.ionPageVisible('nested-tabs-relative-tab1');
cy.get('[data-testid="tab1-content"]').should('exist');
// Home page should be hidden
cy.ionPageHidden('home');
// URL should be correct
cy.url().should('include', '/nested-tabs-relative-links/tab1');
// Verify the container route does NOT have an IonPage wrapper.
// This is the key invariant: NestedTabsRelativeLinks renders IonRouterOutlet
// directly without IonPage. If someone adds IonPage to the container, this
// test would pass trivially and no longer cover the container-without-IonPage
// transition path. The ion-tabs element should have no [data-pageid] ancestors,
// confirming no IonPage wraps the container.
cy.get('ion-tabs').parents('[data-pageid]').should('have.length', 0);
});
it('should switch tabs and maintain correct relative link resolution', () => {
cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
cy.ionPageVisible('nested-tabs-relative-tab1');