Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4c0b9fd
Initial plan
Copilot Jan 29, 2026
a68cd61
Initial exploration - understanding accessibility issue
Copilot Jan 29, 2026
86b36f5
Fix ARIA accessibility violations in grid components
Copilot Jan 29, 2026
9256ecc
fix(grids): improve ARIA role hierarchy in grid composite widgets
viktorkombov Mar 12, 2026
3f97adf
fix(pivot-grid): enhance accessibility by adding ARIA roles to row gr…
viktorkombov Mar 13, 2026
7adbc82
fix(grid-header): add ARIA role to column headers for improved access…
viktorkombov Mar 13, 2026
6838acd
chore(*): enable trial watermark
viktorkombov Mar 13, 2026
d92e3c8
Merge branch 'master' of https://github.com/IgniteUI/igniteui-angular…
viktorkombov Mar 13, 2026
ce0233d
chore(*): mark row role HostBinding as @hidden @internal
viktorkombov Mar 13, 2026
2a847fb
test(grids): update unit tests to match updated ARIA roles
viktorkombov Mar 13, 2026
c1ce108
Potential fix for pull request finding 'Unused variable, import, func…
viktorkombov Mar 13, 2026
f3199d1
Merge branch 'master' into copilot/fix-accessibility-issues
ChronosSF Mar 18, 2026
6d571a0
refactor: move role="presentation" to base IgxGroupByAreaDirective
Copilot Mar 18, 2026
0c45706
Merge branch 'master' into copilot/fix-accessibility-issues
ChronosSF Mar 19, 2026
2e31d3a
chore(*): updating package lock for ci
ChronosSF Mar 19, 2026
29c9b7f
Merge branch 'master' into copilot/fix-accessibility-issues
ChronosSF Mar 23, 2026
f046b23
Merge branch 'master' into copilot/fix-accessibility-issues
kdinev Apr 17, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit, DoC
return !this.column.grid.hasColumnLayouts ? this.column.isFirstPinned : false;;
}

@HostBinding('attr.role')
public role = 'gridcell';

private baseClass = 'igx-grid__filtering-cell-indicator';

constructor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export abstract class IgxGroupByAreaDirective {
@HostBinding('class.igx-grid-grouparea')
public defaultClass = true;

/**
* @hidden @internal
*/
@HostBinding('attr.role')
public role = 'presentation';

/** The parent grid containing the component. */
@Input()
public grid: FlatGridType | GridType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div role="rowgroup" class="igx-grid-thead__wrapper" (scroll)="scroll($event)" [style.width.px]="width"
<div class="igx-grid-thead__wrapper" (scroll)="scroll($event)" [style.width.px]="width"
[class.igx-grid__tr--mrl]="hasMRL">

<!-- Column headers area -->
Expand All @@ -16,7 +16,7 @@

<!-- Row dragging area -->
@if (grid.rowDraggable) {
<div #headerDragContainer class="igx-grid__drag-indicator igx-grid__tr-action" (pointerdown)="$event.preventDefault()" [class.igx-grid__drag-indicator--header]="!grid.isRowSelectable">
<div #headerDragContainer class="igx-grid__drag-indicator igx-grid__tr-action" role="columnheader" (pointerdown)="$event.preventDefault()" [class.igx-grid__drag-indicator--header]="!grid.isRowSelectable">
<div style="visibility: hidden;">
<ng-container *ngTemplateOutlet="grid.dragIndicatorIconTemplate || grid.dragIndicatorIconBase"></ng-container>
</div>
Expand All @@ -25,7 +25,7 @@

<!-- Row selectors area -->
@if (grid.showRowSelectors) {
<div #headerSelectorContainer class="igx-grid__cbx-selection igx-grid__tr-action"
<div #headerSelectorContainer class="igx-grid__cbx-selection igx-grid__tr-action" role="columnheader"
[class.igx-grid__cbx-selection--push]="grid.filteringService.isFilterRowVisible"
(click)="headerRowSelection($event)"
(pointerdown)="$event.preventDefault()">
Comment thread
viktorkombov marked this conversation as resolved.
Expand All @@ -41,6 +41,7 @@
(click)="grid.toggleAll()"
(pointerdown)="$event.preventDefault()"
[hidden]="!grid.hasExpandableChildren || !grid.hasVisibleColumns"
role="columnheader"
[ngClass]="{
'igx-grid__hierarchical-expander igx-grid__hierarchical-expander--header igx-grid__tr-action': grid.hasExpandableChildren,
'igx-grid__hierarchical-expander--push': grid.filteringService.isFilterRowVisible,
Expand All @@ -55,7 +56,8 @@
@if (grid?.groupingExpressions?.length) {
<div #headerGroupContainer class="{{ indentationCSSClasses }}"
(click)="grid.toggleAllGroupRows()"
(pointerdown)="$event.preventDefault()">
(pointerdown)="$event.preventDefault()"
role="columnheader">
<ng-container *ngTemplateOutlet="grid.iconTemplate; context: { $implicit: grid }"></ng-container>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export class IgxGridHeaderRowComponent implements DoCheck {
@Input()
public unpinnedColumnCollection: ColumnType[] = [];

/**
* @hidden @internal
*/
@HostBinding('attr.role')
public role = 'rowgroup';

@HostBinding('attr.aria-activedescendant')
public get activeDescendant() {
const activeElem = this.navigation.activeNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ export class IgxGridToolbarComponent implements OnDestroy {
@HostBinding('class.igx-grid-toolbar')
public defaultStyle = true;

/**
* @hidden
* @internal
*/
@HostBinding('attr.role')
public role = 'presentation';

protected _grid: GridType;
protected sub: Subscription;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2452,6 +2452,11 @@ describe('IgxGrid - Row Selection #grid', () => {
expect(rowSelector.textContent).toBe('CUSTOM SELECTOR: 0');
expect(groupRowSelector.textContent).toBe('CUSTOM GROUP SELECTOR');
expect(headerSelector.textContent).toBe('CUSTOM HEADER SELECTOR');

// ARIA: row-selector wrappers must have the correct cell roles
expect(rowSelector.getAttribute('role')).toBe('gridcell');
expect(groupRowSelector.getAttribute('role')).toBe('gridcell');
expect(headerSelector.getAttribute('role')).toBe('columnheader');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
</ng-template>
<ng-template #defaultTemp>
@if (rowDraggable) {
<div [class]="resolveDragIndicatorClasses" [igxRowDrag]="this" (click)="$event.stopPropagation()" [ghostTemplate]="this.grid.getDragGhostCustomTemplate()">
<div [class]="resolveDragIndicatorClasses" role="gridcell" [igxRowDrag]="this" (click)="$event.stopPropagation()" [ghostTemplate]="this.grid.getDragGhostCustomTemplate()">
<ng-container *ngTemplateOutlet="this.grid.dragIndicatorIconTemplate ? this.grid.dragIndicatorIconTemplate : this.grid.dragIndicatorIconBase"></ng-container>
</div>
}
@if (this.showRowSelectors) {
<div class="igx-grid__cbx-selection igx-grid__tr-action" (pointerdown)="$event.preventDefault()" (click)="onRowSelectorClick($event)">
<div class="igx-grid__cbx-selection igx-grid__tr-action" role="gridcell" (pointerdown)="$event.preventDefault()" (click)="onRowSelectorClick($event)">
<ng-template *ngTemplateOutlet="
this.grid.rowSelectorTemplate ? this.grid.rowSelectorTemplate : rowSelectorBaseTemplate;
context: { $implicit: { index: viewIndex, rowID: key, key, selected: selected }}">
Expand Down
12 changes: 6 additions & 6 deletions projects/igniteui-angular/grids/grid/src/grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
>
</igx-grid-header-row>

<div igxGridBody (keydown.control.c)="copyHandler($event)" (copy)="copyHandler($event)" class="igx-grid__tbody" role="rowgroup">
<div class="igx-grid__tbody-content" tabindex="0" [attr.role]="dataView.length ? null : 'row'" (keydown)="navigation.handleNavigation($event)" (focus)="navigation.focusTbody($event)"
<div igxGridBody (keydown.control.c)="copyHandler($event)" (copy)="copyHandler($event)" class="igx-grid__tbody" role="presentation">
<div class="igx-grid__tbody-content" tabindex="0" [attr.role]="dataView.length ? 'rowgroup' : 'row'" (keydown)="navigation.handleNavigation($event)" (focus)="navigation.focusTbody($event)"
(dragStop)="selectionService.dragMode = $event" (scroll)="preventContainerScroll($event)"
(dragScroll)="dragScroll($event)" [igxGridDragSelect]="selectionService.dragMode"
[style.height.px]="totalHeight" [style.width.px]="totalCalcWidth" [style.width]="!platform.isBrowser ? '100%' : undefined" #tbody [attr.aria-activedescendant]="activeDescendant">
Expand Down Expand Up @@ -186,8 +186,8 @@
</div>


<div class="igx-grid__tfoot" role="rowgroup" [style.height.px]="summaryRowHeight" #tfoot>
<div tabindex="0" (focus)="navigation.focusFirstCell(false)" (keydown)="navigation.summaryNav($event)" [attr.aria-activedescendant]="activeDescendant">
<div class="igx-grid__tfoot" role="presentation" [style.height.px]="summaryRowHeight" #tfoot>
<div tabindex="0" role="rowgroup" (focus)="navigation.focusFirstCell(false)" (keydown)="navigation.summaryNav($event)" [attr.aria-activedescendant]="activeDescendant">
@if (hasSummarizedColumns && rootSummariesEnabled) {
<igx-grid-summary-row [style.width.px]="calcWidth" [style.height.px]="summaryRowHeight"
[gridID]="id" role="row"
Expand All @@ -200,7 +200,7 @@
</div>
</div>

<div class="igx-grid__scroll" [style.height.px]="scrollSize" #scr [hidden]="isHorizontalScrollHidden" (pointerdown)="$event.preventDefault()">
<div class="igx-grid__scroll" role="presentation" [style.height.px]="scrollSize" #scr [hidden]="isHorizontalScrollHidden" (pointerdown)="$event.preventDefault()">
<div class="igx-grid__scroll-start" [style.width.px]="pinnedStartWidth" [style.min-width.px]="pinnedStartWidth"></div>
<div class="igx-grid__scroll-main" [style.width.px]="unpinnedWidth">
<ng-template igxGridFor [igxGridForOf]="EMPTY_DATA" #scrollContainer>
Expand All @@ -209,7 +209,7 @@
<div class="igx-grid__scroll-end" [style.float]="'right'" [style.width.px]="pinnedEndWidth" [style.min-width.px]="pinnedEndWidth" [hidden]="pinnedEndWidth === 0"></div>
</div>

<div class="igx-grid__footer" #footer>
<div class="igx-grid__footer" role="presentation" #footer>
<ng-content select="igx-grid-footer,igc-grid-footer"></ng-content>
<ng-content select="igx-paginator,igc-paginator"></ng-content>
</div>
Expand Down
31 changes: 29 additions & 2 deletions projects/igniteui-angular/grids/grid/src/grid.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,9 @@ describe('IgxGrid Component Tests #grid', () => {
fixture.componentInstance.generateData(30);
fixture.detectChanges();
tick(100);
// Checks if igx-grid__tbody-content attribute is null when there is data in the grid
// With data, igx-grid__tbody-content is the rowgroup focus host
const container = fixture.nativeElement.querySelectorAll('.igx-grid__tbody-content')[0];
expect(container.getAttribute('role')).toBe(null);
expect(container.getAttribute('role')).toBe('rowgroup');

//Filter grid so no results are available and grid is empty
grid.filter('index','111',IgxStringFilteringOperand.instance().condition('contains'),true);
Expand All @@ -339,6 +339,33 @@ describe('IgxGrid Component Tests #grid', () => {

}));

it('should have correct ARIA role structure on tbody and tfoot', fakeAsync(() => {
const fixture = TestBed.createComponent(IgxGridTestComponent);
fixture.componentInstance.columns[0].hasSummary = true;

fixture.componentInstance.generateData(30);
fixture.detectChanges();
tick(100);

// Outer tbody wrapper is layout-only
const tbodyWrapper = fixture.nativeElement.querySelector('.igx-grid__tbody');
expect(tbodyWrapper.getAttribute('role')).toBe('presentation');

// Inner focus host is the rowgroup
const tbodyContent = fixture.nativeElement.querySelector('.igx-grid__tbody-content');
expect(tbodyContent.getAttribute('role')).toBe('rowgroup');
expect(tbodyContent.getAttribute('tabindex')).toBe('0');

// Outer tfoot wrapper is layout-only
const tfootWrapper = fixture.nativeElement.querySelector('.igx-grid__tfoot');
expect(tfootWrapper.getAttribute('role')).toBe('presentation');

// Inner tfoot div is the rowgroup focus host
const tfootContent = fixture.nativeElement.querySelector('.igx-grid__tfoot > div');
expect(tfootContent.getAttribute('role')).toBe('rowgroup');
expect(tfootContent.getAttribute('tabindex')).toBe('0');
}));

it('should render empty message', fakeAsync(() => {
const fixture = TestBed.createComponent(IgxGridTestComponent);
fixture.componentInstance.data = [];
Expand Down
50 changes: 49 additions & 1 deletion projects/igniteui-angular/grids/grid/src/grid.groupby.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,11 +790,59 @@ describe('IgxGrid - GroupBy #grid', () => {
const groupRows = grid.groupsRowList.toArray();
for (const grRow of groupRows) {
const elem = grRow.element.nativeElement;
// host carries role="row", aria-describedby; aria-expanded moved to the toggle cell
expect(elem.getAttribute('role')).toBe('row');
expect(elem.attributes['aria-describedby'].value).toEqual(grid.id + '_Released');
expect(elem.attributes['aria-expanded'].value).toEqual('true');
const toggleCell = elem.querySelector('.igx-grid__grouping-indicator');
expect(toggleCell.getAttribute('role')).toBe('gridcell');
expect(toggleCell.getAttribute('aria-expanded')).toBe('true');
}
}));

it('should update aria-expanded on the toggle cell when a group row is collapsed and expanded', fakeAsync(() => {
const fix = TestBed.createComponent(DefaultGridComponent);
const grid = fix.componentInstance.instance;
grid.primaryKey = 'ID';
fix.detectChanges();

grid.groupBy({ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false });
fix.detectChanges();

const grRow = grid.groupsRowList.toArray()[0];
const toggleCell = grRow.element.nativeElement.querySelector('.igx-grid__grouping-indicator');

expect(toggleCell.getAttribute('aria-expanded')).toBe('true');

grRow.toggle();
fix.detectChanges();

expect(toggleCell.getAttribute('aria-expanded')).toBe('false');

grRow.toggle();
fix.detectChanges();

expect(toggleCell.getAttribute('aria-expanded')).toBe('true');
}));

it('should assign role="gridcell" to all action wrappers inside a group row', fakeAsync(() => {
const fix = TestBed.createComponent(DefaultGridComponent);
const grid = fix.componentInstance.instance;
grid.rowDraggable = true;
grid.rowSelection = GridSelectionMode.multiple;
fix.detectChanges();

grid.groupBy({ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false });
fix.detectChanges();

const grRow = grid.groupsRowList.toArray()[0];
const elem = grRow.element.nativeElement;

expect(elem.querySelector('.igx-grid__drag-indicator').getAttribute('role')).toBe('gridcell');
expect(elem.querySelector('.igx-grid__cbx-selection').getAttribute('role')).toBe('gridcell');
expect(elem.querySelector('.igx-grid__grouping-indicator').getAttribute('role')).toBe('gridcell');
expect(elem.querySelector('.igx-grid__group-content').getAttribute('role')).toBe('gridcell');
}));

it('should not apply grouping if the grouping expressions value is the same reference', fakeAsync(() => {
const fix = TestBed.createComponent(DefaultGridComponent);
fix.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<ng-container #defaultGroupRow>

@if (rowDraggable) {
<div class="igx-grid__drag-indicator igx-grid__tr-action">
<div class="igx-grid__drag-indicator igx-grid__tr-action" role="gridcell">
<igx-icon family="default" name="drag_indicator" [style.visibility]="'hidden'"></igx-icon>
</div>
}

@if (showRowSelectors) {
<div class="igx-grid__cbx-selection igx-grid__tr-action" (pointerdown)="$event.preventDefault()"
<div class="igx-grid__cbx-selection igx-grid__tr-action" role="gridcell" (pointerdown)="$event.preventDefault()"
(click)="onGroupSelectorClick($event)">
<ng-template #groupByRowSelector *ngTemplateOutlet="
this.grid.groupByRowSelectorTemplate ? this.grid.groupByRowSelectorTemplate : groupByRowSelectorBaseTemplate;
Expand All @@ -19,12 +19,12 @@
</div>
}

<div (click)="toggle()" class="igx-grid__grouping-indicator">
<div (click)="toggle()" class="igx-grid__grouping-indicator" role="gridcell" [attr.aria-expanded]="expanded">
<ng-container *ngTemplateOutlet="iconTemplate; context: { $implicit: this }">
</ng-container>
Comment thread
viktorkombov marked this conversation as resolved.
</div>

<div class="igx-grid__group-content" #groupContent>
<div class="igx-grid__group-content" role="gridcell" #groupContent>
<ng-container
*ngTemplateOutlet="grid.groupRowTemplate ? grid.groupRowTemplate : defaultGroupByTemplate; context: { $implicit: groupRow }">
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ export class IgxGridGroupByRowComponent implements OnDestroy {
* const groupRowExpanded = this.grid1.rowList.first.expanded;
* ```
*/
@HostBinding('attr.aria-expanded')
public get expanded(): boolean {
return this.grid.isExpandedGroup(this.groupRow);
}
Comment thread
viktorkombov marked this conversation as resolved.
Expand Down Expand Up @@ -229,6 +228,12 @@ export class IgxGridGroupByRowComponent implements OnDestroy {
(this.isActive() ? ` ${this.defaultCssClass}--active` : '');
}

/**
* @hidden @internal
*/
@HostBinding('attr.role')
public role = 'row';

public isActive() {
return this.grid.navigation.activeNode ? this.grid.navigation.activeNode.row === this.index : false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
>
</igx-grid-header-row>

<div igxGridBody (keydown.control.c)="copyHandler($event)" (copy)="copyHandler($event)" class="igx-grid__tbody" role="rowgroup">
<div igxGridBody (keydown.control.c)="copyHandler($event)" (copy)="copyHandler($event)" class="igx-grid__tbody" role="presentation">
<div class="igx-grid__tbody-content" tabindex="0" (focus)="navigation.focusTbody($event)"
(keydown)="navigation.handleNavigation($event)" (dragStop)="selectionService.dragMode = $event"
(dragScroll)="dragScroll($event)" [igxGridDragSelect]="selectionService.dragMode" [attr.aria-activedescendant]="activeDescendant" [attr.role]="dataView.length ? null : 'row'"
(dragScroll)="dragScroll($event)" [igxGridDragSelect]="selectionService.dragMode" [attr.aria-activedescendant]="activeDescendant" [attr.role]="dataView.length ? 'rowgroup' : 'row'"
[style.height.px]="totalHeight" [style.width.px]="totalCalcWidth" [style.width]="!platform.isBrowser ? '100%' : undefined" #tbody (scroll)="preventContainerScroll($event)">
@if (moving && columnInDrag && pinnedColumns.length <= 0) {
<span
Expand Down Expand Up @@ -150,8 +150,8 @@
</div>
</div>

<div class="igx-grid__tfoot" role="rowgroup" [style.height.px]="summaryRowHeight" #tfoot>
<div tabindex="0" (focus)="navigation.focusFirstCell(false)" [attr.aria-activedescendant]="activeDescendant"
<div class="igx-grid__tfoot" role="presentation" [style.height.px]="summaryRowHeight" #tfoot>
<div tabindex="0" role="rowgroup" (focus)="navigation.focusFirstCell(false)" [attr.aria-activedescendant]="activeDescendant"
(keydown)="navigation.summaryNav($event)">
@if (hasSummarizedColumns && rootSummariesEnabled) {
<igx-grid-summary-row [style.width.px]="calcWidth" [style.height.px]="summaryRowHeight"
Expand All @@ -165,7 +165,7 @@
</div>
</div>

<div class="igx-grid__scroll" [style.height.px]="scrollSize" #scr [hidden]="isHorizontalScrollHidden" (pointerdown)="$event.preventDefault()">
<div class="igx-grid__scroll" role="presentation" [style.height.px]="scrollSize" #scr [hidden]="isHorizontalScrollHidden" (pointerdown)="$event.preventDefault()">
<div class="igx-grid__scroll-start" [style.width.px]="pinnedStartWidth" [style.min-width.px]="pinnedStartWidth"></div>
<div class="igx-grid__scroll-main" [style.width.px]="unpinnedWidth">
<ng-template igxGridFor [igxGridForOf]="[]" #scrollContainer>
Expand All @@ -174,7 +174,7 @@
<div class="igx-grid__scroll-end" [style.width.px]="pinnedEndWidth" [style.min-width.px]="pinnedEndWidth" [hidden]="pinnedEndWidth === 0"></div>
</div>

<div class="igx-grid__footer" #footer>
<div class="igx-grid__footer" role="presentation" #footer>
<ng-content select="igx-grid-footer,igc-grid-footer"></ng-content>
<ng-content select="igx-paginator,igc-paginator"></ng-content>
<ng-container #paginatorOutlet></ng-container>
Expand Down
Loading
Loading