All files / components/shared TabBar.tsx

100% Statements 59/59
100% Branches 5/5
100% Functions 2/2
100% Lines 59/59

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 601x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 841x 841x 841x 841x 1682x 1682x 1682x 1682x 1682x 1682x 1682x 1682x 1682x 1682x 1682x 1682x 841x 841x 1682x 1682x 1682x 1682x 1682x 841x 841x 841x 841x 841x 841x  
import type { ReactNode } from "react";
 
interface Tab {
  id: string;
  label: string;
}
 
interface TabBarProps {
  tabs: Tab[];
  activeTab: string;
  onTabChange: (id: string) => void;
  /**
   * Non-tab actions that sit beside the tablist (spec FR-017, FR-017a) — the
   * paste button. Rendered as a SIBLING of `role="tablist"`, never a child of
   * it: every child of a tablist is announced as a tab, and a plain button
   * inside it would misrepresent the panel's structure to assistive
   * technology. This header row is what makes that possible — a flex
   * container holding the tablist and these children side by side.
   */
  children?: ReactNode;
  /**
   * True while the paste composer is open (spec FR-017b): the tabs MUST NOT
   * be reachable by pointer or keyboard, since their panel is hidden and a
   * click on either would be a silent no-op. `disabled` removes a button
   * from both the click surface and the tab order in one attribute.
   */
  tabsDisabled?: boolean;
}
 
export function TabBar({ tabs, activeTab, onTabChange, children, tabsDisabled }: TabBarProps) {
  return (
    <div className="flex shrink-0 items-center justify-between gap-1 border-b border-gray-200/70 bg-white px-3 py-1.5 dark:border-gray-800 dark:bg-gray-900">
      <div role="tablist" aria-label="Panel sections" className="flex gap-1">
        {tabs.map((tab) => {
          const isActive = tab.id === activeTab;
          return (
            <button
              key={tab.id}
              role="tab"
              aria-selected={isActive}
              aria-controls={`panel-${tab.id}`}
              id={`tab-${tab.id}`}
              disabled={tabsDisabled}
              onClick={() => onTabChange(tab.id)}
              className={`rounded-md px-3 py-1 text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-40 ${
                isActive
                  ? "bg-blue-50 text-blue-700 dark:bg-blue-950/60 dark:text-blue-400"
                  : "text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
              }`}
            >
              {tab.label}
            </button>
          );
        })}
      </div>
      {children}
    </div>
  );
}