mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[ACS-10241]: SR: Personal files: Column sorting state and role not communicated by buttons (#11591)
* [ACS-10241]: updates aria setup for sortable columns * [ACS-10241]: updates UTs * [ACS-10241]: cleanup
This commit is contained in:
@@ -53,12 +53,8 @@
|
|||||||
'adf-datatable__header--sorted-asc': isColumnSorted(col, 'asc'),
|
'adf-datatable__header--sorted-asc': isColumnSorted(col, 'asc'),
|
||||||
'adf-datatable__header--sorted-desc': isColumnSorted(col, 'desc')}"
|
'adf-datatable__header--sorted-desc': isColumnSorted(col, 'desc')}"
|
||||||
[ngStyle]="(col.width) && !lastColumn && {'flex': getFlexValue(col)}"
|
[ngStyle]="(col.width) && !lastColumn && {'flex': getFlexValue(col)}"
|
||||||
[attr.aria-label]="col.srTitle ? (col.srTitle | translate) : (col.title | translate) + (col.subtitle ? ' ' + (col.subtitle | translate) : '')"
|
|
||||||
(click)="onColumnHeaderClick(col, $event)"
|
|
||||||
(keyup.enter)="onColumnHeaderClick(col, $event)"
|
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
[attr.tabindex]="col.sortable ? 0 : null"
|
[attr.aria-sort]="col.sortable ? getAriaSort(col) : null"
|
||||||
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
|
|
||||||
cdkDrag
|
cdkDrag
|
||||||
cdkDragLockAxis="x"
|
cdkDragLockAxis="x"
|
||||||
(cdkDragStarted)="isDraggingHeaderColumn = true"
|
(cdkDragStarted)="isDraggingHeaderColumn = true"
|
||||||
@@ -73,6 +69,21 @@
|
|||||||
<div
|
<div
|
||||||
adf-resizable
|
adf-resizable
|
||||||
#resizableElement="adf-resizable"
|
#resizableElement="adf-resizable"
|
||||||
|
[attr.tabindex]="col.sortable ? 0 : null"
|
||||||
|
[attr.role]="col.sortable ? 'button' : null"
|
||||||
|
[attr.aria-description]="
|
||||||
|
isColumnSortActive(col) ? (getSortLiveAnnouncement(col) | translate: { string: col.title | translate }) : null
|
||||||
|
"
|
||||||
|
[attr.aria-label]="
|
||||||
|
col.srTitle
|
||||||
|
? (col.srTitle | translate)
|
||||||
|
: ('ADF-DATATABLE.ACCESSIBILITY.SORT_DEFAULT' | translate) +
|
||||||
|
' ' +
|
||||||
|
(col.title | translate) +
|
||||||
|
(col.subtitle ? ' ' + (col.subtitle | translate) : '')
|
||||||
|
"
|
||||||
|
(click)="onColumnHeaderClick(col, $event)"
|
||||||
|
(keyup.enter)="onColumnHeaderClick(col, $event)"
|
||||||
[coverPadding]="10"
|
[coverPadding]="10"
|
||||||
(resizing)="onResizing($event, columnIndex)"
|
(resizing)="onResizing($event, columnIndex)"
|
||||||
(keyboardResizing)="onResizing($event, columnIndex)"
|
(keyboardResizing)="onResizing($event, columnIndex)"
|
||||||
@@ -85,6 +96,10 @@
|
|||||||
!isDraggingHeaderColumn &&
|
!isDraggingHeaderColumn &&
|
||||||
!isResizing && col.sortable}"
|
!isResizing && col.sortable}"
|
||||||
>
|
>
|
||||||
|
<span *ngIf="col.title && col.sortable" class="adf-sr-only" aria-live="assertive" aria-atomic="true">
|
||||||
|
{{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<ng-container *ngIf="!col.header">
|
<ng-container *ngIf="!col.header">
|
||||||
<span
|
<span
|
||||||
*ngIf="col.title"
|
*ngIf="col.title"
|
||||||
@@ -102,10 +117,6 @@
|
|||||||
({{col.subtitle | translate}})
|
({{col.subtitle | translate}})
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span *ngIf="col.title && col.sortable && isDraggingHeaderColumn" class="adf-sr-only" aria-live="polite">
|
|
||||||
{{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span *ngIf="!col.title && !col.sortable && !headerFilterTemplate" [attr.title]="'ADF-DATATABLE.ACCESSIBILITY.EMPTY_HEADER' | translate"></span>
|
<span *ngIf="!col.title && !col.sortable && !headerFilterTemplate" [attr.title]="'ADF-DATATABLE.ACCESSIBILITY.EMPTY_HEADER' | translate"></span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|||||||
@@ -440,6 +440,20 @@ describe('DataTable', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fallback to column sortingKey if key is missing', () => {
|
||||||
|
const sorting = new DataSorting('displayName', 'asc');
|
||||||
|
dataTable.data = new ObjectDataTableAdapter([{ name: '1' }, { name: '2' }], [new ObjectDataColumn({ key: 'name' })]);
|
||||||
|
dataTable.data.setSorting(sorting);
|
||||||
|
|
||||||
|
const column1 = new ObjectDataColumn({ key: 'name', sortable: true, direction: 'desc', sortingKey: 'displayName' });
|
||||||
|
const column2 = new ObjectDataColumn({ key: 'displayName', sortable: true, direction: 'desc', sortingKey: 'name' });
|
||||||
|
const column3 = new ObjectDataColumn({ key: 'other', sortable: true, direction: 'desc', sortingKey: 'other' });
|
||||||
|
|
||||||
|
expect(dataTable.isColumnSortActive(column1)).toBeTrue();
|
||||||
|
expect(dataTable.isColumnSortActive(column2)).toBeTrue();
|
||||||
|
expect(dataTable.isColumnSortActive(column3)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
it('should change the rows on changing of the data', () => {
|
it('should change the rows on changing of the data', () => {
|
||||||
const newData = new ObjectDataTableAdapter([{ name: 'TEST' }, { name: 'FAKE' }], [new ObjectDataColumn({ key: 'name' })]);
|
const newData = new ObjectDataTableAdapter([{ name: 'TEST' }, { name: 'FAKE' }], [new ObjectDataColumn({ key: 'name' })]);
|
||||||
dataTable.data = new ObjectDataTableAdapter([{ name: '1' }, { name: '2' }], [new ObjectDataColumn({ key: 'name' })]);
|
dataTable.data = new ObjectDataTableAdapter([{ name: '1' }, { name: '2' }], [new ObjectDataColumn({ key: 'name' })]);
|
||||||
@@ -1588,6 +1602,27 @@ describe('Accessibility', () => {
|
|||||||
let columnCustomTemplate: TemplateRef<any>;
|
let columnCustomTemplate: TemplateRef<any>;
|
||||||
let testingUtils: UnitTestingUtils;
|
let testingUtils: UnitTestingUtils;
|
||||||
|
|
||||||
|
const setupAndCheckHeaderColumns = (sortable: boolean, selector: string, assertions: (element: DebugElement | null) => void) => {
|
||||||
|
dataTable.showHeader = ShowHeaderMode.Always;
|
||||||
|
const dataRows = [{ name: 'name1' }];
|
||||||
|
|
||||||
|
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name', template: columnCustomTemplate, sortable })]);
|
||||||
|
|
||||||
|
dataTable.ngOnChanges({
|
||||||
|
rows: new SimpleChange(null, dataRows, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable.ngAfterViewInit();
|
||||||
|
|
||||||
|
const element = testingUtils.getByCSS(selector);
|
||||||
|
assertions(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerCellSelector = '.adf-datatable-cell-header';
|
||||||
|
const bodyCellSelector = '.adf-datatable-cell';
|
||||||
|
const headerCellContentSelector = '.adf-datatable-cell-header-content';
|
||||||
|
|
||||||
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
|
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
|
||||||
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
|
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
|
||||||
|
|
||||||
@@ -1638,25 +1673,25 @@ describe('Accessibility', () => {
|
|||||||
column = new ObjectDataColumn({ key: 'key' });
|
column = new ObjectDataColumn({ key: 'key' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct translation key when no sort is applied', () => {
|
it('should return correct key without translation when no sort is applied', () => {
|
||||||
spyOn(dataTable, 'isColumnSortActive').and.returnValue(false);
|
spyOn(dataTable, 'isColumnSortActive').and.returnValue(false);
|
||||||
expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_NONE');
|
expect(dataTable.getAriaSort(column)).toBe('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return translation key when column sort is ascending', () => {
|
it('should return key without translation when column sort is ascending', () => {
|
||||||
const isColumnSortedAsc = true;
|
const isColumnSortedAsc = true;
|
||||||
spyOn(dataTable, 'isColumnSortActive').and.returnValue(true);
|
spyOn(dataTable, 'isColumnSortActive').and.returnValue(true);
|
||||||
spyOn(dataTable, 'isColumnSorted').and.returnValue(isColumnSortedAsc);
|
spyOn(dataTable, 'isColumnSorted').and.returnValue(isColumnSortedAsc);
|
||||||
|
|
||||||
expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_ASCENDING');
|
expect(dataTable.getAriaSort(column)).toBe('ascending');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return translation key when column sort is descending', () => {
|
it('should return key without translation when column sort is descending', () => {
|
||||||
const isColumnSortedAsc = false;
|
const isColumnSortedAsc = false;
|
||||||
spyOn(dataTable, 'isColumnSortActive').and.returnValue(true);
|
spyOn(dataTable, 'isColumnSortActive').and.returnValue(true);
|
||||||
spyOn(dataTable, 'isColumnSorted').and.returnValue(isColumnSortedAsc);
|
spyOn(dataTable, 'isColumnSorted').and.returnValue(isColumnSortedAsc);
|
||||||
|
|
||||||
expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING');
|
expect(dataTable.getAriaSort(column)).toBe('descending');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1716,41 +1751,68 @@ describe('Accessibility', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Row cells focus management', () => {
|
describe('Row cells focus management', () => {
|
||||||
const testFocus = (sortable: boolean, selector: string, expectedTabindex: string | null) => {
|
|
||||||
dataTable.showHeader = ShowHeaderMode.Always;
|
|
||||||
const dataRows = [{ name: 'name1' }];
|
|
||||||
|
|
||||||
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name', template: columnCustomTemplate, sortable })]);
|
|
||||||
|
|
||||||
dataTable.ngOnChanges({
|
|
||||||
rows: new SimpleChange(null, dataRows, false)
|
|
||||||
});
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
dataTable.ngAfterViewInit();
|
|
||||||
|
|
||||||
const element = testingUtils.getByCSS(selector);
|
|
||||||
expect(element?.nativeElement.getAttribute('tabindex')).toEqual(expectedTabindex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerCellSelector = '.adf-datatable-cell-header';
|
|
||||||
|
|
||||||
it('should remove header cell focus when cell is not sortable', () => {
|
it('should remove header cell focus when cell is not sortable', () => {
|
||||||
testFocus(false, headerCellSelector, null);
|
setupAndCheckHeaderColumns(false, headerCellSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('tabindex')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow header cell focus when cell is sortable', () => {
|
it('should allow header cell focus when cell is sortable', () => {
|
||||||
testFocus(true, headerCellSelector, '0');
|
setupAndCheckHeaderColumns(true, headerCellSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('tabindex')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set tabindex equal to null on body cell by default (sortable = false)', () => {
|
it('should set tabindex equal to null on body cell by default (sortable = false)', () => {
|
||||||
const bodyCellSelector = '.adf-datatable-cell';
|
setupAndCheckHeaderColumns(false, bodyCellSelector, (element) => {
|
||||||
testFocus(false, bodyCellSelector, null);
|
expect(element?.nativeElement.getAttribute('tabindex')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set tabindex equal to null on body cell by default (sortable = true)', () => {
|
it('should set tabindex equal to null on body cell by default (sortable = true)', () => {
|
||||||
const bodyCellSelector = '.adf-datatable-cell';
|
setupAndCheckHeaderColumns(true, bodyCellSelector, (element) => {
|
||||||
testFocus(true, bodyCellSelector, null);
|
expect(element?.nativeElement.getAttribute('tabindex')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set tabindex equal to 0 on header cell sortable wrapper when cell is sortable', () => {
|
||||||
|
setupAndCheckHeaderColumns(true, headerCellContentSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('tabindex')).toBe('0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set tabindex equal to null on header cell sortable wrapper when cell is not sortable', () => {
|
||||||
|
setupAndCheckHeaderColumns(false, headerCellContentSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('tabindex')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set role="button" on sortable header content cells', () => {
|
||||||
|
setupAndCheckHeaderColumns(true, headerCellContentSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('role')).toEqual('button');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no role set on non-sortable header content cells', () => {
|
||||||
|
setupAndCheckHeaderColumns(false, headerCellContentSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('role')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set aria-description on an active sortable header content cells', () => {
|
||||||
|
spyOn(dataTable, 'isColumnSortActive').and.returnValue(true);
|
||||||
|
|
||||||
|
setupAndCheckHeaderColumns(true, headerCellContentSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('aria-description')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set aria-description on non-active sortable header content cells', () => {
|
||||||
|
spyOn(dataTable, 'isColumnSortActive').and.returnValue(false);
|
||||||
|
|
||||||
|
setupAndCheckHeaderColumns(true, headerCellContentSelector, (element) => {
|
||||||
|
expect(element?.nativeElement.getAttribute('aria-description')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -452,10 +452,11 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
}
|
}
|
||||||
|
|
||||||
isColumnSortActive(column: DataColumn): boolean {
|
isColumnSortActive(column: DataColumn): boolean {
|
||||||
if (!column || !this.data.getSorting()) {
|
const sorting = this.data.getSorting();
|
||||||
|
if (!column || !sorting) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return column.key === this.data.getSorting().key;
|
return column.key === sorting.key || column.sortingKey === sorting.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVisibleColumns(): DataColumn[] {
|
getVisibleColumns(): DataColumn[] {
|
||||||
@@ -747,7 +748,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
|
|
||||||
private isValidClickEvent(event: Event): boolean {
|
private isValidClickEvent(event: Event): boolean {
|
||||||
if (event instanceof MouseEvent) {
|
if (event instanceof MouseEvent) {
|
||||||
return event.eventPhase === event.BUBBLING_PHASE;
|
return event.eventPhase === event.AT_TARGET || event.eventPhase === event.BUBBLING_PHASE;
|
||||||
} else if (event instanceof KeyboardEvent) {
|
} else if (event instanceof KeyboardEvent) {
|
||||||
return event.eventPhase === event.AT_TARGET;
|
return event.eventPhase === event.AT_TARGET;
|
||||||
}
|
}
|
||||||
@@ -1031,10 +1032,10 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
|
|
||||||
getAriaSort(column: DataColumn): string {
|
getAriaSort(column: DataColumn): string {
|
||||||
if (!this.isColumnSortActive(column)) {
|
if (!this.isColumnSortActive(column)) {
|
||||||
return 'ADF-DATATABLE.ACCESSIBILITY.SORT_NONE';
|
return 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.isColumnSorted(column, 'asc') ? 'ADF-DATATABLE.ACCESSIBILITY.SORT_ASCENDING' : 'ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING';
|
return this.isColumnSorted(column, 'asc') ? 'ascending' : 'descending';
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortLiveAnnouncement(column: DataColumn): string {
|
getSortLiveAnnouncement(column: DataColumn): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user