-
Notifications
You must be signed in to change notification settings - Fork 202
Expand file tree
/
Copy pathBreadcrumb.tsx
More file actions
110 lines (99 loc) · 3.19 KB
/
Copy pathBreadcrumb.tsx
File metadata and controls
110 lines (99 loc) · 3.19 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
import { Link } from 'react-router-dom';
export interface BreadcrumbItem {
label: string;
to?: string;
}
export interface BreadcrumbProps {
/** Ordered list of breadcrumb items (root first, current last). */
items: BreadcrumbItem[];
/** Maximum visible items before middle-ellipsis kicks in. Default 4. */
maxVisible?: number;
/** Additional CSS class names. */
className?: string;
}
/**
* Breadcrumb — accessible breadcrumb navigation with middle-ellipsis.
*
* When `items.length > maxVisible`, the middle items collapse into a single
* "…" <li> that is `aria-hidden` but replaced by a screen-reader-only "… more"
* label so assistive technology users understand there are hidden items.
*
* WCAG 2.1 AA:
* - Renders inside a `<nav>` with `aria-label="Breadcrumb"`.
* - Last item uses `aria-current="page"`.
* - Separators are `aria-hidden="true"`.
* - Focus rings use `.focus-ring` from `src/styles/focus.css`.
*/
export function Breadcrumb({
items,
maxVisible = 4,
className = '',
}: BreadcrumbProps) {
if (items.length === 0) return null;
const visible = deriveVisible(items, maxVisible);
const containerClasses = ['breadcrumb', className]
.filter(Boolean)
.join(' ');
return (
<nav aria-label="Breadcrumb" className={containerClasses}>
<ol className="breadcrumb__list">
{visible.map((item, idx) => {
const isLast = idx === visible.length - 1;
if (item === null) {
// Middle ellipsis placeholder
return (
<li
key={`ellipsis-${idx}`}
className="breadcrumb__item breadcrumb__item--ellipsis"
aria-hidden="true"
>
<span className="breadcrumb__ellipsis" aria-hidden="true">…</span>
{/* Screen-reader-only label so AT users know items are hidden */}
<span className="sr-only">… more</span>
</li>
);
}
return (
<li key={item.label} className="breadcrumb__item">
{isLast ? (
<span
className="breadcrumb__label breadcrumb__label--current"
aria-current="page"
>
{item.label}
</span>
) : item.to ? (
<Link to={item.to} className="breadcrumb__link">
{item.label}
</Link>
) : (
<span className="breadcrumb__label">{item.label}</span>
)}
{!isLast && (
<span className="breadcrumb__sep" aria-hidden="true">
/
</span>
)}
</li>
);
})}
</ol>
</nav>
);
}
/**
* Derive the visible items list with middle-ellipsis.
*
* When `items.length > maxVisible`, we show:
* [first, null, ...last (maxVisible-2)]
* where null represents the ellipsis slot.
*/
function deriveVisible(
items: BreadcrumbItem[],
maxVisible: number,
): (BreadcrumbItem | null)[] {
if (items.length <= maxVisible) return items;
const tail = items.slice(-(maxVisible - 2));
return [items[0], null, ...tail];
}
export default Breadcrumb;