From ee3ce877748078f8ba08d3a46ccbe0835c813060 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 10:20:44 +0900 Subject: [PATCH] fix(ui-ux): make analysis-run digest verification keyboard/touch accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reproducibility digest (Code/Config/Result SHA) in the analysis-run detail popup exposed its full hash only via the `title` attribute on a bare ``, which has no keyboard or touch equivalent -- a WCAG 2.1.1 failure with no way to copy/compare the full digest without a mouse. Swap the `` for the same native `
/` disclosure pattern already used elsewhere in App.tsx for "compact summary, full detail on demand". `
/` is natively focusable and toggled by Enter/Space/click/tap with zero JS. The wrapping `

` becomes a `

` since `
` is not valid phrasing content inside `

`; minor CSS keeps the disclosures reading inline in the '·'-separated post-meta line (the other `

` usages in this file are block-level list items). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt --- frontend/src/App.css | 15 +++++++++++++++ frontend/src/App.test.tsx | 30 ++++++++++++++++++++++-------- frontend/src/App.tsx | 13 ++++++++----- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index c72aab078..d2b8becb6 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -327,6 +327,21 @@ font-size: 0.85rem; } +/* Reproducibility digest prefixes read inline in the '·'-separated post-meta line. */ +.analysis-run-digest { + display: inline; +} + +.analysis-run-digest > summary { + display: inline; + cursor: pointer; + list-style: none; +} + +.analysis-run-digest > summary::-webkit-details-marker { + display: none; +} + .visually-hidden { position: absolute; width: 1px; diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..c5f914b55 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -2658,17 +2658,27 @@ describe("App, authenticated", () => { expect(screen.getByText(/Cutoff 2026-01-12/)).toBeInTheDocument(); expect(screen.getByText(/Requested 2026-01-12/)).toBeInTheDocument(); const digests = screen.getByLabelText("Analysis run reproducibility digests"); - expect(digests).toHaveTextContent("Hover a prefix to read the full digest for verification."); + expect(digests).toHaveTextContent("Open a prefix to read the full digest for verification."); expect(digests).toHaveTextContent("Code abcdef012345"); expect(digests).toHaveTextContent("Config 0123456789ab"); - expect(digests).not.toHaveTextContent("abcdef0123456789deadbeefcafebabe"); - expect(digests).not.toHaveTextContent( + // The disclosures are native
/ -- no tabIndex/role/onClick hack -- + // so they are keyboard- and touch-operable with zero JS, unlike a hover-only title tooltip. + const codeDisclosure = screen.getByText("Code abcdef012345").closest("details"); + const configDisclosure = screen.getByText("Config 0123456789ab").closest("details"); + expect(codeDisclosure).not.toHaveAttribute("open"); + expect(configDisclosure).not.toHaveAttribute("open"); + expect(screen.getByText("Code abcdef012345").tagName).toBe("SUMMARY"); + expect(screen.getByText("Code abcdef012345")).not.toHaveAttribute("tabIndex"); + expect(screen.getByText("Code abcdef012345")).not.toHaveAttribute("role"); + await userEvent.click(screen.getByText("Code abcdef012345")); + expect(codeDisclosure).toHaveAttribute("open"); + expect(codeDisclosure).toHaveTextContent("abcdef0123456789deadbeefcafebabe"); + expect(configDisclosure).not.toHaveAttribute("open"); + await userEvent.click(screen.getByText("Config 0123456789ab")); + expect(configDisclosure).toHaveAttribute("open"); + expect(configDisclosure).toHaveTextContent( "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ); - expect(screen.getByTitle("abcdef0123456789deadbeefcafebabe")).toHaveTextContent("Code abcdef012345"); - expect( - screen.getByTitle("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), - ).toHaveTextContent("Config 0123456789ab"); const history = screen.getByRole("list", { name: "Analysis run status history" }); expect(history).toHaveTextContent("Pending 2026-01-12 12:31"); expect(history).toHaveTextContent("Running 2026-01-12 12:32"); @@ -3305,7 +3315,11 @@ describe("App, authenticated", () => { ); const digests = screen.getByLabelText("Analysis run reproducibility digests"); expect(digests).toHaveTextContent("Result aaaaaaaaaaaa"); - expect(screen.getByTitle("aa".repeat(32))).toHaveTextContent("Result aaaaaaaaaaaa"); + const resultDisclosure = screen.getByText("Result aaaaaaaaaaaa").closest("details"); + expect(resultDisclosure).not.toHaveAttribute("open"); + await userEvent.click(screen.getByText("Result aaaaaaaaaaaa")); + expect(resultDisclosure).toHaveAttribute("open"); + expect(resultDisclosure).toHaveTextContent("aa".repeat(32)); const startCall = fetchMock.mock.calls.find((call) => String(call[0]).endsWith("/api/analysis-runs/run-demo-lineage-pending/start"), ); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..d5b934a4d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2527,7 +2527,7 @@ function analysisRunCorpusHint(run: AnalysisRun): string | null { } } -/** Git-style prefix. The full digest stays on `title` for verification. */ +/** Git-style prefix. The full digest is behind a `
` disclosure for verification. */ const ANALYSIS_RUN_DIGEST_PREFIX_LENGTH = 12; function analysisRunDigestPrefix(digest: string): string { @@ -2606,17 +2606,20 @@ function AnalysisRunReproducibilityDigests({ } return (
-

+

- Hover a prefix to read the full digest for verification.{" "} + Open a prefix to read the full digest for verification.{" "} {parts.map((part, index) => ( {index > 0 ? " · " : null} - {`${part.label} ${analysisRunDigestPrefix(part.digest)}`} +
+ {`${part.label} ${analysisRunDigestPrefix(part.digest)}`} + {part.digest} +
))} -

+
); }