mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-31 17:38:48 +00:00
[ADF-5180] Add keyboard accessibility for filters (#5868)
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
*ngFor="let option of options"
|
*ngFor="let option of options"
|
||||||
[checked]="option.checked"
|
[checked]="option.checked"
|
||||||
|
(keydown.enter)="option.checked = !option.checked"
|
||||||
[attr.data-automation-id]="'checkbox-' + (option.name)"
|
[attr.data-automation-id]="'checkbox-' + (option.name)"
|
||||||
(change)="changeHandler($event, option)"
|
(change)="changeHandler($event, option)"
|
||||||
class="adf-facet-filter">
|
class="adf-facet-filter">
|
||||||
|
@@ -51,6 +51,24 @@ describe('SearchCheckListComponent', () => {
|
|||||||
expect(component.options.items).toEqual(options);
|
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', () => {
|
it('should setup operator from the settings', () => {
|
||||||
component.settings = <any> { operator: 'AND' };
|
component.settings = <any> { operator: 'AND' };
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
|
@@ -3,21 +3,24 @@
|
|||||||
<button mat-icon-button [matMenuTriggerFor]="filter"
|
<button mat-icon-button [matMenuTriggerFor]="filter"
|
||||||
id="filter-menu-button"
|
id="filter-menu-button"
|
||||||
#menuTrigger="matMenuTrigger"
|
#menuTrigger="matMenuTrigger"
|
||||||
(click)="onMenuButtonClick($event)"
|
(click)="$event.stopPropagation()"
|
||||||
|
(menuOpened)="onMenuOpen()"
|
||||||
|
(keyup.enter)="$event.stopPropagation()"
|
||||||
class="adf-filter-button"
|
class="adf-filter-button"
|
||||||
[matTooltip]="getTooltipTranslation(col?.title)">
|
[matTooltip]="getTooltipTranslation(col?.title)">
|
||||||
<adf-icon value="adf:filter"
|
<adf-icon value="adf:filter"
|
||||||
[ngClass]="{ 'adf-icon-active': isActive()|| menuTrigger.menuOpen }"
|
[ngClass]="{ 'adf-icon-active': isActive() || menuTrigger.menuOpen }"
|
||||||
matBadge="filter"
|
matBadge="filter"
|
||||||
matBadgeColor="warn"
|
matBadgeColor="warn"
|
||||||
[matBadgeHidden]="!isActive()"></adf-icon>
|
[matBadgeHidden]="!isActive()"></adf-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<mat-menu #filter="matMenu" class="adf-filter-menu">
|
<mat-menu #filter="matMenu" class="adf-filter-menu" (closed)="onClosed()">
|
||||||
<div (click)="onMenuClick($event)" class="adf-filter-container">
|
<div #filterContainer (keydown.tab)="$event.stopPropagation();">
|
||||||
|
<div (click)="$event.stopPropagation()" class="adf-filter-container">
|
||||||
<div class="adf-filter-title">{{ category?.name | translate }}</div>
|
<div class="adf-filter-title">{{ category?.name | translate }}</div>
|
||||||
<adf-search-widget-container
|
<adf-search-widget-container
|
||||||
(keydown.enter)="onApply()"
|
(keydown.enter)="onEnterPressed()"
|
||||||
[id]="category?.id"
|
[id]="category?.id"
|
||||||
[selector]="category?.component?.selector"
|
[selector]="category?.component?.selector"
|
||||||
[settings]="category?.component?.settings">
|
[settings]="category?.component?.settings">
|
||||||
@@ -29,10 +32,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<button mat-button color="primary"
|
<button mat-button color="primary"
|
||||||
id="apply-filter-button"
|
id="apply-filter-button"
|
||||||
class="adf-filter-apply-button" (click)="onApply()">
|
class="adf-filter-apply-button"
|
||||||
{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
|
(click)="onApply()">{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
</div>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -25,6 +25,7 @@ import { fakeNodePaging } from '../../../mock';
|
|||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { SimpleChange } from '@angular/core';
|
import { SimpleChange } from '@angular/core';
|
||||||
|
import { MatMenuTrigger } from '@angular/material/menu';
|
||||||
|
|
||||||
const mockCategory: any = {
|
const mockCategory: any = {
|
||||||
'id': 'queryName',
|
'id': 'queryName',
|
||||||
@@ -209,4 +210,47 @@ describe('SearchHeaderComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
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,
|
ViewEncapsulation,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy
|
OnDestroy,
|
||||||
|
ElementRef
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
|
||||||
import { DataColumn, TranslationService } from '@alfresco/adf-core';
|
import { DataColumn, TranslationService } from '@alfresco/adf-core';
|
||||||
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
|
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
|
||||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||||
@@ -72,13 +74,18 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@ViewChild(SearchWidgetContainerComponent)
|
@ViewChild(SearchWidgetContainerComponent)
|
||||||
widgetContainer: SearchWidgetContainerComponent;
|
widgetContainer: SearchWidgetContainerComponent;
|
||||||
|
|
||||||
|
@ViewChild('filterContainer')
|
||||||
|
filterContainer: ElementRef;
|
||||||
|
|
||||||
category: SearchCategory;
|
category: SearchCategory;
|
||||||
isFilterServiceActive: boolean;
|
isFilterServiceActive: boolean;
|
||||||
|
focusTrap: ConfigurableFocusTrap;
|
||||||
|
|
||||||
private onDestroy$ = new Subject<boolean>();
|
private onDestroy$ = new Subject<boolean>();
|
||||||
|
|
||||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchHeaderQueryBuilder: SearchHeaderQueryBuilderService,
|
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchHeaderQueryBuilder: SearchHeaderQueryBuilderService,
|
||||||
private translationService: TranslationService) {
|
private translationService: TranslationService,
|
||||||
|
private focusTrapFactory: ConfigurableFocusTrapFactory) {
|
||||||
this.isFilterServiceActive = this.searchHeaderQueryBuilder.isFilterServiceActive();
|
this.isFilterServiceActive = this.searchHeaderQueryBuilder.isFilterServiceActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +127,10 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.onDestroy$.complete();
|
this.onDestroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMenuButtonClick(event: Event) {
|
onEnterPressed() {
|
||||||
event.stopPropagation();
|
if (this.widgetContainer.selector !== 'check-list') {
|
||||||
|
this.onApply();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMenuClick(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onApply() {
|
onApply() {
|
||||||
@@ -165,4 +170,16 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
return this.widgetContainer && this.widgetContainer.componentRef && this.widgetContainer.componentRef.instance.isActive;
|
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