diff --git a/ui/src/e2e/chat-run-lifecycle.e2e.test.ts b/ui/src/e2e/chat-run-lifecycle.e2e.test.ts index 411a290d13d1..6ca98c5d97b1 100644 --- a/ui/src/e2e/chat-run-lifecycle.e2e.test.ts +++ b/ui/src/e2e/chat-run-lifecycle.e2e.test.ts @@ -146,14 +146,10 @@ suite.define(() => { await expect .poll(() => currentPage.locator(".chat-working-indicator__elapsed").textContent()) .toBe("2m 57s"); - const workingLabel = currentPage.locator( - ".chat-working-indicator__status > .agent-chat__sr-only", - ); + const workingLabel = currentPage.locator(".chat-working-indicator__status > .sr-only"); expect(await workingLabel.textContent()).toBe("Working…"); expect( - await currentPage - .locator(".chat-working-indicator__status > span:not(.agent-chat__sr-only)") - .count(), + await currentPage.locator(".chat-working-indicator__status > span:not(.sr-only)").count(), ).toBe(0); }); diff --git a/ui/src/e2e/mantis-chat-proof.e2e.test.ts b/ui/src/e2e/mantis-chat-proof.e2e.test.ts index 6944e6185b6a..c81fee5b5591 100644 --- a/ui/src/e2e/mantis-chat-proof.e2e.test.ts +++ b/ui/src/e2e/mantis-chat-proof.e2e.test.ts @@ -113,12 +113,10 @@ describeMantisWebUiChat("Mantis Control UI web chat proof", () => { await page.getByText("saved 875.3k tokens", { exact: true }).waitFor(); await page.locator(".chat-working-indicator").waitFor(); - const workingLabel = page.locator(".chat-working-indicator__status > .agent-chat__sr-only"); + const workingLabel = page.locator(".chat-working-indicator__status > .sr-only"); expect(await workingLabel.textContent()).toBe("Working…"); expect( - await page - .locator(".chat-working-indicator__status > span:not(.agent-chat__sr-only)") - .count(), + await page.locator(".chat-working-indicator__status > span:not(.sr-only)").count(), ).toBe(0); await page.clock.fastForward(177_000); await expect diff --git a/ui/src/pages/about/view.ts b/ui/src/pages/about/view.ts index 7f8458e82afb..90c7e1d5e5e0 100644 --- a/ui/src/pages/about/view.ts +++ b/ui/src/pages/about/view.ts @@ -147,9 +147,7 @@ function renderCommit(props: AboutProps) { - ${copyStatus(props.copyState)} + ${copyStatus(props.copyState)} `; } diff --git a/ui/src/pages/chat/components/chat-background-tasks-status.ts b/ui/src/pages/chat/components/chat-background-tasks-status.ts index 5556fc68f7b2..7837a3cf0a30 100644 --- a/ui/src/pages/chat/components/chat-background-tasks-status.ts +++ b/ui/src/pages/chat/components/chat-background-tasks-status.ts @@ -134,7 +134,7 @@ export function renderBackgroundTasksStatusRow( ` : nothing} - ${label} + ${label} ${renderStatusPreview(remainingTasks)} diff --git a/ui/src/pages/chat/components/chat-composer-controls.ts b/ui/src/pages/chat/components/chat-composer-controls.ts index 8f8d00bd722d..23342002272b 100644 --- a/ui/src/pages/chat/components/chat-composer-controls.ts +++ b/ui/src/pages/chat/components/chat-composer-controls.ts @@ -309,7 +309,7 @@ export function renderChatPrimaryActions(props: ChatRunControlsProps) { ? nothing : html` ${activeSlashMenuOptionLabel} { expect(container.querySelectorAll(".chat-avatar.assistant")).toHaveLength(0); expect(container.querySelector(".chat-reading-indicator")).not.toBeNull(); expect(container.querySelector(".chat-working-indicator__elapsed")).not.toBeNull(); + expect(container.querySelector(".chat-working-indicator__status > .sr-only")?.textContent).toBe( + "Working…", + ); expect( - container.querySelector(".chat-working-indicator__status > .agent-chat__sr-only") - ?.textContent, - ).toBe("Working…"); - expect( - container.querySelectorAll( - ".chat-working-indicator__status > span:not(.agent-chat__sr-only)", - ), + container.querySelectorAll(".chat-working-indicator__status > span:not(.sr-only)"), ).toHaveLength(0); expect(container.querySelector(".chat-group-footer")).toBeNull(); }); @@ -1613,9 +1610,7 @@ describe("grouped chat rendering", () => { label, ); expect(container.querySelector(".chat-working-indicator__elapsed")).not.toBeNull(); - expect( - container.querySelector(".chat-working-indicator__status > .agent-chat__sr-only"), - ).toBeNull(); + expect(container.querySelector(".chat-working-indicator__status > .sr-only")).toBeNull(); }); it("formats terminal recap durations with full localized units", () => { @@ -1765,8 +1760,8 @@ describe("grouped chat rendering", () => { ); const status = container.querySelector(".chat-working-indicator__status"); return { - hidden: status?.querySelector(".agent-chat__sr-only")?.textContent, - visibleLabels: status?.querySelectorAll("span:not(.agent-chat__sr-only)").length, + hidden: status?.querySelector(".sr-only")?.textContent, + visibleLabels: status?.querySelectorAll("span:not(.sr-only)").length, // The whimsical long-wait phrase rides in its own aria-hidden element, // never as a plain status span screen readers would announce. decorativePhrases: status?.querySelectorAll("openclaw-working-phrase[aria-hidden]").length, diff --git a/ui/src/pages/chat/components/chat-thread.ts b/ui/src/pages/chat/components/chat-thread.ts index 973a5feff3a6..93a19fb67a8c 100644 --- a/ui/src/pages/chat/components/chat-thread.ts +++ b/ui/src/pages/chat/components/chat-thread.ts @@ -1922,7 +1922,7 @@ function renderChatThreadContents( @pointerup=${(event: PointerEvent) => handleChatThreadSelectionPointerUp(event, props)} > ${t("common.working")} + ${t("common.working")} ${options.pendingAttachmentReads > 0 - ? html`${t("newSession.readingAttachment")}` + ? html`${t("newSession.readingAttachment")}` : nothing} diff --git a/ui/src/pages/sessions/view.browser.test.ts b/ui/src/pages/sessions/view.browser.test.ts index fbfddd733a05..678314a1682d 100644 --- a/ui/src/pages/sessions/view.browser.test.ts +++ b/ui/src/pages/sessions/view.browser.test.ts @@ -80,9 +80,7 @@ function sessionsTableHtml() { : index === 6 ? "session-actions-col" : "" - }">${ - index === 6 ? `${header}` : header - }`, + }">${index === 6 ? `${header}` : header}`, ) .join("")} diff --git a/ui/src/pages/sessions/view.ts b/ui/src/pages/sessions/view.ts index c117cc44e46e..2327f534ceaf 100644 --- a/ui/src/pages/sessions/view.ts +++ b/ui/src/pages/sessions/view.ts @@ -1286,7 +1286,7 @@ function renderSessionsTable(props: SessionsProps, ctx: SessionsTableContext) { ${sortHeader("updated", t("sessionsView.updated"))} ${sortHeader("tokens", t("sessionsView.tokens"))} - ${t("sessionsView.actions")} + ${t("sessionsView.actions")} diff --git a/ui/src/pages/workboard/view-card-details.ts b/ui/src/pages/workboard/view-card-details.ts index 3f978da6c1e3..bc2311c46ef6 100644 --- a/ui/src/pages/workboard/view-card-details.ts +++ b/ui/src/pages/workboard/view-card-details.ts @@ -265,7 +265,7 @@ export function renderCardDetailsPanel(props: WorkboardProps) {
${formatPriorityLabel(card.priority)}

- ${t("workboard.detailTitle")}: ${card.title} + ${t("workboard.detailTitle")}: ${card.title}

diff --git a/ui/src/styles/about.css b/ui/src/styles/about.css index 657398f37573..0753199da5a5 100644 --- a/ui/src/styles/about.css +++ b/ui/src/styles/about.css @@ -293,16 +293,3 @@ font-size: var(--control-ui-text-xs, 12px); white-space: nowrap; } - -/* Screen-reader-only live region for copy feedback (no shared utility yet). */ -.about-sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index b9d5b6402ac7..e6a87a4c1aec 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -601,6 +601,19 @@ box-sizing: border-box; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + html, body { height: 100%; diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index e8fb480050e4..b7bb6f7c3dc2 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -2419,18 +2419,6 @@ openclaw-chat-video-player { min-width: 0; } -.agent-chat__sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - .agent-chat__composer-combobox > :is(textarea, input) { width: 100%; min-height: var(--chat-composer-control-height); diff --git a/ui/src/styles/sessions.css b/ui/src/styles/sessions.css index 02e277458e1e..93b13da56612 100644 --- a/ui/src/styles/sessions.css +++ b/ui/src/styles/sessions.css @@ -410,19 +410,6 @@ min-width: 760px; } -.sessions-sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - border: 0; - overflow: hidden; - clip: rect(0 0 0 0); - clip-path: inset(50%); - white-space: nowrap; -} - .sessions-table tbody tr.session-data-row > td { white-space: nowrap; } diff --git a/ui/src/styles/sr-only.browser.test.ts b/ui/src/styles/sr-only.browser.test.ts new file mode 100644 index 000000000000..d6bd9d464bbf --- /dev/null +++ b/ui/src/styles/sr-only.browser.test.ts @@ -0,0 +1,71 @@ +import { chromium, type Browser } from "playwright"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js"; +import { + canRunPlaywrightChromium, + resolvePlaywrightChromiumExecutablePath, +} from "../test-helpers/control-ui-e2e.ts"; + +const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath()); +const describeSrOnly = canRunPlaywrightChromium(chromiumExecutablePath) ? describe : describe.skip; + +let browser: Browser; + +beforeAll(async () => { + if (!canRunPlaywrightChromium(chromiumExecutablePath)) { + return; + } + browser = await chromium.launch({ executablePath: chromiumExecutablePath, headless: true }); +}); + +afterAll(async () => { + await browser?.close().catch(() => {}); +}); + +describeSrOnly("screen-reader-only utility", () => { + it("visually hides content while preserving its accessible status", async () => { + const page = await browser.newPage(); + try { + await page.setContent(` + + + Background task active + `); + + const status = page.getByRole("status"); + await expect + .poll(async () => { + return await status.evaluate((element) => { + const style = getComputedStyle(element); + return { + borderWidth: style.borderWidth, + clip: style.clip, + clipPath: style.clipPath, + height: style.height, + margin: style.margin, + overflow: style.overflow, + padding: style.padding, + position: style.position, + whiteSpace: style.whiteSpace, + width: style.width, + }; + }); + }) + .toEqual({ + borderWidth: "0px", + clip: "rect(0px, 0px, 0px, 0px)", + clipPath: "inset(50%)", + height: "1px", + margin: "-1px", + overflow: "hidden", + padding: "0px", + position: "absolute", + whiteSpace: "nowrap", + width: "1px", + }); + expect(await status.ariaSnapshot()).toContain("Background task active"); + } finally { + await page.close().catch(() => {}); + } + }); +}); diff --git a/ui/src/styles/workboard.css b/ui/src/styles/workboard.css index 8124d6341c54..f1a8be897c7b 100644 --- a/ui/src/styles/workboard.css +++ b/ui/src/styles/workboard.css @@ -34,19 +34,6 @@ min-height: 0; } -.workboard-sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - border: 0; - overflow: hidden; - clip: rect(0 0 0 0); - clip-path: inset(50%); - white-space: nowrap; -} - .workboard-toolbar { display: flex; justify-content: space-between; diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts index cafab0c5bdca..cbec7052e883 100644 --- a/ui/vitest.config.ts +++ b/ui/vitest.config.ts @@ -96,6 +96,7 @@ const nodeDrivenBrowserLayoutTests = [ "src/pages/sessions/view.browser.test.ts", "src/styles/cursor-policy.browser.test.ts", "src/styles/chat-file-link-presentation.browser.test.ts", + "src/styles/sr-only.browser.test.ts", ] as const; const mockRegistryUnitTests = [ ...uiIsolatedTestFiles.map((testFile) => testFile.slice("ui/".length)),