[ADF-2990] Datatable - access header row via keyboard (#5206)

* header row as adf-datatable-row

* remove tabindex if header is hidden

* adjust logic if no row data is passed

* skip row focus if disabled

* set active row index on header interaction

* take in account header row

* fix header row and cells focus

* tests

* fix reference

* fix tests
This commit is contained in:
Cilibiu Bogdan 2019-11-04 10:08:15 +02:00 committed by Eugenio Romano
parent 5c4511e42b
commit 040fc52724
6 changed files with 153 additions and 22 deletions

View File

@ -55,6 +55,14 @@ describe('DataTableRowComponent', () => {
.not.toBe(true);
});
it('should not have select class when row data is null', () => {
row.isSelected = false;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.classList.contains('adf-is-selected'))
.not.toBe(true);
});
it('should set aria selected to true when row is selected', () => {
row.isSelected = true;
component.row = row;
@ -71,6 +79,11 @@ describe('DataTableRowComponent', () => {
expect(fixture.debugElement.nativeElement.getAttribute('aria-selected')).toBe('false');
});
it('should set aria selected to false when row is null', () => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('aria-selected')).toBe('false');
});
it('should set aria label', () => {
spyOn(row, 'getValue').and.returnValue('some-name');
component.row = row;
@ -79,6 +92,17 @@ describe('DataTableRowComponent', () => {
expect(fixture.debugElement.nativeElement.getAttribute('aria-label')).toBe('some-name');
});
it('should set tabindex as focusable when row is not disabled', () => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBe('0');
});
it('should not set tabindex when row is disabled', () => {
component.disabled = true;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBe(null);
});
it('should focus element', () => {
expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(false);

View File

@ -41,24 +41,40 @@ import { DataRow } from '../../data/data-row.model';
export class DataTableRowComponent implements FocusableOption {
@Input() row: DataRow;
@Input() disabled = false;
@Output()
select: EventEmitter<any> = new EventEmitter<any>();
@HostBinding('class.adf-is-selected')
get isSelected(): boolean {
if (!this.row) {
return false;
}
return this.row.isSelected;
}
@HostBinding('attr.aria-selected')
get isAriaSelected(): boolean {
if (!this.row) {
return false;
}
return this.row.isSelected;
}
@HostBinding('attr.aria-label')
get ariaLabel(): boolean {
get ariaLabel(): string|null {
if (!this.row) {
return null;
}
return this.row.getValue('name') || '';
}
@HostBinding('attr.tabindex')
get tabindex(): number|null {
return this.disabled ? null : 0;
}
@HostListener('keydown.space', ['$event'])
onKeyDown(event: KeyboardEvent) {
if ((event.target as Element).tagName === this.element.nativeElement.tagName) {

View File

@ -6,7 +6,12 @@
[class.adf-sticky-header]="isStickyHeaderEnabled()"
[class.adf-datatable--empty]="!isHeaderVisible()">
<div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !showHeader }">
<div class="adf-datatable-row" *ngIf="display === 'list'" role="row">
<adf-datatable-row
data-automation-id="datatable-row-header"
[disabled]="!showHeader"
class="adf-datatable-row"
*ngIf="display === 'list'"
role="row">
<!-- Actions (left) -->
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header">
<span class="adf-sr-only">Actions</span>
@ -24,7 +29,7 @@
(click)="onColumnHeaderClick(col)"
(keyup.enter)="onColumnHeaderClick(col)"
role="columnheader"
[tabindex]="showHeader ? 0 : -1"
[attr.tabindex]="showHeader ? 0 : null"
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
title="{{ col.title | translate }}"
adf-drop-zone dropTarget="header" [dropColumn]="col">
@ -35,7 +40,7 @@
<div *ngIf="actions && actionsPosition === 'right'" class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell">
<span class="adf-sr-only">Actions</span>
</div>
</div>
</adf-datatable-row>
<mat-form-field *ngIf="display === 'gallery' && showHeader">
<mat-select [value]="getSortingKey()" [attr.data-automation-id]="'grid-view-sorting'">
<mat-option *ngFor="let col of getSortableColumns()"

View File

@ -220,6 +220,24 @@
color: $data-table-cell-text-color;
}
.adf-datatable-row {
&:hover, &:focus {
background-color: $data-table-hover-color;
}
&:focus {
outline-offset: -1px;
outline: $data-table-outline;
}
.adf-cell-value, .adf-datatable-cell-header {
&:focus {
outline-offset: -1px;
outline: $data-table-outline;
}
}
}
.adf-datatable-body {
display: flex;
flex-direction: column;
@ -234,15 +252,6 @@
@include adf-no-select;
&:hover, &:focus {
background-color: $data-table-hover-color;
}
&:focus {
outline-offset: -1px;
outline: $data-table-outline;
}
&.adf-is-selected, &.adf-is-selected:hover {
background-color: $data-table-selection-color;
}
@ -418,11 +427,6 @@
align-items: center;
word-break: break-all;
width: 100%;
&:focus {
outline-offset: -1px;
outline: $data-table-outline;
}
}
.adf-datatable__actions-cell, .adf-datatable-cell--image {

View File

@ -230,6 +230,8 @@ describe('DataTable', () => {
});
dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.ngAfterViewInit();
dataTable.onColumnHeaderClick(column);
});
@ -709,6 +711,8 @@ describe('DataTable', () => {
it('should not sort if column is missing', () => {
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
fixture.detectChanges();
dataTable.ngAfterViewInit();
const adapter = dataTable.data;
spyOn(adapter, 'setSorting').and.callThrough();
dataTable.onColumnHeaderClick(null);
@ -717,6 +721,8 @@ describe('DataTable', () => {
it('should not sort upon clicking non-sortable column header', () => {
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
fixture.detectChanges();
dataTable.ngAfterViewInit();
const adapter = dataTable.data;
spyOn(adapter, 'setSorting').and.callThrough();
@ -730,6 +736,8 @@ describe('DataTable', () => {
it('should set sorting upon column header clicked', () => {
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
fixture.detectChanges();
dataTable.ngAfterViewInit();
const adapter = dataTable.data;
spyOn(adapter, 'setSorting').and.callThrough();
@ -749,6 +757,8 @@ describe('DataTable', () => {
it('should invert sorting upon column header clicked', () => {
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
fixture.detectChanges();
dataTable.ngAfterViewInit();
const adapter = dataTable.data;
const sorting = new DataSorting('column_1', 'asc');
@ -789,6 +799,8 @@ describe('DataTable', () => {
new ObjectDataColumn({ key: 'other', sortable: true })
]
);
fixture.detectChanges();
dataTable.ngAfterViewInit();
const [col1, col2] = dataTable.getSortableColumns();
@ -1227,8 +1239,8 @@ describe('Accesibility', () => {
it('should focus previous row on ArrowUp event', () => {
const event = new KeyboardEvent('keyup', {
code: 'ArrowDown',
key: 'ArrowDown',
code: 'ArrowUp',
key: 'ArrowUp',
keyCode: 38
} as KeyboardEventInit );
@ -1254,4 +1266,68 @@ describe('Accesibility', () => {
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-0');
});
it('should select header row when `showHeader` is true', () => {
const event = new KeyboardEvent('keyup', {
code: 'ArrowUp',
key: 'ArrowUp',
keyCode: 38
} as KeyboardEventInit );
const dataRows =
[ { name: 'test1'}, { name: 'test2' } ];
dataTable.data = new ObjectDataTableAdapter([],
[new ObjectDataColumn({ key: 'name' })]
);
dataTable.showHeader = true;
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
dataTable.ngAfterViewInit();
const rowElement = document.querySelector('.adf-datatable-row[data-automation-id="datatable-row-0"]');
const rowCellElement = rowElement.querySelector('.adf-datatable-cell');
rowCellElement.dispatchEvent(new MouseEvent('click'));
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-header');
});
it('should not select header row when `showHeader` is false', () => {
const event = new KeyboardEvent('keyup', {
code: 'ArrowUp',
key: 'ArrowUp',
keyCode: 38
} as KeyboardEventInit );
const dataRows =
[ { name: 'test1'}, { name: 'test2' } ];
dataTable.data = new ObjectDataTableAdapter([],
[new ObjectDataColumn({ key: 'name' })]
);
dataTable.showHeader = false;
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
dataTable.ngAfterViewInit();
const rowElement = document.querySelector('.adf-datatable-row[data-automation-id="datatable-row-0"]');
const rowCellElement = rowElement.querySelector('.adf-datatable-cell');
rowCellElement.dispatchEvent(new MouseEvent('click'));
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-1');
});
});

View File

@ -223,7 +223,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
}
ngAfterViewInit() {
this.keyManager = new FocusKeyManager(this.rowsList).withWrap();
this.keyManager = new FocusKeyManager(this.rowsList)
.withWrap()
.skipPredicate(item => item.disabled);
}
ngOnChanges(changes: SimpleChanges) {
@ -406,7 +408,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
}
if (row) {
this.keyManager.setActiveItem(this.data.getRows().indexOf(row));
const rowIndex = this.data.getRows().indexOf(row) + (this.isHeaderVisible() ? 1 : 0);
this.keyManager.setActiveItem(rowIndex);
const dataRowEvent = new DataRowEvent(row, mouseEvent, this);
this.clickObserver.next(dataRowEvent);
}
@ -511,6 +515,8 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
this.data.setSorting(new DataSorting(column.key, newDirection));
this.emitSortingChangedEvent(column.key, newDirection);
}
this.keyManager.updateActiveItemIndex(0);
}
onSelectAllClick(matCheckboxChange: MatCheckboxChange) {