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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user