Wave 4 — the accessibility sweep: chart tables, measured contrast, and a survey you can cross by keyboard - #27
Merged
Conversation
Every chart here carries role="img" and a one-line summary, which tells a screen-reader user that a shape exists and roughly how big it is — not what it says. The radar and the values strips now fold out a real table of the same values: rows are the questions, columns are the people, answers are the answers rather than the 0..1 the shapes are drawn from. Unanswered reads as a dash, never as a zero, because "did not answer" and "answered zero" are different facts and the strip above draws only one dot. The seeking matrix got nothing, deliberately. It turned out to already be a real table — scoped headers, a visible level label in every cell — and a second table saying the same thing is worse for a screen reader than one. A spec now holds it to exactly one table, since the obvious future "fix" is to add the disclosure everywhere for consistency. Contrast: measured, not eyeballed, and three light-theme values were failing. --muted was 3.4:1 under the .fine text it exists for, which is the honest-limits copy — the last text in this app that should be hard to read. --series-4 was 2.1:1 and --series-3 2.7:1, both under the 3:1 that non-text graphics need, so a third or fourth person's line was effectively invisible on a light background. The green was found by the spec checking all four hues after I had eyeballed two: the ones that fail are not the ones that look faint, they are the ones whose luminance sits near the page's. Since it is measurable it is now tested — contrast.spec.ts reads the token file and checks text at 4.5:1 and graphics at 3:1 across both themes, so the next palette change cannot quietly undo this. Also answers prefers-contrast: more (greys give up first; series hues stay put, since darkening them toward each other costs the distinguishability they exist for) and forced-colors: active (tokens map onto system keywords, charts opt out via forced-color-adjust because four lines all painted CanvasText are four identical lines, and the focus ring is kept explicitly because nothing else will draw one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaDanzXTm6kVSgbNh1eNrs
The audit expected these controls to need radio semantics. They do not: every answer control here is deselectable — clicking the chosen option clears the answer, because every question is optional — and an ARIA radio group may not behave that way. Announcing them as radios would have been tidier and would have lied. The aria-pressed toggles stay; the navigation is what was wrong. A fully expanded survey was 462 tab stops, one per option button: seven for a scale, four per interest item, one per choice. Seventy of them to cross "What I value" alone. That is a toll only keyboard users paid, and no mouse user could see it. OptionGroupDirective gives each group the composite-widget treatment — one tab stop, arrows within, Home and End to the ends, wrapping at the edges — so the survey is one stop per question instead. The tab stop follows the selection, so returning to an answered question lands on the answer rather than at the start of the row. The importance control was worse: which tier was selected lived entirely in a highlight class, so a screen reader was told nothing about the state of a control that decides how the whole comparison is weighted. It now says so, and both its rows are named groups rather than bare clusters of buttons. The e2e is what proves this did not break the mouse: it drives these exact controls by clicking, and a roving tabindex is an easy way to make that stop working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaDanzXTm6kVSgbNh1eNrs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The fourth wave of
docs/feature-plan.md, done as one audit rather than a checklist. Two of the three items turned out differently from what the plan predicted, and those are the interesting parts.C3 · The numbers behind the charts
Every chart carries
role="img"and a one-line summary, which says a shape exists without saying what it says. The radar and the values strips now fold out a real table of the same values — rows are questions, columns are people, cells are the answers themselves rather than the 0..1 the shapes are drawn from. Unanswered renders as a dash, never a zero: "didn't answer" and "answered zero" are different facts, and the strip above draws only one dot.The seeking matrix deliberately got nothing. A failing test sent me to look properly, and it was already a real table — scoped headers, a visible level label in every cell. A second table saying the same thing is worse for a screen reader than one. A spec now holds that panel to exactly one table, because the obvious future "consistency fix" is to add the disclosure everywhere.
C4 · Contrast, measured rather than eyeballed
Three light-theme values were failing:
--muted.fine— the honest-limits copy, the last text here that should be hard to read--series-4--series-3The green is the point: the hues that fail are not the ones that look faint, they are the ones whose luminance sits near the page's. Since contrast is measurable it is now tested —
contrast.spec.tsreads the token file and checks text at 4.5:1 and graphics at 3:1 across both themes, so the next palette change cannot quietly undo this.Also answers
prefers-contrast: more(greys give up first; series hues stay put, since darkening them toward each other costs the distinguishability they exist for) andforced-colors: active(tokens map onto system keywords, charts opt out viaforced-color-adjustbecause four lines all paintedCanvasTextare four identical lines, and the focus ring is kept explicitly because nothing else will draw one).C5 · The survey controls
The plan guessed these needed radio semantics. They don't — every answer control here is deselectable (clicking the chosen option clears the answer, because every question is optional), and an ARIA radio group may not behave that way. Announcing them as radios would have been tidier and would have lied. The
aria-pressedtoggles stay; navigation was the real defect:OptionGroupDirectivegives each group the composite-widget treatment: one tab stop, arrows within, Home/End, wrapping. The stop follows the selection, so returning to an answered question lands on the answer.Verification
Full ladder green:
format:check, both typechecks, all four unit suites (23 core files / 5 ui / 8 app / 2 server),build, ande2e.The e2e matters more than usual here: it drives these exact controls by clicking, and a roving tabindex is an easy way to break mouse interaction without noticing.
Generated by Claude Code