fix(react-router): forward index and caseSensitive props through IonRoute

This commit is contained in:
ShaneK
2026-03-10 13:10:55 -07:00
parent 0ff44ec629
commit a864822477
8 changed files with 137 additions and 6 deletions

View File

@@ -2,6 +2,6 @@ import type { IonRouteProps } from '@ionic/react';
import React from 'react';
import { Route } from 'react-router-dom';
export const IonRouteInner = ({ path, element }: IonRouteProps) => {
return <Route path={path} element={element} />;
export const IonRouteInner = ({ path, index, caseSensitive, element }: IonRouteProps) => {
return <Route path={path} index={index} caseSensitive={caseSensitive} element={element} />;
};

View File

@@ -5,7 +5,7 @@
*/
import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react';
import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import { IonRoute, RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import React from 'react';
import { Route } from 'react-router-dom';
@@ -258,7 +258,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
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
(child): child is React.ReactElement =>
React.isValidElement(child) && (child.type === Route || child.type === IonRoute)
);
const hasRelativeRoutes = routeChildren.some((route) => {
@@ -1306,7 +1307,8 @@ function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo, paren
// Collect all route children
const routeChildren = React.Children.toArray(routesChildren).filter(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === Route
(child): child is React.ReactElement =>
React.isValidElement(child) && (child.type === Route || child.type === IonRoute)
);
// Sort routes by specificity (most specific first)

View File

@@ -1,3 +1,4 @@
import { IonRoute } from '@ionic/react';
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
@@ -33,7 +34,8 @@ export const getRoutesChildren = (node: React.ReactNode): React.ReactNode | unde
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
(child): child is React.ReactElement =>
React.isValidElement(child) && (child.type === Route || child.type === IonRoute)
);
};

View File

@@ -47,6 +47,7 @@ import NestedTabsRelativeLinks from './pages/nested-tabs-relative-links/NestedTa
import RootSplatTabs from './pages/root-splat-tabs/RootSplatTabs';
import ContentChangeNavigation from './pages/content-change-navigation/ContentChangeNavigation';
import SearchParams from './pages/search-params/SearchParams';
import IonRoutePropsTest from './pages/ion-route-props/IonRouteProps';
setupIonicReact();
@@ -83,6 +84,7 @@ const App: React.FC = () => {
<Route path="/root-splat-tabs/*" element={<RootSplatTabs />} />
<Route path="/content-change-navigation/*" element={<ContentChangeNavigation />} />
<Route path="/search-params" element={<SearchParams />} />
<Route path="/ion-route-props/*" element={<IonRoutePropsTest />} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>

View File

@@ -86,6 +86,9 @@ const Main: React.FC = () => {
<IonItem routerLink="/search-params">
<IonLabel>Search Params</IonLabel>
</IonItem>
<IonItem routerLink="/ion-route-props">
<IonLabel>IonRoute Props</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>

View File

@@ -0,0 +1,78 @@
import {
IonButton,
IonContent,
IonHeader,
IonPage,
IonRouterOutlet,
IonTitle,
IonToolbar,
IonRoute,
} from '@ionic/react';
import React from 'react';
import { Route } from 'react-router-dom';
/**
* Test page for IonRoute index and caseSensitive props.
*
* Verifies that:
* 1. <IonRoute index> renders as the default when navigating to the parent path
* 2. <IonRoute caseSensitive> correctly enforces case-sensitive path matching
*/
const IndexHome: React.FC = () => (
<IonPage data-pageid="index-home">
<IonHeader>
<IonToolbar>
<IonTitle>Index Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<p id="index-home-text">This is the index route</p>
<IonButton id="go-to-details" routerLink="/ion-route-props/details">
Go to Details
</IonButton>
</IonContent>
</IonPage>
);
const Details: React.FC = () => (
<IonPage data-pageid="ion-route-details">
<IonHeader>
<IonToolbar>
<IonTitle>Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<p id="details-text">Details page</p>
<IonButton id="go-back-home" routerLink="/ion-route-props" routerDirection="back">
Back to Home
</IonButton>
</IonContent>
</IonPage>
);
const CaseSensitivePage: React.FC = () => (
<IonPage data-pageid="case-sensitive-page">
<IonHeader>
<IonToolbar>
<IonTitle>Case Sensitive Match</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<p id="case-sensitive-text">Matched case-sensitive route</p>
</IonContent>
</IonPage>
);
const IonRoutePropsTest: React.FC = () => (
<IonRouterOutlet>
{/* Index route: should render when navigating to /ion-route-props */}
<IonRoute index element={<IndexHome />} />
<IonRoute path="details" element={<Details />} />
<IonRoute path="CaseSensitive" caseSensitive element={<CaseSensitivePage />} />
{/* Catch-all for unmatched routes */}
<Route path="*" element={<IndexHome />} />
</IonRouterOutlet>
);
export default IonRoutePropsTest;

View File

@@ -0,0 +1,42 @@
const port = 3000;
describe('IonRoute Props', () => {
/**
* Tests that IonRoute correctly forwards index and caseSensitive props.
*
* Previously, IonRouteInner only destructured path and element,
* silently dropping index and caseSensitive props.
*/
describe('index prop', () => {
it('should render the index route when navigating to parent path', () => {
cy.visit(`http://localhost:${port}/ion-route-props`);
cy.ionPageVisible('index-home');
cy.get('#index-home-text').should('be.visible').and('contain', 'This is the index route');
});
it('should navigate from index route to details and back', () => {
cy.visit(`http://localhost:${port}/ion-route-props`);
cy.ionPageVisible('index-home');
cy.ionNav('ion-button', 'Go to Details');
cy.ionPageVisible('ion-route-details');
cy.get('#details-text').should('be.visible');
cy.ionNav('ion-button', 'Back to Home');
cy.ionPageVisible('index-home');
});
});
describe('caseSensitive prop', () => {
it('should match the exact case path', () => {
cy.visit(`http://localhost:${port}/ion-route-props/CaseSensitive`);
cy.ionPageVisible('case-sensitive-page');
cy.get('#case-sensitive-text').should('be.visible');
});
it('should not match a different case path when caseSensitive is true', () => {
cy.visit(`http://localhost:${port}/ion-route-props/casesensitive`);
// The case-sensitive page should NOT be visible since the case doesn't match
cy.get('[data-pageid="case-sensitive-page"]').should('not.exist');
});
});
});

View File

@@ -4,6 +4,8 @@ import { NavContext } from '../contexts/NavContext';
export interface IonRouteProps {
path?: string;
index?: boolean;
caseSensitive?: boolean;
show?: boolean;
element: React.ReactElement;
disableIonPageManagement?: boolean;