Skip to content

fix(a11y): gate-32/34/39/43/45/55 — frontend accessibility fixes - #775

Merged
rubenvdlinde merged 13 commits into
developmentfrom
greenday/procest-fe
Aug 9, 2026
Merged

fix(a11y): gate-32/34/39/43/45/55 — frontend accessibility fixes#775
rubenvdlinde merged 13 commits into
developmentfrom
greenday/procest-fe

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What this is

The frontend half of today's gate burn-down on procest, plus four follow-up fixes found while verifying it. Measured against a fresh clone of ConductionNL/.github@main.

Gate packages used (they moved twice during the work, so both are named):

  • c51a225a2d6531e639b92ba81310a4ae32accad1
  • 651e5c5bb3ba8764903e5d6fc5bac5a208bd67fc (final numbers below)

Measured, full-tree

failing gates
origin/development before all of today's work 22
origin/development now (after #774 + #776) 15
this branch 9

Gate-by-gate on this branch versus the 22-gate baseline — every one verified by me on my own run, not taken on report:

gate before after
gate-32 semantic-controls 40 0
gate-34 window-confirm 3 0
gate-39 button-name 24 0
gate-43 table-headers 27 0
gate-45 prefers-reduced-motion 25 0
gate-55 detail-page-discipline 1 0
gate-46 spec-anchor-existence 15 0

No gate's count went up. stderr from the gate run: 0 bytes.

The four follow-ups

1. l10n — two CI jobs the gate suite cannot see. The new accessible names and dialog copy were new source strings, present in neither l10n/en.json nor l10n/nl.json. l10n coverage (en.json) and quality / Frontend Check (test:l10n) both failed, and a Dutch-locale user would have seen raw English keys. en.json extracted with the repo's own test:l10n:write; the 14 Dutch strings written by hand. Remove guard is Voorwaarde verwijderen, not a literal bewaker — in the transition editor a guard is a precondition, not a person.

2. gate-46 — a @spec target that has not existed for months. All 36 signalering-widgets tags pointed at openspec/changes/retrofit-2026-05-24-signalering-widgets/tasks.md, a change directory since archived to openspec/changes/archive/2026-05-31-…. Retargeted at openspec/specs/signalering-widgets/spec.md, which exists. 30 of the 36 predate this branch; the a11y pass added 6 by copying the surrounding docblock, and diff-scoping made all of them this PR's to answer for — CI reported exactly that, 10 findings from 1 target.

⚠️ 48 other @spec targets under src/ are also missing — mostly archived retrofit-2026-05-* dirs, plus two naming a canonical spec that was never written (openspec/specs/process-mining-bottlenecks/spec.md, openspec/specs/realtime-updates-ui/spec.md). They are diff-scoped out today and will surface on the next PR touching each file. Separate sweep; the two missing specs need writing, not retargeting.

3. x-external-register — removed, then reinstated correctly. #776 added this key beside three cross-register $refs as a gate-54 fix. Measured against package 651e5c5, it was inert in both directions — nothing under openregister/lib/ read it, and check_relation_dialect.py had no external-register branch — so I removed it.

.github#286 then landed, adding exactly that notion (_is_external_ref(), present at 365fa31, absent at 651e5c5). So the key is reinstated, in the form the checker actually accepts: x-external-register: <app> on the property, bare identifier, and no $ref — a property declaring both is still reported, because OpenRegister resolves $ref within one register set and cannot reach another app's schema.

case.decisions deliberately does not get the key: decision is one of procest's own 85 schemas and resolves locally, so it is not a cross-app reference and the key there would now create a finding. "Names another app" is not the same as "not resolvable here".

gate-54: 2 → 0, proven both directions with the same checker — putting the $ref back reproduces $ref 'vergunningaanvraag' does not resolve to a schema key in the register set (case-exact); restoring returns it to 0.

4. 🐛 BesluitvormingLeafTab was registered but never placed — a real lost surface. This is the one that matters.

#776's gate-53 note states the standalone Besluitvorming nav (Voorstellen / Advice / Agenda) was "replaced with a BesluitvormingLeafTab sidebar tab on CaseDetail" and that the replacement is "live, wired, and menu-reachable". Measured:

  • src/components/tabs/BesluitvormingLeafTab.vue exists.
  • src/registry.js:412 registers it, in the same shape as CaseNotesTab.
  • CaseDetail.config.sidebar.tabs listed audit, version-history, notes, sharing, emailand nothing else.

The component was resolvable by name and rendered by nobody. consume-decidesk-besluitvorming-leaf removed three menu entries and registered a tab it never placed, so users lost the nav and gained no replacement. Being in the registry is availability; the manifest decides placement, and it did not mention this tab.

Now wired. The component's own header says it is meant to be surfaced through the sidebarTabs[].component path — exactly this wiring — and it already renders a quiet unavailable notice when decidesk is absent rather than a broken tab. Gavel is registered in src/icons.js, so gate-60 stays green (28 manifests, 0 failures) and check:manifest passes with Ajv 0 errors.

This does not move gate-53 — still 8 findings, because that gate walks the static .menu tree and a per-object sidebar tab is not in it. The gate is what led me to look; the lost surface is the point.

Verification

  • a11y fixes are real, not attribute-only. 34 role="button" additions are matched by 68 @keydown additions — enter and space per element, each bound to the same handler as the click.
  • gate-34 — no window.confirm( remains in DeelzaakList.vue, KlachtcategorieenTab.vue or ChecklistsTab.vue; they use the shared CnConfirmDialog, so gate-13 modal-isolation stays green.
  • ajv proven by absolute path, not by exit code: require.resolve('ajv')…/apps-extra/procest/node_modules/ajv/dist/ajv.js (8.20.0), procest's own dependency. gate-53's findings are a real measurement, and check:manifest logs Ajv validation: PASS.

What stays red, and why

gate-19 (1161), gate-25 (128), gate-26 (164), gate-40 (193) are coverage programmes, out of scope here. Beyond those:

  • gate-3 (3) — false positive, filed .github#299. GuardEvaluatorInterface::evaluate() mandates $userId; RoleGuard uses it 4×, MandaatGuard 2×, and the three precondition guards cannot drop a parameter their interface requires. Every local "fix" is worse than the finding.
  • gate-53 (8) — documented in _meta.removalsCoverageNote. The note suppresses nothing: check_manifest_crossref.js skips _meta keys, so all 8 still report. Two of its three claims are now verified — Cases.config.folderSidebar and Cases.config.viewModes: [… "map"] both exist; the third is what item 4 above fixes.
  • gate-54 — now 0. .github#286 landed mid-work and adds _is_external_ref() to the relation-dialect checker (present at 365fa31, absent at 651e5c5). caseType.productsOrServices and case.vergunningaanvraagRef are now expressed as x-external-register: pipelinq / dso with the bare identifier and no $ref. case.decisions deliberately does not get the key — decision is one of procest's own 85 schemas and resolves locally, so the key there would now create a finding. My earlier .github#300 is closed as superseded.
  • gate-57 (17), gate-62 (1) — analysed in the session report; both need a decision, not a patch.

Not a finding

gate-4 composer-audit FAILs on a diff-scoped local run with curl error 28 … packagist.org … timed out in its log — a network timeout rendered as "FAIL — CVEs or advisories". composer audit --locked on this tree reports no advisories. Filed .github#302.

…ick targets

Non-semantic elements (div/li/a) with @click but no role/tabindex/keyboard
handler are unreachable and inoperable for keyboard and screen-reader users
(WCAG 2.1.1 / 4.1.2). Adds role="button" tabindex="0" plus real
@keydown.enter / @keydown.space handlers that call the same action as the
click handler — not decorative attributes.

Modal/dialog backdrop overlays (click-outside-to-close) get the same
treatment so the dismiss action has a keyboard equivalent.

The six "View all" widget footer links move from a bare <a @click.prevent>
to a real <a :href> pointing at the actual destination URL (computed via
generateUrl), which is both a genuinely better fix (native middle-click /
open-in-new-tab support) and clears the gate since a real href exempts an
<a> from the semantic-controls check.

TaskCreateDialog.vue additionally gets an aria-label on its icon-only close
button (gate-39 button-name, same file).

gate-32: 40 findings -> 0.
…ttons

Icon-only NcButton/button elements with no aria-label/title/text content
announce as just "button" to screen readers (WCAG 2.2 AA SC 4.1.2). Adds a
translated aria-label naming the actual action ("Edit {name}", "Delete
{name}", "Remove guard", "Close step configuration", etc.) rather than a
generic label, using the row's own name field for specificity where one
is available.

gate-39: 24 findings -> 0.
…onfirmDialog

Native window.confirm() calls break Nextcloud theming and are inaccessible
to assistive tech (they bypass the app's dialog stack entirely). Replaces
all three call sites with @conduction/nextcloud-vue's shared CnConfirmDialog
(NcDialog-wrapped, its own file in the design-system package — importing it
does not trigger gate-13 modal-isolation since no <NcModal>/<NcDialog> tag
is written inline in these components).

Each site now opens the dialog on the original trigger, performs the delete
in a `@confirm` handler, and reports success/failure back via setResult()
instead of a bare console.error / silent catch.

These three files also carry their gate-43 table-headers (<th scope=>) and,
for DeelzaakList.vue, gate-45 prefers-reduced-motion fixes — committed
together since they land in the same file.

gate-34: 3 findings -> 0.
<th> without a scope declaration leaves screen readers unable to associate
data cells with their column/row header (WCAG 2.2 AA SC 1.3.1). Adds
scope="col" to header-row <th> cells and scope="row" to the row-header
<th> in LhsMatrixAdmin.vue's severity/behavior matrix table.

ComplaintAnalyticsDashboard.vue's "By category" table had no <th> at all
(rule=table-without-th) — adds a real <thead> naming the two actual
columns (Category, Count) instead of a header-less table.

Self-closed spacer/action <th /> columns (row-actions menus, drag handles)
are left alone — they carry no accessible name so scope= on them is inert,
per the gate's own exemption for headers with no name.

gate-43: 27 findings -> 0.
<style> blocks with a transition/animation and no
@media (prefers-reduced-motion: reduce) override force motion on users who
have asked the OS to minimise it (WCAG 2.2 SC 2.3.3). Adds a media query
per affected selector that sets transition/animation: none — a real
override, not an empty block satisfying the gate's text match.

Skeleton-loading shimmer keyframe animations and hover/click transitions
both get the treatment. The remaining ~17 files flagged by this gate had
their reduced-motion fix land in an earlier commit because they were also
touched for gate-32/39/43 in the same file.

gate-45: 25 findings -> 0.
… icon

AiOversightDetail's data widget declared icon "RobotOutline", which is not
in the shared icon registry (nextcloud-vue CnWidgetGrid/widgetIcons.js) and
renders the '?' fallback per ADR-062 rule 8. Swaps it for "ShieldCheckOutline"
— already in the registry and semantically fitting for an AI-oversight /
human-review audit entry (EU AI Act Art. 14 evidence).

gate-55: 1 finding -> 0.

The gate-16 spec-coverage regression this branch introduced (8 new frontend
methods/computed properties added while fixing gate-34/32 lacked @SPEC tags)
was fixed in the earlier gate-32/gate-34 commits by giving each new method
the same @SPEC tag as its sibling methods in the same file.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 2535748

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 15:30 UTC

Download the full PDF report from the workflow artifacts.

The accessible names and the CnConfirmDialog copy added for gate-39/gate-34
were new source strings, and neither l10n file had them. That failed two CI
jobs the gate suite does not see — 'l10n coverage (en.json)' and
'quality / Frontend Check (test:l10n)' — and would have shipped raw English
keys to a Dutch-locale user.

en.json extracted with the repo's own 'test:l10n:write'; nl.json translated by
hand. 'Remove guard' is 'Voorwaarde verwijderen' rather than a literal
'bewaker' — in the transition editor a guard is a precondition on the
transition, not a person.

node tests/l10n/check-l10n.js now reports both halves OK; before this commit it
reported 14 keys missing from en.json and then 4 missing Dutch translations.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ ff43c9b

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 15:42 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ fdbc718

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

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

Download the full PDF report from the workflow artifacts.

…l spec

All 36 tags pointed at openspec/changes/retrofit-2026-05-24-signalering-widgets/tasks.md,
a change directory that was archived to
openspec/changes/archive/2026-05-31-retrofit-2026-05-24-signalering-widgets.
The target has not existed for months, which is why gate-46 reports them as
unresolved. Retargeted at openspec/specs/signalering-widgets/spec.md — the
canonical home, which exists — per the rule that a @SPEC tag names a spec, never
a change dir.

30 of the 36 predate this branch; the a11y pass added 6 more by copying the
surrounding docblock. Diff-scoping means touching these files makes all of them
this PR's to answer for, and CI reported exactly that: 10 unresolved findings
from 1 distinct target.

Measured, not assumed: after this commit no @SPEC target under src/ named
signalering-widgets is missing from disk.

⚠️ NOT fixed here, and not this PR's: 48 OTHER @SPEC targets under src/ are also
missing — mostly archived retrofit-2026-05-* change dirs, plus two that name a
canonical spec which was never written
(openspec/specs/process-mining-bottlenecks/spec.md,
openspec/specs/realtime-updates-ui/spec.md). They are diff-scoped out today and
will surface on the next PR that touches each file. That is a separate sweep,
and the two missing canonical specs need writing, not retargeting.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 8725492

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-09 19:57 UTC

Download the full PDF report from the workflow artifacts.

PR #776 added "x-external-register" next to three cross-register $refs as part
of a gate-54 fix. Measured, it is inert in both directions:

  - OpenRegister does not read it: no occurrence of x-external-register
    anywhere under openregister/lib/.
  - gate-54 does not read it either: check_relation_dialect.py resolves a $ref
    against _global_schema_keys(), the union of schema KEYS and declared SLUGS
    across *register*.json and register.d/*.json in the same settings dir. There
    is no external-register branch.

Proof it changed nothing: gate-54 reports the same 2 findings with and without
the key.

The 'decision' $ref in that commit was a REAL fix and is untouched — "Decision"
-> "decision" resolves against decidesk's declared slug. Only the no-op key is
removed; an unread key introduced under a 'fix(relations)' subject reads like a
mechanism and is not one.

The remaining 2 findings STAY RED, honestly:
caseType.productsOrServices -> 'product' (Pipelinq's product register) and
case.vergunningaanvraagRef -> 'vergunningaanvraag' (the DSO register) both name
schemas owned by another app. Neither is in procest's 85-schema register set, so
the gate cannot resolve them and has no dialect that would let it. Both
properties are live — 6 and 7 code references respectively — so deleting them
would break working relations, and adding foreign schemas to procest's own
register would misstate ownership. Each property's description already documents
the owning register. This needs a cross-register $ref dialect in the gate, not a
change here.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 9ec2e13

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 20:09 UTC

Download the full PDF report from the workflow artifacts.

…laced

Found while verifying the gate-53 rationale merged in #776, which states that
the standalone Besluitvorming nav (Voorstellen / Advice / Agenda) was 'replaced
with a BesluitvormingLeafTab sidebar tab on CaseDetail' and that the replacement
is 'live, wired, and menu-reachable'. The first half was true; the second was
not.

Measured:
  - src/components/tabs/BesluitvormingLeafTab.vue exists.
  - src/registry.js:412 registers it, in the same shape as CaseNotesTab.
  - CaseDetail.config.sidebar.tabs listed audit, version-history, notes,
    sharing, email — and nothing else.

So the component was resolvable by name and rendered by nobody. The
consume-decidesk-besluitvorming-leaf change removed three menu entries and
registered a tab it never placed: users lost the nav and gained no replacement.
Being in the registry is availability, not placement — the manifest decides what
renders, and it did not mention this tab.

The component's own header says it is meant to be surfaced 'through the
 path', which is exactly the wiring added here, and it
already handles decidesk being absent by rendering a quiet unavailable notice
rather than a broken tab. Icon 'Gavel' is registered in src/icons.js, so gate-60
stays green (28 manifests, 0 failures).

This does NOT move gate-53: still the same 8 findings, because that gate walks
the static .menu tree and a per-object sidebar tab is not in it. The point is the
lost surface, not the gate — the gate is what led me to look.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 4b37f81

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 20:20 UTC

Download the full PDF report from the workflow artifacts.

…register dialect (gate-54 2 -> 0)

.github#286 landed after my earlier commit and adds exactly the notion that was
missing: check_relation_dialect.py now has _is_external_ref(), so a property
owned by another app's register is expressible.

Verified against a fresh clone of ConductionNL/.github@main, package
365fa31d09a26f980e6dc76cb0800575ef005a4e — `_is_external_ref` exists at
lib/check_relation_dialect.py:341. It did NOT exist at 651e5c5, which is why my
previous commit removed the key as inert. That measurement was correct at the
time and is now superseded.

The dialect is `x-external-register: <app>` on the property, carrying the BARE
identifier, and NO $ref — the checker still reports a property that declares
both, because OpenRegister resolves $ref within one register set and can never
reach another app's schema:

  caseType.productsOrServices  x-external-register: pipelinq, items lose $ref
  case.vergunningaanvraagRef   x-external-register: dso,      loses $ref

case.decisions is deliberately NOT given the key: `decision` is one of procest's
own 85 schemas and resolves locally, so it is not a cross-app reference. Adding
the key there would now be a finding rather than a fix.

Bidirectional proof, same checker and invocation both ways:
  as committed                              -> 0 findings
  $ref put back on vergunningaanvraagRef    -> "$ref 'vergunningaanvraag' does
                                               not resolve to a schema key in
                                               the register set (case-exact)"
  restored                                  -> 0 findings

Both properties stay live — 6 and 7 code references — and their descriptions
already name the owning register.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ bf8b963

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 20:33 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ ca7d0ef

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
composer ✅ 100/100
npm ✅ 552/552
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-09 21:07 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Merging over a red Hydra Gates — the evidence

CI reports quality / Hydra Gates FAIL on this PR and PASS on development. That comparison is an artefact, not a regression, and here is the measurement.

CI's Hydra Gates is diff-scoped. On a mainline push the diff is near-empty, so development's green is a green over almost nothing. Measured full-tree with the same package (365fa31 / 651e5c5), from a fresh clone of ConductionNL/.github@main:

CI Hydra Gates measured full-tree
development PASS 15 gates failing
this branch 2 gates 8 gates failing

The two gates CI names add nothing. Same package, full-tree, both sides:

gate development this branch
gate-40 form-label-association 193 193
gate-53 effective-manifest-crossref 8 8

Zero added by this PR. They surface here only because diff-scoping makes pre-existing debt in a touched file that PR's to answer for.

  • gate-40 is the explicitly out-of-scope gate for this sweep — ~58% false positives and unclosable as written.
  • gate-53's 8 are deliberate and documented in _meta.removalsCoverageNote. That note suppresses nothing: check_manifest_crossref.js skips _meta keys, so all 8 still report. Two of its three claims are independently verified (Cases.config.folderSidebar, Cases.config.viewModes including map); the third is what the BesluitvormingLeafTab commit in this PR actually fixes.

E2E Tests (Playwright) fails on development too, with the same pre-existing failures (case-types-tabs, spec-coverage/admin-settings, spec-coverage/pdok-via-openconnector). The PDOK ones are the known openconnector adapter 404 (openconnector#1197). Quality Report is a pure aggregator.

Net: development goes from 15 failing gates to 8, with no gate's count increasing, plus a real UI regression fixed (BesluitvormingLeafTab was registered but never placed).

@rubenvdlinde
rubenvdlinde merged commit ad23d98 into development Aug 9, 2026
29 of 32 checks passed
@rubenvdlinde
rubenvdlinde deleted the greenday/procest-fe branch August 9, 2026 21:10
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