authorgravatar for 55418374+Leos-Khai@users.noreply.github.comKhai Shern, Toh <55418374+Leos-Khai@users.noreply.github.com> 2026-09-24 11:36:12+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 00:36:12-03:00
log21e2b7de0062a3c1dca7209ca48b736dd9a36c84
treeb36de83007fd98291bb143b9329629aa96c34549
parent9030a60eafde905100a052f86beeb8be7642b5a9
signature Signed by PGP key B5690EEEBB952194

fix(web): switches announce their real state to screen readers (#11580)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

2 files changed, 24 insertions(+), 1 deletions(-)

apps/web/src/components/ui/switch.test.tsx created+20
......@@ -0,0 +1,20 @@
1import { renderToStaticMarkup } from "react-dom/server";
2import { describe, expect, it } from "vite-plus/test";
3
4import { Switch } from "./switch";
5
6function renderSwitchRoot(props: Partial<Parameters<typeof Switch>[0]>) {
7 const html = renderToStaticMarkup(<Switch aria-label="Enable Claude Code" {...props} />);
8 return html.match(/<span[^>]*role="switch"[^>]*>/)?.[0] ?? "";
9}
10
11describe("Switch accessibility", () => {
12 it("exposes the checked state to assistive tech", () => {
13 expect(renderSwitchRoot({ checked: true })).toContain('aria-checked="true"');
14 expect(renderSwitchRoot({ checked: false })).toContain('aria-checked="false"');
15 });
16
17 it("exposes the mixed state", () => {
18 expect(renderSwitchRoot({ checked: false, mixed: true })).toContain('aria-checked="mixed"');
19 });
20});
apps/web/src/components/ui/switch.tsx+4-1
......@@ -27,7 +27,10 @@ function Switch({
2727 data-size={size}
2828 data-slot="switch"
2929 data-mixed={mixed ? "" : undefined}
30 aria-checked={mixed ? "mixed" : undefined}
30 // Base UI copies every key we pass, even `undefined`, over its own
31 // aria-checked. Only pass the attribute when mixed so the real state
32 // survives for screen readers.
33 {...(mixed ? { "aria-checked": "mixed" as const } : {})}
3134 {...props}
3235 >
3336 <SwitchPrimitive.Thumb