diff --git a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx index 9ab0e4502d..9aecba1334 100644 --- a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx +++ b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx @@ -11,11 +11,11 @@ import React from 'react'; import type { PathMatch } from 'react-router'; import { Navigate, UNSAFE_RouteContext as RouteContext } from 'react-router-dom'; -import { analyzeRouteChildren, computeParentPath, extractRouteChildren } from './utils/computeParentPath'; -import { derivePathnameToMatch } from './utils/derivePathnameToMatch'; -import { matchPath } from './utils/matchPath'; -import { normalizePathnameForComparison } from './utils/normalizePath'; -import { isNavigateElement, sortViewsBySpecificity } from './utils/routeUtils'; +import { analyzeRouteChildren, computeParentPath } from './utils/computeParentPath'; +import { derivePathnameToMatch, matchPath } from './utils/pathMatching'; +import { normalizePathnameForComparison } from './utils/pathNormalization'; +import { extractRouteChildren, isNavigateElement } from './utils/routeElements'; +import { sortViewsBySpecificity } from './utils/viewItemUtils'; /** * Delay in milliseconds before removing a Navigate view item after a redirect. @@ -51,7 +51,6 @@ const createDefaultMatch = ( }; }; - const computeRelativeToParent = (pathname: string, parentPath?: string): string | null => { if (!parentPath) return null; const normalizedParent = normalizePathnameForComparison(parentPath); @@ -238,7 +237,6 @@ export class ReactRouterViewStack extends ViewStacks { // Flag to indicate this view should not be reused for this different parameterized path const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath; - // Don't deactivate views automatically - let the StackManager handle view lifecycle // This preserves views in the stack for navigation history like native apps // Views will be hidden/shown by the StackManager's transition logic instead of being unmounted @@ -519,8 +517,7 @@ export class ReactRouterViewStack extends ViewStacks { // Check if current pathname is within this view's route hierarchy const isWithinRouteHierarchy = - normalizedCurrentPath === normalizedViewPath || - normalizedCurrentPath.startsWith(normalizedViewPath + '/'); + normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/'); if (!isWithinRouteHierarchy) { // View is outside current route hierarchy, remove it diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx index 7c8f43ce3a..4ce73c561b 100644 --- a/packages/react-router/src/ReactRouter/StackManager.tsx +++ b/packages/react-router/src/ReactRouter/StackManager.tsx @@ -10,17 +10,10 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { clonePageElement } from './clonePageElement'; -import { - analyzeRouteChildren, - computeCommonPrefix, - computeParentPath, - extractRouteChildren, -} from './utils/computeParentPath'; -import { derivePathnameToMatch } from './utils/derivePathnameToMatch'; -import { getRoutesChildren } from './utils/getRoutesChildren'; -import { matchPath } from './utils/matchPath'; -import { stripTrailingSlash } from './utils/normalizePath'; -import { isNavigateElement } from './utils/routeUtils'; +import { analyzeRouteChildren, computeCommonPrefix, computeParentPath } from './utils/computeParentPath'; +import { derivePathnameToMatch, matchPath } from './utils/pathMatching'; +import { stripTrailingSlash } from './utils/pathNormalization'; +import { extractRouteChildren, getRoutesChildren, isNavigateElement } from './utils/routeElements'; /** * Delay in milliseconds before unmounting a view after a transition completes. @@ -149,7 +142,7 @@ export class StackManager extends React.PureComponent { enteringViewItem: ViewItem | undefined; leavingViewItem: ViewItem | undefined; } { - let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id); + const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id); let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id); // If we don't have a leaving view item, but the route info indicates @@ -257,7 +250,8 @@ export class StackManager extends React.PureComponent { return false; } - const routesChildren = getRoutesChildren(this.ionRouterOutlet.props.children) ?? this.ionRouterOutlet.props.children; + const routesChildren = + getRoutesChildren(this.ionRouterOutlet.props.children) ?? this.ionRouterOutlet.props.children; const routeChildren = React.Children.toArray(routesChildren).filter( (child): child is React.ReactElement => React.isValidElement(child) && child.type === Route ); @@ -382,11 +376,7 @@ export class StackManager extends React.PureComponent { /** * Handles the delayed unmount of the leaving view item after a replace action. */ - private handleLeavingViewUnmount( - routeInfo: RouteInfo, - enteringViewItem: ViewItem, - leavingViewItem: ViewItem - ): void { + private handleLeavingViewUnmount(routeInfo: RouteInfo, enteringViewItem: ViewItem, leavingViewItem: ViewItem): void { if (routeInfo.routeAction !== 'replace' || !leavingViewItem.ionPageElement) { return; } @@ -538,9 +528,7 @@ export class StackManager extends React.PureComponent { // 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.context.getViewItemsForOutlet(this.id) : []; allViewsInOutlet.forEach((viewItem) => { hideIonPageElement(viewItem.ionPageElement); }); @@ -570,7 +558,9 @@ export class StackManager extends React.PureComponent { } // Find entering and leaving view items - let { enteringViewItem, leavingViewItem } = this.findViewItems(routeInfo); + const viewItems = this.findViewItems(routeInfo); + let enteringViewItem = viewItems.enteringViewItem; + const leavingViewItem = viewItems.leavingViewItem; const shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem); // Get parent path for nested outlets diff --git a/packages/react-router/src/ReactRouter/utils/computeParentPath.ts b/packages/react-router/src/ReactRouter/utils/computeParentPath.ts index 475dfb2ff6..3f47672a44 100644 --- a/packages/react-router/src/ReactRouter/utils/computeParentPath.ts +++ b/packages/react-router/src/ReactRouter/utils/computeParentPath.ts @@ -1,8 +1,6 @@ -import React from 'react'; -import { Route } from 'react-router-dom'; +import type React from 'react'; -import { getRoutesChildren } from './getRoutesChildren'; -import { matchPath } from './matchPath'; +import { matchPath } from './pathMatching'; /** * Finds the longest common prefix among an array of paths. @@ -76,19 +74,6 @@ export interface ParentPathResult { outletMountPath: string | undefined; } -/** - * Extracts Route children from a node (either directly or from a Routes wrapper). - * - * @param children The children to extract routes from. - * @returns An array of Route elements. - */ -export const extractRouteChildren = (children: React.ReactNode): React.ReactElement[] => { - const routesChildren = getRoutesChildren(children) ?? children; - return React.Children.toArray(routesChildren).filter( - (child): child is React.ReactElement => React.isValidElement(child) && child.type === Route - ); -}; - interface RouteAnalysis { hasRelativeRoutes: boolean; hasIndexRoute: boolean; diff --git a/packages/react-router/src/ReactRouter/utils/derivePathnameToMatch.ts b/packages/react-router/src/ReactRouter/utils/derivePathnameToMatch.ts deleted file mode 100644 index adea0435e2..0000000000 --- a/packages/react-router/src/ReactRouter/utils/derivePathnameToMatch.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Determines the portion of a pathname that a given route pattern should match against. - * For absolute route patterns we return the full pathname. For relative patterns we - * strip off the already-matched parent segments so React Router receives the remainder. - */ -export const derivePathnameToMatch = (fullPathname: string, routePath?: string): string => { - if (!routePath || routePath === '' || routePath.startsWith('/')) { - return fullPathname; - } - - const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname; - if (!trimmedPath) { - return ''; - } - - const fullSegments = trimmedPath.split('/').filter(Boolean); - if (fullSegments.length === 0) { - return ''; - } - - const routeSegments = routePath.split('/').filter(Boolean); - if (routeSegments.length === 0) { - return trimmedPath; - } - - const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**'); - - if (wildcardIndex >= 0) { - const baseSegments = routeSegments.slice(0, wildcardIndex); - if (baseSegments.length === 0) { - return trimmedPath; - } - - const startIndex = fullSegments.findIndex((_, idx) => - baseSegments.every((seg, segIdx) => { - const target = fullSegments[idx + segIdx]; - if (!target) { - return false; - } - if (seg.startsWith(':')) { - return true; - } - return target === seg; - }) - ); - - if (startIndex >= 0) { - return fullSegments.slice(startIndex).join('/'); - } - } - - if (routeSegments.length <= fullSegments.length) { - return fullSegments.slice(fullSegments.length - routeSegments.length).join('/'); - } - - return fullSegments[fullSegments.length - 1] ?? trimmedPath; -}; diff --git a/packages/react-router/src/ReactRouter/utils/getRoutesChildren.ts b/packages/react-router/src/ReactRouter/utils/getRoutesChildren.ts deleted file mode 100644 index 2af53ced45..0000000000 --- a/packages/react-router/src/ReactRouter/utils/getRoutesChildren.ts +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { Routes } from 'react-router'; - -export const getRoutesChildren = (node: React.ReactNode) => { - // The use of `` is encouraged with React Router v6. - let routesNode: React.ReactNode; - React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => { - if (child.type === Routes) { - routesNode = child; - } - }); - - if (routesNode) { - // The children of the `` component are most likely - // (and should be) the `` components. - return (routesNode as React.ReactElement).props.children; - } - return undefined; -}; diff --git a/packages/react-router/src/ReactRouter/utils/matchPath.ts b/packages/react-router/src/ReactRouter/utils/pathMatching.ts similarity index 62% rename from packages/react-router/src/ReactRouter/utils/matchPath.ts rename to packages/react-router/src/ReactRouter/utils/pathMatching.ts index 03f4112db5..a0ab74164f 100644 --- a/packages/react-router/src/ReactRouter/utils/matchPath.ts +++ b/packages/react-router/src/ReactRouter/utils/pathMatching.ts @@ -1,6 +1,9 @@ import type { PathMatch } from 'react-router'; import { matchPath as reactRouterMatchPath } from 'react-router-dom'; +/** + * Options for the matchPath function. + */ interface MatchPathOptions { /** * The pathname to match against. @@ -99,3 +102,61 @@ export const matchPath = ({ pathname, componentProps }: MatchPathOptions): PathM return reactRouterMatchPath(matchOptions, pathname); }; + +/** + * Determines the portion of a pathname that a given route pattern should match against. + * For absolute route patterns we return the full pathname. For relative patterns we + * strip off the already-matched parent segments so React Router receives the remainder. + */ +export const derivePathnameToMatch = (fullPathname: string, routePath?: string): string => { + if (!routePath || routePath === '' || routePath.startsWith('/')) { + return fullPathname; + } + + const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname; + if (!trimmedPath) { + return ''; + } + + const fullSegments = trimmedPath.split('/').filter(Boolean); + if (fullSegments.length === 0) { + return ''; + } + + const routeSegments = routePath.split('/').filter(Boolean); + if (routeSegments.length === 0) { + return trimmedPath; + } + + const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**'); + + if (wildcardIndex >= 0) { + const baseSegments = routeSegments.slice(0, wildcardIndex); + if (baseSegments.length === 0) { + return trimmedPath; + } + + const startIndex = fullSegments.findIndex((_, idx) => + baseSegments.every((seg, segIdx) => { + const target = fullSegments[idx + segIdx]; + if (!target) { + return false; + } + if (seg.startsWith(':')) { + return true; + } + return target === seg; + }) + ); + + if (startIndex >= 0) { + return fullSegments.slice(startIndex).join('/'); + } + } + + if (routeSegments.length <= fullSegments.length) { + return fullSegments.slice(fullSegments.length - routeSegments.length).join('/'); + } + + return fullSegments[fullSegments.length - 1] ?? trimmedPath; +}; diff --git a/packages/react-router/src/ReactRouter/utils/normalizePath.ts b/packages/react-router/src/ReactRouter/utils/pathNormalization.ts similarity index 100% rename from packages/react-router/src/ReactRouter/utils/normalizePath.ts rename to packages/react-router/src/ReactRouter/utils/pathNormalization.ts diff --git a/packages/react-router/src/ReactRouter/utils/routeElements.ts b/packages/react-router/src/ReactRouter/utils/routeElements.ts new file mode 100644 index 0000000000..e0e52dd620 --- /dev/null +++ b/packages/react-router/src/ReactRouter/utils/routeElements.ts @@ -0,0 +1,51 @@ +import React from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; + +/** + * Extracts the children from a Routes wrapper component. + * The use of `` is encouraged with React Router v6. + * + * @param node The React node to extract Routes children from. + * @returns The children of the Routes component, or undefined if not found. + */ +export const getRoutesChildren = (node: React.ReactNode): React.ReactNode | undefined => { + let routesNode: React.ReactNode; + React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => { + if (child.type === Routes) { + routesNode = child; + } + }); + + if (routesNode) { + // The children of the `` component are most likely + // (and should be) the `` components. + return (routesNode as React.ReactElement).props.children; + } + return undefined; +}; + +/** + * Extracts Route children from a node (either directly or from a Routes wrapper). + * + * @param children The children to extract routes from. + * @returns An array of Route elements. + */ +export const extractRouteChildren = (children: React.ReactNode): React.ReactElement[] => { + const routesChildren = getRoutesChildren(children) ?? children; + return React.Children.toArray(routesChildren).filter( + (child): child is React.ReactElement => React.isValidElement(child) && child.type === Route + ); +}; + +/** + * Checks if a React element is a Navigate component (redirect). + * + * @param element The element to check. + * @returns True if the element is a Navigate component. + */ +export const isNavigateElement = (element: unknown): boolean => { + return ( + React.isValidElement(element) && + (element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate')) + ); +}; diff --git a/packages/react-router/src/ReactRouter/utils/routeUtils.ts b/packages/react-router/src/ReactRouter/utils/viewItemUtils.ts similarity index 66% rename from packages/react-router/src/ReactRouter/utils/routeUtils.ts rename to packages/react-router/src/ReactRouter/utils/viewItemUtils.ts index d82bfdc98d..c5d8b84d19 100644 --- a/packages/react-router/src/ReactRouter/utils/routeUtils.ts +++ b/packages/react-router/src/ReactRouter/utils/viewItemUtils.ts @@ -1,22 +1,5 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; - import type { ViewItem } from '@ionic/react'; -/** - * Checks if a React element is a Navigate component (redirect). - * - * @param element The element to check. - * @returns True if the element is a Navigate component. - */ -export const isNavigateElement = (element: unknown): boolean => { - return ( - React.isValidElement(element) && - (element.type === Navigate || - (typeof element.type === 'function' && element.type.name === 'Navigate')) - ); -}; - /** * Sorts view items by route specificity (most specific first). * - Exact matches (no wildcards/params) come first