Skip to content

Commit a9f94ec

Browse files
committed
fix(react-router): hide all views in outlet for container route transitions, allowing navigation to pages not properly wrapped in ion-pages
1 parent 073c8ba commit a9f94ec

2 files changed

Lines changed: 58 additions & 11 deletions

File tree

packages/react-router/src/ReactRouter/StackManager.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
232232
this.outOfScopeUnmountTimeout = undefined;
233233
}
234234

235-
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
236-
235+
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
237236
allViewsInOutlet.forEach((viewItem) => {
238237
hideIonPageElement(viewItem.ionPageElement);
239238
this.context.unMountViewItem(viewItem);
@@ -501,8 +500,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
501500
return;
502501
}
503502

504-
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
505-
503+
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
506504
const areSiblingRoutes = (path1: string, path2: string): boolean => {
507505
const path1IsRelative = !path1.startsWith('/');
508506
const path2IsRelative = !path2.startsWith('/');
@@ -569,8 +567,15 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
569567
}
570568
this.pendingPageTransition = false;
571569

572-
// Hide the leaving view immediately for Navigate redirects
573-
hideIonPageElement(leavingViewItem?.ionPageElement);
570+
// Hide ALL other visible views in this outlet for Navigate redirects.
571+
// Same rationale as the timeout path: intermediate redirects can shift
572+
// the leaving view reference, leaving the original page visible.
573+
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
574+
allViewsInOutlet.forEach((viewItem) => {
575+
if (viewItem.id !== enteringViewItem.id && viewItem.ionPageElement) {
576+
hideIonPageElement(viewItem.ionPageElement);
577+
}
578+
});
574579

575580
// Don't unmount if entering and leaving are the same view item
576581
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
@@ -617,11 +622,16 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
617622
/**
618623
* Timeout fired and entering view still has no ionPageElement.
619624
* This happens for container routes that render nested outlets without a direct IonPage.
620-
* Hide the leaving view since there's no entering IonPage to wait for.
625+
* Hide ALL other visible views in this outlet, not just the computed leaving view.
626+
* This handles cases where intermediate redirects (e.g., Navigate in nested routes)
627+
* change the leaving view reference, leaving the original page still visible.
621628
*/
622-
if (latestLeavingView?.ionPageElement) {
623-
hideIonPageElement(latestLeavingView.ionPageElement);
624-
}
629+
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
630+
allViewsInOutlet.forEach((viewItem) => {
631+
if (viewItem.id !== latestEnteringView.id && viewItem.ionPageElement) {
632+
hideIonPageElement(viewItem.ionPageElement);
633+
}
634+
});
625635
this.forceUpdate();
626636
}
627637
}, ION_PAGE_WAIT_TIMEOUT_MS);
@@ -688,7 +698,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
688698
// the nested outlet's componentDidUpdate won't be called, so we must hide
689699
// the ion-page elements here to prevent them from remaining visible on top
690700
// of other content after navigation to a different route.
691-
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
701+
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
692702
allViewsInOutlet.forEach((viewItem) => {
693703
hideIonPageElement(viewItem.ionPageElement);
694704
});

packages/react-router/test/base/tests/e2e/specs/nested-tabs-relative-links.cy.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,43 @@ describe('Nested Tabs with Relative Links', () => {
100100
cy.get('[data-testid="page-a-content"]').should('exist');
101101
});
102102

103+
/**
104+
* This test navigates from the home page (/) to nested-tabs-relative-links
105+
* via routerLink, rather than visiting the URL directly.
106+
*
107+
* This is a distinct scenario because NestedTabsRelativeLinks is a container
108+
* route that does NOT wrap its content in IonPage — it renders IonRouterOutlet
109+
* directly. When the root outlet transitions to a container without IonPage,
110+
* the nested <Navigate to="tab1" replace /> redirect fires before the root
111+
* outlet's timeout, causing the leaving view reference to shift. Without
112+
* proper handling, the home page is never hidden.
113+
*/
114+
it('should navigate from home page to nested tabs', () => {
115+
cy.visit(`http://localhost:${port}/`);
116+
cy.ionPageVisible('home');
117+
118+
// Click the Nested Tabs Relative Links item
119+
cy.get('ion-item[router-link="/nested-tabs-relative-links"]').click();
120+
121+
// Should navigate to tab1 (via index redirect)
122+
cy.ionPageVisible('nested-tabs-relative-tab1');
123+
cy.get('[data-testid="tab1-content"]').should('exist');
124+
125+
// Home page should be hidden
126+
cy.ionPageHidden('home');
127+
128+
// URL should be correct
129+
cy.url().should('include', '/nested-tabs-relative-links/tab1');
130+
131+
// Verify the container route does NOT have an IonPage wrapper.
132+
// This is the key invariant: NestedTabsRelativeLinks renders IonRouterOutlet
133+
// directly without IonPage. If someone adds IonPage to the container, this
134+
// test would pass trivially and no longer cover the container-without-IonPage
135+
// transition path. The ion-tabs element should have no [data-pageid] ancestors,
136+
// confirming no IonPage wraps the container.
137+
cy.get('ion-tabs').parents('[data-pageid]').should('have.length', 0);
138+
});
139+
103140
it('should switch tabs and maintain correct relative link resolution', () => {
104141
cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
105142
cy.ionPageVisible('nested-tabs-relative-tab1');

0 commit comments

Comments
 (0)