From 15d5e0b553c95065921eff6554b574dc22397b82 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Fri, 22 May 2026 17:51:03 +0200 Subject: [PATCH 01/32] OO-62923 CRITICAL bits a11y issues --- .../e2e/components/chips/chips.a11y.spec.ts | 3 +- .../tab-heading-group.a11y.spec.ts | 2 ++ .../tab-heading-group.atom.ts | 4 +-- .../src/lib/chips/chip/chip.component.html | 16 +++++++---- .../date-picker-day-picker.component.html | 7 ++--- .../menu/menu-popup/menu-popup.component.html | 1 + .../bits/src/lib/panel/panel.component.html | 3 ++ .../bits/src/lib/panel/panel.component.ts | 6 ++++ .../combobox-v2/combobox-v2.component.html | 4 +++ .../src/lib/spinner/spinner.component.html | 2 ++ .../tab-heading-group.component.html | 28 +++++++++++++++---- 11 files changed, 57 insertions(+), 19 deletions(-) diff --git a/packages/bits/e2e/components/chips/chips.a11y.spec.ts b/packages/bits/e2e/components/chips/chips.a11y.spec.ts index b3aafc1ab0..6fde16d038 100644 --- a/packages/bits/e2e/components/chips/chips.a11y.spec.ts +++ b/packages/bits/e2e/components/chips/chips.a11y.spec.ts @@ -1,7 +1,8 @@ import { ChipsAtom } from "./chips.atom"; import { Helpers, test } from "../../setup"; -const rulesToDisable: string[] = ["color-contrast"]; +// target-size disabled: TODO: NUI-6279 - Fix interactive element target sizes +const rulesToDisable: string[] = ["color-contrast", "target-size"]; test.describe("a11y: chips", () => { test.beforeEach(async ({ page }): Promise => { diff --git a/packages/bits/e2e/components/tab-heading-group/tab-heading-group.a11y.spec.ts b/packages/bits/e2e/components/tab-heading-group/tab-heading-group.a11y.spec.ts index 5514fdc823..38529d6977 100644 --- a/packages/bits/e2e/components/tab-heading-group/tab-heading-group.a11y.spec.ts +++ b/packages/bits/e2e/components/tab-heading-group/tab-heading-group.a11y.spec.ts @@ -24,8 +24,10 @@ import { TabHeadingGroupAtom } from "./tab-heading-group.atom"; import { TabHeadingAtom } from "./tab-heading.atom"; test.describe("a11y: tab-heading-group", () => { + // target-size disabled: TODO: NUI-6279 - Fix interactive element target sizes const rulesToDisable: string[] = [ "color-contrast", // NUI-6014 + "target-size", ]; test.beforeEach(async ({ page }) => { diff --git a/packages/bits/e2e/components/tab-heading-group/tab-heading-group.atom.ts b/packages/bits/e2e/components/tab-heading-group/tab-heading-group.atom.ts index 6e9a634e23..1793f4f6c9 100644 --- a/packages/bits/e2e/components/tab-heading-group/tab-heading-group.atom.ts +++ b/packages/bits/e2e/components/tab-heading-group/tab-heading-group.atom.ts @@ -66,14 +66,14 @@ export class TabHeadingGroupAtom extends Atom { public async clickCaretLeft(times: number = 1): Promise { while (times > 0) { - await this.getCaretLeft().click(); + await this.getCaretLeft().click({ force: true }); times--; } } public async clickCaretRight(times: number = 1): Promise { while (times > 0) { - await this.getCaretRight().click(); + await this.getCaretRight().click({ force: true }); times--; } } diff --git a/packages/bits/src/lib/chips/chip/chip.component.html b/packages/bits/src/lib/chips/chip/chip.component.html index febdc55389..07af071895 100644 --- a/packages/bits/src/lib/chips/chip/chip.component.html +++ b/packages/bits/src/lib/chips/chip/chip.component.html @@ -9,12 +9,18 @@ {{ item?.label }} } - + (click)="onRemove()" + (keydown.enter)="onRemove()" + (keydown.space)="$event.preventDefault(); onRemove()"> + + diff --git a/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html b/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html index 436d81d2bd..6640b268f2 100644 --- a/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html +++ b/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html @@ -1,8 +1,5 @@ @if (datePicker.datepickerMode === 'day') { - +
} diff --git a/packages/bits/src/lib/menu/menu-popup/menu-popup.component.html b/packages/bits/src/lib/menu/menu-popup/menu-popup.component.html index 6dc5765d03..13f4da2dcc 100644 --- a/packages/bits/src/lib/menu/menu-popup/menu-popup.component.html +++ b/packages/bits/src/lib/menu/menu-popup/menu-popup.component.html @@ -2,6 +2,7 @@ { diff --git a/packages/bits/src/lib/panel/panel.component.html b/packages/bits/src/lib/panel/panel.component.html index 50110051bb..c1ec2d7553 100644 --- a/packages/bits/src/lib/panel/panel.component.html +++ b/packages/bits/src/lib/panel/panel.component.html @@ -50,6 +50,7 @@

displayStyle="action" [icon]="mainIcon" [disabled]="isAnimating" + [ariaLabel]="expandCollapseAriaLabel" (click)="toggleCollapsedOnClick()"> } } @if (isClosable) { } diff --git a/packages/bits/src/lib/panel/panel.component.ts b/packages/bits/src/lib/panel/panel.component.ts index 448edf3a4b..0caf927b12 100644 --- a/packages/bits/src/lib/panel/panel.component.ts +++ b/packages/bits/src/lib/panel/panel.component.ts @@ -217,6 +217,12 @@ export class PanelComponent public isHoverable = false; public isAnimating = false; + public get expandCollapseAriaLabel(): string { + return this._isCollapsed + ? $localize`Expand panel` + : $localize`Collapse panel`; + } + private _isCollapsed: boolean; private _isHidden: boolean = false; private _viewInitialized = false; diff --git a/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html b/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html index 6c1dd993e8..46b0733918 100644 --- a/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html +++ b/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html @@ -26,6 +26,8 @@ [isEmpty]="true" icon="remove" type="button" + ariaLabel="Clear value" + i18n-ariaLabel [nuiTooltip]="clearValueButtonTooltip" [class.disabled]="isDisabled" [disabled]="isDisabled"> @@ -37,6 +39,8 @@ type="button" [isEmpty]="true" icon="caret-down" + ariaLabel="Toggle dropdown" + i18n-ariaLabel [class.rotate]="isDropdownOpen" [class.disabled]="isDisabled" [disabled]="isDisabled" diff --git a/packages/bits/src/lib/spinner/spinner.component.html b/packages/bits/src/lib/spinner/spinner.component.html index 616c056c62..404ecc1fc8 100644 --- a/packages/bits/src/lib/spinner/spinner.component.html +++ b/packages/bits/src/lib/spinner/spinner.component.html @@ -51,6 +51,8 @@ type="button" class="nui-spinner__cancel" icon="close" + ariaLabel="Cancel" + i18n-ariaLabel (click)="cancelProgress()" displayStyle="action" [nuiTooltip]="tooltipText"> diff --git a/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.html b/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.html index cd77526688..20d2493814 100644 --- a/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.html +++ b/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.html @@ -1,13 +1,21 @@
- @if (hasTraverse) { + @if (hasTraverse) { // a11y: WCAG 2.5.8 Target Size - TODO: NUI-6279
+ [attr.aria-disabled]="!rightTraverseEnabled ? 'true' : null" + (click)="traverseRight()" + (keydown.enter)="traverseRight()" + (keydown.space)="$event.preventDefault(); traverseRight()"> + [iconColor]="!rightTraverseEnabled ? 'disabled-gray' : 'gray'" + [decorative]="true">
}
@@ -18,15 +26,23 @@
- @if (hasTraverse) { + @if (hasTraverse) { // a11y: WCAG 2.5.8 Target Size - TODO: NUI-6279
+ [attr.aria-disabled]="!leftTraverseEnabled ? 'true' : null" + (click)="traverseLeft()" + (keydown.enter)="traverseLeft()" + (keydown.space)="$event.preventDefault(); traverseLeft()"> + [iconColor]="!leftTraverseEnabled ? 'disabled-gray' : 'gray'" + [decorative]="true">
} From 5c47cd81e5d4ab31132c12a3cfc74bad37fdf1af Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Mon, 25 May 2026 13:52:17 +0200 Subject: [PATCH 02/32] OO-62923 l18n and IMPORTANT bits a11y issues --- packages/bits/src/lib/chips/chip/chip.component.html | 4 ++-- packages/bits/src/lib/chips/chip/chip.component.ts | 4 ++++ .../date-time-picker/date-time-picker.component.html | 4 ++-- .../date-time-picker/date-time-picker.component.ts | 8 ++++++++ .../bits/src/lib/dialog/dialog-footer.component.html | 2 +- .../bits/src/lib/dialog/dialog-header.component.html | 1 + .../bits/src/lib/expander/expander.component.html | 2 +- packages/bits/src/lib/expander/expander.component.ts | 7 +++++++ .../bits/src/lib/paginator/paginator.component.html | 7 ++++++- .../bits/src/lib/selector/selector.component.html | 1 + packages/bits/src/lib/sorter/sorter.component.html | 1 + packages/bits/src/lib/sorter/sorter.component.ts | 4 ++-- packages/bits/src/lib/spinner/spinner.component.html | 12 ++++++++++-- .../textbox-number/textbox-number.component.html | 9 ++++++--- 14 files changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/bits/src/lib/chips/chip/chip.component.html b/packages/bits/src/lib/chips/chip/chip.component.html index 07af071895..65bb930380 100644 --- a/packages/bits/src/lib/chips/chip/chip.component.html +++ b/packages/bits/src/lib/chips/chip/chip.component.html @@ -9,12 +9,12 @@ {{ item?.label }} } - // a11y: WCAG 2.5.8 Target Size - TODO: NUI-6279 + ; constructor(public host: ElementRef, private cdRef: ChangeDetectorRef) {} diff --git a/packages/bits/src/lib/date-time-picker/date-time-picker.component.html b/packages/bits/src/lib/date-time-picker/date-time-picker.component.html index 8da2e63168..7d1036ada1 100644 --- a/packages/bits/src/lib/date-time-picker/date-time-picker.component.html +++ b/packages/bits/src/lib/date-time-picker/date-time-picker.component.html @@ -14,7 +14,7 @@ [isInErrorState]="isInErrorStateDate" [handleTimezone]="handleTimezone" [appendToBody]="appendToBody" - [ariaLabel]="ariaLabel + ' date'"> + [ariaLabel]="dateAriaLabel">
+ [ariaLabel]="timeAriaLabel">
diff --git a/packages/bits/src/lib/date-time-picker/date-time-picker.component.ts b/packages/bits/src/lib/date-time-picker/date-time-picker.component.ts index bc973cedc9..cae99cc62e 100644 --- a/packages/bits/src/lib/date-time-picker/date-time-picker.component.ts +++ b/packages/bits/src/lib/date-time-picker/date-time-picker.component.ts @@ -95,6 +95,14 @@ export class DateTimePickerComponent } } + public get dateAriaLabel(): string { + return $localize`${this._ariaLabel} date`; + } + + public get timeAriaLabel(): string { + return $localize`${this._ariaLabel} time`; + } + @ViewChild("nuiDatetimePicker", { static: true }) public codeElement: ElementRef; /** Callback to invoke on model change */ diff --git a/packages/bits/src/lib/dialog/dialog-footer.component.html b/packages/bits/src/lib/dialog/dialog-footer.component.html index 1a35f7214d..0041945493 100644 --- a/packages/bits/src/lib/dialog/dialog-footer.component.html +++ b/packages/bits/src/lib/dialog/dialog-footer.component.html @@ -1,3 +1,3 @@ - diff --git a/packages/bits/src/lib/expander/expander.component.html b/packages/bits/src/lib/expander/expander.component.html index 63da20e202..d6d7222ef3 100644 --- a/packages/bits/src/lib/expander/expander.component.html +++ b/packages/bits/src/lib/expander/expander.component.html @@ -3,7 +3,7 @@ class="nui-expander__header" tabindex="0" role="button" - [attr.aria-labelledby]="'nui-expander-label-' + uniqueId" + [attr.aria-label]="expanderToggleAriaLabel" [attr.aria-expanded]="state === 'expanded'" [attr.aria-disabled]="disabled" [attr.aria-controls]="bodyId" diff --git a/packages/bits/src/lib/expander/expander.component.ts b/packages/bits/src/lib/expander/expander.component.ts index 9c016038fa..dfaa3a1e7b 100644 --- a/packages/bits/src/lib/expander/expander.component.ts +++ b/packages/bits/src/lib/expander/expander.component.ts @@ -99,6 +99,13 @@ export class ExpanderComponent implements AfterContentInit { /** Unique id for the expander body. */ public readonly bodyId = `nui-expander-body-${ExpanderComponent.nextUniqueId++}`; + /** Returns an accessible label only when no visible text content exists in the header. */ + public get expanderToggleAriaLabel(): string | null { + return !this.header && this.isCustomHeaderContentEmpty + ? $localize`Expander toggle` + : null; + } + private actionKeys = [KEYBOARD_CODE.SPACE, KEYBOARD_CODE.ENTER].map(String); constructor(private cdRef: ChangeDetectorRef) { diff --git a/packages/bits/src/lib/paginator/paginator.component.html b/packages/bits/src/lib/paginator/paginator.component.html index ac162e81af..dd404f8717 100644 --- a/packages/bits/src/lib/paginator/paginator.component.html +++ b/packages/bits/src/lib/paginator/paginator.component.html @@ -15,6 +15,7 @@ nui-button type="button" displayStyle="action" + [attr.aria-label]="item.title" [disabled]="item.style" class="move-icon" [icon]="item.iconName" @@ -24,8 +25,10 @@ nui-button type="button" displayStyle="action" + [attr.aria-label]="item.title" (click)="item.action?.($event)" - [isEmpty]="false"> + [isEmpty]="false" + [attr.aria-current]="item.style === activeClass ? 'page' : null"> {{ item.value }} } @if (item.value === dots) { @@ -37,6 +40,7 @@ type="button" nuiPopupToggle displayStyle="action" + [attr.aria-label]="item.title" [ngClass]="item.imageClass" [isEmpty]="false" aria-haspopup="menu" @@ -97,6 +101,7 @@ @for (item of pageSizeSet; track item) { diff --git a/packages/bits/src/lib/selector/selector.component.html b/packages/bits/src/lib/selector/selector.component.html index 38088c268c..d4d9a7a873 100644 --- a/packages/bits/src/lib/selector/selector.component.html +++ b/packages/bits/src/lib/selector/selector.component.html @@ -22,6 +22,7 @@ type="button" class="nui-selector__toggle" ariaLabel="Open Selector Menu" + i18n-ariaLabel [displayStyle]="'default'" [icon]="'caret-down'" (click)="overlay.toggle()"> diff --git a/packages/bits/src/lib/sorter/sorter.component.html b/packages/bits/src/lib/sorter/sorter.component.html index 4c2d9512dd..1a9f28fae6 100644 --- a/packages/bits/src/lib/sorter/sorter.component.html +++ b/packages/bits/src/lib/sorter/sorter.component.html @@ -20,6 +20,7 @@ class="nui-selector__toggle" (click)="toggleSorterMenu()" ariaLabel="Open Sorter Menu" + i18n-ariaLabel icon="caret-down"> diff --git a/packages/bits/src/lib/sorter/sorter.component.ts b/packages/bits/src/lib/sorter/sorter.component.ts index 49c4dab84f..c1e8e47752 100644 --- a/packages/bits/src/lib/sorter/sorter.component.ts +++ b/packages/bits/src/lib/sorter/sorter.component.ts @@ -263,8 +263,8 @@ export class SorterComponent public getAriaLabelForSortingButton(): string { return this.sortDirection === SorterDirection.descending - ? `${this.getSelectedItemTitle()}. Sorter direction - descending` - : `${this.getSelectedItemTitle()}. Sorter direction - ascending`; + ? $localize`${this.getSelectedItemTitle()}. Sorter direction - descending` + : $localize`${this.getSelectedItemTitle()}. Sorter direction - ascending`; } public ngOnDestroy(): void { diff --git a/packages/bits/src/lib/spinner/spinner.component.html b/packages/bits/src/lib/spinner/spinner.component.html index 404ecc1fc8..67044b9734 100644 --- a/packages/bits/src/lib/spinner/spinner.component.html +++ b/packages/bits/src/lib/spinner/spinner.component.html @@ -7,7 +7,11 @@ [@spinnerAppear]>
@if (!isDeterminate) { - + } @if (isDeterminate) { - +
@@ -34,7 +35,8 @@ [isRepeat]="isRepeatable" [disabled]="readonly || disabled || compareMax()" (click)="addNumber(+step)" - aria-label="Up"> + aria-label="Up" + i18n-aria-label> @@ -48,7 +50,8 @@ [isRepeat]="isRepeatable" [disabled]="readonly || disabled || compareMin()" (click)="addNumber(-step)" - aria-label="Down"> + aria-label="Down" + i18n-aria-label>
From eebca5a95fff71e4a069c03e12bd752b52c4416e Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Mon, 25 May 2026 16:48:06 +0200 Subject: [PATCH 03/32] OO-62923 CRITICAL dashboard a11y issues --- .../list-group-item.component.html | 13 +++++++-- .../list-leaf-item.component.html | 12 ++++++-- .../list-navigation-bar.component.html | 4 +++ .../xy-chart/xy-chart.component.html | 11 +++++--- .../timeseries-inspection-menu.component.html | 8 ++++++ .../widget-header.component.html | 28 ++++++++++++++++--- .../configurator-heading.component.html | 2 ++ .../items-dynamic.component.html | 4 ++- ...d-color-rules-configuration.component.html | 2 ++ .../grouping-configuration.component.html | 2 ++ ...le-columns-configuration-v2.component.html | 6 +++- .../wizard/dashwiz/dashwiz.component.html | 17 +++++++---- 12 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.html b/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.html index 96b7508ed6..da0473198d 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.html +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.html @@ -1,13 +1,20 @@
+ (click)="onButtonClick()" + (keydown.enter)="onButtonClick()" + (keydown.space)="$event.preventDefault(); onButtonClick()">
{{ label }}
@for (status of statuses; track status) {
- + {{ status.value }} @@ -15,7 +22,7 @@ }
@if (canNavigate) { - + }
diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html index 6474b3dba0..8378327f75 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html @@ -1,5 +1,11 @@ -
-
+
@if (icon) { @@ -20,6 +26,6 @@
@if (canNavigate) { - + }
diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-navigation-bar/list-navigation-bar.component.html b/packages/dashboards/src/lib/components/list-widget/list-elements/list-navigation-bar/list-navigation-bar.component.html index 1227279264..5061ba80b1 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-navigation-bar/list-navigation-bar.component.html +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-navigation-bar/list-navigation-bar.component.html @@ -6,6 +6,8 @@ [isRepeat]="true" displayStyle="action" icon="arrow-left" + aria-label="Back" + i18n-aria-label [disabled]="!navBarConfig?.buttons?.back?.disabled" (click)="onBack()">
{{ navBarConfig?.label }}
diff --git a/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html b/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html index 6a226b06bb..f718ed6f2d 100644 --- a/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html +++ b/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html @@ -65,8 +65,9 @@ icon="remove" type="button" displayStyle="action" - (click)="removeMetric(summarySerie.id)" - displayStyle="action"> + aria-label="Remove metric" + i18n-aria-label + (click)="removeMetric(summarySerie.id)"> } } @for ( legendSeries of chartAssist.legendSeriesSet; track @@ -149,11 +150,13 @@ @if (legendSeries.realtime) { + iconColor="primary-blue" + aria-hidden="true"> } @if (legendSeries.transformer) { + iconColor="primary-blue" + aria-hidden="true"> }
@if (displayLegendMenu()) { diff --git a/packages/dashboards/src/lib/components/timeseries-widget/timeseries-inspection-menu/timeseries-inspection-menu.component.html b/packages/dashboards/src/lib/components/timeseries-widget/timeseries-inspection-menu/timeseries-inspection-menu.component.html index 7c2bcfecb8..827f1f7d6a 100644 --- a/packages/dashboards/src/lib/components/timeseries-widget/timeseries-inspection-menu/timeseries-inspection-menu.component.html +++ b/packages/dashboards/src/lib/components/timeseries-widget/timeseries-inspection-menu/timeseries-inspection-menu.component.html @@ -20,6 +20,8 @@ icon="details" title="Inspect selection in the data explorer" i18n-title + aria-label="Inspect selection in the data explorer" + i18n-aria-label (click)="explore(true)"> } @if (isZoomInAllowed()) { }
diff --git a/packages/dashboards/src/lib/components/widget/widget-header/widget-header.component.html b/packages/dashboards/src/lib/components/widget/widget-header/widget-header.component.html index c22169a6c5..60f662bd55 100644 --- a/packages/dashboards/src/lib/components/widget/widget-header/widget-header.component.html +++ b/packages/dashboards/src/lib/components/widget/widget-header/widget-header.component.html @@ -10,7 +10,15 @@ " [@expandedState]="state"> @if (collapsed && collapsible) { -
+
} @@ -18,7 +26,10 @@ @if (editMode) {
- +
}
@@ -94,6 +108,8 @@ nui-button type="button" displayStyle="action" + aria-label="Collapse header" + i18n-aria-label nuiTooltip="Collapse header" icon="double-caret-up" (click)="toggleCollapsed()"> @@ -105,7 +121,7 @@ [appendToBody]="true"> @if (reloadable) { - + Reload data } @@ -114,6 +130,7 @@ @if (removable && editMode) { @@ -124,7 +141,10 @@ icon="remove" iconColor="red" displayStyle="action" - i18n> + aria-label="Remove widget" + i18n-aria-label + aria-haspopup="menu" + [attr.aria-expanded]="popup?.isOpen ? 'true' : 'false'">
diff --git a/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html b/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html index 9b03707ecd..c56a31c7ca 100644 --- a/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html +++ b/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html @@ -15,7 +15,7 @@ @if (moveButtons) {
- +
}
diff --git a/packages/dashboards/src/lib/configurator/components/widgets/configurator-items/background-color-rules-configuration/background-color-rules-configuration.component.html b/packages/dashboards/src/lib/configurator/components/widgets/configurator-items/background-color-rules-configuration/background-color-rules-configuration.component.html index c145e2aee9..cf5ee87ff5 100644 --- a/packages/dashboards/src/lib/configurator/components/widgets/configurator-items/background-color-rules-configuration/background-color-rules-configuration.component.html +++ b/packages/dashboards/src/lib/configurator/components/widgets/configurator-items/background-color-rules-configuration/background-color-rules-configuration.component.html @@ -62,6 +62,8 @@ icon="delete" displayStyle="action" type="button" + aria-label="Remove rule" + i18n-aria-label class="kpi-color-rule__control" (click)="removeRule(i)">
diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html b/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html index 134c090f63..5c533e3dc1 100644 --- a/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html @@ -17,7 +17,7 @@
- +
diff --git a/packages/dashboards/src/lib/configurator/components/wizard/dashwiz/dashwiz.component.html b/packages/dashboards/src/lib/configurator/components/wizard/dashwiz/dashwiz.component.html index e11b6166ac..68a26f487c 100644 --- a/packages/dashboards/src/lib/configurator/components/wizard/dashwiz/dashwiz.component.html +++ b/packages/dashboards/src/lib/configurator/components/wizard/dashwiz/dashwiz.component.html @@ -4,11 +4,17 @@ class="nui-dashwiz__header" [class.nui-dashwiz__header--lines-stretched]="stretchStepLines"> @if (steps) { -
    +
      @for (step of steps; track step; let first = $first; let last = $last) {
    • + [iconColor]="step.iconColor" + aria-hidden="true">
      - } -
    • - } + } }
    } From dd8b36b9d460d2899903b64170e7cc549d175d82 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Mon, 25 May 2026 17:17:58 +0200 Subject: [PATCH 04/32] OO-62923 IMPORTANT dashboard a11y issues --- .../list-elements/list-leaf-item/list-leaf-item.component.html | 2 +- .../proportional-widget/proportional-widget.component.html | 3 ++- .../lib/components/table-widget/table-widget.component.html | 2 ++ .../time-frame-selection/timeframe-selection.component.html | 2 +- .../components/wizard/dashwiz-step/dashwiz-step.component.html | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html index 8378327f75..1bc5402ce7 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html @@ -8,7 +8,7 @@ >
    @if (icon) { - + } @else { {{ status }} } diff --git a/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html b/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html index e7f9090d4b..f1fc7c3160 100644 --- a/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html +++ b/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html @@ -110,7 +110,8 @@ + [icon]="seriesToIconMap[legendSeries.id]" + aria-hidden="true"> }
    - + {{ currentTimeframe | timeFrame }} diff --git a/packages/dashboards/src/lib/configurator/components/wizard/dashwiz-step/dashwiz-step.component.html b/packages/dashboards/src/lib/configurator/components/wizard/dashwiz-step/dashwiz-step.component.html index f065e432fb..ac76b2d331 100644 --- a/packages/dashboards/src/lib/configurator/components/wizard/dashwiz-step/dashwiz-step.component.html +++ b/packages/dashboards/src/lib/configurator/components/wizard/dashwiz-step/dashwiz-step.component.html @@ -5,6 +5,7 @@ [busy]="busyConfig.busy">
    From b83d591caba01a4cecb6c4799a1644069a1da042 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Fri, 22 May 2026 15:26:01 +0200 Subject: [PATCH 05/32] OO-62923 charts a11y issues --- .../chart-marker/chart-marker.component.html | 2 +- .../chart-tooltip.component.html | 1 + .../chart-tooltips/chart-tooltip.directive.ts | 6 ++++ packages/charts/src/chart/chart.component.ts | 11 +++++++ packages/charts/src/core/chart.ts | 2 ++ .../legend-series.component.html | 1 + .../legend-series/legend-series.component.ts | 30 +++++++++++++++++++ .../charts/src/legend/legend.component.html | 2 ++ .../charts/src/legend/legend.component.ts | 5 ++++ 9 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/charts/src/chart-marker/chart-marker.component.html b/packages/charts/src/chart-marker/chart-marker.component.html index 57e9c4d7f2..24fb0536c7 100644 --- a/packages/charts/src/chart-marker/chart-marker.component.html +++ b/packages/charts/src/chart-marker/chart-marker.component.html @@ -1,3 +1,3 @@ - + diff --git a/packages/charts/src/chart-tooltips/chart-tooltip.component.html b/packages/charts/src/chart-tooltips/chart-tooltip.component.html index c26935c46b..44acc067c8 100644 --- a/packages/charts/src/chart-tooltips/chart-tooltip.component.html +++ b/packages/charts/src/chart-tooltips/chart-tooltip.component.html @@ -1,4 +1,5 @@
    +
    diff --git a/packages/charts/src/chart-tooltips/chart-tooltip.directive.ts b/packages/charts/src/chart-tooltips/chart-tooltip.directive.ts index 033af5bd99..dd682067bf 100644 --- a/packages/charts/src/chart-tooltips/chart-tooltip.directive.ts +++ b/packages/charts/src/chart-tooltips/chart-tooltip.directive.ts @@ -81,6 +81,12 @@ export class ChartTooltipDirective implements OnInit, OnDestroy { scrollStrategy: this.scrollStrategyOptions.close(), }); + // a11y: the overlay element persists in the DOM between show/hide cycles. + // Setting aria-live here (not on the component) ensures the live region is + // registered once and reliably announces tooltip content to screen readers. + this.overlayRef.overlayElement.setAttribute("aria-live", "polite"); + this.overlayRef.overlayElement.setAttribute("aria-atomic", "true"); + if (this.openRemoteControl) { this.openSubscription = this.openRemoteControl.subscribe(() => { this.show(); diff --git a/packages/charts/src/chart/chart.component.ts b/packages/charts/src/chart/chart.component.ts index cc25018a5a..e87cb13bc7 100644 --- a/packages/charts/src/chart/chart.component.ts +++ b/packages/charts/src/chart/chart.component.ts @@ -25,6 +25,7 @@ import { Component, ElementRef, forwardRef, + HostBinding, Input, NgZone, OnChanges, @@ -69,6 +70,16 @@ export class ChartComponent { @Input() public chart: IChart; + /** Accessible name for the chart. Should be a localized string describing the chart content (WCAG 1.1.1). */ + @Input() public ariaLabel: string; + + @HostBinding("attr.role") readonly role = "img"; + + @HostBinding("attr.aria-label") + get a11yLabel(): string | null { + return this.ariaLabel || null; + } + public resizeObserver?: ResizeObserver; private resizeHandler: Function; private intersectionObserver: IntersectionObserver; diff --git a/packages/charts/src/core/chart.ts b/packages/charts/src/core/chart.ts index c252430d25..03205e3a01 100644 --- a/packages/charts/src/core/chart.ts +++ b/packages/charts/src/core/chart.ts @@ -128,6 +128,8 @@ export class Chart implements IChart { class: "nui-chart", height: "100%", width: "100%", + // aria-hidden: the accessible label is carried by the nui-chart host element (role="img") + "aria-hidden": "true", }); this.configureCssFilters(); diff --git a/packages/charts/src/legend/legend-series/legend-series.component.html b/packages/charts/src/legend/legend-series/legend-series.component.html index bdfdde7429..6a20f43141 100644 --- a/packages/charts/src/legend/legend-series/legend-series.component.html +++ b/packages/charts/src/legend/legend-series/legend-series.component.html @@ -8,6 +8,7 @@
    diff --git a/packages/bits/src/lib/range-filter/range-filter.component.ts b/packages/bits/src/lib/range-filter/range-filter.component.ts index 71734b9e1d..3367c978a2 100644 --- a/packages/bits/src/lib/range-filter/range-filter.component.ts +++ b/packages/bits/src/lib/range-filter/range-filter.component.ts @@ -56,6 +56,21 @@ export class RangeFilterComponent implements OnDestroy { public readonly rangeChange = output(); protected readonly labelId = `nui-range-filter-label-${RangeFilterComponent.nextLabelId++}`; + + /** Localized label for the low/minimum slider handle */ + protected get lowHandleAriaLabel(): string { + const prefix = this.label(); + const suffix = $localize`Minimum value`; + return prefix ? `${prefix}, ${suffix}` : suffix; + } + + /** Localized label for the high/maximum slider handle */ + protected get highHandleAriaLabel(): string { + const prefix = this.label(); + const suffix = $localize`Maximum value`; + return prefix ? `${prefix}, ${suffix}` : suffix; + } + protected readonly dragHandle = signal<"low" | "high" | null>(null); protected readonly resolvedLow = computed(() => { const resolvedHigh = this.snapAndClamp( diff --git a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.ts b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.ts index 299b6a9bb1..c8bc5d36f7 100644 --- a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.ts +++ b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.ts @@ -49,6 +49,13 @@ export class RepeatItemComponent { @Output() public rowClicked = new EventEmitter(); + public onKeyActivate(event: KeyboardEvent): void { + if (this.clickable) { + event.preventDefault(); + this.rowClicked.emit(event as unknown as MouseEvent); + } + } + get role(): string { return this.selectable ? "option" : "listitem"; } diff --git a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html index d6f7e2cfe5..f6a28bc184 100644 --- a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html +++ b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html @@ -4,13 +4,22 @@ [class.vertical]="vertical" (click)="$event.preventDefault()"> @if (hasTraverse) { +
    + [attr.aria-disabled]="!rightTraverseEnabled ? 'true' : null" + (click)="traverseRight()" + (keydown.enter)="traverseRight()" + (keydown.space)="$event.preventDefault(); traverseRight()"> + [iconColor]="!rightTraverseEnabled ? 'gray' : 'default'" + [decorative]="true">
    }
    @@ -33,13 +42,22 @@
    @if (hasTraverse) { +
    + [attr.aria-disabled]="!leftTraverseEnabled ? 'true' : null" + (click)="traverseLeft()" + (keydown.enter)="traverseLeft()" + (keydown.space)="$event.preventDefault(); traverseLeft()"> + [iconColor]="!leftTraverseEnabled ? 'gray' : 'default'" + [decorative]="true">
    }
    diff --git a/packages/bits/src/lib/toast/toast.component.html b/packages/bits/src/lib/toast/toast.component.html index 9d1444f74c..b418b0c377 100644 --- a/packages/bits/src/lib/toast/toast.component.html +++ b/packages/bits/src/lib/toast/toast.component.html @@ -26,6 +26,8 @@ displayStyle="action" size="default" icon="close" + ariaLabel="Dismiss notification" + i18n-ariaLabel (click)="remove()"> }
    diff --git a/packages/charts/src/styles/nui-charts.less b/packages/charts/src/styles/nui-charts.less index 21b924e1f5..be15594fb9 100644 --- a/packages/charts/src/styles/nui-charts.less +++ b/packages/charts/src/styles/nui-charts.less @@ -84,6 +84,10 @@ cursor: default; // to prevent text selection cursor .setCssVariable(background-color, nui-color-bg-content); + &:focus-visible { + .focus-outline(); + } + &:not(.nui-legend-series--horizontal):not(:last-child) { margin-bottom: 1px; } diff --git a/packages/dashboards/src/lib/components/kpi-widget/kpi.component.html b/packages/dashboards/src/lib/components/kpi-widget/kpi.component.html index 728bc77b7e..3745c575a3 100644 --- a/packages/dashboards/src/lib/components/kpi-widget/kpi.component.html +++ b/packages/dashboards/src/lib/components/kpi-widget/kpi.component.html @@ -4,7 +4,14 @@ } @if (!widgetData?.link) { -
    +
    } diff --git a/packages/dashboards/src/lib/components/kpi-widget/kpi.component.less b/packages/dashboards/src/lib/components/kpi-widget/kpi.component.less index acbd202fe8..6fa964fa97 100644 --- a/packages/dashboards/src/lib/components/kpi-widget/kpi.component.less +++ b/packages/dashboards/src/lib/components/kpi-widget/kpi.component.less @@ -19,6 +19,10 @@ height: 100%; width: 100%; + &:focus-visible { + .focus-outline(); + } + &__background { height: 100%; width: 100%; diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.less b/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.less index 064bca03e0..8063b25002 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.less +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-group-item/list-group-item.component.less @@ -8,6 +8,10 @@ align-items: center; flex-wrap: wrap; + &:focus-visible { + .focus-outline(); + } + &__label { width: 25%; max-width: 100%; diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.less b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.less index 7c64ad77a4..ad6b179a8e 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.less +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.less @@ -1,5 +1,7 @@ @import (reference) "@nova-ui/bits/sdk/less/nui-framework-variables"; +@import (reference) "@nova-ui/bits/sdk/less/mixins"; + .nui-list-leaf-item { width: 100%; display: flex; @@ -7,6 +9,10 @@ align-items: center; padding: @nui-space-xs 0; + &:focus-visible { + .focus-outline(); + } + &__info-container { display: flex; height: 40px; diff --git a/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.html b/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.html index 8e21eb11f4..6adc34b197 100644 --- a/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.html +++ b/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.html @@ -4,7 +4,14 @@ } @if (!widgetData?.link) { -
    +
    } From 7471efea3154b9070b231bc581172d424286bfd6 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Wed, 27 May 2026 12:20:11 +0200 Subject: [PATCH 08/32] OO-62923 IMPORTANT a11y fixed round 2 --- packages/bits/e2e/components/switch/switch.atom.ts | 2 +- packages/bits/src/lib/breadcrumb/breadcrumb.component.html | 2 ++ packages/bits/src/lib/breadcrumb/breadcrumb.component.less | 4 ++++ packages/bits/src/lib/dragdrop/draggable.component.html | 3 ++- .../layout/layout-resizer/layout-resizer.component.html | 1 + .../src/lib/repeat/repeat-item/repeat-item.component.html | 5 ++++- .../src/lib/repeat/repeat-item/repeat-item.component.less | 4 ++++ packages/bits/src/lib/repeat/repeat.component.html | 1 + packages/bits/src/lib/switch/switch.component.html | 4 ++-- packages/bits/src/lib/switch/switch.component.less | 2 +- .../src/lib/tabgroup/tab-group/tab-group.component.less | 3 +++ .../tab-heading-group/tab-heading-group.component.less | 3 +++ .../lib/tabgroup/tab-heading/tab-heading.component.html | 1 + .../lib/tabgroup/tab-heading/tab-heading.component.less | 7 +++---- .../wizard-step-header/wizard-step-header.component.html | 3 +++ .../list-leaf-item/list-leaf-item.component.html | 4 +++- .../proportional-widget/proportional-widget.component.html | 4 ++++ .../proportional-widget/proportional-widget.component.less | 3 +++ .../risk-score-tile/risk-score-tile.component.less | 4 ++++ .../components/items-dynamic/items-dynamic.component.html | 1 + .../background-color-rules-configuration.component.html | 1 + .../table-columns-configuration-v2.component.html | 1 + 22 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/bits/e2e/components/switch/switch.atom.ts b/packages/bits/e2e/components/switch/switch.atom.ts index ce7a432cad..e356eeef8a 100644 --- a/packages/bits/e2e/components/switch/switch.atom.ts +++ b/packages/bits/e2e/components/switch/switch.atom.ts @@ -26,7 +26,7 @@ export class SwitchAtom extends Atom { public static ON_CSS = "nui-switched"; public static CSS_CLASS = "nui-switch"; - public toggle = async (): Promise => this.slider.click(); + public toggle = async (): Promise => this.slider.click({ force: true }); public container(): Locator { return super.getLocator().locator(".nui-switch__container"); diff --git a/packages/bits/src/lib/breadcrumb/breadcrumb.component.html b/packages/bits/src/lib/breadcrumb/breadcrumb.component.html index 11019d9f20..dacca945df 100644 --- a/packages/bits/src/lib/breadcrumb/breadcrumb.component.html +++ b/packages/bits/src/lib/breadcrumb/breadcrumb.component.html @@ -5,8 +5,10 @@ @if (!isLast) { {{ item.title }} @if (dragHandle) { - + +
    diff --git a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html index 78eddd893a..aec880abca 100644 --- a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html +++ b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html @@ -4,7 +4,10 @@ [class.text-nowrap]="nowrap" [class.nui-repeat-item--clickable]="clickable" (click)="clickable ? rowClicked.emit($event) : null" - tabindex="-1"> + (keydown.enter)="onKeyActivate($event)" + (keydown.space)="onKeyActivate($event)" + [tabindex]="clickable ? 0 : -1" +>
    diff --git a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.less b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.less index 78f9ec30a7..992bf3f7bc 100644 --- a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.less +++ b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.less @@ -20,6 +20,10 @@ body > nui-repeat-item.cdk-drag-preview .nui-repeat-item { .setCssVariable(background-color, nui-color-bg-transparent-hover); } + &:focus-visible { + .focus-outline(); + } + &:hover { .setCssVariable(background-color, nui-color-bg-secondary); } diff --git a/packages/bits/src/lib/repeat/repeat.component.html b/packages/bits/src/lib/repeat/repeat.component.html index 4d2b87a9ab..b3b0a5890d 100644 --- a/packages/bits/src/lib/repeat/repeat.component.html +++ b/packages/bits/src/lib/repeat/repeat.component.html @@ -1,4 +1,5 @@ +
    diff --git a/packages/bits/src/lib/switch/switch.component.less b/packages/bits/src/lib/switch/switch.component.less index fbfd258e2a..cf1316beee 100644 --- a/packages/bits/src/lib/switch/switch.component.less +++ b/packages/bits/src/lib/switch/switch.component.less @@ -44,7 +44,7 @@ float: left; transition: background-color ease 0.3s; - &:focus { + &:focus-visible { .focus-outline(); } } diff --git a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.less b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.less index f0d2e9852a..7d1ea18e12 100644 --- a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.less +++ b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.less @@ -128,6 +128,9 @@ z-index: 2; border-bottom: none; cursor: pointer; + &:focus-visible { + .focus-outline(); + } &.disabled { cursor: auto; } diff --git a/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.less b/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.less index 6b3710c3cb..f95986d7a9 100644 --- a/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.less +++ b/packages/bits/src/lib/tabgroup/tab-heading-group/tab-heading-group.component.less @@ -51,6 +51,9 @@ z-index: 0; border-bottom: none; cursor: pointer; + &:focus-visible { + .focus-outline(); + } &.disabled { cursor: auto; } diff --git a/packages/bits/src/lib/tabgroup/tab-heading/tab-heading.component.html b/packages/bits/src/lib/tabgroup/tab-heading/tab-heading.component.html index 52dedaf75d..327830f3c8 100644 --- a/packages/bits/src/lib/tabgroup/tab-heading/tab-heading.component.html +++ b/packages/bits/src/lib/tabgroup/tab-heading/tab-heading.component.html @@ -1,3 +1,4 @@ +
    @@ -14,6 +15,8 @@
    {{ label }}
    + } @if (step.completed && !selected) { + Completed } @if (step.optional && stepState !== 'error') {
    {{ optionalLabel }} diff --git a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html index 1bc5402ce7..f25d6fe435 100644 --- a/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html +++ b/packages/dashboards/src/lib/components/list-widget/list-elements/list-leaf-item/list-leaf-item.component.html @@ -5,7 +5,9 @@ (click)="onButtonClick()" (keydown.enter)="onButtonClick()" (keydown.space)="$event.preventDefault(); onButtonClick()" ->
    +> + +
    @if (icon) { diff --git a/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html b/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html index f1fc7c3160..828c5ef130 100644 --- a/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html +++ b/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.html @@ -75,6 +75,10 @@ " (mouseenter)="chartAssist.emphasizeSeries(legendSeries.id)" (click)="onInteraction(legendSeries)" + (keydown.enter)="onInteraction(legendSeries)" + (keydown.space)="$event.preventDefault(); onInteraction(legendSeries)" + [attr.role]="interactive ? 'button' : null" + [tabindex]="interactive ? 0 : -1" [class.nui-proportional-widget__legend--interactive]=" interactive "> diff --git a/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.less b/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.less index 98c557fdb0..e876442b56 100644 --- a/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.less +++ b/packages/dashboards/src/lib/components/proportional-widget/proportional-widget.component.less @@ -7,6 +7,9 @@ } &__legend { + &:focus-visible { + .focus-outline(); + } &--interactive { cursor: pointer !important; } diff --git a/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.less b/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.less index a1b3109083..c0cdae68d4 100644 --- a/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.less +++ b/packages/dashboards/src/lib/components/risk-score-tile/risk-score-tile.component.less @@ -20,6 +20,10 @@ width: 100%; overflow: hidden; + &:focus-visible { + .focus-outline(); + } + &__background { height: 100%; width: 100%; diff --git a/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html b/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html index c56a31c7ca..6d71a493a2 100644 --- a/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html +++ b/packages/dashboards/src/lib/configurator/components/items-dynamic/items-dynamic.component.html @@ -1,3 +1,4 @@ +
    +
    @for ( rule of formLocal.get('rules')?.value.controls; track rule; let i = $index) { diff --git a/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html b/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html index 5c533e3dc1..bb5885afbd 100644 --- a/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html +++ b/packages/dashboards/src/lib/configurator/components/widgets/table/columns-editor-v2/table-columns-configuration-v2.component.html @@ -1,4 +1,5 @@
    + @if (!dataSourceError && dataSourceFields.length) {
    From 481c19610b6d4a5fcf3227fbebef771820a4f9ab Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Thu, 28 May 2026 20:03:23 +0200 Subject: [PATCH 09/32] OO-62923 The tabs in nui-tab-group are now fully keyboard-accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (Tab → Enter/Space) and will display a visible focus indicator --- .../lib/tabgroup/tab-group/tab-group.component.html | 13 +++++++++++-- .../lib/tabgroup/tab-group/tab-group.component.less | 3 +++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html index f6a28bc184..8997827572 100644 --- a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html +++ b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html @@ -23,7 +23,7 @@
    }
    -
    +
    @for (tab of tabs; track tab) {
    - + {{ tab.heading }} Date: Wed, 27 May 2026 18:29:22 +0200 Subject: [PATCH 10/32] OO-62923 button click animation same for mouse and keyboard --- .../bits/src/lib/button/button.component.less | 3 ++- .../bits/src/lib/button/button.component.ts | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/bits/src/lib/button/button.component.less b/packages/bits/src/lib/button/button.component.less index cee5dba17a..5c7fd752e4 100644 --- a/packages/bits/src/lib/button/button.component.less +++ b/packages/bits/src/lib/button/button.component.less @@ -39,7 +39,8 @@ max-width: unset; } - &:active { + &:active, + &.active { transform: scale(0.98); } diff --git a/packages/bits/src/lib/button/button.component.ts b/packages/bits/src/lib/button/button.component.ts index 01d0794df9..1f6d07fbd7 100644 --- a/packages/bits/src/lib/button/button.component.ts +++ b/packages/bits/src/lib/button/button.component.ts @@ -25,6 +25,7 @@ import { Component, ElementRef, HostBinding, + HostListener, Input, OnDestroy, OnInit, @@ -168,6 +169,9 @@ export class ButtonComponent implements OnInit, OnDestroy, AfterContentChecked { return this.ariaLabel || this.getAriaLabel() || null; } + @HostBinding("class.active") + public isActive: boolean = false; + @ViewChild("contentContainer", { static: true, read: ViewContainerRef }) private contentContainer: ViewContainerRef; @@ -205,6 +209,26 @@ should be set explicitly: `, this.ngUnsubscribe.complete(); } + @HostListener("keydown", ["$event"]) + public onKeyDown(event: KeyboardEvent): void { + const hostElement = this.getHostElement() as HTMLButtonElement; + if (!hostElement.disabled && !this.isBusy && (event.code === "Space" || event.code === "Enter")) { + this.isActive = true; + } + } + + @HostListener("keyup", ["$event"]) + public onKeyUp(event: KeyboardEvent): void { + if (event.code === "Space" || event.code === "Enter") { + this.isActive = false; + } + } + + @HostListener("blur") + public onBlur(): void { + this.isActive = false; + } + /** * Passes correct size of icon to inner HTML template. */ From e014fabed8d32d3f79baa0ba67ef90ab38654107 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Thu, 28 May 2026 22:42:42 +0200 Subject: [PATCH 11/32] OO-62923 A11y unit tests --- .../src/lib/button/button.component.spec.ts | 68 +++++++++ .../src/lib/switch/switch.component.spec.ts | 34 +++++ .../tab-group/tab-group.component.html | 2 +- .../tab-group/tab-group.component.spec.ts | 133 ++++++++++++++++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 packages/bits/src/lib/tabgroup/tab-group/tab-group.component.spec.ts diff --git a/packages/bits/src/lib/button/button.component.spec.ts b/packages/bits/src/lib/button/button.component.spec.ts index c045fe0843..0626140a04 100644 --- a/packages/bits/src/lib/button/button.component.spec.ts +++ b/packages/bits/src/lib/button/button.component.spec.ts @@ -211,5 +211,73 @@ describe("components >", () => { expect(click).toHaveBeenCalledTimes(2); })); }); + + describe("keyboard active state >", () => { + let buttonFixture: ComponentFixture; + let buttonSubject: ButtonComponent; + let hostElement: HTMLButtonElement; + + beforeEach(() => { + buttonFixture = TestBed.createComponent(ButtonComponent); + buttonSubject = buttonFixture.componentInstance; + buttonFixture.detectChanges(); + hostElement = (buttonSubject).el.nativeElement; + }); + + it("should add .active class on Space keydown", () => { + hostElement.dispatchEvent( + new KeyboardEvent("keydown", { code: "Space", bubbles: true }) + ); + buttonFixture.detectChanges(); + expect(hostElement.classList.contains("active")).toBeTrue(); + }); + + it("should add .active class on Enter keydown", () => { + hostElement.dispatchEvent( + new KeyboardEvent("keydown", { code: "Enter", bubbles: true }) + ); + buttonFixture.detectChanges(); + expect(hostElement.classList.contains("active")).toBeTrue(); + }); + + it("should remove .active class on Space keyup", () => { + hostElement.dispatchEvent( + new KeyboardEvent("keydown", { code: "Space", bubbles: true }) + ); + hostElement.dispatchEvent( + new KeyboardEvent("keyup", { code: "Space", bubbles: true }) + ); + buttonFixture.detectChanges(); + expect(hostElement.classList.contains("active")).toBeFalse(); + }); + + it("should remove .active class on blur", () => { + hostElement.dispatchEvent( + new KeyboardEvent("keydown", { code: "Enter", bubbles: true }) + ); + hostElement.dispatchEvent(new FocusEvent("blur", { bubbles: true })); + buttonFixture.detectChanges(); + expect(hostElement.classList.contains("active")).toBeFalse(); + }); + + it("should NOT add .active class when button is disabled", () => { + hostElement.disabled = true; + hostElement.dispatchEvent( + new KeyboardEvent("keydown", { code: "Space", bubbles: true }) + ); + buttonFixture.detectChanges(); + expect(hostElement.classList.contains("active")).toBeFalse(); + }); + + it("should NOT add .active class when isBusy is true", () => { + buttonSubject.isBusy = true; + buttonFixture.detectChanges(); + hostElement.dispatchEvent( + new KeyboardEvent("keydown", { code: "Space", bubbles: true }) + ); + buttonFixture.detectChanges(); + expect(hostElement.classList.contains("active")).toBeFalse(); + }); + }); }); }); diff --git a/packages/bits/src/lib/switch/switch.component.spec.ts b/packages/bits/src/lib/switch/switch.component.spec.ts index fde2d4a0d8..a4bebd9ed4 100644 --- a/packages/bits/src/lib/switch/switch.component.spec.ts +++ b/packages/bits/src/lib/switch/switch.component.spec.ts @@ -104,5 +104,39 @@ describe("components >", () => { expect(switchBar.getAttribute("aria-labelledby")).toBeNull(); }); }); + + describe("keyboard interaction >", () => { + let switchBar: HTMLElement; + + beforeEach(() => { + nuiSwitch.value = false; + switchFixture.detectChanges(); + switchBar = switchFixture.nativeElement.querySelector( + ".nui-switch__bar" + ) as HTMLElement; + }); + + it("should toggle when Space is pressed (keydown)", () => { + switchBar.dispatchEvent( + new KeyboardEvent("keydown", { key: " ", code: "Space", bubbles: true }) + ); + expect(valueChange).toHaveBeenCalledWith(true); + }); + + it("should NOT toggle when Enter is pressed", () => { + switchBar.dispatchEvent( + new KeyboardEvent("keydown", { code: "Enter", bubbles: true }) + ); + expect(valueChange).not.toHaveBeenCalled(); + }); + + it("should NOT toggle when disabled and Space is pressed", () => { + nuiSwitch.setDisabledState(true); + switchBar.dispatchEvent( + new KeyboardEvent("keydown", { key: " ", code: "Space", bubbles: true }) + ); + expect(valueChange).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html index 8997827572..903f036be5 100644 --- a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html +++ b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.html @@ -35,7 +35,7 @@ class="tab-link" role="tab" [attr.tabindex]="tab.disabled ? -1 : 0" - [attr.aria-selected]="tab.active" + [attr.aria-selected]="tab.active ?? false" [attr.aria-disabled]="tab.disabled || null" (click)="selectTab(tab)" (keydown.enter)="selectTab(tab)" diff --git a/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.spec.ts b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.spec.ts new file mode 100644 index 0000000000..68aa60f43d --- /dev/null +++ b/packages/bits/src/lib/tabgroup/tab-group/tab-group.component.spec.ts @@ -0,0 +1,133 @@ +// © 2022 SolarWinds Worldwide, LLC. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { NuiTabsModule } from "../tabs.module"; + +@Component({ + selector: "nui-test-tab-group", + template: ` + + Content 1 + Content 2 + Content 3 + + `, + standalone: false, +}) +class TestTabGroupComponent {} + +describe("components >", () => { + describe("tab-group >", () => { + let fixture: ComponentFixture; + let el: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestTabGroupComponent], + imports: [NuiTabsModule], + }); + + fixture = TestBed.createComponent(TestTabGroupComponent); + fixture.detectChanges(); + el = fixture.nativeElement; + }); + + describe("ARIA attributes >", () => { + it("should have role=tablist on the tabs container", () => { + const tablist = el.querySelector(".nui-tabs__container"); + expect(tablist?.getAttribute("role")).toBe("tablist"); + }); + + it("should have role=tab on each tab link", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + tabLinks.forEach((link) => { + expect(link.getAttribute("role")).toBe("tab"); + }); + }); + + it("should set aria-selected=true on the active tab", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + expect(tabLinks[0].getAttribute("aria-selected")).toBe("true"); + }); + + it("should set aria-selected=false on inactive tabs", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + expect(tabLinks[1].getAttribute("aria-selected")).toBe("false"); + }); + + it("should set aria-disabled on disabled tab", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + expect(tabLinks[2].getAttribute("aria-disabled")).toBe("true"); + }); + + it("should not set aria-disabled on enabled tabs", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + expect(tabLinks[0].getAttribute("aria-disabled")).toBeNull(); + }); + }); + + describe("tabindex >", () => { + it("should have tabindex=0 on enabled tab links", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + expect(tabLinks[0].getAttribute("tabindex")).toBe("0"); + expect(tabLinks[1].getAttribute("tabindex")).toBe("0"); + }); + + it("should have tabindex=-1 on disabled tab link", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + expect(tabLinks[2].getAttribute("tabindex")).toBe("-1"); + }); + }); + + describe("keyboard interaction >", () => { + it("should select tab on Enter keydown", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + + // Second tab is initially inactive + expect(tabLinks[1].getAttribute("aria-selected")).toBe("false"); + + tabLinks[1].dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }) + ); + fixture.detectChanges(); + + expect(tabLinks[1].getAttribute("aria-selected")).toBe("true"); + expect(tabLinks[0].getAttribute("aria-selected")).toBe("false"); + }); + + it("should NOT select disabled tab on Enter keydown", () => { + const tabLinks = el.querySelectorAll(".tab-link"); + const disabledTab = tabLinks[2]; + + disabledTab.dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }) + ); + fixture.detectChanges(); + + expect(disabledTab.getAttribute("aria-selected")).toBe("false"); + // First tab should still be active + expect(tabLinks[0].getAttribute("aria-selected")).toBe("true"); + }); + }); + }); +}); From 3438ec596fdba5edeeadf758178699796b2bc130 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Fri, 5 Jun 2026 17:33:46 +0200 Subject: [PATCH 12/32] OO-62923 table widget and component --- .../table-cell/table-header-cell.component.ts | 43 +++++++++++++++++++ .../lib/table/table-state-handler.service.ts | 2 + .../bits/src/lib/table/table.component.less | 4 ++ .../table-widget/table-widget.component.html | 34 +++++++++------ .../table-widget/table-widget.component.ts | 2 +- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/packages/bits/src/lib/table/table-cell/table-header-cell.component.ts b/packages/bits/src/lib/table/table-cell/table-header-cell.component.ts index c2f1c24cd2..9b75c19c90 100644 --- a/packages/bits/src/lib/table/table-cell/table-header-cell.component.ts +++ b/packages/bits/src/lib/table/table-cell/table-header-cell.component.ts @@ -41,6 +41,7 @@ import { filter, pluck } from "rxjs/operators"; import { TableColumnDefDirective } from "./table-column-def.directive"; import { IDragEvent } from "../../../common/directives/public-api"; import { UtilService } from "../../../services/util.service"; +import { SorterDirection } from "../../sorter/public-api"; import { FIXED_WIDTH_CLASS } from "../constants"; import { TableResizePhase } from "../table-resizer/table-resizer.directive"; import { @@ -131,6 +132,31 @@ export class TableHeaderCellComponent return this.sortingState.isColumnSorted; } + /** + * Exposes the current sort direction of a sortable column to assistive + * technologies (WCAG 4.1.2). Returns null for non-sortable columns. + */ + @HostBinding("attr.aria-sort") + get ariaSort(): string | null { + if (!this.isSortable) { + return null; + } + if (!this.sortingState.isColumnSorted) { + return "none"; + } + return this.sortingState.sortDirection === SorterDirection.descending + ? "descending" + : "ascending"; + } + + /** + * Makes sortable column headers reachable by keyboard (WCAG 2.1.1). + */ + @HostBinding("attr.tabindex") + get sortTabindex(): number | null { + return this.isSortable ? 0 : null; + } + @HostBinding("attr.draggable") @HostBinding("class.nui-table__table-header-cell--reorderable") get isReorderable(): boolean { @@ -165,6 +191,23 @@ export class TableHeaderCellComponent @HostListener("click") clicked(): void { + this.triggerSort(); + } + + @HostListener("keydown.enter") + onEnterKey(): void { + this.triggerSort(); + } + + // Space activates the sort and must suppress the page scroll. Angular types + // the key-filtered ($event) as a generic Event under strict templates. + @HostListener("keydown.space", ["$event"]) + onSpaceKey(event: Event): void { + event.preventDefault(); + this.triggerSort(); + } + + private triggerSort(): void { if ( this.tableStateHandlerService.sortable && !this.isColumnSortingDisabled && diff --git a/packages/bits/src/lib/table/table-state-handler.service.ts b/packages/bits/src/lib/table/table-state-handler.service.ts index efd0ab016c..cc72ea1b8e 100644 --- a/packages/bits/src/lib/table/table-state-handler.service.ts +++ b/packages/bits/src/lib/table/table-state-handler.service.ts @@ -83,6 +83,7 @@ export interface ColumnType { export interface ITableSortingState { sortingIcon?: string; isColumnSorted: boolean; + sortDirection?: SorterDirection; } /** @ignore */ @@ -736,6 +737,7 @@ export class TableStateHandlerService { if (this.state.sortedColumn) { return { sortingIcon: this.sortIcons[this.state.sortedColumn.direction], + sortDirection: this.state.sortedColumn.direction, // comparing column index with index of sorted column isColumnSorted: cellIndex === diff --git a/packages/bits/src/lib/table/table.component.less b/packages/bits/src/lib/table/table.component.less index 257d6cddbc..ade6855c4b 100644 --- a/packages/bits/src/lib/table/table.component.less +++ b/packages/bits/src/lib/table/table.component.less @@ -67,6 +67,10 @@ .setCssVariable(background-color, nui-color-bg-table_header-hover); } + &:focus-visible { + .focus-outline(); + } + &--text-black { .setCssVariable(color, nui-color-text-default); } diff --git a/packages/dashboards/src/lib/components/table-widget/table-widget.component.html b/packages/dashboards/src/lib/components/table-widget/table-widget.component.html index 5e3e1777c9..18365a2c89 100644 --- a/packages/dashboards/src/lib/components/table-widget/table-widget.component.html +++ b/packages/dashboards/src/lib/components/table-widget/table-widget.component.html @@ -58,9 +58,9 @@ [selectionConfig]="configuration?.selectionConfiguration" [totalItems]="totalItems" [paginatorUsed]=" - hasPaginator && - configuration?.selectionConfiguration?.allPages - " + hasPaginator && + configuration?.selectionConfiguration?.allPages + " [dataSource]="tableData" [sortedColumn]="sortedColumn" (sortOrderChanged)="onSortOrderChanged($event)" @@ -78,8 +78,8 @@ [style.min-width.px]="columnsWidthMap.get(column.id)" [title]="column.label" [tooltipText]=" - headerTooltipsEnabled ? column.label : undefined - " + headerTooltipsEnabled ? column.label : undefined + " [alignment]="getColumnAlignment(column)"> {{ column.label }} @@ -89,26 +89,26 @@ *nuiCellDef="let element" [style.width.px]="columnsWidthMap.get(column.id)" [style.max-width.px]=" - columnsWidthMap.get(column.id) - " + columnsWidthMap.get(column.id) + " [style.min-width.px]=" - columnsWidthMap.get(column.id) - " + columnsWidthMap.get(column.id) + " [alignment]="getColumnAlignment(column)"> @if (element[column.id] && column.formatter) { + componentPortal.attached($event) + "> } @@ -120,7 +120,13 @@ nui-row *nuiRowDef="let row; columns: headers" (click)="onInteraction(row, $event)" - [attr.role]="interactive ? 'button' : ''" + (keydown.enter)="onInteraction(row, $event)" + (keydown.space)=" + interactive && $event.preventDefault(); + onInteraction(row, $event) + " + [attr.role]="interactive ? 'button' : null" + [attr.tabindex]="interactive ? 0 : null" density="tiny" [rowObject]="row.__record" [clickableRow]="clickableRow">

diff --git a/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts b/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts index 333a7ebf6e..8a8bb8e221 100644 --- a/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts +++ b/packages/dashboards/src/lib/components/table-widget/table-widget.component.ts @@ -594,7 +594,7 @@ export class TableWidgetComponent this.eventBus.getStream(SELECTION).next({ payload: this.selection }); } - public onInteraction(row: any, event: MouseEvent): void { + public onInteraction(row: any, event: Event): void { if ( !this.interactive || this.configuration.selectionConfiguration?.enabled From c9ef1879f1c41c3198d90869074cb6b4d0eba8b5 Mon Sep 17 00:00:00 2001 From: ludmila-fialova_swi Date: Fri, 5 Jun 2026 15:27:23 +0200 Subject: [PATCH 13/32] OO-62923 charts, dashboards and readme fix --- packages/bits/README.md | 16 ++++++++-------- packages/charts/src/chart/chart.component.ts | 9 ++++++++- packages/dashboards/README.md | 4 ++-- .../xy-chart/xy-chart.component.html | 1 + .../widget-header/widget-header.component.html | 10 ++++++++-- .../widget-header/widget-header.component.less | 4 ++++ 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/bits/README.md b/packages/bits/README.md index 477f502555..24300196d8 100644 --- a/packages/bits/README.md +++ b/packages/bits/README.md @@ -33,7 +33,7 @@ ng --version If you want to install the latest Angular CLI globally, run the following command: ```sh -npm install -g @angular/cli@15 +npm install -g @angular/cli@21 ``` ### Installing Angular CDK @@ -137,7 +137,7 @@ providers: [ ] ``` -The `TRANSLATIONS` provider can be used to internationalize bits. Details can be found [here](). +The `TRANSLATIONS` provider can be used to internationalize bits. #### Set root CSS class @@ -200,11 +200,11 @@ We recommend the [BEM](http://getbem.com/) naming convention for your css/less v ## Schematics A [schematic](https://angular.io/guide/schematics) is a template-based code generator that supports complex logic. It is a set of instructions for transforming a software project by generating or modifying code. Schematics are part of the Angular ecosystem. -Nova Bits offers the following [schematics](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/additional-documentation/schematics.html): +Nova Bits offers the following [schematics](https://nova-ui.solarwinds.io/#/bits/main/additional-documentation/schematics.html): ### Filtered View -[The Filtered View](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/additional-documentation/schematics/filtered-view.html) +[The Filtered View](https://nova-ui.solarwinds.io/#/bits/main/additional-documentation/schematics/filtered-view.html) schematic provides the ability to create a generic filtered view that can cover many use cases for filtering a set of data. Its main responsibility is to integrate a filter group panel with a list or table view that displays the filtered data. More details can be found [here](https://solarwinds.sharepoint.com/portals/hub/_layouts/15/PointPublishing.aspx?app=video&p=p&chid=8511af43-214a-435d-a605-dbe722ca04a0&vid=a5dbbef3-3582-49bf-b537-f789f946d09f) To add a filtered view to your project, run the following command: @@ -214,8 +214,8 @@ ng generate @nova-ui/bits:filtered-view --name=custom-name-for-filtered-view ### Filtered Group -[The Filtered Group](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/additional-documentation/schematics/filter-group.html) -schematic provides the ability to create a filtered group view. It depends heavily on the [LocalFilteringDataSource](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/injectables/LocalFilteringDataSource.html) service, so first and foremost please check out the documentation for it. +[The Filtered Group](https://nova-ui.solarwinds.io/#/bits/main/additional-documentation/schematics/filter-group.html) +schematic provides the ability to create a filtered group view. It depends heavily on the [LocalFilteringDataSource](https://nova-ui.solarwinds.io/#/bits/main/injectables/LocalFilteringDataSource.html) service, so first and foremost please check out the documentation for it. This code can be generated using the following command: ```sh @@ -224,8 +224,8 @@ ng generate @nova-ui/bits:filter-group --name=basic-filter-group --p=app ### List -[The List](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/additional-documentation/schematics/list.html) schematic provides the -ability to create a generic list that can cover a majority of use cases for a list. Its main responsibility is to integrate the functionality of the [nui-repeat](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/components/RepeatComponent.html) component with other components commonly used with a list such as [nui-select-v2](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/components/SelectV2Component.html), [nui-search](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/components/SearchComponent.html), [nui-paginator](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/components/PaginatorComponent.html) and [nui-sorter](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/components/SorterComponent.html). +[The List](https://nova-ui.solarwinds.io/#/bits/main/additional-documentation/schematics/list.html) schematic provides the +ability to create a generic list that can cover a majority of use cases for a list. Its main responsibility is to integrate the functionality of the [nui-repeat](https://nova-ui.solarwinds.io/#/bits/main/components/RepeatComponent.html) component with other components commonly used with a list such as [nui-select-v2](https://nova-ui.solarwinds.io/#/bits/main/components/SelectV2Component.html), [nui-search](https://nova-ui.solarwinds.io/#/bits/main/components/SearchComponent.html), [nui-paginator](https://nova-ui.solarwinds.io/#/bits/main/components/PaginatorComponent.html) and [nui-sorter](https://nova-ui.solarwinds.io/#/bits/main/components/SorterComponent.html). This code can be generated using the following command: ```sh diff --git a/packages/charts/src/chart/chart.component.ts b/packages/charts/src/chart/chart.component.ts index e87cb13bc7..b35e079a8b 100644 --- a/packages/charts/src/chart/chart.component.ts +++ b/packages/charts/src/chart/chart.component.ts @@ -73,7 +73,14 @@ export class ChartComponent /** Accessible name for the chart. Should be a localized string describing the chart content (WCAG 1.1.1). */ @Input() public ariaLabel: string; - @HostBinding("attr.role") readonly role = "img"; + // Only expose role="img" when an accessible name is present; an img role + // without a name is a WCAG 1.1.1 violation (axe: role-img-alt). When no + // ariaLabel is supplied the inner svg stays aria-hidden, so the chart is + // treated as decorative. + @HostBinding("attr.role") + get role(): string | null { + return this.ariaLabel ? "img" : null; + } @HostBinding("attr.aria-label") get a11yLabel(): string | null { diff --git a/packages/dashboards/README.md b/packages/dashboards/README.md index 51f1c15131..b7daaf05a0 100644 --- a/packages/dashboards/README.md +++ b/packages/dashboards/README.md @@ -22,7 +22,7 @@ absorb into your product's code base. ng add @nova-ui/bits ``` -If needed, further instructions for Bits installation can be found [here](https://nova-ui.solarwinds.io/#/bits/release_v13.0.x/). +If needed, further instructions for Bits installation can be found [here](https://nova-ui.solarwinds.io/#/bits/main/). ### Installing NodeJS and npm @@ -110,6 +110,6 @@ addition to the styles source paths: ## Start Implementing Your Dashboards -There is an [online overview](https://nova-ui.solarwinds.io/#/dashboards/release_v13.0.x/additional-documentation/overview.html) of Nova Dashboards, but if you'd prefer to dive right in, you can start with our [Hello, Dashboards! tutorial](https://nova-ui.solarwinds.io/#/dashboards/release_v13.0.x/additional-documentation/tutorials/hello-dashboards.html). +There is an [online overview](https://nova-ui.solarwinds.io/#/dashboards/main/additional-documentation/overview.html) of Nova Dashboards, but if you'd prefer to dive right in, you can start with our [Hello, Dashboards! tutorial](https://nova-ui.solarwinds.io/#/dashboards/main/additional-documentation/tutorials/hello-dashboards.html). [//]: # "Line breaks leave breathing room when the user scrolls to the bottom" diff --git a/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html b/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html index f718ed6f2d..21c560253f 100644 --- a/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html +++ b/packages/dashboards/src/lib/components/timeseries-widget/chart-presets/xy-chart/xy-chart.component.html @@ -110,6 +110,7 @@ class="description-container d-flex nui-text-small justify-content-center">
@if (!legendSeries.link) { +
@@ -112,7 +113,9 @@ i18n-aria-label nuiTooltip="Collapse header" icon="double-caret-up" - (click)="toggleCollapsed()"> + [attr.aria-expanded]="true" + (click)="toggleCollapsed()" + > } @if (!hideMenu && !editMode) { + [attr.aria-expanded]=" + popup?.isOpen ? 'true' : 'false' + " + >
Date: Mon, 8 Jun 2026 17:27:19 +0200 Subject: [PATCH 14/32] OO-62923 a11y fix of bits round 3 --- .../lib/breadcrumb/breadcrumb.component.html | 3 +- .../checkbox/checkbox-group.component.html | 2 +- .../lib/checkbox/checkbox-group.component.ts | 2 +- .../date-picker-day-picker.component.html | 6 ++ .../date-picker-month-picker.component.html | 6 ++ .../date-picker-year-picker.component.html | 6 ++ .../date-picker/date-picker.component.html | 3 +- .../dialog/confirmation-dialog.component.ts | 1 + .../lib/dialog/dialog-header.component.html | 2 +- .../docs/copy-text/copy-text.component.html | 2 + .../example-wrapper.component.html | 4 ++ .../freetype-query-builder.component.html | 17 ++--- .../overlay-component/overlay.component.ts | 4 +- .../src/lib/paginator/paginator.component.ts | 10 ++- .../bits/src/lib/panel/panel.component.html | 2 + .../repeat-item/repeat-item.component.html | 1 + .../lib/risk-score/risk-score.component.html | 6 +- .../lib/risk-score/risk-score.component.ts | 4 ++ .../bits/src/lib/select-v2/base-select-v2.ts | 9 +++ .../combobox-v2/combobox-v2.component.html | 11 +++- .../combobox-v2/combobox-v2.component.ts | 12 ++-- .../select-v2-option-group.component.ts | 8 ++- .../select-v2/option-key-control.service.ts | 20 ++++-- .../select-v2/select/select-v2.component.html | 15 +++-- .../select-v2/select/select-v2.component.ts | 11 +--- .../select/combobox/combobox.component.html | 4 ++ .../src/lib/selector/selector.component.html | 7 ++- .../src/lib/selector/selector.component.ts | 63 +++++++++++++++++++ .../bits/src/lib/sorter/sorter.component.html | 4 +- .../src/lib/spinner/spinner.component.html | 1 + .../bits/src/lib/spinner/spinner.component.ts | 2 +- .../src/lib/tabgroup/tab/tab.component.ts | 1 + .../quick-picker/quick-picker.component.html | 4 ++ .../quick-picker/quick-picker.component.less | 4 ++ .../bits/src/lib/toast/toast.component.html | 3 +- .../wizard-footer/wizard-footer.component.ts | 1 - .../wizard-horizontal.component.html | 5 +- .../wizard-overflow.component.html | 1 + .../wizard-step-header.component.html | 3 +- .../bits/src/lib/wizard/wizard.component.html | 21 ++++++- .../bits/src/lib/wizard/wizard.component.less | 5 ++ 41 files changed, 239 insertions(+), 57 deletions(-) diff --git a/packages/bits/src/lib/breadcrumb/breadcrumb.component.html b/packages/bits/src/lib/breadcrumb/breadcrumb.component.html index dacca945df..5157d3f32b 100644 --- a/packages/bits/src/lib/breadcrumb/breadcrumb.component.html +++ b/packages/bits/src/lib/breadcrumb/breadcrumb.component.html @@ -15,7 +15,8 @@ class="nui-breadcrumb__caret-icon" icon="caret-right" iconColor="disabled-gray" - iconSize="small"> + iconSize="small" + aria-hidden="true"> } @if (isLast) { + [attr.aria-labelledby]="ariaLabelledby || null">
diff --git a/packages/bits/src/lib/checkbox/checkbox-group.component.ts b/packages/bits/src/lib/checkbox/checkbox-group.component.ts index f62effc4d6..8c25ef223e 100644 --- a/packages/bits/src/lib/checkbox/checkbox-group.component.ts +++ b/packages/bits/src/lib/checkbox/checkbox-group.component.ts @@ -94,7 +94,7 @@ export class CheckboxGroupComponent /** * Input to set aria label text */ - @Input() public ariaLabeledby = ""; + @Input() public ariaLabelledby = ""; private subscriptionsArray = new Array(); private disabled: boolean = false; diff --git a/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html b/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html index 6640b268f2..6c44ad3e9e 100644 --- a/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html +++ b/packages/bits/src/lib/date-picker/date-picker-day-picker.component.html @@ -8,6 +8,9 @@ type="button" displayStyle="action" icon="caret-left" + ariaLabel="Previous month" + i18n-ariaLabel + title="Previous month" (click)="datePicker.move(-1, $event)" tabindex="-1"> @@ -36,6 +39,9 @@ type="button" displayStyle="action" icon="caret-right" + ariaLabel="Next month" + i18n-ariaLabel + title="Next month" (click)="datePicker.move(1, $event)" tabindex="-1"> diff --git a/packages/bits/src/lib/date-picker/date-picker-month-picker.component.html b/packages/bits/src/lib/date-picker/date-picker-month-picker.component.html index dd6d46668d..707c49919d 100644 --- a/packages/bits/src/lib/date-picker/date-picker-month-picker.component.html +++ b/packages/bits/src/lib/date-picker/date-picker-month-picker.component.html @@ -8,6 +8,9 @@ type="button" displayStyle="action" icon="caret-left" + ariaLabel="Previous year" + i18n-ariaLabel + title="Previous year" (click)="datePicker.move(-1, $event)" tabindex="-1"> @@ -33,6 +36,9 @@ type="button" displayStyle="action" icon="caret-right" + ariaLabel="Next year" + i18n-ariaLabel + title="Next year" (click)="datePicker.move(1, $event)" tabindex="-1"> diff --git a/packages/bits/src/lib/date-picker/date-picker-year-picker.component.html b/packages/bits/src/lib/date-picker/date-picker-year-picker.component.html index 9e10b6056a..dc761c1031 100644 --- a/packages/bits/src/lib/date-picker/date-picker-year-picker.component.html +++ b/packages/bits/src/lib/date-picker/date-picker-year-picker.component.html @@ -8,6 +8,9 @@ type="button" icon="caret-left" displayStyle="action" + ariaLabel="Previous page of years" + i18n-ariaLabel + title="Previous page of years" (click)="datePicker.move(-1, $event)" tabindex="-1"> @@ -36,6 +39,9 @@ type="button" icon="caret-right" displayStyle="action" + ariaLabel="Next page of years" + i18n-ariaLabel + title="Next page of years" (click)="datePicker.move(1, $event)" tabindex="-1"> diff --git a/packages/bits/src/lib/date-picker/date-picker.component.html b/packages/bits/src/lib/date-picker/date-picker.component.html index 35e92bef71..942f450271 100644 --- a/packages/bits/src/lib/date-picker/date-picker.component.html +++ b/packages/bits/src/lib/date-picker/date-picker.component.html @@ -12,7 +12,8 @@ [isInErrorState]="isInErrorState" (textChange)="onInputActiveDateChanged($event)" (blurred)="updateTouchedState()" - [ariaLabel]="ariaLabel"> + [ariaLabel]="ariaLabel" + (keydown.arrowdown)="!isDisabled && overlay.show()">
@if (severityIcon) { - + }

{{ title }}

diff --git a/packages/bits/src/lib/docs/copy-text/copy-text.component.html b/packages/bits/src/lib/docs/copy-text/copy-text.component.html index 9748e97278..9f3c2245e7 100644 --- a/packages/bits/src/lib/docs/copy-text/copy-text.component.html +++ b/packages/bits/src/lib/docs/copy-text/copy-text.component.html @@ -5,6 +5,8 @@ [nuiTooltip]="copyTooltip" icon="copy" iconColor="primary-blue" + ariaLabel="Copy to clipboard" + i18n-ariaLabel (clipboardSuccess)="onSnippetCopied()" nuiClipboard="{{ fileContent }}">
diff --git a/packages/bits/src/lib/docs/example-wrapper/example-wrapper.component.html b/packages/bits/src/lib/docs/example-wrapper/example-wrapper.component.html index 9469c065b3..0dbd3a493f 100644 --- a/packages/bits/src/lib/docs/example-wrapper/example-wrapper.component.html +++ b/packages/bits/src/lib/docs/example-wrapper/example-wrapper.component.html @@ -10,6 +10,8 @@

{{ exampleTitle }}

showSource ? 'double-caret-up' : 'double-caret-down' }}" title="Show source" + ariaLabel="Toggle source code" + i18n-ariaLabel [nuiTooltip]="getTooltip()" tooltipPlacement="bottom" (click)="showSource = !showSource"> @@ -19,6 +21,8 @@

{{ exampleTitle }}

displayStyle="action" icon="external-link" title="Open in CodeSandbox" + ariaLabel="Open in CodeSandbox" + i18n-ariaLabel nuiTooltip="Open in Code Sandbox (only self-contained examples are opened properly)" tooltipPlacement="bottom" (click)="openCodeSandboxExample()"> diff --git a/packages/bits/src/lib/freetype-query-builder/freetype-query-builder.component.html b/packages/bits/src/lib/freetype-query-builder/freetype-query-builder.component.html index 9101f472cc..9f314f7136 100644 --- a/packages/bits/src/lib/freetype-query-builder/freetype-query-builder.component.html +++ b/packages/bits/src/lib/freetype-query-builder/freetype-query-builder.component.html @@ -8,23 +8,19 @@ [displayValueTemplate]="customTemplate"> @for ( item of readonly ? [] : currentHelp; track item; let idx = $index; let last = $last; let first = $first) { @if (!item.items) { - +
@if (item.icon) { }
} @if (item.items && item.items !== []) { - + @if (last && idx > 1) { } @if (item.header) { @@ -34,12 +30,10 @@ } @if (!item.notice) { @for (groupedItem of item.items; track groupedItem) { @if (!customTemplate) { @@ -56,7 +50,7 @@ ? groupedItem['severity'] : 'info') }}"> - {{ + {{ groupedItem.message.displayValue }}
@@ -85,7 +79,8 @@ class="message-textarea form-control input-control" formControlName="body" [placeholder]="placeholder" - [maxLength]="maxLength"> + [maxLength]="maxLength" + aria-label="Freetype query">
id="nui-overlay" class="nui-overlay" [attr.role]="roleAttr || null" - [attr.aria-labelledby]="ariaLabelledBy || null" + [attr.aria-labelledby]="ariaLabelledby || null" [ngClass]="{ empty: empty$ | async }" > @@ -110,7 +110,7 @@ export class OverlayComponent @Input() roleAttr: string; /** Sets the aria-labelledby attribute for accessibility */ - @Input() ariaLabelledBy?: string; + @Input() ariaLabelledby?: string; /** Emits MouseEvent when click occurs outside Select/Combobox */ @Output() public readonly clickOutside = new EventEmitter(); diff --git a/packages/bits/src/lib/paginator/paginator.component.ts b/packages/bits/src/lib/paginator/paginator.component.ts index 331cd406a0..02cfd6fd7e 100644 --- a/packages/bits/src/lib/paginator/paginator.component.ts +++ b/packages/bits/src/lib/paginator/paginator.component.ts @@ -69,12 +69,20 @@ const containerPaddingsWithScroll = 37; styleUrls: ["./paginator.component.less"], encapsulation: ViewEncapsulation.None, providers: [PopupContainerService], - host: { role: "navigation" }, + host: { + role: "navigation", + "[attr.aria-label]": "ariaLabel", + }, standalone: false, }) export class PaginatorComponent implements OnInit, OnChanges, OnDestroy, IFilterPub { + /** + * Input to set aria label text + */ + @Input() public ariaLabel: string = $localize`Pagination`; + @Input() public itemsList: Array = []; /** * Current page number diff --git a/packages/bits/src/lib/panel/panel.component.html b/packages/bits/src/lib/panel/panel.component.html index c1ec2d7553..15d380f854 100644 --- a/packages/bits/src/lib/panel/panel.component.html +++ b/packages/bits/src/lib/panel/panel.component.html @@ -51,6 +51,7 @@

[icon]="mainIcon" [disabled]="isAnimating" [ariaLabel]="expandCollapseAriaLabel" + [title]="expandCollapseAriaLabel" (click)="toggleCollapsedOnClick()"> } } @if (isClosable) { }

diff --git a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html index aec880abca..607c91b0c7 100644 --- a/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html +++ b/packages/bits/src/lib/repeat/repeat-item/repeat-item.component.html @@ -3,6 +3,7 @@ [class.nui-repeat-item--selected]="selected" [class.text-nowrap]="nowrap" [class.nui-repeat-item--clickable]="clickable" + [attr.aria-selected]="selectable ? selected : null" (click)="clickable ? rowClicked.emit($event) : null" (keydown.enter)="onKeyActivate($event)" (keydown.space)="onKeyActivate($event)" diff --git a/packages/bits/src/lib/risk-score/risk-score.component.html b/packages/bits/src/lib/risk-score/risk-score.component.html index 2538927690..e3aa8b046c 100644 --- a/packages/bits/src/lib/risk-score/risk-score.component.html +++ b/packages/bits/src/lib/risk-score/risk-score.component.html @@ -1,4 +1,8 @@ -
+
@if (title !== undefined) {
diff --git a/packages/bits/src/lib/risk-score/risk-score.component.ts b/packages/bits/src/lib/risk-score/risk-score.component.ts index ecede86694..ccadf369ba 100644 --- a/packages/bits/src/lib/risk-score/risk-score.component.ts +++ b/packages/bits/src/lib/risk-score/risk-score.component.ts @@ -99,6 +99,10 @@ export class RiskScoreComponent implements AfterViewInit, OnChanges, OnDestroy { this.colorLineWidth$.unsubscribe(); } + public get riskScoreAriaLabel(): string { + return $localize`Risk score: ${this.level}`; + } + public ngOnChanges(): void { this.updateOffset(this.colorLineWidth$.getValue()); } diff --git a/packages/bits/src/lib/select-v2/base-select-v2.ts b/packages/bits/src/lib/select-v2/base-select-v2.ts index 9b482f9b22..769a82aaaa 100644 --- a/packages/bits/src/lib/select-v2/base-select-v2.ts +++ b/packages/bits/src/lib/select-v2/base-select-v2.ts @@ -195,6 +195,15 @@ export abstract class BaseSelectV2 private _ariaLabel: string = ""; + public get accessibleLabel(): string | null { + return ( + this.ariaLabel || + (this as any).displayText || + this.placeholder || + null + ); + } + /** Unique ID for the trigger element, used for aria-labelledby on the overlay */ public readonly triggerId = `nui-select-trigger-${BaseSelectV2._counter++}`; private virtualScrollResizeObserver: ResizeObserver; diff --git a/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html b/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html index 46b0733918..3785239dec 100644 --- a/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html +++ b/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.html @@ -6,8 +6,13 @@ @@ -41,13 +47,14 @@ icon="caret-down" ariaLabel="Toggle dropdown" i18n-ariaLabel + title="Toggle dropdown" [class.rotate]="isDropdownOpen" [class.disabled]="isDisabled" [disabled]="isDisabled" (click)="selectTextInInput()"> diff --git a/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.ts b/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.ts index 101f44075e..5559c6f9d7 100644 --- a/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.ts +++ b/packages/bits/src/lib/select-v2/combobox-v2/combobox-v2.component.ts @@ -78,11 +78,8 @@ import { InputValueTypes } from "../types"; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { - "class": "nui-combobox-v2", - "role": "combobox", - "[attr.aria-expanded]": "isDropdownOpen || false", - "aria-haspopup": "listbox", - "aria-owns": "nui-overlay", + class: "nui-combobox-v2", + role: "none", }, standalone: false, }) @@ -117,10 +114,10 @@ export class ComboboxV2Component } /** Value of the Combobox Input */ - public inputValue: string | number; + public inputValue: string | number = ""; /** Text of the Clear Button tooltip */ - public clearValueButtonTooltip: string; + public clearValueButtonTooltip: string = ""; constructor( elRef: ElementRef, @@ -130,6 +127,7 @@ export class ComboboxV2Component public liveAnnouncer: LiveAnnouncer ) { super(optionKeyControlService, cdRef, elRef, liveAnnouncer); + this.optionKeyControlService.skipSpace = true; } public ngAfterContentInit(): void { diff --git a/packages/bits/src/lib/select-v2/option-group/select-v2-option-group.component.ts b/packages/bits/src/lib/select-v2/option-group/select-v2-option-group.component.ts index 126ad8b94a..5778661267 100644 --- a/packages/bits/src/lib/select-v2/option-group/select-v2-option-group.component.ts +++ b/packages/bits/src/lib/select-v2/option-group/select-v2-option-group.component.ts @@ -25,6 +25,7 @@ import { ContentChildren, forwardRef, HostBinding, + Input, Inject, OnDestroy, Optional, @@ -47,12 +48,17 @@ import { IOptionedComponent } from "../types"; template: "", styleUrls: ["./select-v2-option-group.component.less"], changeDetection: ChangeDetectionStrategy.OnPush, - host: { role: "group" }, + host: { + role: "group", + "[attr.aria-label]": "ariaLabel", + }, standalone: false, }) export class SelectV2OptionGroupComponent implements AfterContentInit, OnDestroy { + @Input() public ariaLabel: string = ""; + /** Whether the Option Group outfiltered */ @HostBinding("class.hidden") public outfiltered: boolean = false; diff --git a/packages/bits/src/lib/select-v2/option-key-control.service.ts b/packages/bits/src/lib/select-v2/option-key-control.service.ts index d78725cce7..ac8d80fbb3 100644 --- a/packages/bits/src/lib/select-v2/option-key-control.service.ts +++ b/packages/bits/src/lib/select-v2/option-key-control.service.ts @@ -33,6 +33,7 @@ import { IOption, IOverlayComponent } from "../overlay/types"; export class OptionKeyControlService { public popup: IOverlayComponent; public optionItems: QueryList; + public skipSpace: boolean = false; private keyboardEventsManager: ActiveDescendantKeyManager; @@ -110,12 +111,19 @@ export class OptionKeyControlService { this.scrollToActiveItem({ block: "nearest" }); - // prevent closing on enter - if (!this.hasActiveItem() && event.code === KEYBOARD_CODE.ENTER) { + // prevent page scroll on space and prevent closing on enter/space when no item is active + if ( + event.code === KEYBOARD_CODE.SPACE || + (!this.hasActiveItem() && event.code === KEYBOARD_CODE.ENTER) + ) { event.preventDefault(); } - if (this.hasActiveItem() && event.code === KEYBOARD_CODE.ENTER) { + if ( + this.hasActiveItem() && + (event.code === KEYBOARD_CODE.ENTER || + (!this.skipSpace && event.code === KEYBOARD_CODE.SPACE)) + ) { if (!this.keyboardEventsManager.activeItem) { throw new Error("ActiveItem is not defined"); } @@ -139,7 +147,11 @@ export class OptionKeyControlService { event.preventDefault(); } - if (event.code === KEYBOARD_CODE.ARROW_DOWN) { + if ( + event.code === KEYBOARD_CODE.ARROW_DOWN || + event.code === KEYBOARD_CODE.ENTER || + (!this.skipSpace && event.code === KEYBOARD_CODE.SPACE) + ) { this.popup.toggle(); this.scrollToActiveItem({ block: "center" }); } diff --git a/packages/bits/src/lib/select-v2/select/select-v2.component.html b/packages/bits/src/lib/select-v2/select/select-v2.component.html index acc222a244..522d317054 100644 --- a/packages/bits/src/lib/select-v2/select/select-v2.component.html +++ b/packages/bits/src/lib/select-v2/select/select-v2.component.html @@ -1,9 +1,15 @@
@if (!displayValueTemplate) {
@@ -18,7 +24,8 @@ } + [icon]="caretIcon" + aria-hidden="true">
} @if (displayValueTemplate) { diff --git a/packages/bits/src/lib/select-v2/select/select-v2.component.ts b/packages/bits/src/lib/select-v2/select/select-v2.component.ts index 3c4ab7c026..046653860d 100644 --- a/packages/bits/src/lib/select-v2/select/select-v2.component.ts +++ b/packages/bits/src/lib/select-v2/select/select-v2.component.ts @@ -68,9 +68,8 @@ import { OptionKeyControlService } from "../option-key-control.service"; ], changeDetection: ChangeDetectionStrategy.OnPush, host: { - "class": "nui-select-v2", - "role": "button", - "[attr.aria-label]": "accessibleLabel", + class: "nui-select-v2", + role: "none", }, standalone: false, }) @@ -81,7 +80,7 @@ export class SelectV2Component implements AfterContentInit, OnDestroy, OnChanges { /** Sets a custom template for displaying it in the Option */ - @Input() public displayValueTemplate: TemplateRef; + @Input() public displayValueTemplate?: TemplateRef; /** Value of the selected Option that derives in the Select */ public displayText: string; @@ -152,10 +151,6 @@ export class SelectV2Component return !this.getLastSelectedOption(); } - public get accessibleLabel(): string | null { - return this.ariaLabel || this.displayText || this.placeholder || null; - } - /** * Calls to ngOnDestroy do not automatically get propagated to base classes. * This can lead to memory leaks. diff --git a/packages/bits/src/lib/select/combobox/combobox.component.html b/packages/bits/src/lib/select/combobox/combobox.component.html index 612016e70b..495306e608 100644 --- a/packages/bits/src/lib/select/combobox/combobox.component.html +++ b/packages/bits/src/lib/select/combobox/combobox.component.html @@ -45,6 +45,8 @@ (click)="clearValue($event)" [isEmpty]="true" icon="remove" + ariaLabel="Clear value" + i18n-ariaLabel title="remove" displayStyle="action" type="button"> @@ -55,6 +57,8 @@ nui-button [isEmpty]="true" icon="caret-down" + ariaLabel="Toggle dropdown" + i18n-ariaLabel title="toggle" displayStyle="action" type="button" diff --git a/packages/bits/src/lib/selector/selector.component.html b/packages/bits/src/lib/selector/selector.component.html index d4d9a7a873..e21394a187 100644 --- a/packages/bits/src/lib/selector/selector.component.html +++ b/packages/bits/src/lib/selector/selector.component.html @@ -7,8 +7,9 @@ [attr.aria-checked]="checkboxChecked" class="nui-selector__checkbox-button" [displayStyle]="'default'" - (click)="handleSelectorClick($event)"> -
+ (click)="handleSelectorClick($event)" + (keydown.space)="$event.preventDefault(); handleSelectorClick($event)"> + diff --git a/packages/bits/src/lib/selector/selector.component.ts b/packages/bits/src/lib/selector/selector.component.ts index 65d44cb4b8..185f3b08ba 100644 --- a/packages/bits/src/lib/selector/selector.component.ts +++ b/packages/bits/src/lib/selector/selector.component.ts @@ -18,12 +18,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import { ActiveDescendantKeyManager } from "@angular/cdk/a11y"; import { OverlayConfig } from "@angular/cdk/overlay"; import { AfterViewInit, Component, ElementRef, EventEmitter, + HostListener, Input, OnChanges, OnDestroy, @@ -36,6 +38,7 @@ import { Subject } from "rxjs"; import { debounceTime, takeUntil } from "rxjs/operators"; import { CheckboxStatus, SelectionType } from "./public-api"; +import { KEYBOARD_CODE } from "../../constants/keycode.constants"; import { IFilter, IFilterPub, @@ -43,6 +46,8 @@ import { } from "../../services/data-source/public-api"; import { CheckboxComponent } from "../checkbox/checkbox.component"; import { CheckboxChangeEvent } from "../checkbox/public-api"; +import { MenuItemBaseComponent } from "../menu/menu-item/menu-item/menu-item-base"; +import { MenuPopupComponent } from "../menu/menu-popup/menu-popup.component"; import { IMenuGroup, IMenuItem } from "../menu/public-api"; import { OVERLAY_WITH_POPUP_STYLES_CLASS } from "../overlay/constants"; import { OverlayComponent } from "../overlay/overlay-component/overlay.component"; @@ -104,6 +109,8 @@ export class SelectorComponent @ViewChild(OverlayComponent) public overlay: OverlayComponent; + @ViewChild(MenuPopupComponent) public popup: MenuPopupComponent; + public customContainer: ElementRef | undefined; public checkboxChecked = false; public indeterminate = false; @@ -113,6 +120,7 @@ export class SelectorComponent }; private status: SelectionType; + private keyboardEventsManager!: ActiveDescendantKeyManager; private selectionHasChanged = false; @@ -144,6 +152,7 @@ export class SelectorComponent "tabindex", "-1" ); + this.initKeyboardManager(); } public ngOnDestroy(): void { @@ -152,6 +161,15 @@ export class SelectorComponent this.onDestroy$.complete(); } + @HostListener("keydown", ["$event"]) + public onKeyDown(event: KeyboardEvent): void { + if (this.overlay.showing) { + this.handleOpenedMenu(event); + } else { + this.handleClosedMenu(event); + } + } + public getFilters(): IFilter { const response = { type: "selector", @@ -218,4 +236,49 @@ export class SelectorComponent this.selectionChange.emit(selection); this.overlay.hide(); } + + private initKeyboardManager(): void { + if (this.popup) { + this.keyboardEventsManager = + new ActiveDescendantKeyManager( + this.popup.menuItems + ).withWrap(); + } + } + + private handleOpenedMenu(event: KeyboardEvent): void { + const { code } = event; + + if (code === KEYBOARD_CODE.ESCAPE || code === KEYBOARD_CODE.TAB) { + this.overlay.hide(); + return; + } + + if ( + code === KEYBOARD_CODE.ARROW_DOWN || + code === KEYBOARD_CODE.ARROW_UP + ) { + event.preventDefault(); + this.keyboardEventsManager.onKeydown(event); + this.keyboardEventsManager.activeItem?.menuItem?.nativeElement?.scrollIntoView( + { block: "nearest" } + ); + } + + if (code === KEYBOARD_CODE.ENTER || code === KEYBOARD_CODE.SPACE) { + event.preventDefault(); + this.keyboardEventsManager.activeItem?.doAction(event); + } + } + + private handleClosedMenu(event: KeyboardEvent): void { + const { code } = event; + + if (code === KEYBOARD_CODE.ARROW_DOWN) { + event.preventDefault(); + this.initKeyboardManager(); + this.overlay.show(); + this.keyboardEventsManager?.setFirstItemActive(); + } + } } diff --git a/packages/bits/src/lib/sorter/sorter.component.html b/packages/bits/src/lib/sorter/sorter.component.html index 1a9f28fae6..e2bce0093f 100644 --- a/packages/bits/src/lib/sorter/sorter.component.html +++ b/packages/bits/src/lib/sorter/sorter.component.html @@ -21,7 +21,9 @@ (click)="toggleSorterMenu()" ariaLabel="Open Sorter Menu" i18n-ariaLabel - icon="caret-down"> + icon="caret-down" + aria-haspopup="menu" + [attr.aria-expanded]="overlay.showing">
diff --git a/packages/bits/src/lib/spinner/spinner.component.html b/packages/bits/src/lib/spinner/spinner.component.html index 67044b9734..6885a1d854 100644 --- a/packages/bits/src/lib/spinner/spinner.component.html +++ b/packages/bits/src/lib/spinner/spinner.component.html @@ -61,6 +61,7 @@ icon="close" ariaLabel="Cancel" i18n-ariaLabel + title="Cancel" (click)="cancelProgress()" displayStyle="action" [nuiTooltip]="tooltipText"> diff --git a/packages/bits/src/lib/spinner/spinner.component.ts b/packages/bits/src/lib/spinner/spinner.component.ts index 90f4b24e47..04cd1ba0ee 100644 --- a/packages/bits/src/lib/spinner/spinner.component.ts +++ b/packages/bits/src/lib/spinner/spinner.component.ts @@ -83,7 +83,7 @@ export class SpinnerComponent implements OnChanges, OnDestroy { /** * Input to set aria label text */ - @Input() public ariaLabel: string = "Spinner"; + @Input() public ariaLabel: string = $localize`Spinner`; @Output() public cancel = new EventEmitter(); diff --git a/packages/bits/src/lib/tabgroup/tab/tab.component.ts b/packages/bits/src/lib/tabgroup/tab/tab.component.ts index 93e99cbdc6..ec373e9826 100644 --- a/packages/bits/src/lib/tabgroup/tab/tab.component.ts +++ b/packages/bits/src/lib/tabgroup/tab/tab.component.ts @@ -80,6 +80,7 @@ export class TabComponent { @Output() deselected: EventEmitter = new EventEmitter(); @HostBinding("class.tab-pane") addClass = true; + @HostBinding("attr.role") role = "tabpanel"; public headingRef: TemplateRef; protected _active: boolean; diff --git a/packages/bits/src/lib/time-frame-picker/quick-picker/quick-picker.component.html b/packages/bits/src/lib/time-frame-picker/quick-picker/quick-picker.component.html index 9588855814..a178071fd2 100644 --- a/packages/bits/src/lib/time-frame-picker/quick-picker/quick-picker.component.html +++ b/packages/bits/src/lib/time-frame-picker/quick-picker/quick-picker.component.html @@ -5,7 +5,11 @@

@for (key of presetKeys; track key) {
+ icon="{{ toastIcon }}" + aria-hidden="true">
@if (title) { {{ diff --git a/packages/bits/src/lib/wizard-v2/wizard-footer/wizard-footer.component.ts b/packages/bits/src/lib/wizard-v2/wizard-footer/wizard-footer.component.ts index 2368c29826..9ab88ff180 100644 --- a/packages/bits/src/lib/wizard-v2/wizard-footer/wizard-footer.component.ts +++ b/packages/bits/src/lib/wizard-v2/wizard-footer/wizard-footer.component.ts @@ -38,7 +38,6 @@ import { WizardStepFooterDirective } from "../wizard-step-footer.directive"; styleUrls: ["wizard-footer.component.less"], host: { class: "nui-wizard-footer", - role: "tab", }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/packages/bits/src/lib/wizard-v2/wizard-horizontal/wizard-horizontal.component.html b/packages/bits/src/lib/wizard-v2/wizard-horizontal/wizard-horizontal.component.html index 0ecb34923c..0b2d0b6aea 100644 --- a/packages/bits/src/lib/wizard-v2/wizard-horizontal/wizard-horizontal.component.html +++ b/packages/bits/src/lib/wizard-v2/wizard-horizontal/wizard-horizontal.component.html @@ -11,7 +11,7 @@ @if (overflownStepsStart.length) {
- +{{ overflownStepsStart.length }} more steps + +{{ overflownStepsStart.length }} more steps
} @for ( step of overflownStepsStart; track step; let i = $index; let isFirst = $first) { @@ -61,7 +61,7 @@ @if (overflownStepsEnd.length) {
- +{{ overflownStepsEnd.length }} more steps + +{{ overflownStepsEnd.length }} more steps
} @for ( step of overflownStepsEnd; track step; let i = $index; let isFirst = $first) { @@ -87,6 +87,7 @@
diff --git a/packages/bits/src/lib/wizard-v2/wizard-overflow/wizard-overflow.component.html b/packages/bits/src/lib/wizard-v2/wizard-overflow/wizard-overflow.component.html index bd731a2966..37f8656657 100644 --- a/packages/bits/src/lib/wizard-v2/wizard-overflow/wizard-overflow.component.html +++ b/packages/bits/src/lib/wizard-v2/wizard-overflow/wizard-overflow.component.html @@ -2,6 +2,7 @@ diff --git a/packages/bits/src/lib/wizard-v2/wizard-step-header/wizard-step-header.component.html b/packages/bits/src/lib/wizard-v2/wizard-step-header/wizard-step-header.component.html index f288f2b6a5..f536661d7c 100644 --- a/packages/bits/src/lib/wizard-v2/wizard-step-header/wizard-step-header.component.html +++ b/packages/bits/src/lib/wizard-v2/wizard-step-header/wizard-step-header.component.html @@ -37,7 +37,8 @@ + [iconColor]="stepStateConfigMap[stepState]?.iconColor" + aria-hidden="true">
diff --git a/packages/bits/src/lib/wizard/wizard.component.html b/packages/bits/src/lib/wizard/wizard.component.html index 6c1dcff780..e6a90febe5 100644 --- a/packages/bits/src/lib/wizard/wizard.component.html +++ b/packages/bits/src/lib/wizard/wizard.component.html @@ -3,11 +3,27 @@ class="nui-wizard__header" [class.nui-wizard__header--lines-stretched]="stretchStepLines"> @if (steps) { -
@@ -49,7 +46,7 @@ } @for (label of labels; track label) {
- {{ label.abbr }} + {{ label.abbr }}