fix(a11y): give every column header a scope, so screen readers can associate cells - #69
Conversation
…sociate cells Five tables rendered `<thead><tr><th>` with no `scope`, so nothing tied a data cell to the header describing it. Assistive technology can infer the association for a trivial table, but inference is not the contract — WCAG 2.2 AA SC 1.3.1 (Info and Relationships) asks for the relationship to be programmatically determinable, and gate-43 table-headers enforces it. All five are single-row, column-header tables: every `<th>` sits in the `<thead>` row and labels the column beneath it, so `scope="col"` is the correct value for all 19 of them. No row headers, no spans, so no `scope="row"` or `headers=`/`id=` pairing is needed. src/modals/TemplateResyncModal.vue 5 src/modals/AcknowledgementReportModal.vue 3 src/components/admin/AdminAnalytics.vue 3 src/components/admin/ConditionalVisibilityOverview.vue 5 src/components/admin/DashboardBulkOperations.vue 3 Attribute-only change: no template structure, no bindings, no script, no styles. gate-43 table-headers, whole tree, hydra-gates at .github@main 756fe89 — the two arms differ only by the attribute: without scope= FAIL — 5 <table>(s) missing <th scope=> with scope= PASS Whole-tree failing gates 15 -> 14, no other gate moved in either direction. Noted, not fixed here: the select-all checkbox in DashboardBulkOperations' first header cell has no accessible name. That is gate-40's subject matter, not gate-43's, and gate-40 measured 58% false-positive fleet-wide, so it wants its own change with its own evidence rather than a drive-by.
CI: one gate red, and it is inherited
Proven pre-existing by running the checker against the base tree with the five All four are in Failure set is therefore a strict subset of
Not fixed hereThe four |
Quality Report — ConductionNL/launchpad @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| composer | ✅ | ✅ 102/102 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-08 11:02 UTC
Download the full PDF report from the workflow artifacts.
What
Adds
scope="col"to all 19 column headers across the five tables that hadnone. WCAG 2.2 AA SC 1.3.1 (Info and Relationships) — without
scope, nothingprogrammatically ties a data cell to the header describing it.
All five are single-row, column-header tables — every
<th>sits in the<thead>row and labels the column beneath it — soscope="col"is correct forall of them. No row headers and no spans, so no
scope="row"and noheaders=/id=pairing is needed. Attribute-only: no template structure, nobindings, no script, no styles.
Proof it can fail
gate-43 table-headers, whole tree, hydra-gates at.github@main(756fe89).The two arms differ only by the attribute:
Whole-tree failing gates 15 → 14; no other gate moved in either direction.
The negative control found something — about the gate
I removed one
scope="col"fromAdminAnalytics.vueand re-ran, expectinggate-43 to report 1 finding. It reported PASS.
That is not a flake. gate-43 is per-table, at-least-one — its own header says
so: "every
<table>in.vuetemplates must have at least one<th>with ascope="col"/scope="row"attribute". So a table with one scoped<th>andnine unscoped ones is green, and nine data columns stay unassociated with a gate
saying the file is fine.
Worth flagging on ConductionNL/.github: the gate counts tables, not header
cells, so its finding count understates the real remediation and it can be
satisfied by a one-attribute change per table. This PR does not take that
shortcut — all 19 headers are scoped, which is strictly more than the gate
requires. I have not touched the gate here; that belongs in its own change with
its own fixture.
Not fixed here, deliberately
The select-all checkbox in
DashboardBulkOperations' first header cell has noaccessible name. That is
gate-40 form-label-association's subject matter, andgate-40 measured 58% false-positive fleet-wide with a remediation that can
override a visible label — so it wants its own change with its own evidence,
not a drive-by in an attribute-only PR.