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.
This commit is contained in:
parent
e81aaef7e3
commit
e03bd4caa4
5 changed files with 53 additions and 11 deletions
|
|
@ -483,7 +483,10 @@ rows as the review table.
|
||||||
A page for writing classification prompts without touching the repo. Pick a
|
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;
|
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.
|
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
|
The page posts the run to `swarm-controller` (`POST
|
||||||
/api/review-classifier/try-out`), which reads the PR with its own forge token
|
/api/review-classifier/try-out`), which reads the PR with its own forge token
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { TextField } from "../../ui/text-field/TextField.js";
|
||||||
import { ClassificationEditor } from "./ClassificationEditor.js";
|
import { ClassificationEditor } from "./ClassificationEditor.js";
|
||||||
import { TryOutResult } from "./TryOutResult.js";
|
import { TryOutResult } from "./TryOutResult.js";
|
||||||
import {
|
import {
|
||||||
|
addProblem,
|
||||||
draftFromRule,
|
draftFromRule,
|
||||||
hasTypedText,
|
hasTypedText,
|
||||||
prNumber,
|
prNumber,
|
||||||
|
|
@ -57,10 +58,9 @@ const PULLS = "/api/pulls";
|
||||||
// does not ask for the PRs of each prefix of it.
|
// does not ask for the PRs of each prefix of it.
|
||||||
const PICK_DELAY_MS = 300;
|
const PICK_DELAY_MS = 300;
|
||||||
const POLL_MS = 2000;
|
const POLL_MS = 2000;
|
||||||
// More classifications than this are not offered a row. It is the try-out
|
// Ids the buttons' `aria-describedby` points at.
|
||||||
// endpoint's own cap, which answers a longer list with a 400 that the page
|
const HINT_ID = "tryout-hint";
|
||||||
// shows as it is.
|
const CAP_ID = "tryout-cap";
|
||||||
const MAX_CLASSIFICATIONS = 10;
|
|
||||||
|
|
||||||
let nextKey = 0;
|
let nextKey = 0;
|
||||||
function blankDraft(): ClassificationDraft {
|
function blankDraft(): ClassificationDraft {
|
||||||
|
|
@ -221,6 +221,7 @@ export function ReviewTryOut() {
|
||||||
|
|
||||||
const openPrProblem = rowsProblem(repo, drafts);
|
const openPrProblem = rowsProblem(repo, drafts);
|
||||||
const problem = runProblem(repo, pr, drafts);
|
const problem = runProblem(repo, pr, drafts);
|
||||||
|
const capProblem = addProblem(drafts.length);
|
||||||
|
|
||||||
async function run(e: Event) {
|
async function run(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -398,7 +399,8 @@ export function ReviewTryOut() {
|
||||||
<div class="ui-tryout-actions">
|
<div class="ui-tryout-actions">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setDrafts((ds) => [...ds, blankDraft()])}
|
onClick={() => setDrafts((ds) => [...ds, blankDraft()])}
|
||||||
disabled={drafts.length >= MAX_CLASSIFICATIONS}
|
disabled={capProblem !== null}
|
||||||
|
describedBy={capProblem !== null ? CAP_ID : undefined}
|
||||||
>
|
>
|
||||||
add a classification
|
add a classification
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -406,17 +408,28 @@ export function ReviewTryOut() {
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={running || problem !== null}
|
disabled={running || problem !== null}
|
||||||
|
describedBy={problem !== null ? HINT_ID : undefined}
|
||||||
>
|
>
|
||||||
{running ? "running…" : "run"}
|
{running ? "running…" : "run"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void openPr()}
|
onClick={() => void openPr()}
|
||||||
disabled={opening || openPrProblem !== null}
|
disabled={opening || openPrProblem !== null}
|
||||||
|
describedBy={openPrProblem !== null ? HINT_ID : undefined}
|
||||||
>
|
>
|
||||||
{opening ? "pushing…" : "open a PR"}
|
{opening ? "pushing…" : "open a PR"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{problem !== null ? <p class="ui-tryout-hint">{problem}</p> : null}
|
{capProblem !== null ? (
|
||||||
|
<p id={CAP_ID} class="ui-tryout-hint">
|
||||||
|
{capProblem}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{problem !== null ? (
|
||||||
|
<p id={HINT_ID} class="ui-tryout-hint">
|
||||||
|
{problem}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
{openError ? (
|
{openError ? (
|
||||||
<ApiErrorPanel
|
<ApiErrorPanel
|
||||||
context="the branch for the PR could not be pushed"
|
context="the branch for the PR could not be pushed"
|
||||||
|
|
@ -450,10 +463,13 @@ export function ReviewTryOut() {
|
||||||
{running || result || error ? (
|
{running || result || error ? (
|
||||||
<Panel title="result" icon="📋">
|
<Panel title="result" icon="📋">
|
||||||
{running ? (
|
{running ? (
|
||||||
<p class="ui-tryout-running" role="status">
|
<>
|
||||||
Running — a run takes a minute or more, and up to half an hour for
|
<p class="ui-tryout-running" role="status">
|
||||||
a long list ({elapsed}s so far).
|
Running — a run takes a minute or more, and up to half an hour
|
||||||
</p>
|
for a long list.
|
||||||
|
</p>
|
||||||
|
<p class="ui-tryout-running">{elapsed}s so far</p>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{error ? (
|
{error ? (
|
||||||
<ApiErrorPanel context="the try-out failed" problem={error} />
|
<ApiErrorPanel context="the try-out failed" problem={error} />
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,17 @@ export function duplicateName(drafts: ClassificationDraft[]): string | null {
|
||||||
return 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
|
/** 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. */
|
* each row, then a shared name. Unlike a run, it needs no PR number. */
|
||||||
export function rowsProblem(
|
export function rowsProblem(
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,15 @@ export function Button({
|
||||||
type = "button",
|
type = "button",
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
|
describedBy,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
type?: "button" | "submit";
|
type?: "button" | "submit";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (e: Event) => void;
|
onClick?: (e: Event) => void;
|
||||||
|
/** Id of the element that says why the button is disabled. */
|
||||||
|
describedBy?: string;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,6 +31,7 @@ export function Button({
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
aria-describedby={describedBy}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import {
|
import {
|
||||||
|
MAX_CLASSIFICATIONS,
|
||||||
|
addProblem,
|
||||||
draftFromRule,
|
draftFromRule,
|
||||||
draftProblem,
|
draftProblem,
|
||||||
duplicateName,
|
duplicateName,
|
||||||
|
|
@ -17,6 +19,12 @@ import {
|
||||||
toBody,
|
toBody,
|
||||||
} from "../src/pages/review-tryout/TryOutTypes.ts";
|
} 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.
|
// A row ready to send, with `over` replacing any of its fields.
|
||||||
function row(over = {}) {
|
function row(over = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue