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:
1 parent
bac0d75451
commit
11369f8fed
18 files changed
+52
-12
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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": "复制失败",
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user