-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathBreadcrumb.tsx
More file actions
146 lines (134 loc) · 5.59 KB
/
Copy pathBreadcrumb.tsx
File metadata and controls
146 lines (134 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { Breadcrumbs } from '@mui/material';
import React, { useMemo } from 'react';
import { BodyText } from '../Typography';
import { Link } from '.';
import { UIMatch, useMatches } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/pro-regular-svg-icons';
export type BreadcrumbProps = {
name: string;
link?: string;
// If true, this crumb will be considered an index page for its parent and will not be used
// when generating the page title, though the breadcrumb will still render unaffected.
title?: string; // Optional custom title for document title generation, if different from the breadcrumb name
isIndex?: boolean;
};
/**
* A component that displays a breadcrumb trail based on the provided crumbs.
* Each crumb can be a link or plain text, and the last crumb is always displayed as plain text.
* @param {Object} props - The properties for the breadcrumb trail component.
* @param {Object[]} props.crumbs - An array of objects representing the breadcrumb items, each with a name and an optional link.
* @param {boolean} [props.smallScreenOnly] - If true, the breadcrumbs will only be displayed on small screens.
* @returns A JSX element representing the breadcrumb trail.
* @example
* <BreadcrumbTrail
* crumbs={[
* { name: 'Home', link: '/' },
* { name: 'Products', link: '/products' },
* { name: 'Electronics' } // Last crumb without a link
* ]}
* smallScreenOnly={true}
* />
*/
export const BreadcrumbTrail: React.FC<{ crumbs: BreadcrumbProps[]; smallScreenOnly?: boolean }> = ({
crumbs,
smallScreenOnly,
}) => {
return (
<Breadcrumbs
aria-label="breadcrumb"
component="nav"
sx={smallScreenOnly ? { display: { xs: 'block', md: 'none' } } : {}}
>
{crumbs.map((crumb, index) =>
crumb.link ? (
<Link size="small" key={crumb.name} to={crumb.link} underline="hover">
{crumb.name}
</Link>
) : (
<BodyText
size="small"
bold={index == crumbs.length - 1}
key={crumb.name}
sx={{ lineHeight: '24px' }}
>
{crumb.name}
</BodyText>
),
)}
</Breadcrumbs>
);
};
export type UICrumbFunction = (data: unknown) => Promise<BreadcrumbProps> | BreadcrumbProps;
export interface UIRouteHandle {
crumb?: UICrumbFunction;
excludeFromTitle?: boolean; // If true, this route's crumb will be excluded from the document title generation
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UIMatchWithCrumb extends UIMatch<unknown, UIRouteHandle> {}
/**
* Automatically generates breadcrumbs based on the `handle.crumb` function of the current route and its parents.
* @param {Object} props - The properties for the AutoBreadcrumbs component.
* @param {boolean} [props.smallScreenOnly] - If true, only displays the breadcrumbs on small screens.
* @returns A list of breadcrumbs.
*/
export const AutoBreadcrumbs: React.FC<{ smallScreenOnly?: boolean }> = ({ smallScreenOnly }) => {
const matches = (useMatches() as UIMatchWithCrumb[]).filter((match) => match.handle?.crumb);
const matchKey = matches.map((m) => m.pathname).join('|');
const crumbs = useMemo(() => {
return matches.map((match) => {
const data = match.loaderData;
const handle = match.handle;
try {
return handle?.crumb?.(data) ?? { name: '', link: '' };
} catch {
return { name: '', link: '' };
}
});
}, [matchKey]); // Recompute only when matches change
const resolvedCrumbs = crumbs.map((unresolvedCrumb) => {
if (unresolvedCrumb instanceof Promise) {
return React.use(unresolvedCrumb);
}
return unresolvedCrumb;
});
const crumbMap = Object.create(null);
resolvedCrumbs.forEach((crumb, index) => {
const pathname = matches[index]?.pathname;
if (pathname) {
crumbMap[pathname] = crumb;
}
});
return (
<Breadcrumbs
aria-label="breadcrumbs"
sx={{
display: smallScreenOnly ? { xs: 'block', md: 'none' } : undefined,
fontSize: '14px',
}}
>
{matches.map((match, index) => {
const resolvedCrumb = crumbMap[match.pathname];
if (!resolvedCrumb) return null;
const name = resolvedCrumb?.name;
const link = index < matches.length - 1 ? (resolvedCrumb?.link ?? match.pathname) : undefined;
return link ? (
<Link size="small" key={match.pathname + name} to={link} sx={{ lineHeight: '24px' }}>
{index === 0 && <FontAwesomeIcon icon={faHome} style={{ marginRight: '4px' }} />}
{name}
</Link>
) : (
<BodyText
size="small"
bold={index === matches.length - 1}
key={match.pathname + name}
sx={{ lineHeight: '24px' }}
>
{index === 0 && <FontAwesomeIcon icon={faHome} style={{ marginRight: '4px' }} />}
{name}
</BodyText>
);
})}
</Breadcrumbs>
);
};