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>
This commit is contained in:
Knacky
2026-06-22 10:48:18 +02:00
parent 9b6b682acc
commit 0378792dc4
9 changed files with 548 additions and 375 deletions

View File

@@ -4,6 +4,7 @@ interface TabItem {
id: string;
label: string;
count?: number;
disabled?: boolean;
}
interface TabsProps {
@@ -13,13 +14,23 @@ interface TabsProps {
}
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(items[(index + 1) % items.length].id);
onChange(nextEnabled(index, 1));
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
onChange(items[(index - 1 + items.length) % items.length].id);
onChange(nextEnabled(index, -1));
}
}
@@ -30,6 +41,7 @@ export function Tabs({ items, activeId, onChange }: TabsProps): JSX.Element {
>
{items.map((item, index) => {
const isActive = item.id === activeId;
const isDisabled = item.disabled ?? false;
return (
<button
key={item.id}
@@ -37,10 +49,12 @@ export function Tabs({ items, activeId, onChange }: TabsProps): JSX.Element {
role="tab"
aria-selected={isActive}
aria-controls={`tabpanel-${item.id}`}
aria-disabled={isDisabled || undefined}
disabled={isDisabled}
type="button"
onClick={() => onChange(item.id)}
onClick={() => { if (!isDisabled) onChange(item.id); }}
onKeyDown={(e) => handleKeyDown(e, index)}
className={`tab-underline${isActive ? ' tab-underline-active' : ''} pb-xs`}
className={`tab-underline${isActive ? ' tab-underline-active' : ''}${isDisabled ? ' tab-underline-disabled' : ''} pb-xs`}
>
{item.label}
{item.count !== undefined ? (