From e28267621a3bcb6665fa075468ecd529451306b6 Mon Sep 17 00:00:00 2001 From: Dominik Iwanek <141320833+dominikiwanekhyland@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:20:24 +0200 Subject: [PATCH] [ACS-12582] [ADW] Document list container is not accessible via keyboard navigation for file selection (#12210) --- .../datatable-row.component.spec.ts | 16 ++++++- .../datatable-row/datatable-row.component.ts | 7 ++- .../datatable/datatable.component.html | 4 +- .../datatable/datatable.component.spec.ts | 47 +++++++++++++++---- .../datatable/datatable.component.ts | 19 +++++++- 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.spec.ts b/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.spec.ts index 6eafac938a..c3ebf2aa63 100644 --- a/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.spec.ts +++ b/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.spec.ts @@ -87,12 +87,26 @@ describe('DataTableRowComponent', () => { expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBeNull(); }); - it('should not set tabindex when row is disabled', () => { + it('should set tabindex as focusable only with the arrow keys when row is enabled but not active', () => { component.disabled = false; fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBe('-1'); + }); + + it('should set tabindex as focusable with the Tab key when row is enabled and active', () => { + component.disabled = false; + component.active = true; + fixture.detectChanges(); expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBe('0'); }); + it('should not set tabindex when row is disabled even if it is active', () => { + component.disabled = true; + component.active = true; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBeNull(); + }); + it('should focus element', () => { expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(false); component.disabled = false; diff --git a/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.ts b/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.ts index 2bd96f418f..d5cb04ea4f 100644 --- a/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.ts +++ b/lib/core/src/lib/datatable/components/datatable-row/datatable-row.component.ts @@ -35,6 +35,8 @@ export class DataTableRowComponent implements FocusableOption { @Input() disabled = true; + @Input() active = false; + @Output() select: EventEmitter = new EventEmitter(); @@ -65,7 +67,10 @@ export class DataTableRowComponent implements FocusableOption { @HostBinding('attr.tabindex') get tabindex(): number | null { - return this.disabled ? null : 0; + if (this.disabled) { + return null; + } + return this.active ? 0 : -1; } @HostListener('keydown.space', ['$event']) diff --git a/lib/core/src/lib/datatable/components/datatable/datatable.component.html b/lib/core/src/lib/datatable/components/datatable/datatable.component.html index 547e3cc5bf..312b6e7ff7 100644 --- a/lib/core/src/lib/datatable/components/datatable/datatable.component.html +++ b/lib/core/src/lib/datatable/components/datatable/datatable.component.html @@ -262,7 +262,9 @@ [class.adf-datatable-row__dragging]="isDraggingRow" [attr.data-automation-id]="'datatable-row-' + idx" (contextmenu)="markRowAsContextMenuSource(row)" - [disabled]="!(enableDragRows || multiselect)" + (focus)="onRowFocus(idx)" + [disabled]="!isRowKeyboardNavigable()" + [active]="isRowActive(idx)" [attr.aria-description]=" (multiselect ? ('ADF-DATATABLE.ACCESSIBILITY.ROW_SELECTION' | translate) : '') + (multiselect && enableDragRows ? '. ' : '') + diff --git a/lib/core/src/lib/datatable/components/datatable/datatable.component.spec.ts b/lib/core/src/lib/datatable/components/datatable/datatable.component.spec.ts index acbfc23637..dfd18cf45c 100644 --- a/lib/core/src/lib/datatable/components/datatable/datatable.component.spec.ts +++ b/lib/core/src/lib/datatable/components/datatable/datatable.component.spec.ts @@ -1881,10 +1881,10 @@ describe('Accessibility', () => { const getBodyRows = (): DebugElement[] => testingUtils.getAllByCSS(rowSelector); - const expectRowsTabindex = (expected: string | null): void => { + const expectRowsTabindex = (expected: (string | null)[]): void => { const rowElements = getBodyRows(); - expect(rowElements.length).toBeGreaterThan(0); - expect(rowElements.every((row) => row.nativeElement.getAttribute('tabindex') === expected)).toBeTrue(); + expect(rowElements.length).toBe(expected.length); + expect(rowElements.map((row) => row.nativeElement.getAttribute('tabindex'))).toEqual(expected); }; const activateRow = (rowIndex: number): void => { @@ -1907,24 +1907,42 @@ describe('Accessibility', () => { dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]); }); - it('should set tabindex to null (disabled === true) on datatable-body rows when neither multiselect nor enableDragRows is enabled', () => { + it('should set tabindex to null (disabled === true) on datatable-body rows when rows cannot be selected nor dragged', () => { + dataTable.selectionMode = 'none'; setRows(); - expectRowsTabindex(null); + expectRowsTabindex([null, null]); }); - it('should set tabindex to 0 (disabled === false) on datatable-body rows when multiselect is enabled', () => { + it('should make only the first row reachable with the Tab key when rows are selectable', () => { + setRows(); + + expectRowsTabindex(['0', '-1']); + }); + + it('should make only the first row reachable with the Tab key when multiselect is enabled', () => { dataTable.multiselect = true; setRows(); - expectRowsTabindex('0'); + expectRowsTabindex(['0', '-1']); }); - it('should set tabindex to 0 (disabled === false) on datatable-body rows when enableDragRows is enabled', () => { + it('should make only the first row reachable with the Tab key when enableDragRows is enabled', () => { dataTable.enableDragRows = true; setRows(); - expectRowsTabindex('0'); + expectRowsTabindex(['0', '-1']); + }); + + it('should move the tabindex to the active row', () => { + setRows(); + dataTable.ngAfterViewInit(); + + activateRow(1); + testingUtils.setDebugElement(fixture.debugElement); + fixture.detectChanges(); + + expectRowsTabindex(['-1', '0']); }); it('should focus next row on ArrowDown event', () => { @@ -1954,6 +1972,17 @@ describe('Accessibility', () => { expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-0'); }); + + it('should navigate between rows with the arrow keys in single selection mode', () => { + dataTable.selectionMode = 'single'; + setRows(); + dataTable.ngAfterViewInit(); + + activateRow(1); + dispatchKeyUp(event); + + expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-0'); + }); }); describe('Row cells focus management', () => { diff --git a/lib/core/src/lib/datatable/components/datatable/datatable.component.ts b/lib/core/src/lib/datatable/components/datatable/datatable.component.ts index ef4bbf5f76..b853dee479 100644 --- a/lib/core/src/lib/datatable/components/datatable/datatable.component.ts +++ b/lib/core/src/lib/datatable/components/datatable/datatable.component.ts @@ -347,6 +347,10 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges, private readonly destroyRef = inject(DestroyRef); + private get rowsOffset(): number { + return this.isHeaderVisible() ? 1 : 0; + } + @HostListener('keyup', ['$event']) onKeydown(event: KeyboardEvent): void { if (event.shiftKey && this.enableDragRows) { @@ -640,7 +644,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges, } if (row) { - const rowIndex = this.data.getRows().indexOf(row) + (this.isHeaderVisible() ? 1 : 0); + const rowIndex = this.data.getRows().indexOf(row) + this.rowsOffset; this.keyManager.setActiveItem(rowIndex); const dataRowEvent = new DataRowEvent(row, mouseEvent, this); @@ -923,6 +927,19 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges, return this.selectionMode && this.selectionMode.toLowerCase() === 'multiple'; } + protected isRowKeyboardNavigable(): boolean { + return this.enableDragRows || this.multiselect || this.isSingleSelectionMode() || this.isMultiSelectionMode(); + } + + protected isRowActive(rowIndex: number): boolean { + const activeItemIndex = this.keyManager?.activeItemIndex ?? -1; + return activeItemIndex < this.rowsOffset ? rowIndex === 0 : activeItemIndex === rowIndex + this.rowsOffset; + } + + protected onRowFocus(rowIndex: number): void { + this.keyManager?.updateActiveItem(rowIndex + this.rowsOffset); + } + getRowStyle(row: DataRow): string { row.cssClass = row.cssClass ? row.cssClass : ''; this.rowStyleClass = this.rowStyleClass ? this.rowStyleClass : '';