Skip to content

Commit 35d5c42

Browse files
authored
fix(breadcrumbs): make breadcrumbs visible on mobile viewports (#111)
1 parent 1cad157 commit 35d5c42

File tree

3 files changed

+80
-8
lines changed

3 files changed

+80
-8
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/kumo": patch
3+
---
4+
5+
fix(breadcrumbs): improve mobile breadcrumb readability
6+
7+
breadcrumbs now render a compact mobile trail for deeper hierarchies by collapsing early levels to `...` and keeping the trailing path visible. labels in breadcrumb links and the current page now truncate correctly to prevent stacking or overlap on narrow viewports.

.changeset/sparkly-words-strive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/kumo": patch
3+
---
4+
5+
fix(breadcrumbs): enhance mobile breadcrumb display for better readability

packages/kumo/src/components/breadcrumbs/breadcrumbs.tsx

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { useEffect, useState, type PropsWithChildren } from "react";
1+
import {
2+
Children,
3+
cloneElement,
4+
isValidElement,
5+
useEffect,
6+
useState,
7+
type PropsWithChildren,
8+
type ReactElement,
9+
type ReactNode,
10+
} from "react";
211
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
312
import { Button } from "../../components/button";
413
import { SkeletonLine } from "../../components/loader/skeleton-line";
@@ -39,7 +48,7 @@ export function breadcrumbsVariants({
3948
size = KUMO_BREADCRUMBS_DEFAULT_VARIANTS.size,
4049
}: KumoBreadcrumbsVariantsProps = {}) {
4150
return cn(
42-
"group mr-4 hidden min-w-0 grow items-center sm:flex",
51+
"group mr-4 flex min-w-0 grow items-center overflow-hidden whitespace-nowrap",
4352
KUMO_BREADCRUMBS_VARIANTS.size[size].classes,
4453
);
4554
}
@@ -59,10 +68,10 @@ const Link = ({
5968
return (
6069
<LinkComponent
6170
to={href}
62-
className="flex min-w-0 items-center gap-1 text-kumo-subtle no-underline"
71+
className="flex min-w-0 max-w-full items-center gap-1 text-kumo-subtle no-underline"
6372
>
6473
{!!icon && <span className="flex shrink-0 items-center">{icon}</span>}
65-
{children}
74+
<span className="truncate">{children}</span>
6675
</LinkComponent>
6776
);
6877
};
@@ -88,18 +97,21 @@ function Current({
8897

8998
return (
9099
<div
91-
className="flex items-center gap-1 truncate font-medium"
100+
className="flex min-w-0 max-w-full items-center gap-1 font-medium"
92101
aria-current="page"
93102
>
94103
{icon && <span className="flex shrink-0 items-center">{icon}</span>}
95-
{children}
104+
<span className="truncate">{children}</span>
96105
</div>
97106
);
98107
}
99108

100109
function Separator() {
101110
return (
102-
<span className="flex items-center text-kumo-inactive" aria-hidden="true">
111+
<span
112+
className="flex shrink-0 items-center text-kumo-inactive"
113+
aria-hidden="true"
114+
>
103115
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
104116
<path
105117
stroke="currentColor"
@@ -113,6 +125,14 @@ function Separator() {
113125
);
114126
}
115127

128+
function MobileEllipsis() {
129+
return (
130+
<span className="flex shrink-0 items-center text-kumo-subtle" aria-hidden>
131+
...
132+
</span>
133+
);
134+
}
135+
116136
function Clipboard({ text }: { text: string }) {
117137
const [isCopied, setIsCopied] = useState(false);
118138

@@ -192,16 +212,56 @@ export function Breadcrumb({
192212
size = "base",
193213
className,
194214
}: BreadcrumbsProps) {
215+
const childArray = Children.toArray(children);
216+
const mobileChildren = getMobileBreadcrumbChildren(childArray);
217+
195218
return (
196219
<nav
197220
className={cn(breadcrumbsVariants({ size }), className)}
198221
aria-label="breadcrumb"
199222
>
200-
{children}
223+
<div className="contents sm:hidden">{mobileChildren}</div>
224+
<div className="hidden sm:contents">{childArray}</div>
201225
</nav>
202226
);
203227
}
204228

229+
function isComponentElement(
230+
child: ReactNode,
231+
component: unknown,
232+
): child is ReactElement {
233+
return isValidElement(child) && child.type === component;
234+
}
235+
236+
function getMobileBreadcrumbChildren(children: ReactNode[]): ReactNode[] {
237+
const breadcrumbItems = children.filter(
238+
(child) =>
239+
isComponentElement(child, Link) || isComponentElement(child, Current),
240+
) as ReactElement[];
241+
242+
if (breadcrumbItems.length <= 2) {
243+
return children;
244+
}
245+
246+
const [parentItem, currentItem] = breadcrumbItems.slice(-2);
247+
const trailingItems: ReactNode[] = [
248+
<MobileEllipsis key="kumo-breadcrumb-mobile-ellipsis" />,
249+
<Separator key="kumo-breadcrumb-mobile-separator-leading" />,
250+
cloneElement(parentItem, { key: "kumo-breadcrumb-mobile-parent" }),
251+
<Separator key="kumo-breadcrumb-mobile-separator-trailing" />,
252+
cloneElement(currentItem, { key: "kumo-breadcrumb-mobile-current" }),
253+
];
254+
255+
const extras = children.filter(
256+
(child) =>
257+
!isComponentElement(child, Link) &&
258+
!isComponentElement(child, Current) &&
259+
!isComponentElement(child, Separator),
260+
);
261+
262+
return [...trailingItems, ...extras];
263+
}
264+
205265
Breadcrumb.Link = Link;
206266
Breadcrumb.Current = Current;
207267
Breadcrumb.Separator = Separator;

0 commit comments

Comments
 (0)