Files
mimic/frontend/src/components/Tabs.tsx
Knacky 0378792dc4 feat(frontend): SimulationFormPage 3-tab layout (Red Team / SOC / Tâche C2)
- Tabs: disabled per-item (aria-disabled, native disabled, tab-underline-disabled,
  arrow-key skip); aria-controls wired
- useHashTab: optional validIds fallback to defaultId
- SimulationFormPage: 3-tab refactor; SOC tab gated on review_required|done;
  C2 tab disabled in create mode; contextual sticky bar hidden on C2 tab;
  safeTab guard against stale hash; SOC-blocked banner removed
- fr.json: simulation.form.tab.{redTeam,soc,c2} keys
- DESIGN.md: disabled tab variant + aria-controls documented
- Tests: 253 passing (5 new Tabs disabled tests, SimulationFormPage rewritten
  for tab layout)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 10:48:18 +02:00

73 lines
2.0 KiB
TypeScript

import type { KeyboardEvent } from 'react';
interface TabItem {
id: string;
label: string;
count?: number;
disabled?: boolean;
}
interface TabsProps {
items: TabItem[];
activeId: string;
onChange: (id: string) => void;
}
export function Tabs({ items, activeId, onChange }: TabsProps): JSX.Element {
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;
}
function handleKeyDown(e: KeyboardEvent<HTMLButtonElement>, index: number) {
if (e.key === 'ArrowRight') {
e.preventDefault();
onChange(nextEnabled(index, 1));
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
onChange(nextEnabled(index, -1));
}
}
return (
<div
role="tablist"
className="flex items-end border-b border-hairline gap-xs"
>
{items.map((item, index) => {
const isActive = item.id === activeId;
const isDisabled = item.disabled ?? false;
return (
<button
key={item.id}
id={`tab-${item.id}`}
role="tab"
aria-selected={isActive}
aria-controls={`tabpanel-${item.id}`}
aria-disabled={isDisabled || undefined}
disabled={isDisabled}
type="button"
onClick={() => { if (!isDisabled) onChange(item.id); }}
onKeyDown={(e) => handleKeyDown(e, index)}
className={`tab-underline${isActive ? ' tab-underline-active' : ''}${isDisabled ? ' tab-underline-disabled' : ''} pb-xs`}
>
{item.label}
{item.count !== undefined ? (
<span
className={`ml-xxs tab-count-pill${isActive ? ' tab-count-pill-active' : ''}`}
>
{item.count}
</span>
) : null}
</button>
);
})}
</div>
);
}