diff --git a/golden/clr-angular.d.ts b/golden/clr-angular.d.ts index afbceb7809..465aeb049c 100644 --- a/golden/clr-angular.d.ts +++ b/golden/clr-angular.d.ts @@ -290,6 +290,7 @@ export declare class ClrDatagridCell { } export declare class ClrDatagridColumn extends DatagridFilterRegistrar { + readonly ariaSort: string; readonly asc: boolean; columnId: string; customFilter: boolean; @@ -416,18 +417,16 @@ export declare class ClrDatagridRow implements AfterContentInit { hideableColumnService: HideableColumnService; id: string; item: any; - role: string; + radioId: string; rowActionService: RowActionService; selected: boolean; selectedChanged: EventEmitter; selection: Selection; constructor(selection: Selection, rowActionService: RowActionService, globalExpandable: ExpandableRowsCount, expand: Expand, hideableColumnService: HideableColumnService); - keypress(event: KeyboardEvent): void; ngAfterContentInit(): void; ngOnDestroy(): void; toggle(selected?: boolean): void; toggleExpand(): void; - toggleSelection(): void; updateCellsForColumns(columnList: DatagridHideableColumnModel[]): void; } diff --git a/src/clr-angular/data/datagrid/_datagrid.clarity.scss b/src/clr-angular/data/datagrid/_datagrid.clarity.scss index 4846ac8d48..c4ee26963e 100644 --- a/src/clr-angular/data/datagrid/_datagrid.clarity.scss +++ b/src/clr-angular/data/datagrid/_datagrid.clarity.scss @@ -89,6 +89,9 @@ display: flex; flex-flow: row nowrap; } + .datagrid-row-clickable { + cursor: pointer; + } .datagrid-column { display: flex; flex: 1 1 auto; diff --git a/src/clr-angular/data/datagrid/datagrid-cell.spec.ts b/src/clr-angular/data/datagrid/datagrid-cell.spec.ts index 24f5f95462..35e50f32c7 100644 --- a/src/clr-angular/data/datagrid/datagrid-cell.spec.ts +++ b/src/clr-angular/data/datagrid/datagrid-cell.spec.ts @@ -43,6 +43,10 @@ export default function(): void { context.detectChanges(); expect(context.clarityElement.classList.contains('datagrid-signpost-trigger')).toBeTruthy(); }); + + it('adds a11y roles to the cell', function() { + expect(context.clarityElement.attributes.role.value).toBe('cell'); + }); }); } diff --git a/src/clr-angular/data/datagrid/datagrid-cell.ts b/src/clr-angular/data/datagrid/datagrid-cell.ts index f122e41aad..a81009183e 100644 --- a/src/clr-angular/data/datagrid/datagrid-cell.ts +++ b/src/clr-angular/data/datagrid/datagrid-cell.ts @@ -13,7 +13,11 @@ import { HideableColumnService } from './providers/hideable-column.service'; template: ` `, - host: { '[class.datagrid-cell]': 'true', '[class.datagrid-signpost-trigger]': 'signpost.length > 0' }, + host: { + '[class.datagrid-cell]': 'true', + '[class.datagrid-signpost-trigger]': 'signpost.length > 0', + role: 'cell', + }, }) export class ClrDatagridCell { /********* diff --git a/src/clr-angular/data/datagrid/datagrid-column.spec.ts b/src/clr-angular/data/datagrid/datagrid-column.spec.ts index 43831a2eb1..a88dfa88f1 100644 --- a/src/clr-angular/data/datagrid/datagrid-column.spec.ts +++ b/src/clr-angular/data/datagrid/datagrid-column.spec.ts @@ -346,6 +346,20 @@ export default function(): void { expect(context.clarityElement.classList.contains('desc')).toBeTruthy(); }); + it('adds a11y roles to the column', function() { + expect(context.clarityElement.attributes.role.value).toEqual('columnheader'); + expect(context.clarityElement.attributes['aria-sort'].value).toBe('none'); + + context.clarityDirective.sortBy = new TestComparator(); + context.clarityDirective.sort(); + context.detectChanges(); + expect(context.clarityElement.attributes['aria-sort'].value).toBe('ascending'); + + context.clarityDirective.sort(); + context.detectChanges(); + expect(context.clarityElement.attributes['aria-sort'].value).toBe('descending'); + }); + it('adds the .datagrid-column--hidden when not visible', function() { context.clarityDirective.hideable = new DatagridHideableColumnModel(null, 'dg-col-0', true); context.detectChanges(); diff --git a/src/clr-angular/data/datagrid/datagrid-column.ts b/src/clr-angular/data/datagrid/datagrid-column.ts index 8c2b2ec68c..38a4498fc4 100644 --- a/src/clr-angular/data/datagrid/datagrid-column.ts +++ b/src/clr-angular/data/datagrid/datagrid-column.ts @@ -57,7 +57,12 @@ let nbCount: number = 0; `, - host: { '[class.datagrid-column]': 'true', '[class.datagrid-column--hidden]': 'hidden' }, + host: { + '[class.datagrid-column]': 'true', + '[class.datagrid-column--hidden]': 'hidden', + '[attr.aria-sort]': 'ariaSort', + role: 'columnheader', + }, }) export class ClrDatagridColumn extends DatagridFilterRegistrar { constructor(private _sort: Sort, filters: FiltersProvider, private _dragDispatcher: DragDispatcher) { @@ -246,6 +251,18 @@ export class ClrDatagridColumn extends DatagridFilterRegistrar(); /** diff --git a/src/clr-angular/data/datagrid/datagrid-row.spec.ts b/src/clr-angular/data/datagrid/datagrid-row.spec.ts index 344d185c1a..5b4ecd970a 100644 --- a/src/clr-angular/data/datagrid/datagrid-row.spec.ts +++ b/src/clr-angular/data/datagrid/datagrid-row.spec.ts @@ -7,7 +7,6 @@ import { Component } from '@angular/core'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { itIgnore } from '../../../../tests/tests.helpers'; import { Expand } from '../../utils/expand/providers/expand'; import { LoadingListener } from '../../utils/loading/loading-listener'; @@ -72,6 +71,15 @@ export default function(): void { context.detectChanges(); expect(context.clarityElement.querySelector('.datagrid-fixed-column')).not.toBeNull(); }); + + it('adds a11y roles to the row', function() { + expect(context.clarityElement.attributes.role.value).toEqual('rowgroup'); + + const rowId = context.clarityDirective.id; + expect(context.clarityElement.attributes['aria-owns'].value).toEqual(rowId); + const rowContent = context.clarityElement.querySelector('.datagrid-row-master'); + expect(rowContent.attributes.id.value).toEqual(rowId); + }); }); describe('Selection', function() { @@ -174,14 +182,18 @@ export default function(): void { context.testComponent.item = { id: 1 }; context.detectChanges(); const row = context.clarityElement; - row.click(); + expect(row.children).toBeDefined(); + + expect(row.children[0] instanceof HTMLLabelElement).toBeFalsy(); + row.children[0].click(); context.detectChanges(); expect(selectionProvider.currentSingle).toBeUndefined(); // Enabling the rowSelectionMode selectionProvider.rowSelectionMode = true; context.detectChanges(); - row.click(); + expect(row.children[0] instanceof HTMLLabelElement).toBeTruthy(); + row.children[0].click(); context.detectChanges(); expect(selectionProvider.currentSingle).toEqual(context.testComponent.item); }); @@ -191,40 +203,22 @@ export default function(): void { context.testComponent.item = { id: 1 }; context.detectChanges(); const row = context.clarityElement; + expect(row.children).toBeDefined(); expect(selectionProvider.current).toEqual([]); - row.click(); + expect(row.children[0] instanceof HTMLLabelElement).toBeFalsy(); + row.children[0].click(); context.detectChanges(); expect(selectionProvider.current).toEqual([]); // Enabling the rowSelectionMode selectionProvider.rowSelectionMode = true; context.detectChanges(); - row.click(); - context.detectChanges(); - expect(selectionProvider.current).toEqual([context.testComponent.item]); - - row.click(); - context.detectChanges(); - expect(selectionProvider.current).toEqual([]); - }); - - // IE doesn't support Event constructor - // @TODO Consider if we care to fix this test for IE support - itIgnore(['ie'], 'select the model on space or enter when `rowSelectionMode` is enabled', function() { - selectionProvider.selectionType = SelectionType.Multi; - selectionProvider.rowSelectionMode = true; - context.testComponent.item = { id: 1 }; - const row = context.clarityElement; - context.detectChanges(); - - const event: any = new Event('keypress'); - event.keyCode = 13; // Enter - row.dispatchEvent(event); + expect(row.children[0] instanceof HTMLLabelElement).toBeTruthy(); + row.children[0].click(); context.detectChanges(); expect(selectionProvider.current).toEqual([context.testComponent.item]); - event.keyCode = 32; // Space - row.dispatchEvent(event); + row.children[0].click(); context.detectChanges(); expect(selectionProvider.current).toEqual([]); }); diff --git a/src/clr-angular/data/datagrid/datagrid-row.ts b/src/clr-angular/data/datagrid/datagrid-row.ts index 4b27b28ac2..5f895202ac 100644 --- a/src/clr-angular/data/datagrid/datagrid-row.ts +++ b/src/clr-angular/data/datagrid/datagrid-row.ts @@ -3,17 +3,7 @@ * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ -import { - AfterContentInit, - Component, - ContentChildren, - EventEmitter, - HostBinding, - HostListener, - Input, - Output, - QueryList, -} from '@angular/core'; +import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList } from '@angular/core'; import { Subscription } from 'rxjs'; import { Expand } from '../../utils/expand/providers/expand'; @@ -31,72 +21,81 @@ let nbRow: number = 0; @Component({ selector: 'clr-dg-row', template: ` -
- - - - -
- - -
-
- - - - - - -
-
-
- - - -
- - + + + + + + + + + + + +
+ + + + +
+ + +
+
+ + + + + + +
+
+
+ + + +
+
- - - - `, host: { '[class.datagrid-row]': 'true', '[class.datagrid-selected]': 'selected', - '[attr.tabindex]': 'selection.rowSelectionMode ? 0 : null', + '[attr.aria-owns]': 'id', + role: 'rowgroup', }, providers: [Expand, { provide: LoadingListener, useExisting: Expand }], }) export class ClrDatagridRow implements AfterContentInit { public id: string; + public radioId: string; /* reference to the enum so that template can access */ public SELECTION_TYPE = SelectionType; - private readonly ENTER_KEY_CODE = 13; - private readonly SPACE_KEY_CODE = 32; - /** * Model of the row, to use for selection */ @Input('clrDgItem') item: any; - @HostBinding('attr.role') role: string; - constructor( public selection: Selection, public rowActionService: RowActionService, @@ -105,7 +104,7 @@ export class ClrDatagridRow implements AfterContentInit { public hideableColumnService: HideableColumnService ) { this.id = 'clr-dg-row' + nbRow++; - this.role = selection.rowSelectionMode ? 'button' : null; + this.radioId = 'clr-dg-row-rd' + nbRow++; } private _selected = false; @@ -156,40 +155,6 @@ export class ClrDatagridRow implements AfterContentInit { } } - @HostListener('click') - public toggleSelection() { - if (!this.selection.rowSelectionMode) { - return; - } - - switch (this.selection.selectionType) { - case SelectionType.None: - break; - case SelectionType.Single: - this.selection.currentSingle = this.item; - break; - case SelectionType.Multi: - this.toggle(); - break; - default: - break; - } - } - - @HostListener('keypress', ['$event']) - public keypress(event: KeyboardEvent) { - if (!this.selection.rowSelectionMode) { - return; - } - - // Check to see if space or enter were pressed - if (event.keyCode === this.ENTER_KEY_CODE || event.keyCode === this.SPACE_KEY_CODE) { - // Prevent the default action to stop scrolling when space is pressed - event.preventDefault(); - this.toggleSelection(); - } - } - private subscription: Subscription; /***** diff --git a/src/clr-angular/data/datagrid/datagrid.html b/src/clr-angular/data/datagrid/datagrid.html index c11005069e..865a70cfdd 100644 --- a/src/clr-angular/data/datagrid/datagrid.html +++ b/src/clr-angular/data/datagrid/datagrid.html @@ -8,11 +8,11 @@
- -
-
+ +
+
-
@@ -20,17 +20,17 @@
-
-
-
diff --git a/src/clr-angular/data/datagrid/datagrid.spec.ts b/src/clr-angular/data/datagrid/datagrid.spec.ts index 17aa2c09a4..261be27db4 100644 --- a/src/clr-angular/data/datagrid/datagrid.spec.ts +++ b/src/clr-angular/data/datagrid/datagrid.spec.ts @@ -211,6 +211,20 @@ export default function(): void { it('projects the footer', function() { expect(context.clarityElement.querySelector('.datagrid-foot')).not.toBeNull(); }); + + it('adds a11y roles to datagrid', function() { + const tableWrapper = context.clarityElement.querySelector('.datagrid-table-wrapper'); + expect(tableWrapper.attributes.role.value).toEqual('grid'); + + const header = context.clarityElement.querySelector('.datagrid-head'); + expect(header.attributes.role.value).toEqual('rowgroup'); + + const row = context.clarityElement.querySelector('.datagrid-row'); + expect(row.attributes.role.value).toEqual('row'); + + const columns = context.clarityElement.querySelectorAll('.datagrid-column'); + columns.forEach(column => expect(column.attributes.role.value).toEqual('columnheader')); + }); }); describe('Iterators', function() { diff --git a/src/clr-angular/data/datagrid/render/table-renderer.spec.ts b/src/clr-angular/data/datagrid/render/table-renderer.spec.ts index 8e4bc6ee6e..a6e25f2ba6 100644 --- a/src/clr-angular/data/datagrid/render/table-renderer.spec.ts +++ b/src/clr-angular/data/datagrid/render/table-renderer.spec.ts @@ -51,6 +51,11 @@ export default function(): void { expect(body.textContent).not.toMatch('Hello'); expect(body.textContent).toMatch('World'); }); + + it('adds a11y roles', function() { + const rowContent = context.clarityElement.querySelector('.datagrid-body'); + expect(rowContent.attributes.role.value).toBe('rowgroup'); + }); }); } diff --git a/src/clr-angular/data/datagrid/render/table-renderer.ts b/src/clr-angular/data/datagrid/render/table-renderer.ts index 6627aa9dde..4b873cde74 100644 --- a/src/clr-angular/data/datagrid/render/table-renderer.ts +++ b/src/clr-angular/data/datagrid/render/table-renderer.ts @@ -14,7 +14,7 @@ import { DatagridRenderOrganizer } from './render-organizer'; template: ` -
+