2026-06-21 22:20:51 +02:00
|
|
|
import type { KeyboardEvent } from 'react';
|
|
|
|
|
|
2026-06-21 22:10:59 +02:00
|
|
|
interface TabItem {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
count?: number;
|
2026-06-22 10:48:18 +02:00
|
|
|
disabled?: boolean;
|
2026-06-21 22:10:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TabsProps {
|
|
|
|
|
items: TabItem[];
|
|
|
|
|
activeId: string;
|
|
|
|
|
onChange: (id: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function Tabs({ items, activeId, onChange }: TabsProps): JSX.Element {
|
2026-06-22 10:48:18 +02:00
|
|
|
function nextEnabled(from: number, direction: 1 | -1): string {
|
|
|
|
|
const len = items.length;
|
|
|
|
|
let i = (from + direction + len) % len;
|
|
|
|
|
while (i !== from) {
|
|
|
|
|
if (!items[i].disabled) return items[i].id;
|
|
|
|
|
i = (i + direction + len) % len;
|
|
|
|
|
}
|
|
|
|
|
return items[from].id;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 22:20:51 +02:00
|
|
|
function handleKeyDown(e: KeyboardEvent<HTMLButtonElement>, index: number) {
|
|
|
|
|
if (e.key === 'ArrowRight') {
|
|
|
|
|
e.preventDefault();
|
2026-06-22 10:48:18 +02:00
|
|
|
onChange(nextEnabled(index, 1));
|
2026-06-21 22:20:51 +02:00
|
|
|
} else if (e.key === 'ArrowLeft') {
|
|
|
|
|
e.preventDefault();
|
2026-06-22 10:48:18 +02:00
|
|
|
onChange(nextEnabled(index, -1));
|
2026-06-21 22:20:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 22:10:59 +02:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
role="tablist"
|
|
|
|
|
className="flex items-end border-b border-hairline gap-xs"
|
|
|
|
|
>
|
2026-06-21 22:20:51 +02:00
|
|
|
{items.map((item, index) => {
|
2026-06-21 22:10:59 +02:00
|
|
|
const isActive = item.id === activeId;
|
2026-06-22 10:48:18 +02:00
|
|
|
const isDisabled = item.disabled ?? false;
|
2026-06-21 22:10:59 +02:00
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={item.id}
|
2026-06-21 22:18:09 +02:00
|
|
|
id={`tab-${item.id}`}
|
2026-06-21 22:10:59 +02:00
|
|
|
role="tab"
|
|
|
|
|
aria-selected={isActive}
|
2026-06-21 22:18:09 +02:00
|
|
|
aria-controls={`tabpanel-${item.id}`}
|
2026-06-22 10:48:18 +02:00
|
|
|
aria-disabled={isDisabled || undefined}
|
|
|
|
|
disabled={isDisabled}
|
2026-06-21 22:10:59 +02:00
|
|
|
type="button"
|
2026-06-22 10:48:18 +02:00
|
|
|
onClick={() => { if (!isDisabled) onChange(item.id); }}
|
2026-06-21 22:20:51 +02:00
|
|
|
onKeyDown={(e) => handleKeyDown(e, index)}
|
2026-06-22 10:48:18 +02:00
|
|
|
className={`tab-underline${isActive ? ' tab-underline-active' : ''}${isDisabled ? ' tab-underline-disabled' : ''} pb-xs`}
|
2026-06-21 22:10:59 +02:00
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
{item.count !== undefined ? (
|
|
|
|
|
<span
|
|
|
|
|
className={`ml-xxs tab-count-pill${isActive ? ' tab-count-pill-active' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
{item.count}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|