From c8a8733381885ab9d1676f339b844b95a58843d4 Mon Sep 17 00:00:00 2001 From: Benjamin Hofer Date: Tue, 14 Jul 2026 13:37:06 +0200 Subject: [PATCH 1/2] feat(ui): add screenreader accessibility groundwork (WCAG 2.2 AA) Make the CDS UI perceivable and operable with screenreaders (NVDA) and keyboard, as a strictly non-visual retrofit: default rendering is pixel identical; only attributes, announcements and tooling change. - document language, page-title announcements on navigation (cdk LiveAnnouncer), landmark roles (banner/navigation/main), skip link - form labels programmatically associated (nzFor + ids) across all views; aria-label fallbacks for placeholder-only inputs - accessible names for icon-only buttons; alt texts; status icons expose their state as text (aria-label "Status: ...") - keyboard/AT retrofit for non-interactive click targets via a new appClickable directive (role=button, tabindex, Enter/Space) - shared tabs expose APG tab semantics (tablist/tab, aria-selected, arrow-key navigation); data-table gains aria-sort + keyboard sort - run logs: step headers as disclosures (aria-expanded), role=log on streaming containers; run status changes announced politely - workflow graph (libs/workflow-graph): focusable container with arrow-key navigation moving real DOM focus, per-node accessible names, selection announcements, +/-/0 zoom keys; key handling no longer intercepts arrows while typing in unrelated inputs - toasts announced via ToastService (polite/assertive); nz-spin and banners exposed as role=status; Monaco editors labelled - tooling: @angular-eslint/template accessibility rules enabled as warnings (ratchet baseline, 0 errors); conventions documented in ui/docs/accessibility.md; audit and phased plan with remaining visual-impact items in ui/docs/accessibility-plan.md Co-Authored-By: Claude Fable 5 Signed-off-by: Benjamin Hofer --- ui/.eslintrc.js | 16 +- ui/docs/accessibility-plan.md | 267 ++++++++++++++++++ ui/docs/accessibility.md | 47 +++ .../workflow-graph/src/lib/graph.component.ts | 98 +++++++ ui/libs/workflow-graph/src/lib/graph.html | 11 +- ui/libs/workflow-graph/src/lib/graph.lib.ts | 38 +++ .../src/lib/node/fork-join-node.html | 2 +- .../src/lib/node/job-node.component.ts | 25 +- .../workflow-graph/src/lib/node/job-node.html | 6 +- .../src/lib/node/matrix-node.component.ts | 28 +- .../src/lib/node/matrix-node.html | 9 +- .../src/lib/node/stage-node.html | 5 +- ui/src/app/app.component.html | 17 +- ui/src/app/app.component.ts | 11 + ui/src/app/shared/action/action.html | 24 +- .../shared/action/step/form/step.form.html | 2 +- ui/src/app/shared/action/step/step.html | 17 +- .../ascode/save-form/ascode.save-form.html | 8 +- .../shared/button/upload/upload.button.html | 2 +- ui/src/app/shared/card/card.html | 2 +- ui/src/app/shared/chart/chart.html | 7 +- ui/src/app/shared/conditions/conditions.html | 8 +- ui/src/app/shared/diff/item/diff.item.html | 6 +- .../shared/directives/clickable.directive.ts | 35 +++ .../favorite-button.component.html | 7 +- .../input/autofocus/autofocus.input.html | 4 +- .../app/shared/input/editor/editor.input.html | 2 +- ui/src/app/shared/input/input-filter.html | 2 +- ui/src/app/shared/keys/form/keys.form.html | 4 +- ui/src/app/shared/keys/list/keys.list.html | 4 +- .../labels/edit/labels.edit.component.html | 14 +- .../shared/parameter/form/parameter.form.html | 10 +- .../app/shared/parameter/list/parameter.html | 8 +- .../parameter/value/parameter.value.html | 16 +- .../permission/form/permission.form.html | 8 +- .../form/workflow-permission.form.html | 6 +- .../permission/list/permission.list.html | 6 +- .../repository-ref-select.html | 6 +- .../requirements/form/requirements.form.html | 14 +- .../value/requirements.value.html | 22 +- .../resizable-panel/resizable-panel.html | 4 +- ui/src/app/shared/scrollview/scrollview.html | 2 +- ui/src/app/shared/searchable/searchable.html | 4 +- ui/src/app/shared/shared.module.ts | 3 + ui/src/app/shared/status/status.icon.html | 4 +- .../app/shared/table/data-table.component.ts | 8 + ui/src/app/shared/table/data-table.html | 27 +- ui/src/app/shared/tabs/tab.html | 2 +- ui/src/app/shared/tabs/tabs.component.ts | 31 ++ ui/src/app/shared/tabs/tabs.html | 12 +- ui/src/app/shared/toast/ToastService.ts | 6 + .../shared/variable/form/variable.form.html | 6 +- ui/src/app/shared/variable/list/variable.html | 10 +- .../shared/variable/value/variable.value.html | 8 +- ui/src/app/shared/vcs/vcs.strategy.html | 20 +- .../workflow-template.apply-form.html | 4 +- .../workflow-template.apply-modal.html | 4 +- .../workflow-template.param-form.html | 4 +- .../menu/edit-hook/menu.edit.hook.html | 6 +- .../menu/edit-node/menu.edit.node.html | 24 +- .../hook/details/hook.details.component.html | 26 +- .../workflow/node/run/node.run.param.html | 14 +- .../workflow.sidebar.run.component.html | 6 +- .../wizard/context/wizard.context.html | 4 +- .../workflow/wizard/hook/hook.form.html | 15 +- .../workflow/wizard/input/wizard.input.html | 1 + .../workflow/wizard/node-add/node.wizard.html | 12 +- .../outgoinghook/wizard.outgoinghook.html | 14 +- .../app/shared/workflow/wnode/hook/hook.html | 2 +- .../shared/workflow/wnode/join/node.join.html | 2 +- .../wnode/pipeline/node.pipeline.html | 6 +- .../shared/workflow/wnode/workflow.node.html | 2 +- .../admin/hook-task/show/hook-task.show.html | 16 +- .../admin/service/list/service.list.html | 12 +- .../form/worker-model-pattern.form.html | 48 ++-- .../application/add/application.add.html | 14 +- .../show/admin/application.admin.html | 10 +- .../deployment/application.deployment.html | 20 +- .../admin/repository/application.repo.html | 10 +- .../views/application/show/application.html | 2 +- .../app/views/auth/ask-reset/ask-reset.html | 6 +- ui/src/app/views/auth/callback/callback.html | 11 +- ui/src/app/views/auth/reset/reset.html | 4 +- ui/src/app/views/auth/signin/signin.html | 40 +-- ui/src/app/views/auth/verify/verify.html | 4 +- .../environment/add/environment.add.html | 4 +- .../show/advanced/environment.advanced.html | 12 +- .../environment/show/environment.show.html | 2 +- ui/src/app/views/home/home.html | 2 +- ui/src/app/views/navbar/navbar.html | 32 ++- .../app/views/pipeline/add/pipeline.add.html | 10 +- .../pipeline/show/admin/pipeline.admin.html | 8 +- .../ascode-editor/pipeline.ascode.editor.html | 8 +- .../views/pipeline/show/pipeline.show.html | 2 +- .../show/workflow/pipeline.workflow.html | 20 +- .../stage/form/pipeline.stage.form.html | 4 +- .../project/activity-bar/activity-bar.html | 16 +- ui/src/app/views/project/add/project.add.html | 26 +- .../from/project.repomanager.form.html | 48 ++-- .../list/project.repomanager.list.html | 6 +- .../settings/concurrency/concurrencies.html | 4 +- .../concurrency.form.component.html | 36 +-- .../integrations/project.integrations.html | 36 +-- .../project/settings/keys/project.keys.html | 4 +- .../settings/retention/retention.component.ts | 3 +- .../project/settings/retention/retention.html | 2 +- .../retention/retention.report.component.ts | 3 +- .../settings/retention/retention.report.html | 2 +- .../variablesets/items/variableset.item.html | 16 +- .../settings/variablesets/variablesets.html | 6 +- .../project/settings/webhooks/webhooks.html | 16 +- .../show/application/application.list.html | 2 +- .../show/environment/environment.list.html | 2 +- .../project/show/pipeline/pipeline.list.html | 2 +- ui/src/app/views/project/show/project.html | 4 +- .../workflow/blocs/workflow.list.blocs.html | 22 +- .../project/show/workflow/workflow.list.html | 12 +- .../explore/explore-entity.component.ts | 3 +- .../projectv2/explore/explore-overview.html | 12 +- .../projectv2/explore/explore-repository.html | 20 +- .../projectv2/explore/explore-sidebar.html | 36 +-- .../repository-add/repository-add.html | 4 +- .../projectv2/run-list/run-list-sidebar.html | 12 +- .../views/projectv2/run-list/run-list.html | 38 +-- .../projectv2/run-start/run-gate-inputs.html | 12 +- .../views/projectv2/run-start/run-start.html | 2 +- .../projectv2/run/run-contexts.component.ts | 3 +- .../views/projectv2/run/run-hook.component.ts | 3 +- ui/src/app/views/projectv2/run/run-info.html | 6 +- ui/src/app/views/projectv2/run/run-job.html | 28 +- .../projectv2/run/run-result.component.ts | 3 +- .../app/views/projectv2/run/run-results.html | 18 +- .../projectv2/run/run-sources.component.ts | 3 +- .../views/projectv2/run/run-test.component.ts | 6 +- ui/src/app/views/projectv2/run/run-tests.html | 38 +-- .../app/views/projectv2/run/run.component.ts | 6 + ui/src/app/views/projectv2/run/run.html | 62 ++-- ui/src/app/views/search/search.html | 18 +- .../settings/action/form/action.form.html | 26 +- .../settings/action/help/action.help.html | 2 +- .../settings/action/usage/action.usage.html | 4 +- .../views/settings/group/edit/group.edit.html | 30 +- .../views/settings/queue/queue.component.html | 8 +- .../consumer-create-modal.html | 26 +- .../consumer-create-modal/scope-detail.html | 2 +- .../consumer-details-modal.html | 10 +- .../consumer-display-signin-token.html | 8 +- .../views/settings/user/edit/user.edit.html | 61 ++-- .../worker-model/edit/worker-model.edit.html | 8 +- .../worker-model/form/worker-model.form.html | 130 ++++----- .../worker-model/help/worker-model.help.html | 4 +- .../worker-model/list/worker-model.list.html | 2 +- .../edit/workflow-template.edit.html | 2 +- .../editor/workflow-template.editor.html | 4 +- .../form/workflow-template.form.html | 52 ++-- .../help/workflow-template.help.html | 2 +- .../app/views/workflow/add/workflow.add.html | 22 +- .../views/workflow/graph/workflow.graph.html | 4 +- .../workflow/run/node/pipeline/pipeline.html | 4 +- .../workflow-run-job/workflow-run-job.html | 20 +- .../run/node/summary/run.summary.html | 11 +- .../run/node/test/table/test.table.html | 6 +- .../run/summary/workflow.run.summary.html | 19 +- .../app/views/workflow/run/workflow.run.html | 14 +- .../show/admin/workflow.admin.component.html | 46 +-- .../form/workflow.notifications.form.html | 64 ++--- .../list/workflow.notification.list.html | 10 +- ui/src/app/views/workflow/show/workflow.html | 9 +- .../workflow/sidebar/code/sidebar.code.html | 6 +- ui/src/app/views/workflow/workflow.html | 4 +- ui/src/index.html | 4 +- ui/src/styles.scss | 36 +++ 172 files changed, 1694 insertions(+), 985 deletions(-) create mode 100644 ui/docs/accessibility-plan.md create mode 100644 ui/docs/accessibility.md create mode 100644 ui/src/app/shared/directives/clickable.directive.ts diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 33627410da..353b6a6d60 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -73,7 +73,21 @@ module.exports = { ], "@angular-eslint/template/prefer-control-flow": [ "off" - ] + ], + // Accessibility rules (see ui/docs/accessibility.md). + // Introduced as "warn"; flipped to "error" per rule once the + // corresponding remediation sweep is complete. + "@angular-eslint/template/alt-text": ["warn"], + "@angular-eslint/template/click-events-have-key-events": ["warn"], + "@angular-eslint/template/elements-content": ["warn"], + "@angular-eslint/template/interactive-supports-focus": ["warn"], + "@angular-eslint/template/label-has-associated-control": ["warn"], + "@angular-eslint/template/mouse-events-have-key-events": ["warn"], + "@angular-eslint/template/no-autofocus": ["off"], + "@angular-eslint/template/no-distracting-elements": ["warn"], + "@angular-eslint/template/role-has-required-aria": ["warn"], + "@angular-eslint/template/table-scope": ["warn"], + "@angular-eslint/template/valid-aria": ["warn"] } } ] diff --git a/ui/docs/accessibility-plan.md b/ui/docs/accessibility-plan.md new file mode 100644 index 0000000000..59823ec78b --- /dev/null +++ b/ui/docs/accessibility-plan.md @@ -0,0 +1,267 @@ +# CDS UI — Screenreader Accessibility Plan + +**Goal:** Full screenreader support (NVDA, JAWS, VoiceOver) for the CDS web UI (`ui/`), targeting **WCAG 2.2 Level AA** conformance (the current normative standard, and the baseline required by EN 301 549 / the European Accessibility Act). Keyboard operability is treated as a prerequisite of screenreader support throughout — a control NVDA cannot reach is a control NVDA cannot announce. + +**Hard constraint for the first PR(s): zero visual change.** Sighted users must see pixel-identical rendering after the change. This plan therefore classifies every work item by visual impact (see §3) and defines a first-PR scope (§6) that is strictly non-visual. Visually observable improvements (better focus styles, a graph list view, reorder buttons, component migrations) are explicitly deferred to later, separately reviewable steps. + +**Scope:** `ui/src/**` (Angular 21 app, ng-zorro-antd) and `ui/libs/workflow-graph` (standalone SVG DAG library). Both UI generations are in scope: v1 (`views/workflow`, `views/pipeline`, …) and v2 (`views/projectv2`), with v2 prioritized. + +--- + +## 1. Current state (audit summary) + +An audit of all 217 templates and 460 TypeScript files (2026-07) found: + +### Global / structural +- **Zero hand-authored ARIA** in the entire app: no `aria-*`, no `role=`, no `[attr.aria…]` bindings. All existing semantics come implicitly from ng-zorro components. +- `ui/src/index.html:2` — `` has **no `lang` attribute** (WCAG 3.1.1 failure on every page). +- **No landmark elements** anywhere: the shell (`src/app/app.component.html`) uses `nz-layout`/`nz-header`/`nz-content`, which render generic `
`s. No `
`, `