All files / packages/design-system-2-site/src/components TabBar.tsx

92.5% Statements 37/40
70.83% Branches 17/24
100% Functions 9/9
100% Lines 36/36

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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 1044x                                     17x 17x   17x 17x     22x   17x 22x 22x 22x 22x 22x 22x       17x 11x 11x 11x 11x 11x 11x 11x 11x 11x         17x 11x 11x 11x 11x 10x 10x 10x   11x     17x               46x                                 1x       46x                    
'use client';
 
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
import { CaretDownIcon } from './icons';
 
export interface TabBarProps {
  tabs: { name: string; url: string; active: boolean }[];
}
 
/**
 * Tab strip for pages that have nav-declared tabs (see `getTabContext` in
 * `src/lib/navigation.ts`). Each tab is a URL — the strip is a `<nav>` of
 * `<Link>`s, not an ARIA tablist, because activating a tab navigates to a new
 * route rather than swapping panels in-place. At the Small breakpoint the strip
 * collapses into a native `<select>`.
 */
export function TabBar({ tabs }: TabBarProps) {
  const router = useRouter();
  const scrollRef = useRef<HTMLDivElement>(null);
  // Edge-fade flags: whether there's scrolled-past content on each side.
  const [fadeStart, setFadeStart] = useState(false);
  const [fadeEnd, setFadeEnd] = useState(false);
 
  // The value that drives the mobile <select>; fall back to the first tab.
  const activeUrl = (tabs.find((t) => t.active) ?? tabs[0])?.url ?? '';
 
  const updateFades = useCallback(() => {
    const el = scrollRef.current;
    Iif (!el) return;
    const { scrollLeft, scrollWidth, clientWidth } = el;
    const canScroll = scrollWidth - clientWidth > 1;
    setFadeStart(canScroll && scrollLeft > 1);
    setFadeEnd(canScroll && scrollLeft + clientWidth < scrollWidth - 1);
  }, []);
 
  // Keep fades in sync with scroll position and any size change.
  useEffect(() => {
    const el = scrollRef.current;
    Iif (!el) return;
    updateFades();
    el.addEventListener('scroll', updateFades, { passive: true });
    const ro = new ResizeObserver(updateFades);
    ro.observe(el);
    return () => {
      el.removeEventListener('scroll', updateFades);
      ro.disconnect();
    };
  }, [updateFades, tabs]);
 
  // On load / navigation, center the active tab so it's never hidden off-screen.
  useEffect(() => {
    const el = scrollRef.current;
    Iif (!el) return;
    const active = el.querySelector<HTMLElement>('.site-tabs__link--active');
    if (active) {
      const sRect = el.getBoundingClientRect();
      const aRect = active.getBoundingClientRect();
      el.scrollLeft += aRect.left - sRect.left - (sRect.width - aRect.width) / 2;
    }
    updateFades();
  }, [tabs, updateFades]);
 
  return (
    <>
      <div
        className={`site-tabs${fadeStart ? ' site-tabs--fade-start' : ''}${fadeEnd ? ' site-tabs--fade-end' : ''}`}
      >
        <div className="site-tabs__scroll" ref={scrollRef}>
          <nav className="site-tabs__track" aria-label="Page sections">
            {tabs.map((tab) => (
              <Link
                key={tab.url}
                href={tab.url}
                aria-current={tab.active ? 'page' : undefined}
                className={tab.active ? 'site-tabs__link site-tabs__link--active' : 'site-tabs__link'}
              >
                {tab.name}
              </Link>
            ))}
          </nav>
        </div>
      </div>
 
      <div className="site-tabs__select">
        <select
          className="site-tabs__select-input"
          value={activeUrl}
          onChange={(e) => router.push(e.target.value)}
          aria-label="Select tab"
        >
          {tabs.map((tab) => (
            <option key={tab.url} value={tab.url}>
              {tab.name}
            </option>
          ))}
        </select>
        <CaretDownIcon className="site-tabs__select-caret" aria-hidden="true" />
      </div>
    </>
  );
}