Fixing test app tabs views

This commit is contained in:
ShaneK
2025-11-07 05:58:40 -08:00
parent 5b735cd3da
commit 881068e66d
5 changed files with 44 additions and 83 deletions

View File

@@ -78,7 +78,7 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
/**
* Determines the parent path that was matched to reach this outlet.
* This is crucial for nested routing in React Router 6.
* This helps with nested routing in React Router 6.
*/
private getParentPath(): string | undefined {
const currentPathname = this.props.routeInfo.pathname;
@@ -266,7 +266,19 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
// CRITICAL: If we have a mount path and current route is outside of it,
// don't process any routes in this outlet - it's completely out of scope
if (this.outletMountPath && !routeInfo.pathname.startsWith(this.outletMountPath)) {
// Don't unmount views - just skip processing
// Hide any visible views in this outlet since it's out of scope
if (leavingViewItem && leavingViewItem.ionPageElement) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
// Also check if there's an entering view that needs to be hidden
if (enteringViewItem && enteringViewItem.ionPageElement && enteringViewItem !== leavingViewItem) {
enteringViewItem.ionPageElement.classList.add('ion-page-hidden');
enteringViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
// Don't unmount views - just hide them and skip processing
// The views should be preserved for when we return to this outlet
this.forceUpdate();
return;
@@ -304,7 +316,7 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
routeInfo,
parentPath
) as React.ReactElement;
if (enteringRoute && process.env.NODE_ENV !== 'production') {
const routeElement = enteringRoute.props?.element;
const isNavigate = React.isValidElement(routeElement) && routeElement.type === Navigate;
@@ -471,13 +483,13 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.ionPageWaitTimeout = undefined;
}
this.pendingPageTransition = false;
// Unmount the leaving view immediately for replace actions
if (shouldUnmountLeavingViewItem && leavingViewItem) {
console.log(`[StackManager] Unmounting leaving view ${leavingViewItem.id} for Navigate redirect`);
leavingViewItem.mount = false;
}
this.forceUpdate();
return;
}
@@ -537,7 +549,7 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.forceUpdate();
} else if (process.env.NODE_ENV !== 'production') {
console.log(
`[StackManager] Still no ionPageElement for ${latestEnteringView?.id ?? enteringViewItem.id}, skipping transition`
`[StackManager] Still no ionPageElement for ${latestEnteringView?.id ?? enteringViewItem?.id ?? 'unknown'}, skipping transition`
);
}
}, 50);
@@ -932,74 +944,23 @@ function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo, paren
const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
// For nested outlets with relative routes, we need to determine if this outlet
// should even attempt to handle this route
if (hasRelativeRoutes || hasIndexRoute) {
// If we have a parent path, check if the current pathname is within scope
if (parentPath) {
const parentPrefix = parentPath.replace('/*', '');
// If the current path doesn't start with the parent prefix, this outlet shouldn't handle it
if (!routeInfo.pathname.startsWith(parentPrefix + '/') && routeInfo.pathname !== parentPrefix) {
// This nested outlet is out of scope for this pathname
return null;
}
} else if (hasIndexRoute && !hasRelativeRoutes) {
// Special case: if we only have an index route and no relative routes,
// and we couldn't determine a parent path, this outlet likely shouldn't
// handle this route (the index route would only match at the outlet's root)
return null;
}
// SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
const parentPrefix = parentPath.replace('/*', '');
const normalizedParent = parentPrefix.endsWith('/') ? parentPrefix.slice(0, -1) : parentPrefix;
const normalizedPathname = routeInfo.pathname.endsWith('/') ? routeInfo.pathname.slice(0, -1) : routeInfo.pathname;
// For nested routing, we need to compute the relative path
// This is the part of the pathname that comes after the parent's matched path
// Try to determine the parent path from the context
// In React Router 6, nested outlets match against the remaining path segment
const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
// If we have a parentPath, use it to compute the relative path
if (parentPath) {
const parentSegments = parentPath.replace('/*', '').split('/').filter(Boolean);
// Remove parent segments from the full path to get the relative path
// Only compute relative path if pathname is within parent scope
if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
const parentSegments = normalizedParent.split('/').filter(Boolean);
const relativeSegments = pathSegments.slice(parentSegments.length);
pathnameToMatch = relativeSegments.join('/'); // This can be empty string, which is correct for index routes
} else if (pathSegments.length > 0) {
// If we have relative routes but couldn't determine a parent path,
// this likely means the current pathname is not within this outlet's scope
// Check if we can find any route that could match
let foundMatch = false;
pathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
// Check each possible parent path length starting from the longest
for (let parentLength = pathSegments.length - 1; parentLength > 0; parentLength--) {
const remainingSegments = pathSegments.slice(parentLength);
const remainingPath = remainingSegments.join('/');
// Check if any route in this outlet could match the remaining path
const couldMatchRemaining = sortedRoutes.some((route) => doesRouteMatchRemainingPath(route, remainingPath));
if (couldMatchRemaining) {
pathnameToMatch = remainingPath;
foundMatch = true;
break;
}
}
// If we couldn't find any match and we have relative routes,
// this outlet shouldn't handle this route
if (!foundMatch && (hasRelativeRoutes || hasIndexRoute)) {
return null;
}
// If no parent could be inferred, try matching the last segment only
// This handles the common case where we directly navigate to /parent/child
if (pathnameToMatch === routeInfo.pathname && pathSegments.length > 0) {
const lastSegment = pathSegments[pathSegments.length - 1];
const couldMatchLastSegment = sortedRoutes.some((route) => doesRouteMatchRemainingPath(route, lastSegment));
if (couldMatchLastSegment) {
pathnameToMatch = lastSegment;
}
if (process.env.NODE_ENV !== 'production') {
console.log(
`[findRouteByRouteInfo] Computed relative path: "${pathnameToMatch}" from pathname="${routeInfo.pathname}" parentPath="${parentPath}"`
);
}
}
}

View File

@@ -59,8 +59,8 @@ const App: React.FC = () => {
<Route path="/outlet-ref" element={<OutletRef />} />
<Route path="/swipe-to-go-back" element={<SwipeToGoBack />} />
<Route path="/dynamic-ionpage-classnames" element={<DynamicIonpageClassnames />} />
<Route path="/tabs" element={<Tabs />} />
<Route path="/tabs-secondary" element={<TabsSecondary />} />
<Route path="/tabs/*" element={<Tabs />} />
<Route path="/tabs-secondary/*" element={<TabsSecondary />} />
<Route path="/refs" element={<Refs />} />
<Route path="/overlays" element={<Overlays />} />
<Route path="/params/:id" element={<Params />} />

View File

@@ -78,7 +78,7 @@ const UserDetails: React.FC = () => {
</IonHeader>
<IonContent>
<IonLabel data-testid="user-details-param">Details view user: {userId ?? 'missing'}</IonLabel>
<IonButton routerLink="settings" id="go-to-settings">
<IonButton routerLink={`/nested-params/user/${userId}/settings`} id="go-to-settings">
Go to Settings
</IonButton>
</IonContent>
@@ -98,7 +98,7 @@ const UserSettings: React.FC = () => {
</IonHeader>
<IonContent>
<IonLabel data-testid="user-settings-param">Settings view user: {userId ?? 'missing'}</IonLabel>
<IonButton routerLink="details">Back to Details</IonButton>
<IonButton routerLink={`/nested-params/user/${userId}/details`}>Back to Details</IonButton>
</IonContent>
</IonPage>
);

View File

@@ -24,11 +24,11 @@ const Tabs: React.FC<TabsProps> = () => {
return (
<IonTabs data-pageid="tabs">
<IonRouterOutlet id="tabs">
<Route path="/tabs/tab1" element={<Tab1 />} />
<Route path="/tabs/tab2" element={<Tab2 />} />
<Route path="/tabs/tab1/child" element={<Tab1Child1 />} />
<Route path="/tabs/tab1/child2" element={<Tab1Child2 />} />
<Route path="/tabs" element={<Navigate to="/tabs/tab1" replace />} />
<Route index element={<Navigate to="/tabs/tab1" replace />} />
<Route path="tab1" element={<Tab1 />} />
<Route path="tab2" element={<Tab2 />} />
<Route path="tab1/child" element={<Tab1Child1 />} />
<Route path="tab1/child2" element={<Tab1Child2 />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tabs/tab1">

View File

@@ -23,9 +23,9 @@ const TabsSecondary: React.FC<TabsSecondaryProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs-secondary">
<Route path="/tabs-secondary/tab1" element={<Tab1 />} />
<Route path="/tabs-secondary/tab2" element={<Tab2 />} />
<Route path="/tabs-secondary" element={<Navigate to="/tabs-secondary/tab1" replace />} />
<Route index element={<Navigate to="/tabs-secondary/tab1" replace />} />
<Route path="tab1" element={<Tab1 />} />
<Route path="tab2" element={<Tab2 />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1-secondary" href="/tabs-secondary/tab1">