mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): hide screen-reader-only status text (#121847)
This commit is contained in:
committed by
GitHub
parent
32e3bd3797
commit
17e11e4341
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -147,9 +147,7 @@ function renderCommit(props: AboutProps) {
|
||||
<span aria-hidden="true">${props.copyState === "copied" ? icons.check : icons.copy}</span>
|
||||
</button>
|
||||
</openclaw-tooltip>
|
||||
<span class="about-sr-only" role="status" aria-live="polite"
|
||||
>${copyStatus(props.copyState)}</span
|
||||
>
|
||||
<span class="sr-only" role="status" aria-live="polite">${copyStatus(props.copyState)}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function renderBackgroundTasksStatusRow(
|
||||
<span class="chat-tasks-status__sep" aria-hidden="true">·</span>
|
||||
`
|
||||
: nothing}
|
||||
<span class="agent-chat__sr-only" role="status">${label}</span>
|
||||
<span class="sr-only" role="status">${label}</span>
|
||||
<openclaw-tooltip class="chat-tasks-status__preview">
|
||||
<button class="chat-tasks-status__link" type="button" @click=${openRail}>${label}</button>
|
||||
${renderStatusPreview(remainingTasks)}
|
||||
|
||||
@@ -309,7 +309,7 @@ export function renderChatPrimaryActions(props: ChatRunControlsProps) {
|
||||
? nothing
|
||||
: html`
|
||||
<span
|
||||
class="agent-chat__sr-only agent-chat__voice-status"
|
||||
class="sr-only agent-chat__voice-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
|
||||
@@ -410,14 +410,14 @@ export function renderChatComposerView(context: ChatComposerViewContext) {
|
||||
></textarea>
|
||||
<span
|
||||
id=${slashMenuAnnouncementId}
|
||||
class="agent-chat__sr-only"
|
||||
class="sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${activeSlashMenuOptionLabel}</span
|
||||
>
|
||||
<span
|
||||
class="agent-chat__run-status-announcement agent-chat__sr-only"
|
||||
class="agent-chat__run-status-announcement sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
|
||||
@@ -1549,14 +1549,11 @@ describe("grouped chat rendering", () => {
|
||||
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,
|
||||
|
||||
@@ -1922,7 +1922,7 @@ function renderChatThreadContents(
|
||||
@pointerup=${(event: PointerEvent) => handleChatThreadSelectionPointerUp(event, props)}
|
||||
>
|
||||
<span
|
||||
class="chat-transcript-announcement agent-chat__sr-only"
|
||||
class="chat-transcript-announcement sr-only"
|
||||
role="status"
|
||||
aria-live=${props.announceTranscript !== false ? "polite" : "off"}
|
||||
aria-atomic="true"
|
||||
|
||||
@@ -163,9 +163,7 @@ export function renderChatWorkingIndicator(
|
||||
${renderLiveOutputTokens(options.outputTokens)}
|
||||
`
|
||||
: html`
|
||||
<span class=${continuation ? "" : "agent-chat__sr-only"}
|
||||
>${t("common.working")}</span
|
||||
>
|
||||
<span class=${continuation ? "" : "sr-only"}>${t("common.working")}</span>
|
||||
<openclaw-elapsed-time
|
||||
class="chat-working-indicator__elapsed"
|
||||
.startMs=${part.startedAt}
|
||||
|
||||
@@ -270,9 +270,7 @@ function renderNewSessionComposer(options: NewSessionComposerOptions) {
|
||||
</div>
|
||||
</div>
|
||||
${options.pendingAttachmentReads > 0
|
||||
? html`<span class="agent-chat__sr-only" role="status"
|
||||
>${t("newSession.readingAttachment")}</span
|
||||
>`
|
||||
? html`<span class="sr-only" role="status">${t("newSession.readingAttachment")}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,9 +80,7 @@ function sessionsTableHtml() {
|
||||
: index === 6
|
||||
? "session-actions-col"
|
||||
: ""
|
||||
}">${
|
||||
index === 6 ? `<span class="sessions-sr-only">${header}</span>` : header
|
||||
}</th>`,
|
||||
}">${index === 6 ? `<span class="sr-only">${header}</span>` : header}</th>`,
|
||||
)
|
||||
.join("")}
|
||||
</tr>
|
||||
|
||||
@@ -1286,7 +1286,7 @@ function renderSessionsTable(props: SessionsProps, ctx: SessionsTableContext) {
|
||||
${sortHeader("updated", t("sessionsView.updated"))}
|
||||
${sortHeader("tokens", t("sessionsView.tokens"))}
|
||||
<th class="session-actions-col">
|
||||
<span class="sessions-sr-only">${t("sessionsView.actions")}</span>
|
||||
<span class="sr-only">${t("sessionsView.actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -265,7 +265,7 @@ export function renderCardDetailsPanel(props: WorkboardProps) {
|
||||
<div>
|
||||
<span class="workboard-card__priority">${formatPriorityLabel(card.priority)}</span>
|
||||
<h2 id=${workboardCardDetailTitleId}>
|
||||
<span class="workboard-sr-only">${t("workboard.detailTitle")}: </span>${card.title}
|
||||
<span class="sr-only">${t("workboard.detailTitle")}: </span>${card.title}
|
||||
</h2>
|
||||
</div>
|
||||
<openclaw-tooltip .content=${t("common.cancel")}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(`<!doctype html>
|
||||
<html>
|
||||
<head><style>${readStyleSheet("ui/src/styles/base.css")}</style></head>
|
||||
<body><span class="sr-only" role="status">Background task active</span></body>
|
||||
</html>`);
|
||||
|
||||
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(() => {});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user