Skip to content

Commit 415b8e7

Browse files
authored
Merge pull request #17265 from IgniteUI/copilot/fix-accessibility-issues-21.2.x
Copilot/fix accessibility issues 21.2.x
1 parent 6e9bcb7 commit 415b8e7

19 files changed

Lines changed: 197 additions & 49 deletions

projects/igniteui-angular/grids/core/src/filtering/base/grid-filtering-cell.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit, DoC
7979
return !this.column.grid.hasColumnLayouts ? this.column.isFirstPinned : false;;
8080
}
8181

82+
@HostBinding('attr.role')
83+
public role = 'gridcell';
84+
8285
private baseClass = 'igx-grid__filtering-cell-indicator';
8386

8487
constructor() {

projects/igniteui-angular/grids/core/src/grouping/group-by-area.directive.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export abstract class IgxGroupByAreaDirective {
3737
@HostBinding('class.igx-grid-grouparea')
3838
public defaultClass = true;
3939

40+
/**
41+
* @hidden @internal
42+
*/
43+
@HostBinding('attr.role')
44+
public role = 'presentation';
45+
4046
/** The parent grid containing the component. */
4147
@Input()
4248
public grid: FlatGridType | GridType;

projects/igniteui-angular/grids/core/src/headers/grid-header-row.component.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div role="rowgroup" class="igx-grid-thead__wrapper" (scroll)="scroll($event)" [style.width.px]="width"
1+
<div class="igx-grid-thead__wrapper" (scroll)="scroll($event)" [style.width.px]="width"
22
[class.igx-grid__tr--mrl]="hasMRL">
33

44
<!-- Column headers area -->
@@ -16,7 +16,7 @@
1616

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

2626
<!-- Row selectors area -->
2727
@if (grid.showRowSelectors) {
28-
<div #headerSelectorContainer class="igx-grid__cbx-selection igx-grid__tr-action"
28+
<div #headerSelectorContainer class="igx-grid__cbx-selection igx-grid__tr-action" role="columnheader"
2929
[class.igx-grid__cbx-selection--push]="grid.filteringService.isFilterRowVisible"
3030
(click)="headerRowSelection($event)"
3131
(pointerdown)="$event.preventDefault()">
@@ -41,6 +41,7 @@
4141
(click)="grid.toggleAll()"
4242
(pointerdown)="$event.preventDefault()"
4343
[hidden]="!grid.hasExpandableChildren || !grid.hasVisibleColumns"
44+
role="columnheader"
4445
[ngClass]="{
4546
'igx-grid__hierarchical-expander igx-grid__hierarchical-expander--header igx-grid__tr-action': grid.hasExpandableChildren,
4647
'igx-grid__hierarchical-expander--push': grid.filteringService.isFilterRowVisible,
@@ -55,7 +56,8 @@
5556
@if (grid?.groupingExpressions?.length) {
5657
<div #headerGroupContainer class="{{ indentationCSSClasses }}"
5758
(click)="grid.toggleAllGroupRows()"
58-
(pointerdown)="$event.preventDefault()">
59+
(pointerdown)="$event.preventDefault()"
60+
role="columnheader">
5961
<ng-container *ngTemplateOutlet="grid.iconTemplate; context: { $implicit: grid }"></ng-container>
6062
</div>
6163
}

projects/igniteui-angular/grids/core/src/headers/grid-header-row.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export class IgxGridHeaderRowComponent implements DoCheck {
6262
@Input()
6363
public unpinnedColumnCollection: ColumnType[] = [];
6464

65+
/**
66+
* @hidden @internal
67+
*/
68+
@HostBinding('attr.role')
69+
public role = 'rowgroup';
70+
6571
@HostBinding('attr.aria-activedescendant')
6672
public get activeDescendant() {
6773
const activeElem = this.navigation.activeNode;

projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ export class IgxGridToolbarComponent implements OnDestroy {
8888
@HostBinding('class.igx-grid-toolbar')
8989
public defaultStyle = true;
9090

91+
/**
92+
* @hidden
93+
* @internal
94+
*/
95+
@HostBinding('attr.role')
96+
public role = 'presentation';
97+
9198
protected _grid: GridType;
9299
protected sub: Subscription;
93100

projects/igniteui-angular/grids/grid/src/grid-row-selection.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,6 +2452,11 @@ describe('IgxGrid - Row Selection #grid', () => {
24522452
expect(rowSelector.textContent).toBe('CUSTOM SELECTOR: 0');
24532453
expect(groupRowSelector.textContent).toBe('CUSTOM GROUP SELECTOR');
24542454
expect(headerSelector.textContent).toBe('CUSTOM HEADER SELECTOR');
2455+
2456+
// ARIA: row-selector wrappers must have the correct cell roles
2457+
expect(rowSelector.getAttribute('role')).toBe('gridcell');
2458+
expect(groupRowSelector.getAttribute('role')).toBe('gridcell');
2459+
expect(headerSelector.getAttribute('role')).toBe('columnheader');
24552460
});
24562461
});
24572462
});

projects/igniteui-angular/grids/grid/src/grid-row.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
</ng-template>
88
<ng-template #defaultTemp>
99
@if (rowDraggable) {
10-
<div [class]="resolveDragIndicatorClasses" [igxRowDrag]="this" (click)="$event.stopPropagation()" [ghostTemplate]="this.grid.getDragGhostCustomTemplate()">
10+
<div [class]="resolveDragIndicatorClasses" role="gridcell" [igxRowDrag]="this" (click)="$event.stopPropagation()" [ghostTemplate]="this.grid.getDragGhostCustomTemplate()">
1111
<ng-container *ngTemplateOutlet="this.grid.dragIndicatorIconTemplate ? this.grid.dragIndicatorIconTemplate : this.grid.dragIndicatorIconBase"></ng-container>
1212
</div>
1313
}
1414
@if (this.showRowSelectors) {
15-
<div class="igx-grid__cbx-selection igx-grid__tr-action" (pointerdown)="$event.preventDefault()" (click)="onRowSelectorClick($event)">
15+
<div class="igx-grid__cbx-selection igx-grid__tr-action" role="gridcell" (pointerdown)="$event.preventDefault()" (click)="onRowSelectorClick($event)">
1616
<ng-template *ngTemplateOutlet="
1717
this.grid.rowSelectorTemplate ? this.grid.rowSelectorTemplate : rowSelectorBaseTemplate;
1818
context: { $implicit: { index: viewIndex, rowID: key, key, selected: selected }}">

projects/igniteui-angular/grids/grid/src/grid.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
>
3131
</igx-grid-header-row>
3232

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

188188

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

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

212-
<div class="igx-grid__footer" #footer>
212+
<div class="igx-grid__footer" role="presentation" #footer>
213213
<ng-content select="igx-grid-footer,igc-grid-footer"></ng-content>
214214
<ng-content select="igx-paginator,igc-paginator"></ng-content>
215215
</div>

projects/igniteui-angular/grids/grid/src/grid.component.spec.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,9 @@ describe('IgxGrid Component Tests #grid', () => {
319319
fixture.componentInstance.generateData(30);
320320
fixture.detectChanges();
321321
tick(100);
322-
// Checks if igx-grid__tbody-content attribute is null when there is data in the grid
322+
// With data, igx-grid__tbody-content is the rowgroup focus host
323323
const container = fixture.nativeElement.querySelectorAll('.igx-grid__tbody-content')[0];
324-
expect(container.getAttribute('role')).toBe(null);
324+
expect(container.getAttribute('role')).toBe('rowgroup');
325325

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

340340
}));
341341

342+
it('should have correct ARIA role structure on tbody and tfoot', fakeAsync(() => {
343+
const fixture = TestBed.createComponent(IgxGridTestComponent);
344+
fixture.componentInstance.columns[0].hasSummary = true;
345+
346+
fixture.componentInstance.generateData(30);
347+
fixture.detectChanges();
348+
tick(100);
349+
350+
// Outer tbody wrapper is layout-only
351+
const tbodyWrapper = fixture.nativeElement.querySelector('.igx-grid__tbody');
352+
expect(tbodyWrapper.getAttribute('role')).toBe('presentation');
353+
354+
// Inner focus host is the rowgroup
355+
const tbodyContent = fixture.nativeElement.querySelector('.igx-grid__tbody-content');
356+
expect(tbodyContent.getAttribute('role')).toBe('rowgroup');
357+
expect(tbodyContent.getAttribute('tabindex')).toBe('0');
358+
359+
// Outer tfoot wrapper is layout-only
360+
const tfootWrapper = fixture.nativeElement.querySelector('.igx-grid__tfoot');
361+
expect(tfootWrapper.getAttribute('role')).toBe('presentation');
362+
363+
// Inner tfoot div is the rowgroup focus host
364+
const tfootContent = fixture.nativeElement.querySelector('.igx-grid__tfoot > div');
365+
expect(tfootContent.getAttribute('role')).toBe('rowgroup');
366+
expect(tfootContent.getAttribute('tabindex')).toBe('0');
367+
}));
342368
it('should render empty message', fakeAsync(() => {
343369
const fixture = TestBed.createComponent(IgxGridTestComponent);
344370
fixture.componentInstance.data = [];

projects/igniteui-angular/grids/grid/src/grid.groupby.spec.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,11 +790,59 @@ describe('IgxGrid - GroupBy #grid', () => {
790790
const groupRows = grid.groupsRowList.toArray();
791791
for (const grRow of groupRows) {
792792
const elem = grRow.element.nativeElement;
793+
// host carries role="row", aria-describedby; aria-expanded moved to the toggle cell
794+
expect(elem.getAttribute('role')).toBe('row');
793795
expect(elem.attributes['aria-describedby'].value).toEqual(grid.id + '_Released');
794-
expect(elem.attributes['aria-expanded'].value).toEqual('true');
796+
const toggleCell = elem.querySelector('.igx-grid__grouping-indicator');
797+
expect(toggleCell.getAttribute('role')).toBe('gridcell');
798+
expect(toggleCell.getAttribute('aria-expanded')).toBe('true');
795799
}
796800
}));
797801

802+
it('should update aria-expanded on the toggle cell when a group row is collapsed and expanded', fakeAsync(() => {
803+
const fix = TestBed.createComponent(DefaultGridComponent);
804+
const grid = fix.componentInstance.instance;
805+
grid.primaryKey = 'ID';
806+
fix.detectChanges();
807+
808+
grid.groupBy({ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false });
809+
fix.detectChanges();
810+
811+
const grRow = grid.groupsRowList.toArray()[0];
812+
const toggleCell = grRow.element.nativeElement.querySelector('.igx-grid__grouping-indicator');
813+
814+
expect(toggleCell.getAttribute('aria-expanded')).toBe('true');
815+
816+
grRow.toggle();
817+
fix.detectChanges();
818+
819+
expect(toggleCell.getAttribute('aria-expanded')).toBe('false');
820+
821+
grRow.toggle();
822+
fix.detectChanges();
823+
824+
expect(toggleCell.getAttribute('aria-expanded')).toBe('true');
825+
}));
826+
827+
it('should assign role="gridcell" to all action wrappers inside a group row', fakeAsync(() => {
828+
const fix = TestBed.createComponent(DefaultGridComponent);
829+
const grid = fix.componentInstance.instance;
830+
grid.rowDraggable = true;
831+
grid.rowSelection = GridSelectionMode.multiple;
832+
fix.detectChanges();
833+
834+
grid.groupBy({ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false });
835+
fix.detectChanges();
836+
837+
const grRow = grid.groupsRowList.toArray()[0];
838+
const elem = grRow.element.nativeElement;
839+
840+
expect(elem.querySelector('.igx-grid__drag-indicator').getAttribute('role')).toBe('gridcell');
841+
expect(elem.querySelector('.igx-grid__cbx-selection').getAttribute('role')).toBe('gridcell');
842+
expect(elem.querySelector('.igx-grid__grouping-indicator').getAttribute('role')).toBe('gridcell');
843+
expect(elem.querySelector('.igx-grid__group-content').getAttribute('role')).toBe('gridcell');
844+
}));
845+
798846
it('should not apply grouping if the grouping expressions value is the same reference', fakeAsync(() => {
799847
const fix = TestBed.createComponent(DefaultGridComponent);
800848
fix.detectChanges();

0 commit comments

Comments
 (0)