From 5167dbbc28cdb9236c14fa01f271e8eee413f195 Mon Sep 17 00:00:00 2001 From: kurilova Date: Tue, 27 Oct 2026 12:19:07 +0000 Subject: [PATCH] feat(ui): implement GAR 2027 (WCAG 2.2 AA) accessibility improvements Update components, templates, and styles across modules/ui to satisfy GAR 2027 accessibility requirements: - Accessibility Labels & Relationships (P0): - Fix broken aria-labelledby reference and associate description text via aria-describedby in settings-dropdown. - Fix uninterpolated aria-label expression on delete-report button. - Add role="button" and contextual aria-label host bindings to app-download-report-zip. - Associate multi-select checkbox groups and test module groups with their headings/descriptions using role="group", aria-labelledby, and aria-describedby in dynamic-form, device-qualification-from, and device-tests; remove overriding aria-label on required field errors. - Fix copy-pasted mat-sort-header sortActionDescription values and add accessible labels to icon-only table header columns in certificates-table and testrun-table. - Add aria-label attributes to start/end date range inputs in filter-dialog. - Add semantic list roles (role="list", role="listitem"), region labels, and contextual action menu labels ("Actions for ") in list-layout and list-item. - Screen Reader Updates, Focus & Keyboard Navigation (P0): - Change testrun-status-card live region from assertive alert to polite status and deduplicate countdown timer live announcements. - Add visible :focus-within outline and >= 3:1 border contrast to the custom report search input. - Add Escape key dismissal and aria-haspopup/aria-expanded attributes to download-options and help-tip popups, and Space key activation to role="button" links in consent-dialog. - Clear Purpose, Instructions, Text Alternatives & Headings (P1): - Replace vague "here" link text with descriptive "Google Terms of Service" and underline inline links so they do not rely on color alone. - Add persistent mat-hint format instructions for device form fields and date range inputs. - Hide decorative icons/illustrations from assistive tech (alt="", aria-hidden="true") in empty-message, side-button-menu, and profile-item. - Add role="heading" aria-level="2" to modal dialog titles. - Target Size, Contrast, Text Spacing & Responsive Reflow (P2): - Expand stepper indicator hit area to >= 24x24px, increase inactive border contrast, and expose aria-current="step". - Add tooltips for truncated report metadata values and replace fixed heights and hidden overflow with min-height and responsive reflow rules. --- modules/ui/src/app/app.component.scss | 2 +- .../device-item/device-item.component.scss | 2 +- .../device-tests/device-tests.component.html | 11 ++++++++--- .../download-report-zip.component.spec.ts | 7 ++++++- .../download-report-zip.component.ts | 10 ++++++++++ .../download-zip-modal.component.html | 6 ++++-- .../dynamic-form/dynamic-form.component.html | 11 +++++++---- .../empty-message/empty-message.component.html | 2 +- .../components/help-tip/help-tip.component.ts | 5 +++++ .../list-item/list-item.component.html | 5 +++-- .../components/list-item/list-item.component.ts | 12 ++++++++++++ .../list-layout/list-layout.component.html | 9 ++++++--- .../list-layout/list-layout.component.scss | 13 ++++++++++--- .../side-button-menu.component.html | 3 ++- .../simple-dialog/simple-dialog.component.html | 6 ++++-- .../components/stepper/stepper.component.html | 3 ++- .../components/stepper/stepper.component.scss | 8 +++++++- .../consent-dialog.component.html | 2 ++ .../certificates-table.component.html | 6 +++--- .../device-qualification-from.component.html | 9 +++++++-- .../settings-dropdown.component.html | 5 +++-- .../general-settings.component.html | 5 ++--- .../delete-report/delete-report.component.html | 2 +- .../delete-report.component.spec.ts | 5 ++++- .../filter-dialog/filter-dialog.component.html | 3 +++ .../components/search/search.component.scss | 8 +++++++- .../app/pages/reports/reports.component.html | 17 +++++++++++++---- .../profile-item/profile-item.component.html | 4 +--- .../download-options.component.html | 2 ++ .../download-options.component.spec.ts | 8 ++++++++ .../download-options.component.ts | 2 +- .../test-result-dialog.component.html | 4 +++- .../testrun-initiate-form.component.html | 4 +++- .../testrun-status-card.component.html | 4 ++-- .../testrun-table/testrun-table.component.html | 8 ++++++-- .../components/timer/timer.component.html | 7 ++----- modules/ui/src/styles.scss | 2 +- 37 files changed, 163 insertions(+), 59 deletions(-) diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss index 3af857820..98e3a8f07 100644 --- a/modules/ui/src/app/app.component.scss +++ b/modules/ui/src/app/app.component.scss @@ -209,7 +209,7 @@ $nav-width: 96px; position: relative; display: grid; grid-template-rows: auto 0 1fr; - overflow: hidden; + overflow: auto; } .settings-drawer { diff --git a/modules/ui/src/app/components/device-item/device-item.component.scss b/modules/ui/src/app/components/device-item/device-item.component.scss index 7ea530c0b..27ea054cc 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.scss +++ b/modules/ui/src/app/components/device-item/device-item.component.scss @@ -35,7 +35,7 @@ $border-radius: 12px; .device-item-basic { padding: 0 24px 0 32px; width: variables.$device-item-width; - height: 92px; + min-height: 92px; box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.html b/modules/ui/src/app/components/device-tests/device-tests.component.html index 5818cc44d..fd9644ee4 100644 --- a/modules/ui/src/app/components/device-tests/device-tests.component.html +++ b/modules/ui/src/app/components/device-tests/device-tests.component.html @@ -14,11 +14,16 @@ limitations under the License. -->
-

Test modules

-

+

Test modules

+

All modules are required to pass for qualification

-
+
@for (test of test_modules.controls; track test; let i = $index) {

diff --git a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts index 4965779de..b03d3d9f6 100644 --- a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts +++ b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts @@ -87,12 +87,17 @@ describe('DownloadReportZipComponent', () => { })); }); - it('should have title', () => { + it('should have title and accessible role and label', () => { component.ngOnInit(); + fixture.detectChanges(); expect(component.tooltip.message).toEqual( 'Download zip for Testrun # Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20' ); + expect(compiled.getAttribute('role')).toEqual('button'); + expect(compiled.getAttribute('aria-label')).toEqual( + 'Download zip for Testrun # Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20' + ); }); }); diff --git a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts index 5460b087e..48afc8af5 100644 --- a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts +++ b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts @@ -77,6 +77,16 @@ export class DownloadReportZipComponent @HostBinding('tabIndex') readonly tabIndex = 0; + @HostBinding('attr.role') + readonly role = 'button'; + + @HostBinding('attr.aria-label') + get ariaLabel(): string { + return this.data + ? `Download zip for Testrun # ${this.getTestRunId(this.data)}` + : 'Download ZIP file'; + } + @HostListener('mouseenter') @HostListener('keyup') onEvent(): void { diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html index 914b03916..9ca9ec2b8 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html @@ -15,7 +15,7 @@ --> @if (data.testrunStatus) {

-

+

{{ data.testrunStatus.device.manufacturer }} {{ data.testrunStatus.device.model }} {{ data.testrunStatus.device.firmware }} @@ -43,7 +43,9 @@

} @if (!data.testrunStatus) { - Download ZIP file + Download ZIP file } @if (profiles.length === 1) {

diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html index 708d8f42c..7791bfd0c 100644 --- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html @@ -37,7 +37,10 @@ let-description="description" let-options="options" let-formControlName="formControlName"> -

@for (option of options; track option; let i = $index) {

@@ -280,9 +285,7 @@ } @if (getControl(formControlName).hasError('required')) { - - The field is required - + The field is required } diff --git a/modules/ui/src/app/components/empty-message/empty-message.component.html b/modules/ui/src/app/components/empty-message/empty-message.component.html index ad15f66f0..a479c970a 100644 --- a/modules/ui/src/app/components/empty-message/empty-message.component.html +++ b/modules/ui/src/app/components/empty-message/empty-message.component.html @@ -17,7 +17,7 @@ class="empty-message" [ngClass]="isHorizontal() ? 'horizontal' : 'vertical'">

- empty message image +
{{ header() }} diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.ts b/modules/ui/src/app/components/help-tip/help-tip.component.ts index c19680bf7..109787ce4 100644 --- a/modules/ui/src/app/components/help-tip/help-tip.component.ts +++ b/modules/ui/src/app/components/help-tip/help-tip.component.ts @@ -52,6 +52,11 @@ export class HelpTipComponent implements OnInit { this.updateTipPosition(this.target()); } + @HostListener('keydown.escape') + onEscape() { + this.onCLoseTip.emit(true); + } + ngOnInit() { this.updateTipPosition(this.target()); this.liveAnnouncer.announce( diff --git a/modules/ui/src/app/components/list-item/list-item.component.html b/modules/ui/src/app/components/list-item/list-item.component.html index 91d79acb9..2fb07a65e 100644 --- a/modules/ui/src/app/components/list-item/list-item.component.html +++ b/modules/ui/src/app/components/list-item/list-item.component.html @@ -15,6 +15,7 @@ -->
@@ -23,8 +24,8 @@ [disabled]="disabled" fontSet="material-symbols-outlined" mat-icon-button - aria-label="Actions" - matTooltip="Actions" + [attr.aria-label]="actionsLabel" + [matTooltip]="actionsLabel" [matMenuTriggerFor]="menu"> more_vert diff --git a/modules/ui/src/app/components/list-item/list-item.component.ts b/modules/ui/src/app/components/list-item/list-item.component.ts index 78169b17b..ff2d2ec69 100644 --- a/modules/ui/src/app/components/list-item/list-item.component.ts +++ b/modules/ui/src/app/components/list-item/list-item.component.ts @@ -64,6 +64,18 @@ export class ListItemComponent implements OnInit { return false; } + get actionsLabel(): string { + const entity = this.entity() as { + name?: string; + manufacturer?: string; + model?: string; + }; + const itemName = + entity?.name || + [entity?.manufacturer, entity?.model].filter(Boolean).join(' ').trim(); + return itemName ? `Actions for ${itemName}` : 'Actions'; + } + @HostListener('mouseenter', ['$event']) onEvent(event: MouseEvent): void { this.zone.run(() => { diff --git a/modules/ui/src/app/components/list-layout/list-layout.component.html b/modules/ui/src/app/components/list-layout/list-layout.component.html index 8b6636347..a424d5704 100644 --- a/modules/ui/src/app/components/list-layout/list-layout.component.html +++ b/modules/ui/src/app/components/list-layout/list-layout.component.html @@ -60,9 +60,12 @@
-
+
@if (isOpenEntityForm() && !initialEntity()) { -
+
edit_note {{ title() === LayoutType.Device @@ -92,7 +95,7 @@ @if (content()) { -
+
} diff --git a/modules/ui/src/app/components/list-layout/list-layout.component.scss b/modules/ui/src/app/components/list-layout/list-layout.component.scss index 8010d9efe..376094b19 100644 --- a/modules/ui/src/app/components/list-layout/list-layout.component.scss +++ b/modules/ui/src/app/components/list-layout/list-layout.component.scss @@ -99,7 +99,7 @@ align-self: stretch; border-radius: variables.$corner-extra-large; background-color: colors.$surface-container-high; - height: 40px; + min-height: 40px; width: 100%; input { width: calc(100% - #{variables.$icon-size * 2}); @@ -137,7 +137,7 @@ font-size: 16px; border-radius: 16px; width: fit-content; - height: 56px; + min-height: 56px; color: colors.$on-secondary-container; background-color: colors.$secondary-container; } @@ -145,7 +145,7 @@ .fake-list-item { border-radius: 16px; background-color: colors.$primary-container; - height: 92px; + min-height: 92px; padding: 0 24px 0 32px; display: flex; gap: 24px; @@ -156,3 +156,10 @@ font-size: 16px; letter-spacing: 0; } + +@media (max-width: 768px) { + .layout-container-left-panel { + width: 100%; + max-width: 100vw; + } +} diff --git a/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html index efdc68355..0cfe137c0 100644 --- a/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html +++ b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html @@ -19,8 +19,9 @@ #menu="matMenu" class="side-add-menu" backdropClass="side-add-menu-backdrop"> -
+