diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.ts b/projects/igniteui-angular/src/lib/grids/cell.component.ts index 6efcdc9e210..588481a16c8 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/cell.component.ts @@ -703,6 +703,17 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT } } + @HostBinding('attr.aria-rowindex') + protected get ariaRowIndex(): number { + // +2 because aria-rowindex is 1-based and the first row is the header + return this.rowIndex + 2; + } + + @HostBinding('attr.aria-colindex') + protected get ariaColIndex(): number { + return this.column.index + 1; + } + @ViewChild('defaultCell', { read: TemplateRef, static: true }) protected defaultCellTemplate: TemplateRef; diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 8db7949bf1f..c18341a7159 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -1839,6 +1839,15 @@ export abstract class IgxGridBaseDirective implements GridType, @HostBinding('class.igx-grid') protected baseClass = 'igx-grid'; + @HostBinding('attr.aria-colcount') + protected get ariaColCount(): number { + return this.visibleColumns.length; + } + + @HostBinding('attr.aria-rowcount') + protected get ariaRowCount(): number { + return this._rendered ? this._rowCount : null; + } /** * Gets/Sets the resource strings. @@ -2765,13 +2774,10 @@ export abstract class IgxGridBaseDirective implements GridType, public get activeDescendant() { const activeElem = this.navigation.activeNode; - if (!activeElem || !Object.keys(activeElem).length) { - return this.id; + if (!activeElem || !Object.keys(activeElem).length || activeElem.row < 0) { + return null; } - - return activeElem.row < 0 ? - `${this.id}_${activeElem.row}_${activeElem.mchCache.level}_${activeElem.column}` : - `${this.id}_${activeElem.row}_${activeElem.column}`; + return `${this.id}_${activeElem.row}_${activeElem.column}`; } /** @hidden @internal */ @@ -3302,6 +3308,7 @@ export abstract class IgxGridBaseDirective implements GridType, private _sortDescendingHeaderIconTemplate: TemplateRef = null; private _gridSize: Size = Size.Large; private _defaultRowHeight = 50; + private _rowCount: number; /** * @hidden @internal @@ -4123,6 +4130,7 @@ export abstract class IgxGridBaseDirective implements GridType, if (this.hasColumnsToAutosize) { this.autoSizeColumnsInView(); } + this._calculateRowCount(); this._rendered = true; }); Promise.resolve().then(() => this.rendered.next(true)); @@ -6765,6 +6773,7 @@ export abstract class IgxGridBaseDirective implements GridType, this.initColumns(this._columns, (col: IgxColumnComponent) => this.columnInit.emit(col)); this.columnListDiffer.diff(this.columnList); + this._calculateRowCount(); this.columnList.changes .pipe(takeUntil(this.destroy$)) @@ -7988,4 +7997,15 @@ export abstract class IgxGridBaseDirective implements GridType, return recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree; } } + + private _calculateRowCount(): void { + if (this.verticalScrollContainer?.isRemote) { + this._rowCount = this.verticalScrollContainer.totalItemCount ?? 0; + } else if (this.paginator) { + this._rowCount = this.totalRecords ?? 0; + } else { + this._rowCount = this.verticalScrollContainer?.igxForOf?.length ?? 0; + } + this._rowCount += 1; // include header row + } } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav-headers.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav-headers.spec.ts index d7a37146f98..f68e82eb0df 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav-headers.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav-headers.spec.ts @@ -122,11 +122,13 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('OnPTO', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press arrow right again UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); for (let index = 5; index > 1; index--) { UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); @@ -135,6 +137,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { } header = GridFunctions.getColumnHeader('ParentID', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate to first/last header', async () => { @@ -145,6 +148,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press end key UIInteractions.triggerEventHandlerKeyDown('End', gridHeader); @@ -155,6 +159,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { expect(header).toBeTruthy(); expect(grid.navigation.activeNode.column).toEqual(5); expect(grid.navigation.activeNode.row).toEqual(-1); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press Home ket UIInteractions.triggerEventHandlerKeyDown('home', gridHeader); @@ -165,6 +170,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { expect(header).toBeTruthy(); expect(grid.navigation.activeNode.column).toEqual(0); expect(grid.navigation.activeNode.row).toEqual(-1); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press Ctrl+ Arrow right UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader, false, false, true); @@ -175,6 +181,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { expect(header).toBeTruthy(); expect(grid.navigation.activeNode.column).toEqual(5); expect(grid.navigation.activeNode.row).toEqual(-1); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press Ctrl+ Arrow left UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader, false, false, true); @@ -185,6 +192,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { expect(header).toBeTruthy(); expect(grid.navigation.activeNode.column).toEqual(0); expect(grid.navigation.activeNode.row).toEqual(-1); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should not change active header on arrow up or down pressed', () => { @@ -201,24 +209,28 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press arrow up key UIInteractions.triggerEventHandlerKeyDown('ArrowUp', gridHeader, false, false, true); fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press pageUp key UIInteractions.triggerEventHandlerKeyDown('PageUp', gridHeader); fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Press pageDown key UIInteractions.triggerEventHandlerKeyDown('PageUp', gridHeader); fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('Verify navigation when there are pinned columns', async () => { @@ -232,6 +244,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused let header = GridFunctions.getColumnHeader('ParentID', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Navigate to last cell UIInteractions.triggerEventHandlerKeyDown('End', gridHeader); @@ -242,6 +255,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { expect(header).toBeTruthy(); expect(grid.navigation.activeNode.column).toEqual(5); expect(grid.navigation.activeNode.row).toEqual(-1); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Click on the pinned column header = GridFunctions.getColumnHeader('ParentID', fix); @@ -258,6 +272,8 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('OnPTO', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); + const hScroll = grid.headerContainer.getScroll().scrollLeft; // Navigate with home key @@ -722,6 +738,24 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { expect(grid.groupingExpressions[0].strategy).toBeUndefined(); expect(grid.groupingExpressions[0].groupingComparer).toEqual(comparer); }); + it('should set aria-activedescendant to the currently focused header', async () => { + let header = GridFunctions.getColumnHeader('ID', fix); + UIInteractions.simulateClickAndSelectEvent(header); + await wait(DEBOUNCETIME); + fix.detectChanges(); + + GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); + + UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); + await wait(DEBOUNCETIME); + fix.detectChanges(); + + header = GridFunctions.getColumnHeader('ParentID', fix); + + GridFunctions.verifyHeaderIsFocused(header.parent) + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); + }); }); describe('MRL Headers Navigation', () => { @@ -757,6 +791,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); await wait(DEBOUNCETIME); @@ -764,6 +799,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('City', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); await wait(DEBOUNCETIME); @@ -771,6 +807,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Country', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); await wait(DEBOUNCETIME); @@ -778,6 +815,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Phone', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); await wait(DEBOUNCETIME); @@ -785,6 +823,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Country', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); await wait(DEBOUNCETIME); @@ -792,6 +831,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('City', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); await wait(DEBOUNCETIME); @@ -799,6 +839,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('CompanyName', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate through a layout with right and left arrow keys in second level', async () => { @@ -808,6 +849,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); await wait(DEBOUNCETIME); @@ -815,6 +857,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('City', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); await wait(DEBOUNCETIME); @@ -822,6 +865,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Fax', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); await wait(DEBOUNCETIME); @@ -829,6 +873,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('City', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); await wait(DEBOUNCETIME); @@ -836,6 +881,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('ContactTitle', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); await wait(DEBOUNCETIME); @@ -843,6 +889,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('ContactName', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate through a layout with home and end keys', async () => { @@ -852,6 +899,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader, false, false, true); await wait(DEBOUNCETIME); @@ -859,6 +907,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Fax', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader, false, false, true); await wait(DEBOUNCETIME); @@ -866,6 +915,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('ContactName', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); header = GridFunctions.getColumnHeader('Address', fix); UIInteractions.simulateClickAndSelectEvent(header); @@ -877,6 +927,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Fax', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('home', gridHeader); await wait(DEBOUNCETIME); @@ -884,6 +935,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { header = GridFunctions.getColumnHeader('Address', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate through a layout with up and down arrow keys', () => { @@ -893,38 +945,44 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowUp', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('CompanyName', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ContactTitle', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Address', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowUp', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ContactTitle', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); - it('should focus the first element when focus the header', () => { + it('should focus the first element when focus the header', async () => { gridHeader.nativeElement.focus(); //('focus', null); fix.detectChanges(); const header = GridFunctions.getColumnHeader('CompanyName', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); }); @@ -961,40 +1019,47 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ID', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('Address Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ID', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('General Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('General Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate through groups with right and left arrow keys in child level', () => { @@ -1004,54 +1069,63 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ID', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Region', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Country', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('City Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Country', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Region', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ID', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('ContactTitle', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate through groups with Home and End keys', () => { @@ -1061,18 +1135,21 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowRight', gridHeader, false, false, true); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('Address Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', gridHeader, false, false, true); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('General Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); header = GridFunctions.getColumnHeader('City', fix); UIInteractions.simulateClickAndSelectEvent(header); @@ -1080,18 +1157,21 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('Home', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('CompanyName', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('End', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Address', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should navigate through groups with arrowUp and down keys', () => { @@ -1101,47 +1181,55 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { // Verify first header is focused GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowUp', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('City Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowUp', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('Country Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowUp', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('Address Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('Country Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnGroupHeaderCell('City Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('City', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // click on parent header = GridFunctions.getColumnGroupHeaderCell('Address Information', fix); @@ -1150,11 +1238,14 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { fix.detectChanges(); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', gridHeader); fix.detectChanges(); header = GridFunctions.getColumnHeader('Region', fix); GridFunctions.verifyHeaderIsFocused(header.parent); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); }); it('should focus the first element when focus the header', () => { @@ -1163,6 +1254,7 @@ describe('IgxGrid - Headers Keyboard navigation #grid', () => { let header = GridFunctions.getColumnGroupHeaderCell('General Information', fix); GridFunctions.verifyHeaderIsFocused(header); + GridFunctions.verifyHeaderActiveDescendant(gridHeader, header.nativeElement.id); // Verify children are not focused header = GridFunctions.getColumnGroupHeaderCell('Person Details', fix); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav.spec.ts index b5d9f025d2d..974219ee406 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-keyBoardNav.spec.ts @@ -43,7 +43,7 @@ describe('IgxGrid - Keyboard navigation #grid', () => { let selectedCell: CellType; grid.selected.subscribe((event: IGridCellEventArgs) => { - selectedCell = event.cell; + selectedCell = grid.gridAPI.get_cell_by_index(event.cell.row.index, event.cell.column.field); }); // Focus and select first cell @@ -54,30 +54,34 @@ describe('IgxGrid - Keyboard navigation #grid', () => { expect(selectedCell.value).toEqual(2); expect(selectedCell.column.field).toMatch('ID'); + GridFunctions.verifyGridContentActiveDescendant(gridContent, selectedCell.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('arrowright', gridContent); fix.detectChanges(); expect(selectedCell.value).toEqual('Gilberto Todd'); expect(selectedCell.column.field).toMatch('Name'); + GridFunctions.verifyGridContentActiveDescendant(gridContent, selectedCell.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('arrowup', gridContent); fix.detectChanges(); expect(selectedCell.value).toEqual('Casey Houston'); expect(selectedCell.column.field).toMatch('Name'); + GridFunctions.verifyGridContentActiveDescendant(gridContent, selectedCell.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('arrowleft', gridContent); fix.detectChanges(); expect(selectedCell.value).toEqual(1); expect(selectedCell.column.field).toMatch('ID'); + GridFunctions.verifyGridContentActiveDescendant(gridContent, selectedCell.nativeElement.id); }); it('should jump to first/last cell with Ctrl', () => { let selectedCell: CellType; grid.selected.subscribe((event: IGridCellEventArgs) => { - selectedCell = event.cell; + selectedCell = grid.gridAPI.get_cell_by_index(event.cell.row.index, event.cell.column.field); }); GridFunctions.focusFirstCell(fix, grid); @@ -87,12 +91,14 @@ describe('IgxGrid - Keyboard navigation #grid', () => { expect(selectedCell.value).toEqual('Company A'); expect(selectedCell.column.field).toMatch('Company'); + GridFunctions.verifyGridContentActiveDescendant(gridContent, selectedCell.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('arrowleft', gridContent, false, false, true); fix.detectChanges(); expect(selectedCell.value).toEqual(1); expect(selectedCell.column.field).toMatch('ID'); + GridFunctions.verifyGridContentActiveDescendant(gridContent, selectedCell.nativeElement.id); }); it('should allow vertical keyboard navigation in pinned area.', () => { @@ -256,6 +262,7 @@ describe('IgxGrid - Keyboard navigation #grid', () => { expect(cell).toBeDefined(); GridSelectionFunctions.verifyCellActive(cell); GridSelectionFunctions.verifyCellSelected(cell); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); }); it('should allow navigating down', async () => { @@ -401,7 +408,7 @@ describe('IgxGrid - Keyboard navigation #grid', () => { fix.detectChanges(); const rows = GridFunctions.getRows(fix); - const cell = grid.gridAPI.get_cell_by_index(3, '1'); + let cell = grid.gridAPI.get_cell_by_index(3, '1'); const bottomRowHeight = rows[4].nativeElement.offsetHeight; const displayContainer = GridFunctions.getGridDisplayContainer(fix).nativeElement; const bottomCellVisibleHeight = displayContainer.parentElement.offsetHeight % bottomRowHeight; @@ -409,16 +416,22 @@ describe('IgxGrid - Keyboard navigation #grid', () => { await wait(); fix.detectChanges(); - expect(fix.componentInstance.selectedCell.value).toEqual(30); - expect(fix.componentInstance.selectedCell.column.field).toMatch('1'); + let selectedCell = fix.componentInstance.selectedCell; + expect(selectedCell.value).toEqual(30); + expect(selectedCell.column.field).toMatch('1'); + cell = grid.gridAPI.get_cell_by_index(selectedCell.row.index, selectedCell.column.field); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); UIInteractions.triggerEventHandlerKeyDown('arrowdown', gridContent); await wait(DEBOUNCETIME); fix.detectChanges(); + selectedCell = fix.componentInstance.selectedCell; expect(parseInt(displayContainer.style.top, 10)).toBeLessThanOrEqual(-1 * (grid.rowHeight - bottomCellVisibleHeight)); expect(displayContainer.parentElement.scrollTop).toEqual(0); - expect(fix.componentInstance.selectedCell.value).toEqual(40); - expect(fix.componentInstance.selectedCell.column.field).toMatch('1'); + expect(selectedCell.value).toEqual(40); + expect(selectedCell.column.field).toMatch('1'); + cell = grid.gridAPI.get_cell_by_index(selectedCell.row.index, selectedCell.column.field); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); }); it('should scroll into view the not fully visible cells when navigating up', async () => { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-mrl-keyboard-nav.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-mrl-keyboard-nav.spec.ts index 13fa654dde0..dcffd2d8e89 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-mrl-keyboard-nav.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-mrl-keyboard-nav.spec.ts @@ -290,8 +290,11 @@ describe('IgxGrid Multi Row Layout - Keyboard navigation #grid', () => { GridFunctions.simulateGridContentKeydown(fix, 'ArrowUp'); fix.detectChanges(); - expect(fix.componentInstance.selectedCell.value).toEqual(fix.componentInstance.data[0].City); - expect(fix.componentInstance.selectedCell.column.field).toMatch('City'); + const selectedCell = fix.componentInstance.selectedCell; + expect(selectedCell.value).toEqual(fix.componentInstance.data[0].City); + expect(selectedCell.column.field).toMatch('City'); + const cell = fix.componentInstance.grid.gridAPI.get_cell_by_index(selectedCell.row.index, selectedCell.column.field); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); }); it('should navigate up correctly', () => { @@ -794,15 +797,20 @@ describe('IgxGrid Multi Row Layout - Keyboard navigation #grid', () => { fix.detectChanges(); // check correct cell has focus - const cell2 = grid.getCellByColumn(0, 'ID'); + let cell2 = grid.getCellByColumn(0, 'ID'); expect(cell2.active).toBe(true); + let cellElement = fix.componentInstance.grid.gridAPI.get_cell_by_index(cell2.row.index, cell2.column.field).nativeElement; + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cellElement.id); // arrow right GridFunctions.simulateGridContentKeydown(fix, 'ArrowRight'); fix.detectChanges(); // check correct cell has focus - expect(grid.getCellByColumn(0, 'Address').active).toBe(true); + cell2 = grid.getCellByColumn(0, 'Address'); + expect(cell2.active).toBe(true); + cellElement = fix.componentInstance.grid.gridAPI.get_cell_by_index(cell2.row.index, cell2.column.field).nativeElement; + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cellElement.id); }); }); @@ -1914,6 +1922,7 @@ describe('IgxGrid Multi Row Layout - Keyboard navigation #grid', () => { // check next cell is active and is fully in view cell = grid.gridAPI.get_cell_by_index(2, 'Phone'); expect(cell.active).toBe(true); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); expect(grid.verticalScrollContainer.getScroll().scrollTop).toBeGreaterThan(50); let diff = grid.gridAPI.get_cell_by_index(2, 'Phone') .nativeElement.getBoundingClientRect().bottom - grid.tbody.nativeElement.getBoundingClientRect().bottom; @@ -1932,6 +1941,7 @@ describe('IgxGrid Multi Row Layout - Keyboard navigation #grid', () => { // check next cell is active and is fully in view cell = grid.gridAPI.get_cell_by_index(0, 'ContactName'); expect(cell.active).toBe(true); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); expect(grid.verticalScrollContainer.getScroll().scrollTop).toBe(0); diff = grid.gridAPI.get_cell_by_index(0, 'ContactName') .nativeElement.getBoundingClientRect().top - grid.tbody.nativeElement.getBoundingClientRect().top; @@ -1950,6 +1960,7 @@ describe('IgxGrid Multi Row Layout - Keyboard navigation #grid', () => { // check next cell is active and is fully in view cell = grid.gridAPI.get_cell_by_index(2, 'Address'); expect(cell.active).toBe(true); + GridFunctions.verifyGridContentActiveDescendant(GridFunctions.getGridContent(fix), cell.nativeElement.id); expect(grid.verticalScrollContainer.getScroll().scrollTop).toBeGreaterThan(50); diff = grid.gridAPI.get_cell_by_index(2, 'Address') .nativeElement.getBoundingClientRect().bottom - grid.tbody.nativeElement.getBoundingClientRect().bottom; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html index 38c010bae76..d8e35ec2aac 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -17,7 +17,6 @@ { expect(grid.columns[1].field).toBe('firstName'); expect(grid.columns[2].field).toBe('lastName'); })); + + it('should set correct aria attributes related to total rows/cols count and indexes', async () => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + fix.componentInstance.initColumnsRows(80, 20); + fix.detectChanges(); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const gridHeader = GridFunctions.getGridHeader(grid); + const headerRowElement = gridHeader.nativeElement.querySelector('[role="row"]'); + + grid.navigateTo(50, 16); + fix.detectChanges(); + await wait(); + fix.detectChanges(); + + expect(headerRowElement.getAttribute('aria-rowindex')).toBe('1'); + expect(grid.nativeElement.getAttribute('aria-rowcount')).toBe('81'); + expect(grid.nativeElement.getAttribute('aria-colcount')).toBe('20'); + + const cell = grid.gridAPI.get_cell_by_index(50, 'col16'); + // The following attributes indicate to assistive technologies which portions + // of the content are displayed in case not all are rendered, + // such as with the built-in virtualization of the grid. 1-based index. + expect(cell.nativeElement.getAttribute('aria-rowindex')).toBe('52'); + expect(cell.nativeElement.getAttribute('aria-colindex')).toBe('17'); + }); }); describe('IgxGrid - min/max width constraints rules', () => { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts index 3611ecc5a7d..1477bd3648e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts @@ -41,6 +41,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { spyOn(grid.sortingDone, 'emit').and.callThrough(); const currentColumn = 'Name'; const lastNameColumn = 'LastName'; + const nameHeaderCell = GridFunctions.getColumnHeader(currentColumn, fixture); grid.sort({ fieldName: currentColumn, dir: SortingDirection.Asc, ignoreCase: false }); tick(30); fixture.detectChanges(); @@ -50,6 +51,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { sortingExpressions: grid.sortingExpressions, owner: grid }); + expect(nameHeaderCell.attributes['aria-sort']).toEqual('ascending'); expect(grid.getCellByColumn(0, currentColumn).value).toEqual('ALex'); expect(grid.getCellByColumn(0, lastNameColumn).value).toEqual('Smith'); @@ -72,6 +74,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { it('Should sort grid descending by column name', () => { const currentColumn = 'Name'; + const nameHeaderCell = GridFunctions.getColumnHeader(currentColumn, fixture); // Ignore case on sorting set to false grid.sort({ fieldName: currentColumn, dir: SortingDirection.Desc, ignoreCase: false }); fixture.detectChanges(); @@ -79,6 +82,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { expect(grid.getCellByColumn(0, currentColumn).value).toEqual('Rick'); expect(grid.getCellByColumn(grid.data.length - 1, currentColumn).value).toEqual('ALex'); + expect(nameHeaderCell.attributes['aria-sort']).toEqual('descending'); // Ignore case on sorting set to true grid.sort({ fieldName: currentColumn, dir: SortingDirection.Desc, ignoreCase: true }); @@ -476,6 +480,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { sortingExpressions: grid.sortingExpressions, owner: grid }); + expect(firstHeaderCell.attributes['aria-sort']).toEqual('ascending'); const firstRowFirstCell = GridFunctions.getCurrentCellFromGrid(grid, 0, 0); const firstRowSecondCell = GridFunctions.getCurrentCellFromGrid(grid, 0, 1); @@ -506,6 +511,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { sortingExpressions: grid.sortingExpressions, owner: grid }); + expect(firstHeaderCell.attributes['aria-sort']).toEqual('ascending'); GridFunctions.clickHeaderSortIcon(firstHeaderCell); tick(30); @@ -516,6 +522,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { sortingExpressions: grid.sortingExpressions, owner: grid }); + expect(firstHeaderCell.attributes['aria-sort']).toEqual('descending'); const firstRowFirstCell = GridFunctions.getCurrentCellFromGrid(grid, 0, 0); const firstRowSecondCell = GridFunctions.getCurrentCellFromGrid(grid, 0, 1); @@ -562,6 +569,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { expect(GridFunctions.getColumnSortingIndex(GridFunctions.getColumnHeader('ID', fixture))).toBeNull(); expect(grid.sorting.emit).toHaveBeenCalledTimes(3); expect(grid.sortingDone.emit).toHaveBeenCalledTimes(3); + expect(firstHeaderCell.attributes['aria-sort']).toEqual(undefined); })); it('Should have a valid sorting icon when sorting using the API.', () => { diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html b/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html index 0532a6916af..2aef2f1f20b 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html @@ -43,9 +43,13 @@
-
+
@if (grid.moving && grid.columnInDrag && pinnedColumnCollection.length <= 0) { diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header-row.component.ts b/projects/igniteui-angular/src/lib/grids/headers/grid-header-row.component.ts index 3a33a198ec8..756c61d0afa 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header-row.component.ts @@ -4,6 +4,7 @@ import { Component, DoCheck, ElementRef, + HostBinding, Input, QueryList, TemplateRef, @@ -52,8 +53,15 @@ export class IgxGridHeaderRowComponent implements DoCheck { @Input() public unpinnedColumnCollection: ColumnType[] = []; - @Input() - public activeDescendant: string; + @HostBinding('attr.aria-activedescendant') + public get activeDescendant() { + const activeElem = this.navigation.activeNode; + + if (!activeElem || !Object.keys(activeElem).length || activeElem.row >= 0) { + return null; + } + return `${this.grid.id}_${activeElem.row}_${activeElem.level}_${activeElem.column}`; + } @Input({ transform: booleanAttribute }) public hasMRL: boolean; diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts index c691ba3c8d3..f9b319eeab4 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts @@ -55,6 +55,13 @@ export class IgxGridHeaderComponent implements DoCheck, OnDestroy { @ViewChild('sortIconContainer', { read: ElementRef }) protected sortIconContainer: ElementRef; + /** + * @hidden + */ + @Input() + @HostBinding('attr.id') + public id: string; + /** * Returns the `aria-selected` of the header. */ @@ -63,6 +70,31 @@ export class IgxGridHeaderComponent implements DoCheck, OnDestroy { return this.column.selected; } + /** + * Returns the `aria-sort` of the header. + */ + @HostBinding('attr.aria-sort') + public get ariaSort() { + return this.sortDirection === SortingDirection.Asc ? 'ascending' + : this.sortDirection === SortingDirection.Desc ? 'descending' : null; + } + + /** + * @hidden + */ + @HostBinding('attr.aria-colindex') + public get ariaColIndx() { + return this.column.index + 1; + } + + /** + * @hidden + */ + @HostBinding('attr.aria-rowindex') + public get ariaRowIndx() { + return 1; + } + @HostBinding('class.igx-grid-th') public get columnGroupStyle() { return !this.column.columnGroup; diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html index d5f31ada6b4..ae82a0155dd 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html @@ -4,7 +4,6 @@ { const icon = GridFunctions.getHeaderSortIcon(childHeader); expect(icon).not.toBeNull(); expect(icon.nativeElement.textContent.toLowerCase().trim()).toBe('arrow_downward'); + expect(childHeader.attributes['aria-sort']).toEqual('descending'); })); }); diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.spec.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.spec.ts index f0dd4cc9d53..69f87fbed2b 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.spec.ts @@ -604,7 +604,8 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { it('should update aria-activeDescendants when navigating around', () => { hierarchicalGrid.cellSelection = 'single'; - expect(hierarchicalGrid.tbody.nativeElement.attributes['aria-activedescendant'].value).toEqual(hierarchicalGrid.id); + // aria-activedescendant on the tbody should not be defined unless a cell among it is active + expect(hierarchicalGrid.tbody.nativeElement.attributes['aria-activedescendant']).not.toBeDefined(); let cellElem = (hierarchicalGrid.gridAPI.get_row_by_index(0).cells as QueryList).toArray()[1]; UIInteractions.simulatePointerOverElementEvent('pointerdown', cellElem.nativeElement); @@ -616,13 +617,11 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { fixture.detectChanges(); const childGrid = hierarchicalGrid.getChildGrids()[0]; - expect(childGrid.tbody.nativeElement.attributes['aria-activedescendant'].value).toEqual(childGrid.id); cellElem = (childGrid.gridAPI.get_row_by_index(0).cells as QueryList).toArray()[1]; UIInteractions.simulatePointerOverElementEvent('pointerdown', cellElem.nativeElement); fixture.detectChanges(); - expect(hierarchicalGrid.tbody.nativeElement.attributes['aria-activedescendant'].value).toEqual(hierarchicalGrid.id); expect(childGrid.tbody.nativeElement.attributes['aria-activedescendant'].value).toEqual(`${childGrid.id}_0_1`); }); diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid-keyboard-nav.spec.ts b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid-keyboard-nav.spec.ts index 316aeb956a9..dd9c96e349a 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid-keyboard-nav.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid-keyboard-nav.spec.ts @@ -6,6 +6,9 @@ import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestBaseComponent } from import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { IgxPivotGridComponent } from './pivot-grid.component'; import { IgxPivotRowDimensionHeaderComponent } from './pivot-row-dimension-header.component'; +import { DebugElement } from '@angular/core'; +import { IgxPivotHeaderRowComponent } from './pivot-header-row.component'; +import { PivotRowLayoutType } from 'igniteui-angular'; const DEBOUNCE_TIME = 250; const PIVOT_TBODY_CSS_CLASS = '.igx-grid__tbody'; @@ -13,11 +16,15 @@ const PIVOT_ROW_DIMENSION_CONTENT = 'igx-pivot-row-dimension-content'; const PIVOT_HEADER_ROW = 'igx-pivot-header-row'; const HEADER_CELL_CSS_CLASS = '.igx-grid-th'; const ACTIVE_CELL_CSS_CLASS = '.igx-grid-th--active'; +const CSS_CLASS_ROW_DIMENSION_CONTAINER = '.igx-grid__tbody-pivot-dimension' +const CSS_CLASS_TBODY_CONTENT = '.igx-grid__tbody-content'; describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { describe('General Keyboard Navigation', () => { let fixture: ComponentFixture; let pivotGrid: IgxPivotGridComponent; + let rowDimension: DebugElement; + let headerRow: DebugElement; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -28,10 +35,14 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { }).compileComponents(); })); - beforeEach(fakeAsync(() => { + beforeEach(fakeAsync(async () => { fixture = TestBed.createComponent(IgxPivotGridMultipleRowComponent); fixture.detectChanges(); pivotGrid = fixture.componentInstance.pivotGrid; + await fixture.whenStable(); + rowDimension = fixture.debugElement.query( + By.css(CSS_CLASS_ROW_DIMENSION_CONTAINER)); + headerRow = fixture.debugElement.query(By.directive(IgxPivotHeaderRowComponent)); })); it('should allow navigating between row headers', () => { @@ -43,12 +54,18 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(firstCell.parent); - let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); + // for the row dimensions headers, the active descendant is set on the div having + // tabindex="0" and class '.igx-grid__tbody-pivot-dimension'; + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, firstCell.nativeElement.id); + expect(firstCell.nativeElement.getAttribute('role')).toBe('rowheader'); + let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', firstCell.nativeElement); fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(secondCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, secondCell.nativeElement.id); + expect(firstCell.nativeElement.getAttribute('role')).toBe('rowheader'); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); @@ -56,11 +73,13 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { UIInteractions.simulateClickAndSelectEvent(firstCell); fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(firstCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, firstCell.nativeElement.id); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); UIInteractions.triggerKeyDownEvtUponElem('h', firstCell.nativeElement); fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(firstCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, firstCell.nativeElement.id); }); it('should not go outside of the boundaries of the row dimensions content', () => { @@ -75,6 +94,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(firstCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, firstCell.nativeElement.id); let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); @@ -85,6 +105,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(thirdCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, thirdCell.nativeElement.id); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); }); @@ -100,12 +121,14 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { UIInteractions.triggerKeyDownEvtUponElem('End', firstCell.nativeElement); fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(thirdCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, thirdCell.nativeElement.id); let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); UIInteractions.triggerKeyDownEvtUponElem('Home', thirdCell.nativeElement); fixture.detectChanges(); GridFunctions.verifyHeaderIsFocused(firstCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, firstCell.nativeElement.id); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); }); @@ -124,6 +147,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { By.directive(IgxPivotRowDimensionHeaderComponent)); const lastCell = allGroups[allGroups.length - 1]; GridFunctions.verifyHeaderIsFocused(lastCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, lastCell.nativeElement.id); const activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); }); @@ -143,6 +167,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { By.directive(IgxPivotRowDimensionHeaderComponent)); const firstCell = allGroups[0]; GridFunctions.verifyHeaderIsFocused(firstCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, firstCell.nativeElement.id); let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); @@ -156,6 +181,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { By.directive(IgxPivotRowDimensionHeaderComponent)); const secondCell = allGroups.filter(x => x.componentInstance.column.field === 'ProductCategory')[1]; GridFunctions.verifyHeaderIsFocused(secondCell.parent); + GridFunctions.verifyPivotElementActiveDescendant(rowDimension, secondCell.nativeElement.id); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); @@ -170,7 +196,9 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { firstHeader = fixture.debugElement.queryAll( By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[0]; GridFunctions.verifyHeaderIsFocused(firstHeader.parent); - let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); + // for the column headers, the active descendant is set on the header row element + GridFunctions.verifyPivotElementActiveDescendant(headerRow, firstHeader.nativeElement.id); + let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', pivotGrid.theadRow.nativeElement); @@ -179,6 +207,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { const secondHeader = fixture.debugElement.queryAll( By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[1]; GridFunctions.verifyHeaderIsFocused(secondHeader.parent); + GridFunctions.verifyPivotElementActiveDescendant(headerRow, secondHeader.nativeElement.id); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); }); @@ -193,6 +222,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[0]; GridFunctions.verifyHeaderIsFocused(firstHeader.parent); + GridFunctions.verifyPivotElementActiveDescendant(headerRow, firstHeader.nativeElement.id); let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); @@ -204,6 +234,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`)); const lastHeader = allHeaders[allHeaders.length - 1]; GridFunctions.verifyHeaderIsFocused(lastHeader.parent); + GridFunctions.verifyPivotElementActiveDescendant(headerRow, lastHeader.nativeElement.id); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); }); @@ -222,7 +253,8 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { firstHeader = fixture.debugElement.queryAll( By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[0]; GridFunctions.verifyHeaderIsFocused(firstHeader.parent); - let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); + GridFunctions.verifyPivotElementActiveDescendant(headerRow, firstHeader.nativeElement.id); + let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', firstHeader.nativeElement); @@ -230,12 +262,49 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { const secondHeader = fixture.debugElement.queryAll( By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[1]; GridFunctions.verifyHeaderIsFocused(secondHeader.parent); + GridFunctions.verifyPivotElementActiveDescendant(headerRow, secondHeader.nativeElement.id); + activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); + expect(activeCells.length).toBe(1); + }); + + it('should navigate properly among row dimension column headers for horizontal row layout', () => { + pivotGrid.pivotUI = { + ...pivotGrid.pivotUI, + rowLayout: PivotRowLayoutType.Horizontal, + showRowHeaders: true + }; + fixture.detectChanges(); + + let firstHeader = fixture.debugElement.queryAll( + By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[0]; + UIInteractions.simulateClickAndSelectEvent(firstHeader); + fixture.detectChanges(); + + firstHeader = fixture.debugElement.queryAll( + By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[0]; + GridFunctions.verifyHeaderIsFocused(firstHeader.parent); + // for the row dimensions column headers in horizontal layout, + // the active descendant is set on the header row element. + GridFunctions.verifyPivotElementActiveDescendant(headerRow, firstHeader.nativeElement.id); + expect(firstHeader.nativeElement.getAttribute('role')).toBe('columnheader'); + let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); + expect(activeCells.length).toBe(1); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', pivotGrid.theadRow.nativeElement); + fixture.detectChanges(); + + const secondHeader = fixture.debugElement.queryAll( + By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`))[1]; + GridFunctions.verifyHeaderIsFocused(secondHeader.parent); + GridFunctions.verifyPivotElementActiveDescendant(headerRow, secondHeader.nativeElement.id); + expect(firstHeader.nativeElement.getAttribute('role')).toBe('columnheader'); activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`)); expect(activeCells.length).toBe(1); }); it('should allow navigating within the cells of the body', async () => { const cell = pivotGrid.rowList.first.cells.first; + const tBodyContent = fixture.debugElement.query(By.css(CSS_CLASS_TBODY_CONTENT)); GridFunctions.focusFirstCell(fixture, pivotGrid); fixture.detectChanges(); expect(pivotGrid.navigation.activeNode.row).toBeUndefined(); @@ -248,8 +317,11 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { fixture.detectChanges(); expect(pivotGrid.navigation.activeNode.row).toBeDefined(); expect(pivotGrid.navigation.activeNode.column).toBeDefined(); + // The activedescendant attribute for cells in the grid body + // is set on the tbody content div with tabindex='0' + GridFunctions.verifyPivotElementActiveDescendant(tBodyContent, cell.nativeElement.id); - let activeCells = fixture.debugElement.queryAll(By.css(`.igx-grid__td--active`)); + let activeCells = fixture.debugElement.queryAll(By.css(`.igx-grid__td--active`)); expect(activeCells.length).toBe(1); expect(cell.column.field).toEqual('Stanley-UnitsSold'); @@ -261,6 +333,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { activeCells = fixture.debugElement.queryAll(By.css(`.igx-grid__td--active`)); expect(activeCells.length).toBe(1); expect(activeCells[0].componentInstance.column.field).toEqual('Stanley-UnitPrice') + GridFunctions.verifyPivotElementActiveDescendant(tBodyContent, activeCells[0].nativeElement.id); }); }); describe('Row Dimension Expand/Collapse Keyboard Interactions', () => { @@ -282,7 +355,7 @@ describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => { it('should allow row dimension expand(Alt + ArrowDown/ArrowRight) and collapse(Alt + ArrowUp/ArrowLeft)', async () => { const rowDimension = fixture.debugElement.queryAll( - By.css(`.igx-grid__tbody-pivot-dimension`)); + By.css(CSS_CLASS_ROW_DIMENSION_CONTAINER)); let allHeaders = fixture.debugElement.queryAll( By.directive(IgxPivotRowDimensionHeaderComponent)); diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html index 9ca659192f9..1841f3b0371 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html @@ -5,7 +5,6 @@ -
@@ -139,7 +138,8 @@ @for (dim of rowDimensions; track dim.memberName; let dimIndex = $index) { -
+
-
+
@if (dataView | pivotGridHorizontalRowGrouping:pivotConfiguration:pipeTrigger:regroupTrigger; as groupedData) { -
+
@if ((columnDimensions.length > 0 || values.length > 0) && data.length > 0) { - ; + /** + * @hidden @internal + */ + @ViewChild(IgxPivotRowDimensionMrlRowComponent, { read: IgxPivotRowDimensionMrlRowComponent }) + public rowDimensionMrlComponent: IgxPivotRowDimensionMrlRowComponent; + /** * @hidden @internal */ @@ -2072,17 +2077,27 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni /** @hidden @internal */ public override get activeDescendant() { - const activeElem = this.navigation.activeNode; - if ((this.navigation as IgxPivotGridNavigationService).isRowHeaderActive || - (this.navigation as IgxPivotGridNavigationService).isRowDimensionHeaderActive) { - if (!activeElem || !Object.keys(activeElem).length) { - return this.id; - } + if (this.navigation.isRowHeaderActive || this.navigation.isRowDimensionHeaderActive) { + return null; + } + return super.activeDescendant; + } - return `${this.id}_${activeElem.row}_${activeElem.column}`; + /** @hidden @internal */ + public get headerRowActiveDescendant() { + const activeElem = this.navigation.activeNode; + if (!activeElem || !Object.keys(activeElem).length || !this.navigation.isRowHeaderActive) { + return null; } - return super.activeDescendant; + const rowDimensions = this.rowDimensionContentCollection.length > 0 ? + this.rowDimensionContentCollection.toArray() : + this.rowDimensionMrlComponent.rowDimensionContentCollection.toArray(); + + const rowDimensionContentActive = rowDimensions.find(rd => rd && rd.headerGroups?.some(hg => hg.active)); + const activeHeader = rowDimensionContentActive?.headerGroups.toArray().find(hg => hg.active); + + return activeHeader ? `${this.id}_${activeHeader.title}` : null; } protected resolveToggle(groupColumn: IgxColumnComponent, state: boolean) { diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.spec.ts b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.spec.ts index 18b73905396..9ca902a7e02 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.spec.ts @@ -1398,6 +1398,7 @@ describe('IgxPivotGrid #pivotGrid', () => { let expectedOrder = [829, undefined, 240, 293, 296]; let columnValues = pivotGrid.dataView.map(x => (x as IPivotGridRecord).aggregationValues.get('USA-UnitsSold')); expect(columnValues).toEqual(expectedOrder); + expect(headerCell.attributes['aria-sort']).toBe('ascending'); headerCell = GridFunctions.getColumnHeader('USA-UnitsSold', fixture); // sort desc @@ -1407,6 +1408,7 @@ describe('IgxPivotGrid #pivotGrid', () => { expectedOrder = [829, 296, 293, 240, undefined]; columnValues = pivotGrid.dataView.map(x => (x as IPivotGridRecord).aggregationValues.get('USA-UnitsSold')); expect(columnValues).toEqual(expectedOrder); + expect(headerCell.attributes['aria-sort']).toBe('descending'); // remove sort headerCell = GridFunctions.getColumnHeader('USA-UnitsSold', fixture); diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-header-row.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-header-row.component.html index 289bdae4020..6585cf8c3e5 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-header-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-header-row.component.html @@ -194,7 +194,7 @@
+ [class.igx-grid__tr--mrl]="hasMRL">
@@ -248,7 +248,7 @@ } @if (grid.pivotUI.showRowHeaders && grid.rowDimensions.length > 0) { -
+
@for (dim of grid.visibleRowDimensions; track dim; let colIndex = $index; let isLast = $last) { @if (getRowDimensionColumn(dim); as dimCol) { ; + public rowDimensionHeaders: QueryList; public override get headerForOf() { return this.headerContainers?.last; } + @HostBinding('attr.aria-activedescendant') + public override get activeDescendant(): string { + const activeElem = this.navigation.activeNode; + if (!activeElem || !Object.keys(activeElem).length || this.grid.navigation.headerRowActiveDescendant) { + return null; + } + + if (this.navigation.isRowDimensionHeaderActive) { + const activeHeader = this.grid.theadRow.rowDimensionHeaders.find(h => h.active); + if (activeHeader) { + const key = activeHeader.title ?? activeHeader.rootDimension?.memberName; + return key ? `${this.grid.id}_${key}` : null; + } + return null; + } + + return super.activeDescendant; + } + constructor( @Inject(IGX_GRID_BASE) public override grid: PivotGridType, ref: ElementRef, diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-content.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-content.component.html index 381eec6315f..f1ff0f00e2c 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-content.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-content.component.html @@ -1,4 +1,4 @@ -
} , diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-mrl-row.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-mrl-row.component.html index 7e917a0eeaf..d52e66262da 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-mrl-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-mrl-row.component.html @@ -2,7 +2,7 @@ cell of rowGroup | pivotGridHorizontalRowCellMerging:grid.pivotConfiguration:grid.pipeTrigger; track getGroupKey(cell); let cellIndex = $index ) { - ; @HostBinding('class.igx-grid__tbody-pivot-dimension') public pivotDim = true; diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-header-group.component.ts b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-header-group.component.ts index 4af9f47c472..a1838ab37bb 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-header-group.component.ts +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-header-group.component.ts @@ -31,6 +31,13 @@ export class IgxPivotRowHeaderGroupComponent extends IgxGridHeaderGroupComponent @HostBinding('style.user-select') public userSelect = 'none'; + /** + * @hidden + */ + public get role(): string { + return 'columnheader'; + } + constructor(private cdRef: ChangeDetectorRef, @Inject(IGX_GRID_BASE) public override grid: PivotGridType, private elementRef: ElementRef, diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts index 9d51e633787..6c99d5707f9 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts @@ -26,12 +26,15 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { it('should sort descending all treeGrid levels by column name through API', () => { treeGrid.sort({ fieldName: 'Name', dir: SortingDirection.Desc, ignoreCase: false, strategy: DefaultSortingStrategy.instance() }); + const nameHeaderCell = GridFunctions.getColumnHeader('Name', fix); + fix.detectChanges(); // Verify first level records are desc sorted expect(treeGrid.getCellByColumn(0, 'Name').value).toEqual('Yang Wang'); expect(treeGrid.getCellByColumn(1, 'Name').value).toEqual('John Winchester'); expect(treeGrid.getCellByColumn(8, 'Name').value).toEqual('Ana Sanders'); + expect(nameHeaderCell.attributes['aria-sort']).toEqual('descending'); // Verify second level records are desc sorted expect(treeGrid.getCellByColumn(2, 'Name').value).toEqual('Thomas Hardy'); @@ -205,6 +208,7 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { expect(treeGrid.getCellByColumn(0, 'Name').value).toEqual('Yang Wang'); expect(treeGrid.getCellByColumn(1, 'Name').value).toEqual('John Winchester'); expect(treeGrid.getCellByColumn(8, 'Name').value).toEqual('Ana Sanders'); + expect(header.attributes['aria-sort']).toEqual('descending'); // Verify second level records are desc sorted expect(treeGrid.getCellByColumn(2, 'Name').value).toEqual('Thomas Hardy'); @@ -226,6 +230,7 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { expect(treeGrid.getCellByColumn(0, 'Age').value).toEqual(42); expect(treeGrid.getCellByColumn(2, 'Age').value).toEqual(55); expect(treeGrid.getCellByColumn(9, 'Age').value).toEqual(61); + expect(header.attributes['aria-sort']).toEqual('ascending'); // Verify second level records are asc sorted expect(treeGrid.getCellByColumn(3, 'Age').value).toEqual(29); diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index e6689a977de..4f84dcda2a4 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts @@ -273,6 +273,25 @@ export class GridFunctions { expect(header.nativeElement.classList.contains(ACTIVE_HEADER_CLASS)).toBe(focused); } + public static verifyPivotElementActiveDescendant(elem: DebugElement, id: string): void { + const activeDescendant = elem.nativeElement.getAttribute('aria-activedescendant'); + expect(activeDescendant).toBe(id); + } + + public static verifyHeaderActiveDescendant(headerRow: IgxGridHeaderRowComponent, id: string): void { + const headerRowElem = headerRow.nativeElement; + expect(headerRow.activeDescendant).toBe(id); + const activeDescendant = headerRowElem.getAttribute('aria-activedescendant'); + expect(activeDescendant).toBe(id); + } + + public static verifyGridContentActiveDescendant(gridContent: DebugElement, id: string): void { + const gridContentElem = gridContent.nativeElement; + expect(gridContent.componentInstance.activeDescendant).toBe(id); + const activeDescendant = gridContentElem.getAttribute('aria-activedescendant'); + expect(activeDescendant).toBe(id); + } + public static getCurrentCellFromGrid(grid, row, cell) { const gridRow = grid.rowList.toArray()[row]; const gridCell = gridRow.cells.toArray()[cell]; @@ -1808,7 +1827,7 @@ export class GridSummaryFunctions { } export class GridSelectionFunctions { public static selectCellsRange = - async (fix, startCell, endCell, ctrl = false, shift = false) => { + async (fix, startCell, endCell, ctrl = false, shift = false) => { UIInteractions.simulatePointerOverElementEvent('pointerdown', startCell.nativeElement, shift, ctrl); fix.detectChanges(); await wait(); @@ -1894,6 +1913,10 @@ export class GridSelectionFunctions { expect(selectedCellFromGrid.value).toEqual(cell.value); expect(selectedCellFromGrid.column.field).toMatch(cell.column.field); expect(selectedCellFromGrid.row.index).toEqual(cell.row.index); + // Check if the cell id is assigned as the active descendant of the grid content + const cellElement = cell.grid.gridAPI.get_cell_by_index(cell.row.index, cell.column.field).nativeElement; + const gridContent = GridFunctions.getGridContent(fix); + GridFunctions.verifyGridContentActiveDescendant(gridContent, cellElement.id); } public static verifyRowSelected(row, selected = true, hasCheckbox = true) { diff --git a/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts index 67cca900537..b2c60567777 100644 --- a/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts @@ -22,6 +22,7 @@ export const TREE_CELL_LOADING_CSS_CLASS = '.igx-grid__tree-loading-indicator'; export const NUMBER_CELL_CSS_CLASS = 'igx-grid__td--number'; export const CELL_VALUE_DIV_CSS_CLASS = '.igx-grid__td-text'; export const ROW_EDITING_BANNER_OVERLAY_CLASS = 'igx-overlay__content'; +export const TREE_GRID_CONTENT_CLASS = '.igx-grid__tbody-content'; export class TreeGridFunctions { public static getHeaderRow(fix) { @@ -415,6 +416,16 @@ export class TreeGridFunctions { expect(selectedCell.column.field).toEqual(cell.column.field); expect(selectedCell.row.index).toEqual(cell.row.index); expect(selectedCell.value).toEqual(cell.value); + + // Verify the selected cell is the active descendant of the content element. + let cellElement = cell.nativeElement; + if (cell instanceof IgxGridCellComponent) { + cellElement = treeGrid.gridAPI.get_cell_by_index(cell.row.index, cell.column.field).nativeElement; + } + expect(treeGrid.activeDescendant).toEqual(cellElement.id); + const gridContentEl = treeGrid.nativeElement.querySelector(TREE_GRID_CONTENT_CLASS); + const activeDescendant = gridContentEl.getAttribute('aria-activedescendant'); + expect(activeDescendant).toBe(cellElement.id); } } }