Commit 8046187
committed
Explorer: accessibility, mobile performance budget, artifact revalidation
Five improvements from a repository review. Every figure below was
measured, and each change is pinned by a test that fails against the
defect it describes.
Mobile total blocking time 1210 ms -> 120 ms. Unlabeled sliders 8 -> 0.
1. Accessible names and a text equivalent for the chart
All 8 range sliders had no accessible name -- a screen reader
announced "slider, 353" with no indication of the ingredient
(WCAG 4.1.2). The sibling value input was already labelled; the range
input was missed. Both now share one shortIngredientName() helper and
follow the unit toggle, so the announced unit cannot drift from the
displayed one. The canvases had an aria-label but exposed no data, so
#curve-summary publishes the 1/7/28-day predictions into a polite live
region -- debounced, emitted only once the composition settles, and
skipped when unchanged, since assigning identical text re-announces.
2. Lighthouse measures mobile
The config used preset "desktop" only, so the platform where the
problems live was never scored -- which is how a 6 px touch target and
1.2 s of blocking time both shipped unnoticed. The new profile failed
on its first run.
Kept as one job with two steps rather than a matrix: a matrix job's
check runs are named "lighthouse (<values>)", which would strand any
branch protection requiring the "lighthouse" context.
Budgets are calibrated honestly. No "perf" preset -- it silently
forces throttlingMethod "devtools", which measured LCP 3.2 s against
simulate's 11.6 s for the same page and diverged from the desktop
profile. Error gates are limited to metrics this harness measures
representatively (CPU, accessibility, layout); http-server serves
uncompressed bytes where Pages gzips them (188 KB -> 22 KB), which
inflates transfer-bound metrics ~7x, so those are warn-only and say so
in the config.
3. The UI shell renders before the model
buildSliders() and setupEventListeners() ran after awaiting the
strength GP, so moving that work into a worker removed the freeze but
not the delay. The shell now builds from compositions.json plus the
linear GWP/cost models, and the draw paths tolerate a null model.
The model-dependent guard in drawScatter is scoped to the selected-
composition marker alone. An early return there also skipped the axes
and the canvas._pad/_xMin/... scale stash, and the mousemove handler
bails without _pad -- so for the whole model-load window hover did
nothing and clicking a point was a silent no-op.
4. Startup animation was the real mobile cost
Profiling under 4x CPU throttle showed only two large init tasks
followed by a continuous stream of ~55 ms tasks from 1.2 s to 2.8 s:
the startup fade-in, where every animated frame redraws both canvases
and is therefore itself a main-thread long task. Time-to-interactive
can never find a quiet window against that.
Easing now snaps on the first animated frame and under
prefers-reduced-motion, which also covers the interaction transitions
(composition slide, Material Source crossfade, unit transition) and
CSS -- gradientShift is a 30 s infinite animation with no pause
control, a WCAG 2.2.2 Level A exposure rather than merely 2.3.3.
Slider input additionally coalesces onto one rAF, since touch and
high-polling mice fire faster than 60 Hz.
Measured but deliberately unchanged: a drag redraw is 24 ms at 4x
throttle (~40 fps) and 98% of that is the strength-curve GP, not the
scatter (2%). Cutting curve resolution would trade visible quality --
32 vs 48 points already differ by 17% in the steep gate region.
5. Model artifacts revalidate together
loadJSON was a bare fetch. The artifacts are mutually index-dependent
(mix_analyses.json is keyed by position in compositions.json) but
Pages serves them with max-age=600 and independent age values, so a
visitor could hold a fresh compositions.json beside a stale
mix_analyses.json for ten minutes after any deploy. Observed in
production after the 3-class merge. `cache: "no-cache"` revalidates
while still reusing the cached body on a 304, and a non-ok response
now throws instead of surfacing later as a JSON parse error.
Accessibility items found while reviewing the above:
- The Pareto pill failed WCAG 1.4.3 AA in dark theme: white on #fb7185 is
2.69:1, and at 9.6 px bold the bar is 4.5:1, not 3:1. Added --pareto-on
following the existing --accent-on precedent, which already documents
the identical problem for --accent. 4.70:1 light, 5.81:1 dark.
Only visible on the locked toolchain -- node_modules held Lighthouse
12.1.0 while the lockfile and CI pin 12.6.1, and 12.1.0 does not flag
it. Both profiles re-verified on 12.6.1.
- Range sliders had `outline: none` with no replacement anywhere, a WCAG
2.4.7 (AA) failure on the control this change otherwise improves.
Lighthouse's accessibility category does not audit focus visibility, so
the score gate cannot catch it; there is now an explicit test.
- Both modal overlays were hidden with opacity and pointer-events only,
leaving their close buttons, links and iframe permanently tabbable and
in the accessibility tree. Added visibility toggling.
That in turn exposed a real heading skip the invisible dialog had been
masking: with the modal's h2 gone, the mobile outline read H1 -> H3.
The two insight panels are siblings of the h2 chart panels, so their
titles are now h2.
- The unit toggle showed "SI" but was named "Toggle metric/imperial
units", failing WCAG 2.5.3 Label in Name. The name now contains the
visible text and tracks it. Pre-existing and weight-zero in the
Lighthouse score, so nothing flagged it.
- No aria-describedby on the curve canvas. The sr-only summary is
clip-hidden rather than display:none, so it already sits in reading
order after the canvas; referencing it as well makes a browse-mode user
hear the same sentence twice, and the canvas is not focusable so the
description would be unreliable regardless.
Also exposes __test.modelReady: "page loaded" no longer implies "model
ready", and several specs were asserting on predictions without waiting
for one.
Deliberately not addressed here, so the next person does not have to
re-derive why:
- Modals still have no focus management -- no role="dialog", focus is not
moved in on open nor restored on close. This change only stops them
being reachable while closed.
- requestRedraw and animLoop both redraw during a drag, so the work
happens twice per frame. Pre-existing; untangling redraw ownership is a
separate refactor.
- #scatter-canvas has no text equivalent; only the curve canvas got one.
- _reduceMotion is sampled once at module load, so flipping the OS
setting at runtime updates the CSS half but not the JS canvas easing.
- Whether "lighthouse" is a required status check could not be verified
from here. The workflow deliberately keeps a single job so the existing
check name is preserved, but confirm against branch protection.
Verification: lint 0, test-js 13/13, test-py 270 at 100% coverage,
e2e 100 on macOS and 104 on Linux in the matching Playwright image (visual
baselines still matching), and both Lighthouse profiles green on the
locked toolchain (12.6.1) with zero failing accessibility audits and zero
phantom assertions. The pre-model,
unit-toggle, reduced-motion CSS, animation-delay and script-animation
tests were each confirmed to fail against the defect they pin.1 parent 31843c4 commit 8046187
12 files changed
Lines changed: 984 additions & 49 deletions
File tree
- .github/workflows
- docs
- test/e2e
- visual-regression.spec.ts-snapshots
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
33 | | - | |
| 34 | + | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| |||
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
46 | | - | |
47 | | - | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
48 | 58 | | |
49 | 59 | | |
50 | 60 | | |
51 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
182 | | - | |
| 182 | + | |
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
| |||
208 | 208 | | |
209 | 209 | | |
210 | 210 | | |
211 | | - | |
| 211 | + | |
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
235 | 250 | | |
236 | 251 | | |
237 | 252 | | |
| |||
340 | 355 | | |
341 | 356 | | |
342 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
343 | 362 | | |
| 363 | + | |
344 | 364 | | |
345 | 365 | | |
346 | 366 | | |
| |||
351 | 371 | | |
352 | 372 | | |
353 | 373 | | |
354 | | - | |
| 374 | + | |
355 | 375 | | |
356 | 376 | | |
357 | 377 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
14 | 18 | | |
15 | 19 | | |
16 | 20 | | |
| |||
30 | 34 | | |
31 | 35 | | |
32 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
33 | 41 | | |
34 | 42 | | |
35 | 43 | | |
| |||
232 | 240 | | |
233 | 241 | | |
234 | 242 | | |
235 | | - | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
236 | 250 | | |
237 | 251 | | |
238 | 252 | | |
239 | 253 | | |
| 254 | + | |
240 | 255 | | |
241 | 256 | | |
242 | 257 | | |
| |||
325 | 340 | | |
326 | 341 | | |
327 | 342 | | |
328 | | - | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
329 | 350 | | |
330 | 351 | | |
331 | 352 | | |
332 | 353 | | |
| 354 | + | |
333 | 355 | | |
334 | 356 | | |
335 | 357 | | |
| |||
606 | 628 | | |
607 | 629 | | |
608 | 630 | | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
609 | 637 | | |
610 | 638 | | |
611 | 639 | | |
| |||
622 | 650 | | |
623 | 651 | | |
624 | 652 | | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
625 | 662 | | |
626 | 663 | | |
627 | 664 | | |
| |||
898 | 935 | | |
899 | 936 | | |
900 | 937 | | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
901 | 952 | | |
902 | 953 | | |
903 | 954 | | |
| |||
951 | 1002 | | |
952 | 1003 | | |
953 | 1004 | | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
954 | 1009 | | |
955 | 1010 | | |
956 | 1011 | | |
| |||
1183 | 1238 | | |
1184 | 1239 | | |
1185 | 1240 | | |
1186 | | - | |
| 1241 | + | |
1187 | 1242 | | |
1188 | 1243 | | |
1189 | 1244 | | |
| |||
1624 | 1679 | | |
1625 | 1680 | | |
1626 | 1681 | | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
| 1695 | + | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| 1699 | + | |
| 1700 | + | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
| 1707 | + | |
| 1708 | + | |
| 1709 | + | |
| 1710 | + | |
| 1711 | + | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
0 commit comments