Skip to content

fix(a11y): label form controls, scope table headers, make KPI tiles real links - #301

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/gates-a11y
Aug 9, 2026
Merged

fix(a11y): label form controls, scope table headers, make KPI tiles real links#301
rubenvdlinde merged 1 commit into
developmentfrom
fix/gates-a11y

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes four accessibility gates, measured at origin/development with a fresh clone of ConductionNL/.github.

[hydra-gates] gate package: 48c88ba1e0d049f8f38538c33e790d3e603c55d0

Before → after

gate before after
gate-32 semantic-controls FAIL — 3 PASS
gate-40 form-label-association FAIL — 7 PASS
gate-43 table-headers FAIL — 10 tables (41 <th>) PASS
gate-44 autocomplete-attr FAIL — 1 PASS

No other gate changed verdict. gate-16 spec-coverage briefly went red on the two methods this PR adds and is green again now that both carry a @spec anchor that resolves (gate-46 confirms).

What was actually wrong

gate-32 — both KPI tiles hung @click on a bare <div>, and MyMandatoryTrainingWidget's "Start" was an <a> with no href, so none of the three was focusable. Rather than bolt role/tabindex/@keydown onto a div, the tiles now pass CnStatsBlock its route prop, which renders a real <router-link>; "Start" became a <router-link> too. Verified against the declared @conduction/nextcloud-vue@2.2.0-vue3.3, which declares emits: ['click'] and renders route as router-link — note the checked-in node_modules had 1.0.0-beta.190 (the Vue 2 line), so reading that copy would have described a different component.

gate-40 — a placeholder is not a label. The two textareas already sitting under a visible <h3> are associated with it via aria-labelledby rather than gaining a second, duplicated visible label; controls with genuinely no visible text get an aria-label naming their row.

gate-43 — all 41 <th> are column headers inside <thead>; each gets scope="col". No table has row headers, so no scope="row" was applied blindly.

gate-44recipient-email is autocomplete="off", not "email". It collects a third party's address (an employer, a receiving school), never the signed-in user's own; suggesting the user's own address there would be wrong. The reasoning is recorded in a comment next to the attribute.

Evidence each gate can still fail

No waivers, no exclude, no .skip, no thresholds. After all four went green, one fix per gate was reverted and the suite re-run — every gate returned to FAIL naming exactly the reverted item:

[gate-32] semantic-controls: FAIL — 1 non-semantic element(s) ...
[gate-40] form-label-association: FAIL — 1 form input(s) ...
[gate-43] table-headers: FAIL — 1 <table>(s) with a <th> missing scope= ...
[gate-44] autocomplete-attr: FAIL — 1 semantic input(s) ...

Then restored, and all four returned to PASS. npx eslint on all 17 touched files: 0 errors (89 pre-existing style/v-html warnings, unchanged by this PR; npm run lint sets no --max-warnings).

…eal links

Closes four accessibility gates measured at origin/development with hydra-gates
48c88ba (gate-32: 3, gate-40: 7, gate-43: 10, gate-44: 1).

gate-32 semantic-controls — the two KPI tiles hung @click on a plain <div> and
MyMandatoryTrainingWidget's "Start" was an <a> with no href, so none of the three
was in the tab order. All three now render a real link: the tiles pass CnStatsBlock
its `route` prop (which renders a <router-link>) instead of `clickable` + a manual
$router.push, and "Start" is a <router-link>. Middle-click and open-in-new-tab now
work, which they never did.

gate-40 form-label-association — 7 controls had only a placeholder, which is not a
label. The two textareas that already sit under a visible <h3> are now associated
with it via aria-labelledby rather than growing a second, duplicate visible label;
the three controls with no visible text (an option row inside a v-for, a per-row
cohort input in a table) get an aria-label naming their row.

gate-43 table-headers — 41 <th> across 10 tables were all column headers in
<thead>; each gets scope="col". No table has row headers.

gate-44 autocomplete-attr — recipient-email is declared autocomplete="off", NOT
"email": it collects a third party's address (an employer, a receiving school),
never the signed-in user's own, so offering the user's own address is the wrong
suggestion. Comment records the reasoning.

No waivers, no excludes, no threshold changes. Each of the four gates was
re-run with one fix reverted and confirmed to return to FAIL naming that exact
item, so none of these passes is a gate that simply stopped looking.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/scholiq @ 3fa1f2f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
composer ✅ 100/100
npm ✅ 750/750
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 14:50 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit e0a8138 into development Aug 9, 2026
30 of 32 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/gates-a11y branch August 9, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant