Skip to content

Commit 3b97ab1

Browse files
fix(dashboards): use correct aria roles for widgets in none edit mode
groups dashboard widgets as list in none edit mode so screen reader can allow to jump between widgets without going through content using keyboard.
1 parent 4dda7b1 commit 3b97ab1

6 files changed

Lines changed: 52 additions & 2 deletions

File tree

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
<div class="grid-stack">
1+
<div
2+
class="grid-stack"
3+
[attr.role]="editable() ? null : 'list'"
4+
[attr.aria-label]="editable() ? null : (a11yWidgetsListLabel | translate)"
5+
>
26
<ng-container #gridstackContainer />
37
</div>

projects/dashboards-ng/src/components/gridstack-wrapper/si-gridstack-wrapper.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ describe('SiGridstackWrapperComponent', () => {
8383

8484
expect(fixture.debugElement.queryAll(By.css('si-widget-host')).length).toBe(2);
8585
});
86+
87+
it('should group widgets as a labeled list when not editable', () => {
88+
fixture = TestBed.createComponent(HostComponent);
89+
host = fixture.componentInstance;
90+
host.widgetCatalogMap = new Map([[TEST_WIDGET.id, TEST_WIDGET]]);
91+
host.widgets = [TEST_WIDGET_CONFIG_0, TEST_WIDGET_CONFIG_1];
92+
fixture.detectChanges();
93+
94+
const gridStack = fixture.nativeElement.querySelector('.grid-stack');
95+
expect(gridStack.getAttribute('role')).toBe('list');
96+
expect(gridStack.getAttribute('aria-label')).toBe('Dashboard widgets');
97+
});
8698
});
8799

88100
describe('updating grid items', () => {

projects/dashboards-ng/src/components/gridstack-wrapper/si-gridstack-wrapper.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ViewContainerRef,
2424
WritableSignal
2525
} from '@angular/core';
26+
import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate';
2627
import { GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions } from 'gridstack';
2728

2829
import { DEFAULT_GRIDSTACK_OPTIONS, GridConfig } from '../../model/gridstack.model';
@@ -42,6 +43,7 @@ export interface GridWrapperEvent {
4243

4344
@Component({
4445
selector: 'si-gridstack-wrapper',
46+
imports: [SiTranslatePipe],
4547
templateUrl: './si-gridstack-wrapper.component.html',
4648
styleUrl: './si-gridstack-wrapper.component.scss'
4749
})
@@ -90,6 +92,10 @@ export class SiGridstackWrapperComponent implements OnInit, OnChanges {
9092

9193
readonly gridstackContainer = viewChild('gridstackContainer', { read: ViewContainerRef });
9294

95+
protected readonly a11yWidgetsListLabel = t(
96+
() => $localize`:@@DASHBOARD.A11Y.WIDGETS_LIST:Dashboard widgets`
97+
);
98+
9399
private grid!: GridStack;
94100
private markedForRender: WidgetConfig[] = [];
95101
private readonly gridItems = signal<

projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,20 @@ describe('SiWidgetHostComponent', () => {
225225
expect(cardEl.getAttribute('tabindex')).toBeNull();
226226
});
227227

228+
it('should expose the host as a labeled list item when not editable', () => {
229+
fixture.componentRef.setInput('editable', false);
230+
fixture.detectChanges();
231+
expect(hostEl.getAttribute('role')).toBe('listitem');
232+
expect(hostEl.getAttribute('aria-roledescription')).toBe('widget');
233+
expect(hostEl.getAttribute('aria-label')).toBe('Test');
234+
});
235+
236+
it('should not expose list item semantics on the host when editable', () => {
237+
expect(hostEl.getAttribute('role')).toBeNull();
238+
expect(hostEl.getAttribute('aria-roledescription')).toBeNull();
239+
expect(hostEl.getAttribute('aria-label')).toBeNull();
240+
});
241+
228242
it('should activate keyboard mode on Enter', () => {
229243
cardEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
230244
expect(component.keyboardActive()).toBe(true);

projects/dashboards-ng/src/components/widget-host/si-widget-host.component.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ import { setupWidgetInstance } from '../../widget-loader';
5555
templateUrl: './si-widget-host.component.html',
5656
styleUrl: './si-widget-host.component.scss',
5757
host: {
58-
class: 'grid-stack-item'
58+
class: 'grid-stack-item',
59+
// In non-edit mode, expose each widget as a labeled list item so screen
60+
// readers group the widgets and let users jump between them. In edit mode
61+
// the inner card carries the interactive `application` role instead.
62+
'[attr.role]': "editable() ? null : 'listitem'",
63+
'[attr.aria-roledescription]': "editable() ? null : 'widget'",
64+
'[attr.aria-label]': 'editable() || !widgetAriaLabel() ? null : widgetAriaLabel()'
5965
}
6066
})
6167
export class SiWidgetHostComponent implements OnInit, OnChanges {
@@ -127,6 +133,13 @@ export class SiWidgetHostComponent implements OnInit, OnChanges {
127133
() =>
128134
$localize`:@@DASHBOARD.WIDGET.A11Y.DESCRIPTION:Press Enter or Space to activate. Then use arrow keys to move, Shift+arrow keys to resize, Escape to exit.`
129135
);
136+
protected readonly widgetAriaLabel = computed(() => {
137+
const widgetHeading = this.widgetConfig().heading;
138+
if (widgetHeading) {
139+
return this.translateService.translateSync(widgetHeading);
140+
}
141+
return null;
142+
});
130143

131144
widgetInstance?: WidgetInstance;
132145
widgetRef?: ComponentRef<WidgetInstance>;

projects/dashboards-ng/translate/si-translatable-keys.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Auto-generated file. Run 'npx update-translatable-keys' to update.
44

55
export interface SiTranslatableKeys {
6+
'DASHBOARD.A11Y.WIDGETS_LIST'?: string;
67
'DASHBOARD.ADD_WIDGET'?: string;
78
'DASHBOARD.CANCEL'?: string;
89
'DASHBOARD.EDIT'?: string;

0 commit comments

Comments
 (0)