All files / src/components/layout Breadcrumbs.tsx

100% Statements 5/5
100% Branches 11/11
100% Functions 3/3
100% Lines 5/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                          14x                                 10x 10x   10x                                                                 26x                                    
import Link from "next/link";
 
interface BreadcrumbItem {
  label: string;
  href?: string;
}
 
interface BreadcrumbsProps {
  items: BreadcrumbItem[];
}
 
// Identical params to the carousel chevron so the two glyphs read as one family.
function BackChevron() {
  return (
    <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
      <path
        d="M15 6l-6 6 6 6"
        stroke="currentColor"
        strokeWidth={1.8}
        strokeLinecap="round"
        strokeLinejoin="round"
        fill="none"
      />
    </svg>
  );
}
 
export function Breadcrumbs({ items }: BreadcrumbsProps) {
  // The back arrow always targets the parent crumb (second-to-last), never
  // browser history; it needs >=2 crumbs and a linkable parent to exist.
  const parent = items.length >= 2 ? items[items.length - 2] : undefined;
  const backHref = parent?.href;
 
  return (
    // The crumb list scrolls in its own wrapper below: overflow on the nav
    // itself would clip the back link's 44px hit area to the ~20px row.
    <nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm text-ink-soft mb-6">
      {parent && backHref && (
        <>
          {/* Tablet: pointer-only shortcut — the parent crumb link is the
              accessible/keyboard path, so a second tab stop would be noise.
              Negative margins keep the 44px hit area from changing row height
              and optically align the glyph with the content edge. */}
          <Link
            href={backHref}
            aria-hidden="true"
            tabIndex={-1}
            className="hidden sm:flex h-11 w-11 -my-3 -ml-4 items-center justify-center text-primary hover:text-primary-hover"
          >
            <BackChevron />
          </Link>
          {/* Phone: the trail collapses to this single fully accessible link. */}
          <Link
            href={backHref}
            aria-label={`Back to ${parent.label}`}
            className="flex sm:hidden min-h-11 -my-3 -ml-2 items-center gap-1 rounded-control text-primary hover:text-primary-hover focus:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary"
          >
            <BackChevron />
            <span className="truncate max-w-[70vw] pr-2">{parent.label}</span>
          </Link>
        </>
      )}
      <div
        className={`${backHref ? "hidden sm:flex" : "flex"} items-center gap-2 overflow-x-auto`}
      >
        {items.map((item, index) => (
          <div key={index} className="flex items-center gap-2 whitespace-nowrap">
            {index > 0 && <span className="text-ink-soft">/</span>}
            {item.href ? (
              <Link
                href={item.href}
                className="text-primary hover:text-primary-hover hover:underline"
              >
                {item.label}
              </Link>
            ) : (
              <span className="text-ink font-medium">{item.label}</span>
            )}
          </div>
        ))}
      </div>
    </nav>
  );
}