mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
chore(react-router): cleaning up util files
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<StackManagerProps> {
|
||||
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<StackManagerProps> {
|
||||
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<StackManagerProps> {
|
||||
/**
|
||||
* 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<StackManagerProps> {
|
||||
// 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<StackManagerProps> {
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Routes } from 'react-router';
|
||||
|
||||
export const getRoutesChildren = (node: React.ReactNode) => {
|
||||
// The use of `<Routes />` 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 `<Routes />` component are most likely
|
||||
// (and should be) the `<Route />` components.
|
||||
return (routesNode as React.ReactElement).props.children;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
51
packages/react-router/src/ReactRouter/utils/routeElements.ts
Normal file
51
packages/react-router/src/ReactRouter/utils/routeElements.ts
Normal file
@@ -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 `<Routes />` 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 `<Routes />` component are most likely
|
||||
// (and should be) the `<Route />` 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'))
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user