mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 13:33:30 +01:00
MAESTRO: improve keyboard access for test selector
This commit is contained in:
@@ -36,7 +36,7 @@ This phase implements the test selection panel and analysis configuration contro
|
|||||||
- [x] Search/filter narrows visible tests by name
|
- [x] Search/filter narrows visible tests by name
|
||||||
- [x] DNS resolution toggle defaults to off
|
- [x] DNS resolution toggle defaults to off
|
||||||
- [x] Decode-all toggle is functional
|
- [x] Decode-all toggle is functional
|
||||||
- [ ] All controls are keyboard accessible (Tab, Enter, Space)
|
- [x] All controls are keyboard accessible (Tab, Enter, Space)
|
||||||
- [ ] Linting passes (`ruff check backend/`, `npx eslint src/`)
|
- [ ] Linting passes (`ruff check backend/`, `npx eslint src/`)
|
||||||
- [ ] Run `/speckit.analyze` to verify consistency
|
- [ ] Run `/speckit.analyze` to verify consistency
|
||||||
|
|
||||||
|
|||||||
@@ -178,4 +178,42 @@ describe("AnalysisControls", () => {
|
|||||||
expect.objectContaining({ resolve: true, decodeAll: true }),
|
expect.objectContaining({ resolve: true, decodeAll: true }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates toggles on Space key presses", async () => {
|
||||||
|
setupFetchMock(sampleTests);
|
||||||
|
const handleChange = vi.fn();
|
||||||
|
|
||||||
|
const AnalysisControlsHarness = () => {
|
||||||
|
const [config, setConfig] = useState<AnalysisConfig>({
|
||||||
|
testIds: [],
|
||||||
|
resolve: false,
|
||||||
|
decodeAll: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateConfig = (next: AnalysisConfig) => {
|
||||||
|
setConfig(next);
|
||||||
|
handleChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AnalysisControls config={config} onChange={updateConfig} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(<AnalysisControlsHarness />);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolveToggle = getToggle(container, "toggle-resolve");
|
||||||
|
act(() => {
|
||||||
|
resolveToggle.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", { key: " ", bubbles: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveToggle.getAttribute("aria-checked")).toBe("true");
|
||||||
|
expect(handleChange).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({ resolve: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,6 +164,43 @@ describe("TestSelector", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("supports keyboard activation for select and deselect", async () => {
|
||||||
|
setupFetchMock(sampleTests);
|
||||||
|
|
||||||
|
const TestSelectorHarness = () => {
|
||||||
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
|
return <TestSelector selectedTestIds={selected} onSelectionChange={setSelected} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(<TestSelectorHarness />);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectAllButton = getSelectAllButton(container);
|
||||||
|
act(() => {
|
||||||
|
selectAllButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sampleTests.forEach((test) => {
|
||||||
|
expect(getCheckbox(container, test.id).checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const deselectAllButton = getDeselectAllButton(container);
|
||||||
|
act(() => {
|
||||||
|
deselectAllButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", { key: " ", bubbles: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sampleTests.forEach((test) => {
|
||||||
|
expect(getCheckbox(container, test.id).checked).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("filters tests by search text", async () => {
|
it("filters tests by search text", async () => {
|
||||||
setupFetchMock(sampleTests);
|
setupFetchMock(sampleTests);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState, type KeyboardEvent } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
@@ -46,6 +46,16 @@ const buildGroups = (tests: TestInfo[]): TestGroup[] => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleActionKeyDown = (
|
||||||
|
event: KeyboardEvent<HTMLButtonElement>,
|
||||||
|
onAction: () => void,
|
||||||
|
): void => {
|
||||||
|
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
||||||
|
event.preventDefault();
|
||||||
|
onAction();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function TestSelector({ selectedTestIds, onSelectionChange }: TestSelectorProps) {
|
export default function TestSelector({ selectedTestIds, onSelectionChange }: TestSelectorProps) {
|
||||||
const [tests, setTests] = useState<TestInfo[]>([]);
|
const [tests, setTests] = useState<TestInfo[]>([]);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
@@ -152,6 +162,7 @@ export default function TestSelector({ selectedTestIds, onSelectionChange }: Tes
|
|||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
||||||
onClick={handleSelectAll}
|
onClick={handleSelectAll}
|
||||||
|
onKeyDown={(event) => handleActionKeyDown(event, handleSelectAll)}
|
||||||
data-testid="select-all-tests"
|
data-testid="select-all-tests"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-xs" />
|
<FontAwesomeIcon icon={faCheck} className="text-xs" />
|
||||||
@@ -161,6 +172,7 @@ export default function TestSelector({ selectedTestIds, onSelectionChange }: Tes
|
|||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
||||||
onClick={handleDeselectAll}
|
onClick={handleDeselectAll}
|
||||||
|
onKeyDown={(event) => handleActionKeyDown(event, handleDeselectAll)}
|
||||||
data-testid="deselect-all-tests"
|
data-testid="deselect-all-tests"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} className="text-xs" />
|
<FontAwesomeIcon icon={faXmark} className="text-xs" />
|
||||||
|
|||||||
Reference in New Issue
Block a user