fix(react-router): use stored parent paths for index route matching in findViewItemByPath

This commit is contained in:
ShaneK
2026-03-10 14:03:25 -07:00
parent 6a681e480a
commit 8ea275b555
3 changed files with 57 additions and 2 deletions

View File

@@ -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<string, string>();
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<string> | 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
*/

View File

@@ -82,6 +82,9 @@ const UserDetails: React.FC = () => {
</IonHeader>
<IonContent>
<IonLabel data-testid="user-details-param">Details view user: {userId ?? 'missing'}</IonLabel>
<IonButton routerLink="/nested-params" id="back-to-landing">
Back to Landing
</IonButton>
<IonButton routerLink={`/nested-params/user/${userId}/settings`} id="go-to-settings">
Go to Settings
</IonButton>

View File

@@ -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');