From e03bd4caa406893b12bf34eb2497052856d065b0 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 21 Oct 2026 21:30:01 +0200 Subject: [PATCH] swarm-ui try-out: stable live region, reasons for disabled buttons The running line is a live region whose text changed every second, so a screen reader announced the counter again each time. The sentence is now stable and the elapsed seconds sit outside it. The run and open-a-PR buttons point at the hint that says why they are off, and add-a-classification says why it is off at the cap. --- docs/swarm/review-classifier.md | 5 ++- .../src/pages/review-tryout/ReviewTryOut.tsx | 36 +++++++++++++------ .../src/pages/review-tryout/TryOutTypes.ts | 11 ++++++ .../swarm-ui/src/ui/button/Button.tsx | 4 +++ .../swarm-ui/test/tryout-types.test.mjs | 8 +++++ 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/docs/swarm/review-classifier.md b/docs/swarm/review-classifier.md index edb83bea..d7de8f52 100644 --- a/docs/swarm/review-classifier.md +++ b/docs/swarm/review-classifier.md @@ -483,7 +483,10 @@ rows as the review table. A page for writing classification prompts without touching the repo. Pick a repo and a PR, write or edit a classification, and run it against that PR; change the prompt and run it again until it answers the way you want. -Nothing reaches the forge while you iterate. +Nothing reaches the forge while you iterate. A button the page disables +points at a line that says why, such as the 10-classification cap on **add a +classification**, and a screen reader announces the run's progress once, with +the elapsed seconds outside the announcement. The page posts the run to `swarm-controller` (`POST /api/review-classifier/try-out`), which reads the PR with its own forge token diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx index 2445ee00..ec5425f6 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx @@ -33,6 +33,7 @@ import { TextField } from "../../ui/text-field/TextField.js"; import { ClassificationEditor } from "./ClassificationEditor.js"; import { TryOutResult } from "./TryOutResult.js"; import { + addProblem, draftFromRule, hasTypedText, prNumber, @@ -57,10 +58,9 @@ const PULLS = "/api/pulls"; // does not ask for the PRs of each prefix of it. const PICK_DELAY_MS = 300; const POLL_MS = 2000; -// More classifications than this are not offered a row. It is the try-out -// endpoint's own cap, which answers a longer list with a 400 that the page -// shows as it is. -const MAX_CLASSIFICATIONS = 10; +// Ids the buttons' `aria-describedby` points at. +const HINT_ID = "tryout-hint"; +const CAP_ID = "tryout-cap"; let nextKey = 0; function blankDraft(): ClassificationDraft { @@ -221,6 +221,7 @@ export function ReviewTryOut() { const openPrProblem = rowsProblem(repo, drafts); const problem = runProblem(repo, pr, drafts); + const capProblem = addProblem(drafts.length); async function run(e: Event) { e.preventDefault(); @@ -398,7 +399,8 @@ export function ReviewTryOut() {
@@ -406,17 +408,28 @@ export function ReviewTryOut() { variant="primary" type="submit" disabled={running || problem !== null} + describedBy={problem !== null ? HINT_ID : undefined} > {running ? "running…" : "run"}
- {problem !== null ?

{problem}

: null} + {capProblem !== null ? ( +

+ {capProblem} +

+ ) : null} + {problem !== null ? ( +

+ {problem} +

+ ) : null} {openError ? ( {running ? ( -

- Running — a run takes a minute or more, and up to half an hour for - a long list ({elapsed}s so far). -

+ <> +

+ Running — a run takes a minute or more, and up to half an hour + for a long list. +

+

{elapsed}s so far

+ ) : null} {error ? ( diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts index 7143ac3c..2ade456b 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts @@ -150,6 +150,17 @@ export function duplicateName(drafts: ClassificationDraft[]): string | null { return null; } +/** More classifications than this are not offered a row. It is the try-out + * endpoint's own cap, which answers a longer list with a 400. */ +export const MAX_CLASSIFICATIONS = 10; + +/** Why another row cannot be added, or null while there is room. */ +export function addProblem(count: number): string | null { + return count >= MAX_CLASSIFICATIONS + ? `a run takes at most ${MAX_CLASSIFICATIONS} classifications` + : null; +} + /** What keeps the rows from going to the repo as a PR, or null: the repo, then * each row, then a shared name. Unlike a run, it needs no PR number. */ export function rowsProblem( diff --git a/frontend/packages/swarm-ui/src/ui/button/Button.tsx b/frontend/packages/swarm-ui/src/ui/button/Button.tsx index 006f4c7d..cdf77e4b 100644 --- a/frontend/packages/swarm-ui/src/ui/button/Button.tsx +++ b/frontend/packages/swarm-ui/src/ui/button/Button.tsx @@ -14,12 +14,15 @@ export function Button({ type = "button", disabled, onClick, + describedBy, children, }: { variant?: ButtonVariant; type?: "button" | "submit"; disabled?: boolean; onClick?: (e: Event) => void; + /** Id of the element that says why the button is disabled. */ + describedBy?: string; children: ComponentChildren; }) { return ( @@ -28,6 +31,7 @@ export function Button({ type={type} disabled={disabled} onClick={onClick} + aria-describedby={describedBy} > {children} diff --git a/frontend/packages/swarm-ui/test/tryout-types.test.mjs b/frontend/packages/swarm-ui/test/tryout-types.test.mjs index c64d036f..e90eda79 100644 --- a/frontend/packages/swarm-ui/test/tryout-types.test.mjs +++ b/frontend/packages/swarm-ui/test/tryout-types.test.mjs @@ -5,6 +5,8 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { + MAX_CLASSIFICATIONS, + addProblem, draftFromRule, draftProblem, duplicateName, @@ -17,6 +19,12 @@ import { toBody, } from "../src/pages/review-tryout/TryOutTypes.ts"; +test("a row is offered until the endpoint's cap is reached", () => { + assert.equal(addProblem(1), null); + assert.equal(addProblem(MAX_CLASSIFICATIONS - 1), null); + assert.match(addProblem(MAX_CLASSIFICATIONS), /at most 10 classifications/); +}); + // A row ready to send, with `over` replacing any of its fields. function row(over = {}) { return {