From 8ea275b555555fc7bbf35a87838bf739fc539b24 Mon Sep 17 00:00:00 2001 From: ShaneK Date: Tue, 10 Mar 2026 14:03:25 -0700 Subject: [PATCH] fix(react-router): use stored parent paths for index route matching in findViewItemByPath --- .../src/ReactRouter/ReactRouterViewStack.tsx | 28 +++++++++++++++++-- .../src/pages/nested-params/NestedParams.tsx | 3 ++ .../base/tests/e2e/specs/nested-params.cy.js | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx index a62d520bcd..902d0b327e 100644 --- a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx +++ b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx @@ -207,6 +207,13 @@ const resolveIndexRouteMatch = ( }; export class ReactRouterViewStack extends ViewStacks { + /** + * Stores the computed parent path for each outlet. + * Used by findViewItemByPath to correctly evaluate index route matches + * without requiring the outlet's React element or route children. + */ + private outletParentPaths = new Map(); + constructor() { super(); } @@ -542,6 +549,11 @@ export class ReactRouterViewStack extends ViewStacks { // Non-fatal: if we fail to compute parentPath, fall back to previous behavior } + // Store the computed parentPath for use in findViewItemByPath + if (parentPath !== undefined) { + this.outletParentPaths.set(outletId, parentPath); + } + // Sync child elements with stored viewItems (e.g. to reflect new props) React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => { // Ensure the child is a valid React element since we @@ -661,6 +673,8 @@ export class ReactRouterViewStack extends ViewStacks { let viewItem: ViewItem | undefined; let match: PathMatch | null = null; let viewStack: ViewItem[]; + // Capture stored parent paths for use in nested matchView/matchDefaultRoute functions + const storedParentPaths = this.outletParentPaths; if (outletId) { viewStack = sortViewsBySpecificity(this.getViewItemsForOutlet(outletId)); @@ -693,7 +707,8 @@ export class ReactRouterViewStack extends ViewStacks { const result = v.reactElement ? matchComponent(v.reactElement, pathname) : null; if (!result) { - const indexMatch = resolveIndexRouteMatch(v, pathname, undefined); + const outletParentPath = storedParentPaths.get(v.outletId); + const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath); if (indexMatch) { match = indexMatch; viewItem = v; @@ -763,7 +778,8 @@ export class ReactRouterViewStack extends ViewStacks { const isIndexRoute = !!childProps.index; if (isIndexRoute) { - const indexMatch = resolveIndexRouteMatch(v, pathname, undefined); + const outletParentPath = storedParentPaths.get(v.outletId); + const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath); if (indexMatch) { match = indexMatch; viewItem = v; @@ -836,6 +852,14 @@ export class ReactRouterViewStack extends ViewStacks { this.cleanupStaleViewItems(viewItem.outletId); }; + /** + * Override clear to also clean up the stored parent path for the outlet. + */ + clear = (outletId: string) => { + this.outletParentPaths.delete(outletId); + return super.clear(outletId); + }; + /** * Override remove */ diff --git a/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx b/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx index 58714b3ea4..be9c6ebe7a 100644 --- a/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx +++ b/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx @@ -82,6 +82,9 @@ const UserDetails: React.FC = () => { Details view user: {userId ?? 'missing'} + + Back to Landing + Go to Settings diff --git a/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js b/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js index 35d7e1f7bc..1b3f84189b 100644 --- a/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js +++ b/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js @@ -49,6 +49,34 @@ describe('Nested Params', () => { cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 123'); }); + it('/nested-params > Deep link to child then navigate to landing > Landing should show correctly', () => { + // Deep-link directly to a nested child route + cy.visit(`http://localhost:${port}/nested-params/user/42/details`); + cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42'); + + // Navigate back to the landing (index route) + cy.get('#back-to-landing').click(); + cy.ionPageVisible('nested-params-landing'); + + // The user layout page should be hidden (Ionic preserves it in DOM for back navigation) + cy.get('[data-pageid^="nested-params-user-"]').should('have.class', 'ion-page-hidden'); + }); + + it('/nested-params > Deep link then navigate to landing and back > Round-trip should work', () => { + // Deep-link directly to a nested child route + cy.visit(`http://localhost:${port}/nested-params/user/42/details`); + cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42'); + + // Navigate to the landing page + cy.get('#back-to-landing').click(); + cy.ionPageVisible('nested-params-landing'); + + // Navigate back to user details + cy.get('#go-to-user-42').click(); + cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42'); + cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42'); + }); + it('/nested-params > Navigate to user then back > No visual overlap during transition', () => { cy.visit(`http://localhost:${port}/nested-params`); cy.ionPageVisible('nested-params-landing');