From 43db6150b87cffbe58c4734ae9382a3183307e95 Mon Sep 17 00:00:00 2001 From: ddaribo Date: Tue, 24 Jun 2025 14:40:20 +0300 Subject: [PATCH 1/8] test(grids): check active descendant for focused/selected cells --- .../grid/grid-keyBoardNav-headers.spec.ts | 94 ++++++++++++++++++- .../lib/grids/grid/grid-keyBoardNav.spec.ts | 27 ++++-- .../grids/grid/grid-mrl-keyboard-nav.spec.ts | 19 +++- .../hierarchical-grid.spec.ts | 5 +- .../src/lib/test-utils/grid-functions.spec.ts | 18 ++++ 5 files changed, 148 insertions(+), 15 deletions(-) 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/hierarchical-grid/hierarchical-grid.spec.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.spec.ts index c4186f15c9f..2bd139f4bd6 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/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index e6689a977de..d9838053a87 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,20 @@ export class GridFunctions { expect(header.nativeElement.classList.contains(ACTIVE_HEADER_CLASS)).toBe(focused); } + 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]; @@ -1894,6 +1908,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) { From feefc108c69699c2641ffa1ce258a108edd43618 Mon Sep 17 00:00:00 2001 From: ddaribo Date: Tue, 24 Jun 2025 14:43:07 +0300 Subject: [PATCH 2/8] fix(grids): active descendant points to the active column header --- .../src/lib/grids/grid-base.directive.ts | 9 +++------ .../src/lib/grids/grid/grid.component.html | 1 - .../grids/headers/grid-header-group.component.html | 3 ++- .../lib/grids/headers/grid-header-group.component.ts | 1 - .../lib/grids/headers/grid-header-row.component.ts | 12 ++++++++++-- .../hierarchical-grid.component.html | 1 - .../src/lib/test-utils/tree-grid-functions.spec.ts | 12 +++++++++++- 7 files changed, 26 insertions(+), 13 deletions(-) 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 5213151e54e..9fe59834396 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2765,13 +2765,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 */ 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 @@ = 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/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 @@ Date: Thu, 26 Jun 2025 13:14:46 +0300 Subject: [PATCH 3/8] fix(pivot): headers activedescendants & clean up redundant row/rowgroup roles --- .../pivot-grid/pivot-grid.component.html | 16 +++++---- .../grids/pivot-grid/pivot-grid.component.ts | 33 ++++++++++++++----- .../pivot-header-row.component.html | 4 +-- .../pivot-grid/pivot-header-row.component.ts | 25 ++++++++++++-- ...pivot-row-dimension-content.component.html | 2 +- ...-row-dimension-header-group.component.html | 4 +-- ...ot-row-dimension-header-group.component.ts | 7 ++++ ...pivot-row-dimension-mrl-row.component.html | 2 +- .../pivot-row-dimension-mrl-row.component.ts | 2 ++ .../pivot-row-header-group.component.ts | 7 ++++ 10 files changed, 77 insertions(+), 25 deletions(-) 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-header-row.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-header-row.component.html index b238803b861..b4bc14149d7 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, From f9c683d2da8a8ace4e16fbcbec09b5bca15b9fb2 Mon Sep 17 00:00:00 2001 From: ddaribo Date: Thu, 26 Jun 2025 13:16:27 +0300 Subject: [PATCH 4/8] test(pivot): column & row headers activedescendant --- .../pivot-grid-keyboard-nav.spec.ts | 85 +++++++++++++++++-- .../src/lib/test-utils/grid-functions.spec.ts | 7 +- 2 files changed, 85 insertions(+), 7 deletions(-) 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/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index d9838053a87..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,11 @@ 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); @@ -1822,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(); From 8d68f829dc6dc4b2c95bafa07f17f21ec7f24f09 Mon Sep 17 00:00:00 2001 From: ddaribo Date: Thu, 26 Jun 2025 13:55:45 +0300 Subject: [PATCH 5/8] fix(pivot): correct header ID binding logic in template --- .../pivot-grid/pivot-row-dimension-header-group.component.html | 2 +- .../src/lib/test-utils/tree-grid-functions.spec.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-header-group.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-header-group.component.html index 1bd0d228e9f..9f29a9c3d74 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-header-group.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row-dimension-header-group.component.html @@ -16,7 +16,7 @@ Date: Fri, 27 Jun 2025 16:35:01 +0300 Subject: [PATCH 6/8] fix(cell): point describedby to correct ID and fix tests --- .../igniteui-angular/src/lib/grids/cell.component.ts | 3 ++- .../src/lib/grids/grid/grid.pinning.spec.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.ts b/projects/igniteui-angular/src/lib/grids/cell.component.ts index eaacd3064e5..ca41d8fbd5e 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/cell.component.ts @@ -517,7 +517,8 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT /** @hidden @internal */ @HostBinding('attr.aria-describedby') public get ariaDescribeBy() { - let describeBy = (this.gridID + '_' + this.column.field).replace('.', '_'); + let describeBy = this.grid.headerGroupsList + .find(hg => hg.column.field === this.column.field)?.headerID || ''; if (this.isInvalid) { describeBy += ' ' + this.ariaErrorMessage; } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pinning.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pinning.spec.ts index 7fb0029c904..0611448d855 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pinning.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pinning.spec.ts @@ -577,8 +577,10 @@ describe('IgxGrid - Column Pinning #grid', () => { GridSelectionFunctions.verifyRowHasCheckbox(row); expect(GridFunctions.getRowDisplayContainer(fix, 0)).toBeDefined(); - expect(row.children[2].getAttribute('aria-describedby')).toBe(grid.id + '_CompanyName'); - expect(row.children[3].getAttribute('aria-describedby')).toBe(grid.id + '_ContactName'); + const companyNameHeader = GridFunctions.getColumnHeader('CompanyName', fix); + const contactNameHeader = GridFunctions.getColumnHeader('ContactName', fix); + expect(row.children[2].getAttribute('aria-describedby')).toBe(companyNameHeader.nativeElement.id); + expect(row.children[3].getAttribute('aria-describedby')).toBe(contactNameHeader.nativeElement.id); // check scrollbar DOM const scrBarStartSection = fix.debugElement.query(By.css(`${GRID_SCROLL_CLASS}-start`)); @@ -695,7 +697,8 @@ describe('IgxGrid - Column Pinning #grid', () => { for (let i = 0; i <= pinnedCols.length - 1; i++) { const elem = row.children[i + 1]; expect(parseInt((elem as any).style.left, 10)).toBe(-330); - expect(elem.getAttribute('aria-describedby')).toBe(grid.id + '_' + pinnedCols[i].field); + const cellColumnHeader = GridFunctions.getColumnHeader(pinnedCols[i].field, fix); + expect(elem.getAttribute('aria-describedby')).toBe(cellColumnHeader.nativeElement.id); } // check correct headers have left border From d7fd748f5793fbb89762a608ad3ce0fc0119a96c Mon Sep 17 00:00:00 2001 From: ddaribo Date: Fri, 27 Jun 2025 17:03:08 +0300 Subject: [PATCH 7/8] feat(grids): add aria-sort attribute to column headers + test checks --- .../src/lib/grids/grid/grid.sorting.spec.ts | 8 ++++++++ .../src/lib/grids/headers/grid-header.component.ts | 9 +++++++++ .../hierarchical-grid.integration.spec.ts | 1 + .../src/lib/grids/pivot-grid/pivot-grid.spec.ts | 2 ++ .../src/lib/grids/tree-grid/tree-grid-sorting.spec.ts | 5 +++++ 5 files changed, 25 insertions(+) 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.component.ts b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts index c691ba3c8d3..b87bbbb2420 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 @@ -63,6 +63,15 @@ 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; + } + @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.integration.spec.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts index 6dcf55889e9..9b0937c4fcc 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts @@ -342,6 +342,7 @@ describe('IgxHierarchicalGrid Integration #hGrid', () => { 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/pivot-grid/pivot-grid.spec.ts b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.spec.ts index 30777c67385..3715c72954b 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/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); From 64f9aeb1aa59b6223a2f396196626074d169bfcb Mon Sep 17 00:00:00 2001 From: ddaribo Date: Mon, 30 Jun 2025 15:45:08 +0300 Subject: [PATCH 8/8] fix(cell): add row and col index aria attrs --- .../src/lib/grids/cell.component.ts | 10 +++++++++ .../src/lib/grids/grid/grid.component.spec.ts | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.ts b/projects/igniteui-angular/src/lib/grids/cell.component.ts index ca41d8fbd5e..b3cb2c0c0d1 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/cell.component.ts @@ -700,6 +700,16 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT } } + @HostBinding('attr.aria-rowindex') + protected get ariaRowIndex(): number { + return this.rowIndex + 1; + } + + @HostBinding('attr.aria-colindex') + protected get ariaColIndex(): number { + return this.visibleColumnIndex + 1; + } + @ViewChild('defaultCell', { read: TemplateRef, static: true }) protected defaultCellTemplate: TemplateRef; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts index 1e62728eb04..819d8f9d688 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts @@ -2016,6 +2016,28 @@ describe('IgxGrid Component Tests #grid', () => { expect(grid.columns[1].field).toBe('firstName'); expect(grid.columns[2].field).toBe('lastName'); })); + + it('should specify the correct aria-rowindex and aria-colindex attributes for cells', async () => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + fix.componentInstance.initColumnsRows(80, 20); + fix.detectChanges(); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + grid.navigateTo(50, 16); + fix.detectChanges(); + await wait(); + fix.detectChanges(); + + 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('51'); + expect(cell.nativeElement.getAttribute('aria-colindex')).toBe('17'); + + }); }); describe('IgxGrid - min/max width constraints rules', () => {