fix(a11y): heading hierarchy, error page h1, cover-modal alt, live region (bookshelf-vqy9.19) #738
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-vqy9.19"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Four minor a11y fixes from the vqy9.2 audit (M-4, M-7, M-8, N-4):
settings_shell.html): 7 panel headings changed from<h1 class="page-title">to<h2>— they live insiderole="tabpanel"sections that are already below the page-level<h1>inbase.html, so<h1>was creating a duplicate top-level heading (WCAG 1.3.1).error.html): Error status promoted from<p class="error-page-status">to<h1>so screen-reader users can jump to the error message via heading navigation shortcuts.books_show.html): Cover-modal<img>hadalt=""(correct only for decorative images). Replaced withalt="Cover of {{.Book.DisplayTitle}}"since the enlarged cover is informational.books_show.html):metadata-refetch-statusspan lackedrole="status" aria-live="polite", so async fetch-result text was never announced to AT.No inline
style=added. No canonical class changes.Test plan
internal/tmpl/a11y_render_test.gocovering each changemake test— all passnpm test— 2533 JS tests passmake lint— 0 issues in this worktreeCloses bead bookshelf-vqy9.19 on merge.
- M-4: settings_shell.html — panel headings changed from h1 to h2 (7 occurrences) so they sit correctly below the page-level h1 in base layout - M-7: error.html — error-page-status promoted from <p> to <h1> so screen-reader users can navigate to the error via heading shortcuts - M-8: books_show.html cover-modal — empty alt="" replaced with alt="Cover of {{.Book.DisplayTitle}}" so enlarged cover is described - N-4: books_show.html metadata-refetch-status — added role="status" aria-live="polite" so async fetch completion is announced to AT Tests: 8 new assertions in internal/tmpl/a11y_render_test.go covering each change with either real-template render or static source checks.ea3f385c820caf08182bCODE REVIEW: NOT APPROVED
Phase 0: DEMO Verification
No DEMO block is required for a pure a11y/template PR — CI acts as the behavioral gate. Proceeding to Phase 1.
Phase 1: Spec Compliance
All four stated changes are present in the diff:
settings_shell.html: 7 h1→h2 conversionserror.html: status p→h1books_show.html: cover-modal alt text + live-region attributesinternal/tmpl/a11y_render_test.go: 8 new assertionsPhase 2: Code Quality
[MAJOR] internal/tmpl/a11y_render_test.go — live-region tests are tautological (zero regression protection)
The two
Itblocks for the metadata-refetch-status live region assert:Both of these strings already appear in
books_show.htmlbefore this PR — on the flash<div>elements at lines 33 and 42 of the template:The test reads the entire raw file and uses
ContainSubstring. It would have passed unchanged onorigin/main— before the metadata-refetch span had anyroleoraria-liveattribute. This is the same tautological-test pattern caught in #736: the test passes because of a pre-existing element, not because the fix is in place. Removing the new attributes from the<span>would not cause these tests to fail.Fix: scope the assertion to the
metadata-refetch-statuselement, e.g.:[MAJOR] templates/pages/settings_shell.html — heading hierarchy is now broken (no h1 on the page)
Before this PR, each visible settings tab panel had an
<h1 class="page-title">— one h1 visible at a time (hidden panels are hidden). That was a valid heading structure.After this PR, the panel headings are
<h2>but there is no h1 anywhere on the page. Thebase.htmllayout only renders its<h1 class="top-bar-title">{{.PageTitle}}</h1>whenPageTitleis non-empty, andinternal/settings/shell_handler.go:254setsbase.Title = "Settings"but never setsbase.PageTitle. The settings_shell.html template itself contains no<h1>at all.Result: screen-reader users navigate to "Email Settings" as an h2 with no h1 parent — a skipped heading level, which WCAG 2.1 SC 1.3.1 treats as worse than the single-h1-per-tab approach that existed before.
The fix should either (a) set
base.PageTitle = "Settings"in the shell handler so the top-bar h1 renders, or (b) add an explicit visually-hidden<h1>Settings</h1>insidesettings_shell.html.The test for this (
does not contain any h1 with class page-title) correctly asserts the panel headings changed, but it doesn't verify that any h1 exists on the page — so it doesn't catch the broken hierarchy.Minor issues (do not block)
[MINOR]
internal/tmpl/a11y_render_test.go— the settings_shell test (does not contain any h1 with class page-title/renders panel headings as h2) correctly confirms the panel-heading element change but does not assert a h1 exists anywhere on the rendered page, leaving the broken-hierarchy scenario undetected by tests.Verdict
REVIEW VERDICT: 0 blocker, 2 major, 1 minor
Review fix: 2 MAJOR + 1 MINOR applied
[MAJOR] h1 hierarchy regression (PageTitle):
Added
base.PageTitle = "Settings"ininternal/settings/shell_handler.go:254so the base.html top-bar renders<h1 class="top-bar-title">Settings</h1>. Without this the settings page had no h1 after the panel headings were changed to h2.Delete-confirm-fail (h1): Temporarily removed
base.PageTitle = "Settings"from the handler → the new regression-guard test inshell_handler_test.go(GetSettingsShellHandler: heading hierarchy) immediately FAILED with Expected `<h1 class="top-bar-title">Settings</h1>`. Restored → test PASSES. ✓[MINOR] h1 regression-guard test:
Added a new
Describe("GetSettingsShellHandler: heading hierarchy (regression: PageTitle→h1)")block ininternal/settings/shell_handler_test.gothat drives the handler through a base-layout stub rendering the same{{if .PageTitle}}<h1>{{end}}guard as the realbase.html. If anyone removesbase.PageTitlefrom the handler, the test will catch it.[MAJOR] Tautological live-region assertions:
Replaced both
ContainSubstring(\role="status"`)/ContainSubstring(`aria-live="polite"`)withMatchRegexp(`metadata-refetch-status[^>]+role="status"`)andMatchRegexp(`metadata-refetch-status[^>]+aria-live="polite"`)ininternal/tmpl/a11y_render_test.go`. The old assertions passed even when the span had neither attribute (flash divs in books_show.html also carry those strings). The new assertions are scoped to the element under test.Delete-confirm-fail (live-region role): Temporarily removed
role="status"from themetadata-refetch-statusspan in books_show.html → test FAILED: Expectedmetadata-refetch-status[^>]+role="status". The oldContainSubstringwould have PASSED (flash divs still had the attribute). Restored → test PASSES. ✓Delete-confirm-fail (live-region aria-live): Removed
aria-live="polite"from the span (leaving role intact) → test FAILED: Expectedmetadata-refetch-status[^>]+aria-live="polite". Restored → test PASSES. ✓CI: green ✓
2179607830423fc0a721