From c3f1ba101967d4611891af31910a51aee0a2fab8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:52:09 +0800 Subject: [PATCH] feat(ui): harden design-system accessibility contracts --- src/components/AccessibleTable.tsx | 4 +++- tests/accessible-table.dom.test.tsx | 12 +++++++++++ tests/design-token-contract.test.ts | 32 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/components/AccessibleTable.tsx b/src/components/AccessibleTable.tsx index aea33441b3..0a77faabb4 100644 --- a/src/components/AccessibleTable.tsx +++ b/src/components/AccessibleTable.tsx @@ -328,7 +328,9 @@ function AccessibleTableMarkup({ className={cn( "block min-w-0", renderDensePreview - ? "truncate text-2xs leading-4" + ? cell + ? "truncate text-2xs leading-4" + : "whitespace-normal break-words text-2xs leading-4" : "text-sm leading-6 md:text-inherit md:leading-inherit", )} > diff --git a/tests/accessible-table.dom.test.tsx b/tests/accessible-table.dom.test.tsx index 547fefef64..6611cc5f28 100644 --- a/tests/accessible-table.dom.test.tsx +++ b/tests/accessible-table.dom.test.tsx @@ -107,6 +107,18 @@ describe("AccessibleTable (jsdom)", () => { expect(screen.queryByText("-")).not.toBeInTheDocument(); }); + it("keeps the full missing-value phrase readable in the dense 320px preview", () => { + render(); + + const missingValue = screen.getByTestId("missing-value"); + const valueWrapper = missingValue.parentElement; + + expect(missingValue).toHaveTextContent("Not recorded"); + expect(valueWrapper).not.toBeNull(); + expect(valueWrapper!).toHaveClass("whitespace-normal", "break-words"); + expect(valueWrapper!).not.toHaveClass("truncate"); + }); + it("opens the full-screen dialog when the mobile expand control is clicked", async () => { setMatchMedia(true); // emulate the mobile/coarse-pointer breakpoint that enables expansion const user = userEvent.setup(); diff --git a/tests/design-token-contract.test.ts b/tests/design-token-contract.test.ts index 013f90d7ae..30289c2842 100644 --- a/tests/design-token-contract.test.ts +++ b/tests/design-token-contract.test.ts @@ -158,6 +158,33 @@ describe("elevation ladder", () => { expect(tokens.get("--e0")).toBe("none"); }); + // `--shadow-focus` is retired (`#261`). It was not an elevation alias at all: + // it packed a 3px accent halo in FRONT of `--shadow-soft`, so its one consumer + // — `.chat-composer-shell-delta:focus-within` — painted a companion ring on + // top of the accent border swap, which is the second focus affordance the + // shared `:focus-visible` treatment is written to prevent. The composer now + // uses the sanctioned `outline: 2px solid var(--focus)`. + // + // Unlike the `--shadow-tight` assertion above this is not a raw substring + // check: the stylesheet comment at the composer rule names the retired token + // on purpose, so that the next author reaching for a focus halo finds the + // reason it is gone rather than re-deriving it. The two spellings below are + // the only ways the token can actually come back to life — a declaration and + // a `var()` consumer — so they are what the gate rejects. + it("keeps the retired --shadow-focus token deleted in every scope", () => { + for (const [name, stylesheet] of [ + ["globals.css", globals], + ["ckb-v2-tokens.css", v2Stylesheet], + ] as const) { + expect(stylesheet, `${name} redeclares --shadow-focus; focus is an outline, not a ring`).not.toContain( + "--shadow-focus:", + ); + expect(stylesheet, `${name} consumes --shadow-focus; focus is an outline, not a ring`).not.toContain( + "var(--shadow-focus)", + ); + } + }); + it("flattens the ladder itself under forced colors, not only the role aliases", () => { const forced = globals.slice(globals.indexOf("@media (forced-colors: active)")); for (const tier of ["--e1", "--e2", "--e3", "--e4"]) { @@ -167,6 +194,11 @@ describe("elevation ladder", () => { }); describe("status colour ranking", () => { + it.each(themes)("keeps warning body text AA-safe on the default surface in $name", ({ tokens, name }) => { + const ratio = contrastRatio(colourOf(tokens, "--warning"), colourOf(tokens, "--surface")); + expect(ratio, `${name} --warning body text on --surface`).toBeGreaterThanOrEqual(4.5); + }); + // Every status pair used to sit in a 4.6–5.2:1 band, so nothing read as more // urgent than anything else. it("clears 5.5:1 for every light-mode status pair", () => {