mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-09-17 14:21:29 +00:00
[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:
committed by
Eugenio Romano
parent
5c4511e42b
commit
040fc52724
@@ -55,6 +55,14 @@ describe('DataTableRowComponent', () => {
|
|||||||
.not.toBe(true);
|
.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', () => {
|
it('should set aria selected to true when row is selected', () => {
|
||||||
row.isSelected = true;
|
row.isSelected = true;
|
||||||
component.row = row;
|
component.row = row;
|
||||||
@@ -71,6 +79,11 @@ describe('DataTableRowComponent', () => {
|
|||||||
expect(fixture.debugElement.nativeElement.getAttribute('aria-selected')).toBe('false');
|
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', () => {
|
it('should set aria label', () => {
|
||||||
spyOn(row, 'getValue').and.returnValue('some-name');
|
spyOn(row, 'getValue').and.returnValue('some-name');
|
||||||
component.row = row;
|
component.row = row;
|
||||||
@@ -79,6 +92,17 @@ describe('DataTableRowComponent', () => {
|
|||||||
expect(fixture.debugElement.nativeElement.getAttribute('aria-label')).toBe('some-name');
|
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', () => {
|
it('should focus element', () => {
|
||||||
expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(false);
|
expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(false);
|
||||||
|
|
||||||
|
@@ -41,24 +41,40 @@ import { DataRow } from '../../data/data-row.model';
|
|||||||
export class DataTableRowComponent implements FocusableOption {
|
export class DataTableRowComponent implements FocusableOption {
|
||||||
@Input() row: DataRow;
|
@Input() row: DataRow;
|
||||||
|
|
||||||
|
@Input() disabled = false;
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
select: EventEmitter<any> = new EventEmitter<any>();
|
select: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
@HostBinding('class.adf-is-selected')
|
@HostBinding('class.adf-is-selected')
|
||||||
get isSelected(): boolean {
|
get isSelected(): boolean {
|
||||||
|
if (!this.row) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return this.row.isSelected;
|
return this.row.isSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding('attr.aria-selected')
|
@HostBinding('attr.aria-selected')
|
||||||
get isAriaSelected(): boolean {
|
get isAriaSelected(): boolean {
|
||||||
|
if (!this.row) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return this.row.isSelected;
|
return this.row.isSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding('attr.aria-label')
|
@HostBinding('attr.aria-label')
|
||||||
get ariaLabel(): boolean {
|
get ariaLabel(): string|null {
|
||||||
|
if (!this.row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return this.row.getValue('name') || '';
|
return this.row.getValue('name') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostBinding('attr.tabindex')
|
||||||
|
get tabindex(): number|null {
|
||||||
|
return this.disabled ? null : 0;
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('keydown.space', ['$event'])
|
@HostListener('keydown.space', ['$event'])
|
||||||
onKeyDown(event: KeyboardEvent) {
|
onKeyDown(event: KeyboardEvent) {
|
||||||
if ((event.target as Element).tagName === this.element.nativeElement.tagName) {
|
if ((event.target as Element).tagName === this.element.nativeElement.tagName) {
|
||||||
|
@@ -6,7 +6,12 @@
|
|||||||
[class.adf-sticky-header]="isStickyHeaderEnabled()"
|
[class.adf-sticky-header]="isStickyHeaderEnabled()"
|
||||||
[class.adf-datatable--empty]="!isHeaderVisible()">
|
[class.adf-datatable--empty]="!isHeaderVisible()">
|
||||||
<div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !showHeader }">
|
<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) -->
|
<!-- Actions (left) -->
|
||||||
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header">
|
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header">
|
||||||
<span class="adf-sr-only">Actions</span>
|
<span class="adf-sr-only">Actions</span>
|
||||||
@@ -24,7 +29,7 @@
|
|||||||
(click)="onColumnHeaderClick(col)"
|
(click)="onColumnHeaderClick(col)"
|
||||||
(keyup.enter)="onColumnHeaderClick(col)"
|
(keyup.enter)="onColumnHeaderClick(col)"
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
[tabindex]="showHeader ? 0 : -1"
|
[attr.tabindex]="showHeader ? 0 : null"
|
||||||
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
|
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
|
||||||
title="{{ col.title | translate }}"
|
title="{{ col.title | translate }}"
|
||||||
adf-drop-zone dropTarget="header" [dropColumn]="col">
|
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">
|
<div *ngIf="actions && actionsPosition === 'right'" class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell">
|
||||||
<span class="adf-sr-only">Actions</span>
|
<span class="adf-sr-only">Actions</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</adf-datatable-row>
|
||||||
<mat-form-field *ngIf="display === 'gallery' && showHeader">
|
<mat-form-field *ngIf="display === 'gallery' && showHeader">
|
||||||
<mat-select [value]="getSortingKey()" [attr.data-automation-id]="'grid-view-sorting'">
|
<mat-select [value]="getSortingKey()" [attr.data-automation-id]="'grid-view-sorting'">
|
||||||
<mat-option *ngFor="let col of getSortableColumns()"
|
<mat-option *ngFor="let col of getSortableColumns()"
|
||||||
|
@@ -220,6 +220,24 @@
|
|||||||
color: $data-table-cell-text-color;
|
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 {
|
.adf-datatable-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -234,15 +252,6 @@
|
|||||||
|
|
||||||
@include adf-no-select;
|
@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 {
|
&.adf-is-selected, &.adf-is-selected:hover {
|
||||||
background-color: $data-table-selection-color;
|
background-color: $data-table-selection-color;
|
||||||
}
|
}
|
||||||
@@ -418,11 +427,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline-offset: -1px;
|
|
||||||
outline: $data-table-outline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.adf-datatable__actions-cell, .adf-datatable-cell--image {
|
.adf-datatable__actions-cell, .adf-datatable-cell--image {
|
||||||
|
@@ -230,6 +230,8 @@ describe('DataTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dataTable.ngOnChanges({});
|
dataTable.ngOnChanges({});
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
dataTable.onColumnHeaderClick(column);
|
dataTable.onColumnHeaderClick(column);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -709,6 +711,8 @@ describe('DataTable', () => {
|
|||||||
|
|
||||||
it('should not sort if column is missing', () => {
|
it('should not sort if column is missing', () => {
|
||||||
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
const adapter = dataTable.data;
|
const adapter = dataTable.data;
|
||||||
spyOn(adapter, 'setSorting').and.callThrough();
|
spyOn(adapter, 'setSorting').and.callThrough();
|
||||||
dataTable.onColumnHeaderClick(null);
|
dataTable.onColumnHeaderClick(null);
|
||||||
@@ -717,6 +721,8 @@ describe('DataTable', () => {
|
|||||||
|
|
||||||
it('should not sort upon clicking non-sortable column header', () => {
|
it('should not sort upon clicking non-sortable column header', () => {
|
||||||
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
const adapter = dataTable.data;
|
const adapter = dataTable.data;
|
||||||
spyOn(adapter, 'setSorting').and.callThrough();
|
spyOn(adapter, 'setSorting').and.callThrough();
|
||||||
|
|
||||||
@@ -730,6 +736,8 @@ describe('DataTable', () => {
|
|||||||
|
|
||||||
it('should set sorting upon column header clicked', () => {
|
it('should set sorting upon column header clicked', () => {
|
||||||
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
const adapter = dataTable.data;
|
const adapter = dataTable.data;
|
||||||
spyOn(adapter, 'setSorting').and.callThrough();
|
spyOn(adapter, 'setSorting').and.callThrough();
|
||||||
|
|
||||||
@@ -749,6 +757,8 @@ describe('DataTable', () => {
|
|||||||
|
|
||||||
it('should invert sorting upon column header clicked', () => {
|
it('should invert sorting upon column header clicked', () => {
|
||||||
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) });
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
|
|
||||||
const adapter = dataTable.data;
|
const adapter = dataTable.data;
|
||||||
const sorting = new DataSorting('column_1', 'asc');
|
const sorting = new DataSorting('column_1', 'asc');
|
||||||
@@ -789,6 +799,8 @@ describe('DataTable', () => {
|
|||||||
new ObjectDataColumn({ key: 'other', sortable: true })
|
new ObjectDataColumn({ key: 'other', sortable: true })
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
|
|
||||||
const [col1, col2] = dataTable.getSortableColumns();
|
const [col1, col2] = dataTable.getSortableColumns();
|
||||||
|
|
||||||
@@ -1227,8 +1239,8 @@ describe('Accesibility', () => {
|
|||||||
|
|
||||||
it('should focus previous row on ArrowUp event', () => {
|
it('should focus previous row on ArrowUp event', () => {
|
||||||
const event = new KeyboardEvent('keyup', {
|
const event = new KeyboardEvent('keyup', {
|
||||||
code: 'ArrowDown',
|
code: 'ArrowUp',
|
||||||
key: 'ArrowDown',
|
key: 'ArrowUp',
|
||||||
keyCode: 38
|
keyCode: 38
|
||||||
} as KeyboardEventInit );
|
} as KeyboardEventInit );
|
||||||
|
|
||||||
@@ -1254,4 +1266,68 @@ describe('Accesibility', () => {
|
|||||||
|
|
||||||
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-0');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -223,7 +223,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.keyManager = new FocusKeyManager(this.rowsList).withWrap();
|
this.keyManager = new FocusKeyManager(this.rowsList)
|
||||||
|
.withWrap()
|
||||||
|
.skipPredicate(item => item.disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
@@ -406,7 +408,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (row) {
|
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);
|
const dataRowEvent = new DataRowEvent(row, mouseEvent, this);
|
||||||
this.clickObserver.next(dataRowEvent);
|
this.clickObserver.next(dataRowEvent);
|
||||||
}
|
}
|
||||||
@@ -511,6 +515,8 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
|
|||||||
this.data.setSorting(new DataSorting(column.key, newDirection));
|
this.data.setSorting(new DataSorting(column.key, newDirection));
|
||||||
this.emitSortingChangedEvent(column.key, newDirection);
|
this.emitSortingChangedEvent(column.key, newDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.keyManager.updateActiveItemIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectAllClick(matCheckboxChange: MatCheckboxChange) {
|
onSelectAllClick(matCheckboxChange: MatCheckboxChange) {
|
||||||
|
Reference in New Issue
Block a user