[ACS-9267] a11y testing: Datatable - columns settings menu (#10668)

* [ACS-9267] a11y testing: Datatable - columns settings menu

* delete comment

* [ACS-9267] add empty line

* [ACS-9267] adress sonarcloud issues

* [ACS-9267] fix unit tests
This commit is contained in:
Mykyta Maliarchuk 2025-02-25 08:26:36 +01:00 committed by GitHub
parent ba322b70b2
commit 1e83be9194
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 81 additions and 14 deletions

View File

@ -1137,7 +1137,7 @@ describe('DocumentList', () => {
it('should display [empty folder] template ', () => {
fixture.detectChanges();
runInInjectionContext(injector, () => {
documentList.dataTable = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock);
documentList.dataTable = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock, null);
});
expect(documentList.dataTable).toBeDefined();
expect(fixture.debugElement.query(By.css('adf-empty-list'))).not.toBeNull();
@ -1158,7 +1158,7 @@ describe('DocumentList', () => {
it('should empty folder NOT show the pagination', () => {
runInInjectionContext(injector, () => {
documentList.dataTable = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock);
documentList.dataTable = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock, null);
});
expect(documentList.isEmpty()).toBeTruthy();

View File

@ -1,8 +1,7 @@
<div
class="adf-columns-selector"
role="presentation"
data-automation-id="adf-columns-selector"
tabindex="0"
role="button"
(keyup.enter)="$event.stopPropagation()"
(click)="$event.stopPropagation();"
>
@ -14,6 +13,8 @@
<button
data-automation-id="adf-columns-selector-close-button"
mat-icon-button
role="menuitem"
[attr.aria-label]="'CLOSE' | translate"
(click)="closeMenu()"
>
<mat-icon>close</mat-icon>
@ -33,6 +34,7 @@
class="adf-columns-selector-search-input"
data-automation-id="adf-columns-selector-search-input"
type="text"
role="menuitem"
[placeholder]='"ADF-DATATABLE.COLUMNS_SELECTOR.SEARCH" | translate'>
</div>
@ -43,7 +45,9 @@
class="adf-columns-selector-column-checkbox"
[attr.data-automation-id]="'adf-columns-selector-column-checkbox-' + column.title"
[checked]="!column.isHidden"
role="menuitem"
[disabled]="isCheckboxDisabled(column)"
(keydown.enter)="changeColumnVisibility(column)"
(change)="changeColumnVisibility(column)">
<div class="adf-columns-selector-list-content">
{{column.title | translate}}
@ -68,6 +72,7 @@
<div class="adf-columns-selector-footer">
<button
mat-flat-button
role="menuitem"
data-automation-id="adf-columns-selector-apply-button"
color="primary"
(click)="apply()">

View File

@ -161,16 +161,18 @@
title="{{ 'ADF-DATATABLE.CONTENT-ACTIONS.SELECT_COLUMNS' | translate }}"
mat-icon-button
#mainMenuTrigger="matMenuTrigger"
(keydown.enter)="mainMenuTrigger.openMenu()"
(click)="onMainMenuOpen()"
[matMenuTriggerFor]="mainMenu">
<mat-icon>view_week_outline</mat-icon>
</button>
<mat-menu #mainMenu>
<ng-container
[ngTemplateOutlet]="mainActionTemplate"
[ngTemplateOutletContext]="{
$implicit: mainMenuTrigger
}" />
<mat-menu #mainMenu (closed)="onMainMenuClosed()">
<div #mainMenuTemplate role="presentation" (keydown.tab)="$event.stopPropagation()">
<ng-container
[ngTemplateOutlet]="mainActionTemplate"
[ngTemplateOutletContext]="{
$implicit: mainMenuTrigger
}" />
</div>
</mat-menu>
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
</ng-container>

View File

@ -34,6 +34,7 @@ import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { UnitTestingUtils } from '../../../testing/unit-testing-utils';
import { HarnessLoader } from '@angular/cdk/testing';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
@Component({
selector: 'adf-custom-column-template-component',
@ -1508,15 +1509,19 @@ describe('DataTable', () => {
});
});
describe('Accesibility', () => {
describe('Accessibility', () => {
let fixture: ComponentFixture<DataTableComponent>;
let dataTable: DataTableComponent;
let columnCustomTemplate: TemplateRef<any>;
let testingUtils: UnitTestingUtils;
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule, CustomColumnTemplateComponent],
providers: [{ provide: ConfigurableFocusTrapFactory, useValue: focusTrapFactory }],
schemas: [NO_ERRORS_SCHEMA]
});
columnCustomTemplate = TestBed.createComponent(CustomColumnTemplateComponent).componentInstance.templateRef;
@ -1708,6 +1713,36 @@ describe('Accesibility', () => {
const cell = testingUtils.getByCSS('.adf-datatable-row[data-automation-id="datatable-row-0"] .adf-cell-value');
expect(cell?.nativeElement.getAttribute('tabindex')).toBe('0');
});
it('should create focus trap on main menu open', () => {
dataTable.showHeader = ShowHeaderMode.Always;
dataTable.showMainDatatableActions = true;
dataTable.mainActionTemplate = columnCustomTemplate;
focusTrapFactory.create.and.returnValue(focusTrap);
spyOn(dataTable, 'onMainMenuOpen').and.callThrough();
dataTable.ngOnChanges({
rows: new SimpleChange(null, [{ name: 'test1' }, { name: 'test2' }], false)
});
fixture.detectChanges();
dataTable.ngAfterViewInit();
testingUtils.clickByDataAutomationId('adf-datatable-main-menu-button');
fixture.detectChanges();
expect(dataTable.onMainMenuOpen).toHaveBeenCalled();
expect(focusTrapFactory.create).toHaveBeenCalledWith(dataTable.mainMenuTemplate.nativeElement);
expect(focusTrap.focusInitialElement).toHaveBeenCalled();
});
it('should destroy focus trap on main menu closed', () => {
dataTable.focusTrap = focusTrap;
dataTable.onMainMenuClosed();
expect(focusTrap.destroy).toHaveBeenCalled();
expect(dataTable.focusTrap).toBeNull();
});
});
describe('Drag&Drop column header', () => {

View File

@ -38,10 +38,11 @@ import {
SimpleChange,
SimpleChanges,
TemplateRef,
ViewChild,
ViewChildren,
ViewEncapsulation
} from '@angular/core';
import { FocusKeyManager } from '@angular/cdk/a11y';
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory, FocusKeyManager } from '@angular/cdk/a11y';
import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
import { Observable, Observer, Subscription } from 'rxjs';
@ -133,6 +134,9 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
@ViewChildren(DataTableRowComponent)
rowsList: QueryList<DataTableRowComponent>;
@ViewChild('mainMenuTemplate')
mainMenuTemplate: ElementRef;
@ContentChild(DataColumnListComponent)
columnList: DataColumnListComponent;
@ -324,6 +328,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
hoveredHeaderColumnIndex = -1;
resizingColumnIndex = -1;
isDraggingRow = false;
focusTrap: ConfigurableFocusTrap;
private keyManager: FocusKeyManager<DataTableRowComponent>;
private clickObserver: Observer<DataRowEvent>;
@ -342,7 +347,13 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
this.keyManager.onKeydown(event);
}
constructor(private elementRef: ElementRef, differs: IterableDiffers, private matIconRegistry: MatIconRegistry, private sanitizer: DomSanitizer) {
constructor(
private readonly elementRef: ElementRef,
differs: IterableDiffers,
private readonly matIconRegistry: MatIconRegistry,
private readonly sanitizer: DomSanitizer,
private readonly focusTrapFactory: ConfigurableFocusTrapFactory
) {
if (differs) {
this.differ = differs.find([]).create(null);
}
@ -465,6 +476,20 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
return null;
}
onMainMenuOpen() {
if (this.mainMenuTemplate && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.mainMenuTemplate.nativeElement);
this.focusTrap.focusInitialElement();
}
}
onMainMenuClosed() {
if (this.focusTrap) {
this.focusTrap.destroy();
this.focusTrap = null;
}
}
private initAndSubscribeClickStream() {
this.unsubscribeClickStream();
const singleClickStream = this.click$.pipe(