From a71762bd389a4767e3667c82b384cf8b7a914197 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 18:49:47 +0900 Subject: [PATCH 1/2] fix(a11y): include leftover-pair evidence in button names Pair actions were named only by kind, title, and criterion, so assistive technology could not read the finite leftover badges already shown on the same control. Keep those formatters as the only evidence source. --- docs/product-technical-gap-baseline.md | 8 ++++++ frontend/src/App.test.tsx | 6 +++- .../src/components/LeftoverPairList.test.tsx | 14 ++++++++-- frontend/src/components/LeftoverPairList.tsx | 28 +++++++++++++++---- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5db914a52..fdeca0d63 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,13 @@ # Product & Technical Gap Baseline +> Leftover-pair accessible-name overlay: 2026-09-07 KST. Issue #976 stacks +> onto leftover-map single-writer `#802` at `32f1cda10` so pair-button +> names include the same finite leftover evidence badges already rendered +> (`R`, `Y/E`, rank, unexplained, shares, reconstruction, coordinates, +> distance). Grouping-comparison pair actions stay on `#829`/`#830` and +> are not expanded here. This branch is Draft; it is not protected-main +> or independently approved evidence. +> > Exact-head rendered-UX overlay: 2026-08-31 12:16 KST. PR #802 > implementation revision `1e972d7f6` retains persisted-only `d`, `R̂`, and > `e` projection while moving dense segment captions off their plot segments diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 790c4da69..07c30ed61 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4175,7 +4175,11 @@ describe("App, authenticated", () => { expect(closestPair).toHaveTextContent("R̂ +0.25"); expect(closestPair).toHaveTextContent("ξ (+0.50, +0.10) ζ (+0.50, −0.02)"); expect(closestPair).toHaveTextContent("d 0.12"); - expect(closestPair).toHaveAccessibleName("Open leftover closest pair: Public post · sales-lead"); + expect(closestPair).toHaveAccessibleName(/Open leftover closest pair: Public post · sales-lead/); + expect(closestPair).toHaveAccessibleName(/R \+0\.40/); + expect(closestPair).toHaveAccessibleName(/Y 2\.40 · E 2\.00/); + expect(closestPair).toHaveAccessibleName(/rank 1/); + expect(closestPair).toHaveAccessibleName(/d 0\.12/); expect(farthestPair).toHaveTextContent("Farthest leftover: Specification revision requested · negative"); expect(farthestPair).toHaveTextContent( "Leftover map places this post at ξ (+0.90, +0.80) and the criterion at ζ (−0.70, −0.40) after IRT main effects. Open this post to read negative.", diff --git a/frontend/src/components/LeftoverPairList.test.tsx b/frontend/src/components/LeftoverPairList.test.tsx index 74f4e8a64..b1282ca1f 100644 --- a/frontend/src/components/LeftoverPairList.test.tsx +++ b/frontend/src/components/LeftoverPairList.test.tsx @@ -46,9 +46,13 @@ describe("LeftoverPairList", () => { expect(screen.getByLabelText("Leftover pairs")).toBeInTheDocument(); const closest = screen.getByRole("button", { - name: "Open leftover closest pair: Public post · sales-lead", + name: /Open leftover closest pair: Public post · sales-lead/, }); expect(closest).toHaveTextContent("Closest leftover: Public post · sales-lead"); + expect(closest).toHaveAccessibleName(/R \+0\.40/); + expect(closest).toHaveAccessibleName(/Y 2\.40 · E 2\.00/); + expect(closest).toHaveAccessibleName(/rank 1/); + expect(closest).toHaveAccessibleName(/d 0\.12/); expect(closest).toHaveTextContent( "Read leftover map rank 1, observed Y 2.40, and expected E 2.00 after IRT main effects, then open this post.", ); @@ -58,8 +62,9 @@ describe("LeftoverPairList", () => { expect(closest).toHaveTextContent("d 0.12"); const farthest = screen.getByRole("button", { - name: "Open leftover farthest pair: Specification revision requested · negative", + name: /Open leftover farthest pair: Specification revision requested · negative/, }); + expect(farthest).toHaveAccessibleName(/R −1\.10/); expect(farthest).toHaveTextContent("R −1.10"); expect(farthest).toHaveTextContent("Y 0.90 · E 2.00"); expect(farthest).toHaveTextContent("rank 1"); @@ -148,8 +153,11 @@ describe("LeftoverPairList", () => { ); const closest = screen.getByRole("button", { - name: "Open leftover closest pair: Public post · sales-lead", + name: /Open leftover closest pair: Public post · sales-lead/, }); + expect(closest).toHaveAccessibleName(/R \+0\.40/); + expect(closest).toHaveAccessibleName(/R̂²\/R² 0\.76/); + expect(closest).toHaveAccessibleName(/ξ \(\+0\.50, \+0\.10\) ζ \(\+0\.50, −0\.02\)/); expect(closest).toHaveTextContent( "Leftover map places this post at ξ (+0.50, +0.10) and the criterion at ζ (+0.50, −0.02) after IRT main effects. Open this post to read sales-lead.", ); diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index 75e500821..b11af71ef 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -33,6 +33,7 @@ import { formatLeftoverMapUnexplainedShare, LEFTOVER_MAP_UNEXPLAINED_SHARE_ACTION, } from "../leftoverMapUnexplainedShare"; +import { formatLeftoverMapDistance } from "../leftoverMapPlotLayout"; import { LeftoverMapPlot } from "./LeftoverMapPlot"; export type LeftoverPairListProps = { @@ -42,6 +43,11 @@ export type LeftoverPairListProps = { onSelectPost: (pair: LeftoverPair) => void; }; +/** Join finite leftover evidence already selected by the pair-row formatters. */ +function leftoverPairAccessibleName(parts: Array): string { + return parts.filter((part): part is string => typeof part === "string" && part.length > 0).join(" "); +} + /** * Closest and farthest leftover post–criterion pairs after IRT main effects. * @@ -216,11 +222,23 @@ export function LeftoverPairList({ );