Q200: one-of choices and selected items say which is chosen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 10:23:42 -04:00
1 parent bac0d75451
commit 11369f8fed
18 files changed
+52 -12

No files matched your search

+3 -2
View File
@@ -181,7 +181,7 @@ export function Onboarding({
</p>
<div className="onboarding-steps">
{STEPS.map((entry) => (
<span key={entry.id} className={`step-chip ${entry.id === step ? "active" : ""}`}>{entry.label}</span>
<span key={entry.id} aria-current={entry.id === step ? "step" : undefined} className={`step-chip ${entry.id === step ? "active" : ""}`}>{entry.label}</span>
))}
</div>
</div>
@@ -277,6 +277,7 @@ function WelcomeStep({
<div className="row">
<h2 className="page-h1" style={{ margin: 0, flex: 1 }}>{t("Welcome")}</h2>
<Segmented
label={t("Language")}
value={language}
onChange={(next) => { if (next !== language) onLanguage(next); }}
options={[{ id: "en", label: "English" }, { id: "zh", label: "中文" }]}
@@ -386,7 +387,7 @@ function ModeCard({
onClick: () => void;
}) {
return (
<button type="button" className={`option-card ${active ? "active" : ""}`.trim()} onClick={onClick}>
<button type="button" aria-pressed={active} className={`option-card ${active ? "active" : ""}`.trim()} onClick={onClick}>
<div className="opt-title">{title}</div>
<div className="opt-sub">{description}</div>
</button>
@@ -33,6 +33,7 @@ export function ProviderStep({
<Fragment key={provider.key}>
<button
type="button"
aria-pressed={draft.providerKey === provider.key}
className={`option-card ${draft.providerKey === provider.key ? "active" : ""}`.trim()}
onClick={() => onChange({
providerKey: provider.key,
+4
View File
@@ -698,6 +698,10 @@ export const zh: Record<string, string> = {
"Working…": "处理中…",
"Waiting for the {name} sign-in in your browser (check {check} of {checks}).": "正在等待你在浏览器中完成 {name} 登录(第 {check}/{checks} 次检查)。",
"Stop waiting": "停止等待",
"Layout": "布局",
"View": "视图",
"Run status": "运行状态",
"Task sections": "任务分区",
"Copy": "复制",
"Copied": "已复制",
"Copy failed": "复制失败",
+1
View File
@@ -81,6 +81,7 @@ export function FilesPage({ snapshot, actions, focus }: PageProps) {
<TopbarActions>
<FilesFilter scan={scanFilter} onScan={setScanFilter} />
<Segmented<ViewMode>
label={t("Layout")}
value={view}
onChange={setView}
options={[
+2 -1
View File
@@ -101,7 +101,7 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
onChange={(e) => setSearch(e.target.value)}
style={{ marginBottom: 8 }}
/>
<Segmented value={filter} onChange={setFilter} options={FILTERS} />
<Segmented label={t("Run status")} value={filter} onChange={setFilter} options={FILTERS} />
</div>
<div className="pane-scroll">
{/* Newest first, so "show older runs" continues below. */}
@@ -113,6 +113,7 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
<button
key={run.id}
type="button"
aria-current={active ? "true" : undefined}
className={`nav-row ${active ? "active" : ""}`}
onClick={() => setSelectedId(run.id)}
>
+1
View File
@@ -105,6 +105,7 @@ function NavGroup({
key={item.id}
type="button"
aria-label={item.label}
aria-current={item.id === active ? "page" : undefined}
className={`nav-item ${item.id === active ? "active" : ""}`.trim()}
onClick={() => onChange(item.id)}
>
+1 -1
View File
@@ -100,7 +100,7 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
return (
<div className="page-wide work-page">
<TopbarActions>
<Segmented value={view} onChange={setView} options={VIEW_OPTIONS} />
<Segmented label={t("View")} value={view} onChange={setView} options={VIEW_OPTIONS} />
<WorkFilter
agents={agents}
priority={priorityFilter}
@@ -117,6 +117,7 @@ function ConversationRow({
return (
<button
type="button"
aria-current={active ? "true" : undefined}
className={`convo-item ${active ? "active" : ""}`.trim()}
onClick={onClick}
>
+1 -1
View File
@@ -91,7 +91,7 @@ function TreeItem({
onClick: () => void;
}) {
return (
<button type="button" className={`nav-item ${active ? "active" : ""}`.trim()} onClick={onClick}>
<button type="button" aria-current={active ? "true" : undefined} className={`nav-item ${active ? "active" : ""}`.trim()} onClick={onClick}>
<span className="icon-box"><Icon name={icon} size={18} /></span>
<span>{label}</span>
<span className="nav-badge num">{count}</span>
@@ -99,6 +99,7 @@ export function ProviderRequestPanel({
<span>{t("Mode")}</span>
<div style={{ alignSelf: "flex-start" }}>
<Segmented
label={t("Mode")}
value={selectedMode}
onChange={setMode}
options={backgroundAwareOptions(canRunBackground)}
@@ -159,7 +159,7 @@ export function TaskDetailDialog({
{task.requiresFilesystem && <Pill>{t("workspace")}</Pill>}
</div>
<div style={{ marginBottom: 12 }}><Segmented value={tab} onChange={setTab} options={TABS} /></div>
<div style={{ marginBottom: 12 }}><Segmented label={t("Task sections")} value={tab} onChange={setTab} options={TABS} /></div>
{tab === "overview" && (
<OverviewTab
+1
View File
@@ -94,6 +94,7 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete }
<button
type="button"
role="menuitem"
aria-current={org.id === activeOrgId ? "true" : undefined}
className={`org-menu-row ${org.id === activeOrgId ? "active" : ""}`.trim()}
onClick={() => handleSelect(org.id)}
>
+1
View File
@@ -61,6 +61,7 @@ export function MenuRow({
<button
type="button"
role="menuitem"
aria-current={active ? "true" : undefined}
className={`menu-row ${active ? "active" : ""} ${danger ? "danger" : ""}`.replace(/\s+/g, " ").trim()}
onClick={onClick}
>
+18 -2
View File
@@ -1,4 +1,4 @@
import { createContext, useContext, type ReactNode } from "react";
import { createContext, useContext, type ReactNode, type KeyboardEvent } from "react";
import { Icon, type IconName } from "./Icon";
export interface AvatarLook {
@@ -181,21 +181,37 @@ export function Field({ label, children }: { label: string; children: ReactNode
);
}
/** A one-of choice: a labelled radio group, the chosen option checked and the only one in the Tab order; arrows, Home and End move the choice. */
export function Segmented<T extends string>({
label,
value,
onChange,
options,
}: {
label: string;
value: T;
onChange: (next: T) => void;
options: ReadonlyArray<{ id: T; label: string }>;
}) {
const move = (event: KeyboardEvent<HTMLDivElement>) => {
const index = options.findIndex((option) => option.id === value);
const next = event.key === "ArrowRight" || event.key === "ArrowDown" ? (index + 1) % options.length
: event.key === "ArrowLeft" || event.key === "ArrowUp" ? (index - 1 + options.length) % options.length
: event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : null;
if (next === null) return;
event.preventDefault();
onChange(options[next]!.id);
event.currentTarget.querySelectorAll<HTMLButtonElement>("[role=radio]")[next]?.focus();
};
return (
<div className="segmented">
<div className="segmented" role="radiogroup" aria-label={label} onKeyDown={move}>
{options.map((option) => (
<button
key={option.id}
type="button"
role="radio"
aria-checked={value === option.id}
tabIndex={value === option.id ? 0 : -1}
className={`segment ${value === option.id ? "active" : ""}`.trim()}
onClick={() => onChange(option.id)}
>
+1 -1
View File
@@ -404,7 +404,7 @@ test("the pinned CEO conversation is the Messages front door", async () => {
await parentDialog.getByRole("button", { name: "Delete", exact: true }).click();
await expect(parentDialog.getByRole("alertdialog", { name: "Delete this task?" })).toContainText("1 subtasks stay and move to the top level.");
// Switching to another task drops a pending confirmation, so it can never delete the wrong task.
await parentDialog.getByRole("button", { name: "Subtasks" }).click();
await parentDialog.getByRole("radio", { name: "Subtasks" }).click();
await parentDialog.getByRole("button", { name: "Open task Child step" }).click();
const childDialog = page.getByRole("dialog", { name: "Child step" });
await expect(childDialog).toBeVisible();
+1 -1
View File
@@ -244,7 +244,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Advanced" }).click();
await expect(page.getByText("Requests", { exact: true }).first()).toBeVisible();
await expect(page.getByLabel("Request provider")).toBeVisible();
await expect(page.getByRole("button", { name: "Background" })).toBeVisible();
await expect(page.getByRole("radio", { name: "Background" })).toBeVisible();
await page.getByLabel("Request prompt").fill("Draft a provider control memo.");
await page.getByLabel("Request instructions").fill("Use direct language.");
+11
View File
@@ -55,6 +55,17 @@ test("a dialog keeps the keyboard inside it, closes on Escape, and gives focus b
await expect(dialog).toHaveCount(0);
// Both layers closed at once; focus still returns to what opened the dialog.
await expect(opener).toBeFocused();
// A one-of choice says which option is chosen, and the arrow keys move the choice.
const views = page.getByRole("radiogroup", { name: "View" });
await expect(views.getByRole("radio", { name: "Board" })).toHaveAttribute("aria-checked", "true");
await views.getByRole("radio", { name: "Board" }).focus();
await page.keyboard.press("ArrowRight");
await expect(views.getByRole("radio", { name: "List" })).toHaveAttribute("aria-checked", "true");
await expect(views.getByRole("radio", { name: "List" })).toBeFocused();
await expect(views.getByRole("radio", { name: "Board" })).toHaveAttribute("aria-checked", "false");
await page.keyboard.press("Home");
await expect(views.getByRole("radio", { name: "Board" })).toHaveAttribute("aria-checked", "true");
} finally {
await closeApp(app);
}
+2 -2
View File
@@ -75,8 +75,8 @@ test("onboarding resumes where it was left after the app is closed", async () =>
await expect.poll(savedModel).toBe("cancelled-model");
// Saving a preference refreshes the in-memory snapshot, which now holds the draft too.
await page.getByRole("button", { name: "Back", exact: true }).click();
await page.getByRole("button", { name: "中文" }).click();
await page.getByRole("button", { name: "English" }).click();
await page.getByRole("radio", { name: "中文" }).click();
await page.getByRole("radio", { name: "English" }).click();
// Past the 800 ms change-event reload window, so only Cancel itself can refresh the snapshot.
await page.waitForTimeout(1_200);
await page.getByRole("button", { name: "Cancel" }).click();