Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
43db615
test(grids): check active descendant for focused/selected cells
ddaribo Jun 24, 2025
feefc10
fix(grids): active descendant points to the active column header
ddaribo Jun 24, 2025
2a350c4
Merge branch '20.0.x' into bpachilova/column-headers-a11y-15962
ddaribo Jun 25, 2025
e7c5d8f
fix(pivot): headers activedescendants & clean up redundant row/rowgro…
ddaribo Jun 26, 2025
f9c683d
test(pivot): column & row headers activedescendant
ddaribo Jun 26, 2025
aae6481
Merge branch '20.0.x' into bpachilova/column-headers-a11y-15962
ddaribo Jun 26, 2025
8d68f82
fix(pivot): correct header ID binding logic in template
ddaribo Jun 26, 2025
c3e6824
fix(cell): point describedby to correct ID and fix tests
ddaribo Jun 27, 2025
d7fd748
feat(grids): add aria-sort attribute to column headers + test checks
ddaribo Jun 27, 2025
a5c8d73
Merge branch '20.0.x' into bpachilova/column-headers-a11y-15962
ddaribo Jun 27, 2025
64f9aeb
fix(cell): add row and col index aria attrs
ddaribo Jun 30, 2025
2c9a5b3
feat(grid): correct aria attributes related to total rows/cols count …
ddaribo Jun 30, 2025
693a735
Merge branch '20.0.x' into bpachilova/column-headers-a11y-15962
ddaribo Jul 2, 2025
05faaa2
refactor(cell, header): use headerCell.id for aria-describedby
ddaribo Jul 21, 2025
5eccd74
refactor(grid-base): aria-rowcount calculation
ddaribo Jul 21, 2025
45bd8e3
Merge branch '20.0.x' into bpachilova/column-headers-a11y-15962
ddaribo Aug 7, 2025
293b082
Merge branch '20.0.x' into bpachilova/column-headers-a11y-15962
kacheshmarova Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion projects/igniteui-angular/src/lib/grids/cell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, is this because the column didn't have a field or because the description moved? Either way, while the header group list might be large in most cases, would generally avoid having getters for bindings that run on each change detection do any meaningful amount of work (like search) if it can be avoided.

if (this.isInvalid) {
describeBy += ' ' + this.ariaErrorMessage;
}
Expand Down Expand Up @@ -699,6 +700,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<any>;

Expand Down
19 changes: 13 additions & 6 deletions projects/igniteui-angular/src/lib/grids/grid-base.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,16 @@ 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 {
const totalRows = (this as any).totalItemCount ?? this.data?.length ?? 0;
return (this.paginator ? this._totalRecords : totalRows) + 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, (this as any)? Surely that's not needed 👀
Also, rows are manipulated not just by the paginator, so that might not be the only case. Was going to ask if dataView length might do the trick, but by the look of it we do want to announce the full data set?
If so, I'm not entirely sure the virtualization index (which I believe drive row index) will match that in all cases.
Also would filtering and grouping need to be involved to determine the final count? Because those will affect the row index regardless IIRC. Basically, anything in the pipeline that manipulates the data collection is before the for-of even gets the data.

It might be the simplest way to poke the for-of state for the length of items and only detect remote/paginated scenarios to look for _totalRecords / totalItemCount

}

/**
* Gets/Sets the resource strings.
Expand Down Expand Up @@ -2765,13 +2775,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 */
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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.', () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -401,24 +408,30 @@ 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;
UIInteractions.simulateClickAndSelectEvent(cell);
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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<igx-grid-header-row class="igx-grid-thead" tabindex="0"
[grid]="this"
[hasMRL]="hasColumnLayouts"
[activeDescendant]="activeDescendant"
[width]="calcWidth"
[pinnedColumnCollection]="pinnedColumns"
[unpinnedColumnCollection]="unpinnedColumns"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { FilteringLogic } from '../../data-operations/filtering-expression.inter
import { IgxTabContentComponent, IgxTabHeaderComponent, IgxTabItemComponent, IgxTabsComponent } from '../../tabs/tabs/public_api';
import { IgxGridRowComponent } from './grid-row.component';
import { ISortingExpression, SortingDirection } from '../../data-operations/sorting-strategy';
import { GRID_SCROLL_CLASS } from '../../test-utils/grid-functions.spec';
import { GRID_SCROLL_CLASS, GridFunctions } from '../../test-utils/grid-functions.spec';
import { AsyncPipe } from '@angular/common';
import { IgxPaginatorComponent, IgxPaginatorContentDirective } from '../../paginator/paginator.component';
import { IGridRowEventArgs, IgxColumnGroupComponent, IgxGridEmptyTemplateDirective, IgxGridFooterComponent, IgxGridLoadingTemplateDirective, IgxGridRow, IgxGroupByRow, IgxSummaryRow } from '../public_api';
Expand Down Expand Up @@ -2016,6 +2016,33 @@ describe('IgxGrid Component Tests #grid', () => {
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`));
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading