diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js
new file mode 100644
index 00000000000..a792a7df846
--- /dev/null
+++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { PageContainer } from '@toolpad/core/PageContainer';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { Link, useDemoRouter } from '@toolpad/core/internals';
+import { useActivePage } from '@toolpad/core/useActivePage';
+import { useTheme } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import Paper from '@mui/material/Paper';
+import invariant from 'invariant';
+
+const NAVIGATION = [
+ {
+ segment: 'inbox',
+ title: 'Orders',
+ pattern: '/inbox/:id',
+ },
+];
+
+function Content({ router }) {
+ const id = Number(router.pathname.replace('/inbox/', ''));
+
+ const activePage = useActivePage();
+ invariant(activePage, 'No navigation match');
+
+ const title = `Item ${id}`;
+ const path = `${activePage.path}/${id}`;
+
+ const breadCrumbs = [...activePage.breadCrumbs, { title, path }];
+
+ return (
+ // preview-start
+
+ {/* preview-end */}
+
+ previous
+ next
+
+
+ );
+}
+
+Content.propTypes = {
+ router: PropTypes.shape({
+ navigate: PropTypes.func.isRequired,
+ pathname: PropTypes.string.isRequired,
+ searchParams: PropTypes.instanceOf(URLSearchParams).isRequired,
+ }).isRequired,
+};
+
+export default function CustomPageContainer() {
+ const router = useDemoRouter('/inbox/123');
+
+ const theme = useTheme();
+
+ let content = (
+
+ Item 123
+
+ );
+
+ if (router.pathname.startsWith('/inbox/')) {
+ content = ;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx
new file mode 100644
index 00000000000..3b913515a49
--- /dev/null
+++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import { PageContainer } from '@toolpad/core/PageContainer';
+import { AppProvider, Router } from '@toolpad/core/AppProvider';
+import { Link, useDemoRouter } from '@toolpad/core/internals';
+import { useActivePage } from '@toolpad/core/useActivePage';
+import { useTheme } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import Paper from '@mui/material/Paper';
+import invariant from 'invariant';
+
+const NAVIGATION = [
+ {
+ segment: 'inbox',
+ title: 'Orders',
+ pattern: '/inbox/:id',
+ },
+];
+
+interface ContentProps {
+ router: Router;
+}
+
+function Content({ router }: ContentProps) {
+ const id = Number(router.pathname.replace('/inbox/', ''));
+
+ const activePage = useActivePage();
+ invariant(activePage, 'No navigation match');
+
+ const title = `Item ${id}`;
+ const path = `${activePage.path}/${id}`;
+
+ const breadCrumbs = [...activePage.breadCrumbs, { title, path }];
+
+ return (
+ // preview-start
+
+ {/* preview-end */}
+
+ previous
+ next
+
+
+ );
+}
+
+export default function CustomPageContainer() {
+ const router = useDemoRouter('/inbox/123');
+
+ const theme = useTheme();
+
+ let content = (
+
+ Item 123
+
+ );
+
+ if (router.pathname.startsWith('/inbox/')) {
+ content = ;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview
new file mode 100644
index 00000000000..06545ed2211
--- /dev/null
+++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js
index a90486b361e..f0604f19386 100644
--- a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js
+++ b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js
@@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles';
import Paper from '@mui/material/Paper';
const NAVIGATION = [
+ { segment: '', title: 'ACME' },
{
segment: 'inbox',
title: 'Home',
@@ -24,12 +25,7 @@ export default function TitleBreadcrumbsPageContainer() {
const theme = useTheme();
return (
-
+
diff --git a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx
index a90486b361e..f0604f19386 100644
--- a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx
+++ b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx
@@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles';
import Paper from '@mui/material/Paper';
const NAVIGATION = [
+ { segment: '', title: 'ACME' },
{
segment: 'inbox',
title: 'Home',
@@ -24,12 +25,7 @@ export default function TitleBreadcrumbsPageContainer() {
const theme = useTheme();
return (
-
+
diff --git a/docs/data/toolpad/core/components/page-container/page-container.md b/docs/data/toolpad/core/components/page-container/page-container.md
index a39e4161402..81cd357c368 100644
--- a/docs/data/toolpad/core/components/page-container/page-container.md
+++ b/docs/data/toolpad/core/components/page-container/page-container.md
@@ -22,7 +22,6 @@ For example, under the following navigation structure:
```tsx
();
+ const activePage = useActivePage();
+ invariant(activePage, 'No navigation match');
+
+ const title = `Item ${params.id}`;
+ const path = `${activePage.path}/${params.id}`;
+
+ const breadCrumbs = [...activePage.breadCrumbs, { title, path }];
+
+ return (
+
+ ...
+
+ );
+}
+```
+
## Actions
You can configure additional actions in the area that is reserved on the right. To do so provide the `toolbar` slot to the `PageContainer` component. You can wrap the `PageContainerToolbar` component to create a custom toolbar component, as shown here:
diff --git a/docs/pages/toolpad/core/api/app-provider.json b/docs/pages/toolpad/core/api/app-provider.json
index 222066ae3ea..27ff3b8a3f4 100644
--- a/docs/pages/toolpad/core/api/app-provider.json
+++ b/docs/pages/toolpad/core/api/app-provider.json
@@ -19,7 +19,7 @@
"router": {
"type": {
"name": "shape",
- "description": "{ navigate: func, pathname: string, searchParams?: URLSearchParams }"
+ "description": "{ navigate: func, pathname: string, searchParams: URLSearchParams }"
},
"default": "null"
},
diff --git a/docs/pages/toolpad/core/api/page-container.json b/docs/pages/toolpad/core/api/page-container.json
index 82219960b02..79293eba86a 100644
--- a/docs/pages/toolpad/core/api/page-container.json
+++ b/docs/pages/toolpad/core/api/page-container.json
@@ -1,5 +1,15 @@
{
- "props": {},
+ "props": {
+ "breadCrumbs": {
+ "type": { "name": "arrayOf", "description": "Array<{ path: string, title: string }>" }
+ },
+ "slotProps": { "type": { "name": "shape", "description": "{ toolbar: { children?: node } }" } },
+ "slots": {
+ "type": { "name": "shape", "description": "{ toolbar?: elementType }" },
+ "additionalInfo": { "slotsApi": true }
+ },
+ "title": { "type": { "name": "string" } }
+ },
"name": "PageContainer",
"imports": [
"import { PageContainer } from '@toolpad/core/PageContainer';",
diff --git a/docs/translations/api-docs/page-container/page-container.json b/docs/translations/api-docs/page-container/page-container.json
index 643fe8e0f28..39823940d46 100644
--- a/docs/translations/api-docs/page-container/page-container.json
+++ b/docs/translations/api-docs/page-container/page-container.json
@@ -1,6 +1,13 @@
{
- "componentDescription": "",
- "propDescriptions": {},
+ "componentDescription": "A container component to provide a title and breadcrumbs for your pages.",
+ "propDescriptions": {
+ "breadCrumbs": {
+ "description": "The breadcrumbs of the page. Leave blank to use the active page breadcrumbs."
+ },
+ "slotProps": { "description": "The props used for each slot inside." },
+ "slots": { "description": "The components used for each slot inside." },
+ "title": { "description": "The title of the page. Leave blank to use the active page title." }
+ },
"classDescriptions": {
"disableGutters": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
diff --git a/package.json b/package.json
index 35a1d598f67..eabb25451d2 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
"@mui/internal-babel-plugin-resolve-imports": "1.0.18",
"@mui/internal-docs-utils": "1.0.13",
"@mui/internal-markdown": "1.0.14",
- "@mui/internal-scripts": "1.0.21",
+ "@mui/internal-scripts": "1.0.21-dev.20240919-130050-82a6448768",
"@mui/monorepo": "github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906",
"@mui/x-charts": "7.17.0",
"@next/eslint-plugin-next": "14.2.11",
diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx
index 029960cdadc..ba183f2bc88 100644
--- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx
+++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx
@@ -239,10 +239,10 @@ AppProvider.propTypes /* remove-proptypes */ = {
* Router implementation used inside Toolpad components.
* @default null
*/
- router: PropTypes /* @typescript-to-proptypes-ignore */.shape({
+ router: PropTypes.shape({
navigate: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
- searchParams: PropTypes.instanceOf(URLSearchParams),
+ searchParams: PropTypes.instanceOf(URLSearchParams).isRequired,
}),
/**
* Session info about the current user.
diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx
index 431cf8454dc..f6509911bd3 100644
--- a/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx
+++ b/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx
@@ -48,6 +48,7 @@ describe('PageContainer', () => {
test('renders nested', async () => {
const navigation = [
+ { segment: '', title: 'ACME' },
{
segment: 'home',
title: 'Home',
@@ -79,4 +80,36 @@ describe('PageContainer', () => {
expect(within(breadCrumbs).getByText('Home')).toBeTruthy();
expect(within(breadCrumbs).getByText('Orders')).toBeTruthy();
});
+
+ test('renders dynamic correctly', async () => {
+ const user = await userEvent.setup();
+ const router = {
+ pathname: '/orders/123',
+ searchParams: new URLSearchParams(),
+ navigate: vi.fn(),
+ };
+ render(
+
+
+ ,
+ );
+
+ const breadCrumbs = screen.getByRole('navigation', { name: 'breadcrumb' });
+
+ const homeLink = within(breadCrumbs).getByRole('link', { name: 'Home' });
+ await user.click(homeLink);
+
+ expect(router.navigate).toHaveBeenCalledWith('/', expect.objectContaining({}));
+ router.navigate.mockClear();
+
+ expect(within(breadCrumbs).getByText('Orders')).toBeTruthy();
+
+ expect(screen.getByText('Orders', { ignore: 'nav *' }));
+ });
});
diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx
index ba5f5052932..7ff87f6caf2 100644
--- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx
+++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx
@@ -10,77 +10,16 @@ import useSlotProps from '@mui/utils/useSlotProps';
import { styled } from '@mui/material';
import { Link as ToolpadLink } from '../shared/Link';
import { PageContainerToolbar, PageContainerToolbarProps } from './PageContainerToolbar';
-import { NavigationContext, RouterContext } from '../shared/context';
-import { getItemTitle, isPageItem } from '../shared/navigation';
-import { NavigationItem, NavigationPageItem, Navigation } from '../AppProvider';
-import { useApplicationTitle } from '../shared/branding';
+import { getItemTitle } from '../shared/navigation';
+import { useActivePage } from '../useActivePage';
const PageContentHeader = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
- jusifyCOntent: 'space-between',
+ jusifyContent: 'space-between',
gap: theme.spacing(2),
}));
-const isRootPage = (item: NavigationItem) => isPageItem(item) && !item.segment;
-
-interface BreadCrumbItem extends NavigationPageItem {
- path: string;
-}
-
-function createPageLookup(
- navigation: Navigation,
- segments: BreadCrumbItem[] = [],
- base = '',
-): Map {
- const result = new Map();
-
- const resolveSegment = (segment?: string) => `${base}${segment ? `/${segment}` : ''}` || '/';
-
- const root = navigation.find((item) => isRootPage(item)) as NavigationPageItem | undefined;
- const rootCrumb = root ? { path: resolveSegment(''), ...root } : undefined;
-
- for (const item of navigation) {
- if (!isPageItem(item)) {
- continue;
- }
-
- const isNonProdEnv = process.env.NODE_ENV !== 'production';
-
- const path = resolveSegment(item.segment);
- if (isNonProdEnv && result.has(path)) {
- console.warn(`Duplicate path in navigation: ${path}`);
- }
-
- const itemCrumb: BreadCrumbItem = { path, ...item };
-
- const navigationSegments: BreadCrumbItem[] = [
- ...segments,
- ...(rootCrumb && !isRootPage(item) ? [rootCrumb] : []),
- itemCrumb,
- ];
-
- result.set(path, navigationSegments);
-
- if (item.children) {
- const childrenLookup = createPageLookup(item.children, navigationSegments, path);
- for (const [childPath, childItems] of childrenLookup) {
- if (isNonProdEnv && result.has(childPath)) {
- console.warn(`Duplicate path in navigation: ${childPath}`);
- }
- result.set(childPath, childItems);
- }
- }
- }
-
- return result;
-}
-
-function matchPath(navigation: Navigation, path: string): BreadCrumbItem[] | null {
- const lookup = createPageLookup(navigation);
- return lookup.get(path) ?? null;
-}
-
export interface PageContainerSlotProps {
toolbar: PageContainerToolbarProps;
}
@@ -93,13 +32,39 @@ export interface PageContainerSlots {
toolbar: React.ElementType;
}
+export interface BreadCrumb {
+ /**
+ * The title of the breadcrumb segment.
+ */
+ title: string;
+ /**
+ * The path the breadcrumb links to.
+ */
+ path: string;
+}
+
export interface PageContainerProps extends ContainerProps {
children?: React.ReactNode;
+ /**
+ * The title of the page. Leave blank to use the active page title.
+ */
title?: string;
+ /**
+ * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
+ */
+ breadCrumbs?: BreadCrumb[];
+ /**
+ * The components used for each slot inside.
+ */
slots?: PageContainerSlots;
+ /**
+ * The props used for each slot inside.
+ */
slotProps?: PageContainerSlotProps;
}
+
/**
+ * A container component to provide a title and breadcrumbs for your pages.
*
* Demos:
*
@@ -111,28 +76,11 @@ export interface PageContainerProps extends ContainerProps {
*/
function PageContainer(props: PageContainerProps) {
const { children, slots, slotProps, ...rest } = props;
- const routerContext = React.useContext(RouterContext);
- const navigationContext = React.useContext(NavigationContext);
- const pathname = routerContext?.pathname ?? '/';
- const applicationTitle = useApplicationTitle();
- const breadCrumbs = React.useMemo(() => {
- let crumbs = matchPath(navigationContext, pathname) ?? [];
- if (crumbs.length <= 0 || crumbs[0].path !== '/') {
- crumbs = [
- {
- segment: '',
- path: '/',
- title: applicationTitle,
- },
- ...crumbs,
- ];
- }
- return crumbs;
- }, [navigationContext, pathname, applicationTitle]);
+ const activePage = useActivePage();
- const title =
- (breadCrumbs ? getItemTitle(breadCrumbs[breadCrumbs.length - 1]) : '') ?? props.title;
+ const breadCrumbs = props.breadCrumbs ?? activePage?.breadCrumbs ?? [];
+ const title = props.title ?? activePage?.title ?? '';
const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar;
const toolbarSlotProps = useSlotProps({
@@ -184,12 +132,21 @@ PageContainer.propTypes /* remove-proptypes */ = {
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
+ */
+ breadCrumbs: PropTypes.arrayOf(
+ PropTypes.shape({
+ path: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ }),
+ ),
/**
* @ignore
*/
children: PropTypes.node,
/**
- * @ignore
+ * The props used for each slot inside.
*/
slotProps: PropTypes.shape({
toolbar: PropTypes.shape({
@@ -197,13 +154,13 @@ PageContainer.propTypes /* remove-proptypes */ = {
}).isRequired,
}),
/**
- * @ignore
+ * The components used for each slot inside.
*/
slots: PropTypes.shape({
toolbar: PropTypes.elementType,
}),
/**
- * @ignore
+ * The title of the page. Leave blank to use the active page title.
*/
title: PropTypes.string,
} as any;
diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts
index c8ff0dade87..dcb98090703 100644
--- a/packages/toolpad-core/src/index.ts
+++ b/packages/toolpad-core/src/index.ts
@@ -8,6 +8,8 @@ export * from './Account';
export * from './PageContainer';
+export * from './useActivePage';
+
export * from './useDialogs';
export * from './useNotifications';
diff --git a/packages/toolpad-core/src/internals/demo.tsx b/packages/toolpad-core/src/internals/demo.tsx
index 0b41c363166..bdda55216fe 100644
--- a/packages/toolpad-core/src/internals/demo.tsx
+++ b/packages/toolpad-core/src/internals/demo.tsx
@@ -30,3 +30,5 @@ export function useDemoRouter(initialUrl: string = '/') {
return router;
}
+
+export { Link } from '../shared/Link';
diff --git a/packages/toolpad-core/src/shared/navigation.tsx b/packages/toolpad-core/src/shared/navigation.tsx
index 851f7902af2..f986a3fd67b 100644
--- a/packages/toolpad-core/src/shared/navigation.tsx
+++ b/packages/toolpad-core/src/shared/navigation.tsx
@@ -1,5 +1,11 @@
import { pathToRegexp } from 'path-to-regexp';
-import type { NavigationItem, NavigationPageItem, NavigationSubheaderItem } from '../AppProvider';
+import invariant from 'invariant';
+import type {
+ Navigation,
+ NavigationItem,
+ NavigationPageItem,
+ NavigationSubheaderItem,
+} from '../AppProvider';
export const getItemKind = (item: NavigationItem) => item.kind ?? 'page';
@@ -51,3 +57,110 @@ export function hasSelectedNavigationChildren(
return false;
}
+
+/**
+ * Builds a map of navigation page items to their respective paths. This map is used to quickly
+ * lookup the path of a navigation item. It will be cached for the lifetime of the navigation.
+ */
+function buildItemToPathMap(navigation: Navigation): Map {
+ const map = new Map();
+
+ const visit = (item: NavigationItem, base: string) => {
+ if (isPageItem(item)) {
+ const path = `${base}${item.segment ? `/${item.segment}` : ''}` || '/';
+ map.set(item, path);
+ if (item.children) {
+ for (const child of item.children) {
+ visit(child, path);
+ }
+ }
+ }
+ };
+
+ for (const item of navigation) {
+ visit(item, '');
+ }
+
+ return map;
+}
+
+const itemToPathMapCache = new WeakMap>();
+
+/**
+ * Gets the cached map of navigation page items to their respective paths.
+ */
+function getItemToPathMap(navigation: Navigation) {
+ let map = itemToPathMapCache.get(navigation);
+ if (!map) {
+ map = buildItemToPathMap(navigation);
+ itemToPathMapCache.set(navigation, map);
+ }
+ return map;
+}
+
+/**
+ * Build a lookup map of paths to navigation items. This map is used to match paths against
+ * to find the active page.
+ */
+function buildItemLookup(navigation: Navigation) {
+ const map = new Map();
+ const visit = (item: NavigationItem) => {
+ if (isPageItem(item)) {
+ const path = getItemPath(navigation, item);
+ if (map.has(path)) {
+ console.warn(`Duplicate path in navigation: ${path}`);
+ }
+ map.set(path, item);
+ if (item.pattern) {
+ map.set(pathToRegexp(item.pattern), item);
+ }
+ if (item.children) {
+ for (const child of item.children) {
+ visit(child);
+ }
+ }
+ }
+ };
+ for (const item of navigation) {
+ visit(item);
+ }
+ return map;
+}
+const itemLookupMapCache = new WeakMap>();
+function getItemLookup(navigation: Navigation) {
+ let map = itemLookupMapCache.get(navigation);
+ if (!map) {
+ map = buildItemLookup(navigation);
+ itemLookupMapCache.set(navigation, map);
+ }
+ return map;
+}
+
+/**
+ * Matches a path against the navigation to find the active page. i.e. the page that should be
+ * marked as selected in the navigation.
+ */
+export function matchPath(navigation: Navigation, path: string): NavigationPageItem | null {
+ const lookup = getItemLookup(navigation);
+
+ for (const [key, item] of lookup.entries()) {
+ if (typeof key === 'string' && key === path) {
+ return item;
+ }
+ if (key instanceof RegExp && key.test(path)) {
+ return item;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Gets the path for a specific navigation page item.
+ */
+export function getItemPath(navigation: Navigation, item: NavigationPageItem): string {
+ const map = getItemToPathMap(navigation);
+ const path = map.get(item);
+ invariant(path, `Item not found in navigation: ${item.title}`);
+ return path;
+}
diff --git a/packages/toolpad-core/src/useActivePage/index.ts b/packages/toolpad-core/src/useActivePage/index.ts
new file mode 100644
index 00000000000..52662dbd290
--- /dev/null
+++ b/packages/toolpad-core/src/useActivePage/index.ts
@@ -0,0 +1 @@
+export * from './useActivePage';
diff --git a/packages/toolpad-core/src/useActivePage/useActivePage.ts b/packages/toolpad-core/src/useActivePage/useActivePage.ts
new file mode 100644
index 00000000000..e1b6a9762d5
--- /dev/null
+++ b/packages/toolpad-core/src/useActivePage/useActivePage.ts
@@ -0,0 +1,54 @@
+'use client';
+import * as React from 'react';
+import { NavigationContext, RouterContext } from '../shared/context';
+import { getItemPath, getItemTitle, matchPath } from '../shared/navigation';
+import type { BreadCrumb } from '../PageContainer';
+
+export function useActivePage() {
+ const navigationContext = React.useContext(NavigationContext);
+ const routerContext = React.useContext(RouterContext);
+ const pathname = routerContext?.pathname ?? '/';
+ const activeItem = matchPath(navigationContext, pathname);
+
+ const rootItem = matchPath(navigationContext, '/');
+
+ return React.useMemo(() => {
+ if (!activeItem) {
+ return null;
+ }
+
+ const breadCrumbs: BreadCrumb[] = [];
+
+ if (rootItem) {
+ breadCrumbs.push({
+ title: getItemTitle(rootItem),
+ path: '/',
+ });
+ }
+
+ const segments = pathname.split('/').filter(Boolean);
+ let prefix = '';
+ for (const segment of segments) {
+ const path = `${prefix}/${segment}`;
+ prefix = path;
+ const item = matchPath(navigationContext, path);
+ if (!item) {
+ continue;
+ }
+ const itemPath = getItemPath(navigationContext, item);
+ const lastCrumb = breadCrumbs[breadCrumbs.length - 1];
+ if (lastCrumb?.path !== itemPath) {
+ breadCrumbs.push({
+ title: getItemTitle(item),
+ path: itemPath,
+ });
+ }
+ }
+
+ return {
+ title: getItemTitle(activeItem),
+ path: getItemPath(navigationContext, activeItem),
+ breadCrumbs,
+ };
+ }, [activeItem, rootItem, pathname, navigationContext]);
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c0b4090e51f..0af1141a20b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -98,8 +98,8 @@ importers:
specifier: 1.0.14
version: 1.0.14
'@mui/internal-scripts':
- specifier: 1.0.21
- version: 1.0.21
+ specifier: 1.0.21-dev.20240919-130050-82a6448768
+ version: 1.0.21-dev.20240919-130050-82a6448768
'@mui/monorepo':
specifier: github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906
version: https://codeload.github.com/mui/material-ui/tar.gz/3732be30e0ae3287b55dc6a96cf1ffcfdc194906(encoding@0.1.13)
@@ -195,7 +195,7 @@ importers:
version: 7.35.2(eslint@8.57.0)
eslint-plugin-react-compiler:
specifier: latest
- version: 0.0.0-experimental-7670337-20240918(eslint@8.57.0)
+ version: 0.0.0-experimental-92aaa43-20240919(eslint@8.57.0)
eslint-plugin-react-hooks:
specifier: 4.6.2
version: 4.6.2(eslint@8.57.0)
@@ -2852,8 +2852,8 @@ packages:
'@mui/internal-markdown@1.0.14':
resolution: {integrity: sha512-Df8Uo54TyE7Lm3otWWxw2OQzv3ztbdhRJMJ2sCYrTDqluAvGe/65KpVr95/AcY/An3qWpvBrbn5k5Oj/NG/lXw==}
- '@mui/internal-scripts@1.0.21':
- resolution: {integrity: sha512-nVjt+Yjjff83C+urxDD63LmUhIR1bMbGVexHdFq1v9odyVKXw3tgL1yiCoEETLUfOkBbXECmjuRcbw+meWu5+g==}
+ '@mui/internal-scripts@1.0.21-dev.20240919-130050-82a6448768':
+ resolution: {integrity: sha512-feyp1s9pHvSOdO+ERv8oPGp2qGUCnngTt0+Yb+yTBo1tH8DN1Ks5JA7gIQR/elTnDusrbBQPpI7IAG5ZPrYlzQ==}
'@mui/joy@5.0.0-beta.48':
resolution: {integrity: sha512-OhTvjuGl9I5IvpBr0BQyDehIW/xb2yteW6YglHJMdOb/279nItn76X1NBtPV9ImldNlBjReGwvpOXmBTTGER9w==}
@@ -5824,8 +5824,8 @@ packages:
peerDependencies:
eslint: '>=7.0.0'
- eslint-plugin-react-compiler@0.0.0-experimental-7670337-20240918:
- resolution: {integrity: sha512-FZXoYqGK3BiJw2BFQIUL0C9GgEF2QS+y6oiVPm4GooX6SXrg/UVtCi1nMAaBhqTpsF/KF2RjobgePY8KgbqXow==}
+ eslint-plugin-react-compiler@0.0.0-experimental-92aaa43-20240919:
+ resolution: {integrity: sha512-l1tEUmxnZcMNkpUffbyNPAV91kZMZYLVeCCRZajK/s1QdP9FquunJQ9uT4c3f0RqdV6n0kloVfnJ0lPD1FTPlg==}
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
peerDependencies:
eslint: '>=7'
@@ -12040,7 +12040,7 @@ snapshots:
marked: 13.0.3
prismjs: 1.29.0
- '@mui/internal-scripts@1.0.21':
+ '@mui/internal-scripts@1.0.21-dev.20240919-130050-82a6448768':
dependencies:
'@babel/core': 7.25.2
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2)
@@ -15628,7 +15628,7 @@ snapshots:
globals: 13.24.0
rambda: 7.5.0
- eslint-plugin-react-compiler@0.0.0-experimental-7670337-20240918(eslint@8.57.0):
+ eslint-plugin-react-compiler@0.0.0-experimental-92aaa43-20240919(eslint@8.57.0):
dependencies:
'@babel/core': 7.25.2
'@babel/parser': 7.25.6