mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ADF-5180] Add keyboard accessibility for filters (#5868)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
<mat-checkbox
|
||||
*ngFor="let option of options"
|
||||
[checked]="option.checked"
|
||||
(keydown.enter)="option.checked = !option.checked"
|
||||
[attr.data-automation-id]="'checkbox-' + (option.name)"
|
||||
(change)="changeHandler($event, option)"
|
||||
class="adf-facet-filter">
|
||||
|
@@ -51,6 +51,24 @@ describe('SearchCheckListComponent', () => {
|
||||
expect(component.options.items).toEqual(options);
|
||||
});
|
||||
|
||||
it('should handle enter key as click on checkboxes', () => {
|
||||
component.options = new SearchFilterList<SearchListOption>([
|
||||
{ name: 'Folder', value: "TYPE:'cm:folder'", checked: false },
|
||||
{ name: 'Document', value: "TYPE:'cm:content'", checked: false }
|
||||
]);
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const optionElements = fixture.debugElement.queryAll(By.css('mat-checkbox'));
|
||||
|
||||
optionElements[0].triggerEventHandler('keydown.enter', {});
|
||||
expect(component.options.items[0].checked).toBeTruthy();
|
||||
|
||||
optionElements[0].triggerEventHandler('keydown.enter', {});
|
||||
expect(component.options.items[0].checked).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should setup operator from the settings', () => {
|
||||
component.settings = <any> { operator: 'AND' };
|
||||
component.ngOnInit();
|
||||
|
@@ -3,36 +3,40 @@
|
||||
<button mat-icon-button [matMenuTriggerFor]="filter"
|
||||
id="filter-menu-button"
|
||||
#menuTrigger="matMenuTrigger"
|
||||
(click)="onMenuButtonClick($event)"
|
||||
(click)="$event.stopPropagation()"
|
||||
(menuOpened)="onMenuOpen()"
|
||||
(keyup.enter)="$event.stopPropagation()"
|
||||
class="adf-filter-button"
|
||||
[matTooltip]="getTooltipTranslation(col?.title)">
|
||||
<adf-icon value="adf:filter"
|
||||
[ngClass]="{ 'adf-icon-active': isActive()|| menuTrigger.menuOpen }"
|
||||
[ngClass]="{ 'adf-icon-active': isActive() || menuTrigger.menuOpen }"
|
||||
matBadge="filter"
|
||||
matBadgeColor="warn"
|
||||
[matBadgeHidden]="!isActive()"></adf-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #filter="matMenu" class="adf-filter-menu">
|
||||
<div (click)="onMenuClick($event)" class="adf-filter-container">
|
||||
<div class="adf-filter-title">{{ category?.name | translate }}</div>
|
||||
<adf-search-widget-container
|
||||
(keydown.enter)="onApply()"
|
||||
[id]="category?.id"
|
||||
[selector]="category?.component?.selector"
|
||||
[settings]="category?.component?.settings">
|
||||
</adf-search-widget-container>
|
||||
<mat-menu #filter="matMenu" class="adf-filter-menu" (closed)="onClosed()">
|
||||
<div #filterContainer (keydown.tab)="$event.stopPropagation();">
|
||||
<div (click)="$event.stopPropagation()" class="adf-filter-container">
|
||||
<div class="adf-filter-title">{{ category?.name | translate }}</div>
|
||||
<adf-search-widget-container
|
||||
(keydown.enter)="onEnterPressed()"
|
||||
[id]="category?.id"
|
||||
[selector]="category?.component?.selector"
|
||||
[settings]="category?.component?.settings">
|
||||
</adf-search-widget-container>
|
||||
</div>
|
||||
<mat-dialog-actions class="adf-filter-actions">
|
||||
<button mat-button id="clear-filter-button"
|
||||
(click)="onClearButtonClick($event)">{{ 'SEARCH.SEARCH_HEADER.CLEAR' | translate | uppercase }}
|
||||
</button>
|
||||
<button mat-button color="primary"
|
||||
id="apply-filter-button"
|
||||
class="adf-filter-apply-button"
|
||||
(click)="onApply()">{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
<mat-dialog-actions class="adf-filter-actions">
|
||||
<button mat-button id="clear-filter-button"
|
||||
(click)="onClearButtonClick($event)">{{ 'SEARCH.SEARCH_HEADER.CLEAR' | translate | uppercase }}
|
||||
</button>
|
||||
<button mat-button color="primary"
|
||||
id="apply-filter-button"
|
||||
class="adf-filter-apply-button" (click)="onApply()">
|
||||
{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -25,6 +25,7 @@ import { fakeNodePaging } from '../../../mock';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SimpleChange } from '@angular/core';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
|
||||
const mockCategory: any = {
|
||||
'id': 'queryName',
|
||||
@@ -209,4 +210,47 @@ describe('SearchHeaderComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
|
||||
it('should set up a focus trap on the filter when the menu is opened', async () => {
|
||||
expect(component.focusTrap).toBeUndefined();
|
||||
|
||||
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
|
||||
menuButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.focusTrap).toBeDefined();
|
||||
expect(component.focusTrap._element).toBe(component.filterContainer.nativeElement);
|
||||
});
|
||||
|
||||
it('should focus the input element when the menu is opened', async () => {
|
||||
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
|
||||
menuButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const inputElement = fixture.debugElement.query(By.css('.mat-input-element'));
|
||||
expect(document.activeElement).toBe(inputElement.nativeElement);
|
||||
|
||||
});
|
||||
|
||||
it('should focus the menu trigger when the menu is closed', async () => {
|
||||
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
|
||||
menuButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const matMenuTrigger = fixture.debugElement.query(By.directive(MatMenuTrigger)).injector.get(MatMenuTrigger);
|
||||
matMenuTrigger.closeMenu();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const matMenuButton = fixture.debugElement.query(By.css('#filter-menu-button'));
|
||||
expect(document.activeElement).toBe(matMenuButton.nativeElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -26,8 +26,10 @@ import {
|
||||
ViewEncapsulation,
|
||||
ViewChild,
|
||||
Inject,
|
||||
OnDestroy
|
||||
OnDestroy,
|
||||
ElementRef
|
||||
} from '@angular/core';
|
||||
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
|
||||
import { DataColumn, TranslationService } from '@alfresco/adf-core';
|
||||
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
|
||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||
@@ -72,13 +74,18 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@ViewChild(SearchWidgetContainerComponent)
|
||||
widgetContainer: SearchWidgetContainerComponent;
|
||||
|
||||
@ViewChild('filterContainer')
|
||||
filterContainer: ElementRef;
|
||||
|
||||
category: SearchCategory;
|
||||
isFilterServiceActive: boolean;
|
||||
focusTrap: ConfigurableFocusTrap;
|
||||
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchHeaderQueryBuilder: SearchHeaderQueryBuilderService,
|
||||
private translationService: TranslationService) {
|
||||
private translationService: TranslationService,
|
||||
private focusTrapFactory: ConfigurableFocusTrapFactory) {
|
||||
this.isFilterServiceActive = this.searchHeaderQueryBuilder.isFilterServiceActive();
|
||||
}
|
||||
|
||||
@@ -120,12 +127,10 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
onMenuButtonClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onMenuClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
onEnterPressed() {
|
||||
if (this.widgetContainer.selector !== 'check-list') {
|
||||
this.onApply();
|
||||
}
|
||||
}
|
||||
|
||||
onApply() {
|
||||
@@ -165,4 +170,16 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
isActive(): boolean {
|
||||
return this.widgetContainer && this.widgetContainer.componentRef && this.widgetContainer.componentRef.instance.isActive;
|
||||
}
|
||||
|
||||
onMenuOpen() {
|
||||
if (this.filterContainer && !this.focusTrap) {
|
||||
this.focusTrap = this.focusTrapFactory.create(this.filterContainer.nativeElement);
|
||||
this.focusTrap.focusInitialElement();
|
||||
}
|
||||
}
|
||||
|
||||
onClosed() {
|
||||
this.focusTrap.destroy();
|
||||
this.focusTrap = null;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user