Skip to content

Commit 4b25ef5

Browse files
committed
Fix handling of routes with no path
1 parent 9c02c93 commit 4b25ef5

3 files changed

Lines changed: 122 additions & 2 deletions

File tree

packages/ra-router-tanstack/src/tanStackRouterProvider.spec.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
NestedRoutesWithOutlet,
2424
NestedResources,
2525
QueryParameters,
26+
PathlessLayoutRoutes,
2627
} from './tanStackRouterProvider.stories';
2728
import { tanStackRouterProvider } from './tanStackRouterProvider';
2829

@@ -1410,4 +1411,35 @@ describe('tanStackRouterProvider', () => {
14101411
});
14111412
});
14121413
});
1414+
1415+
describe('Pathless Layout Routes', () => {
1416+
it('should match pathless layout routes with child routes', async () => {
1417+
window.location.hash = '#/posts';
1418+
1419+
render(<PathlessLayoutRoutes />);
1420+
1421+
await waitFor(() => {
1422+
expect(screen.getByText('Layout Wrapper')).toBeInTheDocument();
1423+
expect(screen.getByTestId('posts-page')).toBeInTheDocument();
1424+
});
1425+
});
1426+
1427+
it('should navigate between child routes within pathless layout', async () => {
1428+
window.location.hash = '#/posts';
1429+
1430+
render(<PathlessLayoutRoutes />);
1431+
1432+
await waitFor(() => {
1433+
expect(screen.getByText('Layout Wrapper')).toBeInTheDocument();
1434+
expect(screen.getByTestId('posts-page')).toBeInTheDocument();
1435+
});
1436+
1437+
fireEvent.click(screen.getByText('Comments'));
1438+
1439+
await waitFor(() => {
1440+
expect(screen.getByText('Layout Wrapper')).toBeInTheDocument();
1441+
expect(screen.getByTestId('comments-page')).toBeInTheDocument();
1442+
});
1443+
});
1444+
});
14131445
});

packages/ra-router-tanstack/src/tanStackRouterProvider.stories.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
CustomRoutes,
2727
Form,
2828
useInput,
29+
RouterProviderContext,
2930
type InputProps,
3031
} from 'ra-core';
3132

@@ -1308,3 +1309,57 @@ export const NestedRoutesWithOutlet = () => {
13081309
</CoreAdmin>
13091310
);
13101311
};
1312+
1313+
export const PathlessLayoutRoutes = () => {
1314+
const { RouterWrapper } = tanStackRouterProvider;
1315+
1316+
return (
1317+
<RouterProviderContext.Provider value={tanStackRouterProvider}>
1318+
<RouterWrapper>
1319+
<Routes>
1320+
<Route
1321+
element={
1322+
<div data-testid="layout-wrapper">
1323+
<h2>Layout Wrapper</h2>
1324+
<nav>
1325+
<LinkBase
1326+
to="/posts"
1327+
style={{ marginRight: 10 }}
1328+
>
1329+
Posts
1330+
</LinkBase>
1331+
<LinkBase to="/comments">Comments</LinkBase>
1332+
</nav>
1333+
<div
1334+
style={{
1335+
border: '2px solid blue',
1336+
padding: 20,
1337+
marginTop: 10,
1338+
}}
1339+
>
1340+
<RouterOutlet />
1341+
</div>
1342+
</div>
1343+
}
1344+
>
1345+
<Route
1346+
path="/posts"
1347+
element={
1348+
<div data-testid="posts-page">Posts Page</div>
1349+
}
1350+
/>
1351+
<Route
1352+
path="/comments"
1353+
element={
1354+
<div data-testid="comments-page">
1355+
Comments Page
1356+
</div>
1357+
}
1358+
/>
1359+
</Route>
1360+
</Routes>
1361+
<LocationDisplay />
1362+
</RouterWrapper>
1363+
</RouterProviderContext.Provider>
1364+
);
1365+
};

packages/ra-router-tanstack/src/tanStackRouterProvider.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ const Navigate = ({ to, replace, state }: RouterNavigateProps) => {
523523
*/
524524
const Route = (_props: RouterRouteProps): null => null;
525525
interface RouteConfig {
526-
path: string;
526+
path?: string;
527527
element?: ReactNode;
528528
index?: boolean;
529529
children?: RouteConfig[];
@@ -590,7 +590,7 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
590590
React.isValidElement(child) && isRouteElement(child)
591591
)
592592
.map(child => ({
593-
path: (child.props as RouterRouteProps).path ?? '',
593+
path: (child.props as RouterRouteProps).path,
594594
element: (child.props as RouterRouteProps).element,
595595
index: (child.props as RouterRouteProps).index,
596596
children: (child.props as RouterRouteProps).children
@@ -606,6 +606,28 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
606606
// Get parent params to merge with current match params
607607
const parentParams = React.useContext(ParamsContext);
608608

609+
// Helper to check if any child route matches the pathname
610+
const childRouteMatches = (
611+
children: RouteConfig[] | undefined,
612+
path: string
613+
): boolean => {
614+
if (!children) return false;
615+
for (const child of children) {
616+
if (child.index && (path === '/' || path === '')) {
617+
return true;
618+
}
619+
if (child.path) {
620+
const match = matchPath({ path: child.path, end: false }, path);
621+
if (match) return true;
622+
}
623+
// Recursively check nested pathless layouts
624+
if (!child.path && child.children) {
625+
if (childRouteMatches(child.children, path)) return true;
626+
}
627+
}
628+
return false;
629+
};
630+
609631
// Find matching route and calculate the new matched path
610632
const matchResult = useMemo(() => {
611633
for (const route of routes) {
@@ -614,6 +636,17 @@ const Routes = ({ children, location: locationProp }: RouterRoutesProps) => {
614636
const newMatchedPath = parentMatchedPath || '/';
615637
return { route, matchedPath: newMatchedPath, params: {} };
616638
}
639+
640+
// Pathless layout route: path is undefined (not empty string) and has children
641+
// Match if any child route would match the pathname
642+
if (route.path === undefined && route.children) {
643+
if (childRouteMatches(route.children, pathname)) {
644+
// Pathless layout doesn't consume any path
645+
const newMatchedPath = parentMatchedPath || '/';
646+
return { route, matchedPath: newMatchedPath, params: {} };
647+
}
648+
}
649+
617650
if (route.path !== undefined) {
618651
const match = matchPath(route.path, pathname);
619652
if (match) {

0 commit comments

Comments
 (0)