From 586562c4ddea50cfad40915d23b2328834d40c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baptiste=20Mah=C3=A9?= Date: Wed, 15 Jul 2020 10:48:05 +0100 Subject: [PATCH] [ADF-5180] Add keyboard accessibility for filters (#5868) --- .../search-check-list.component.html | 1 + .../search-check-list.component.spec.ts | 18 ++++++++ .../search-header.component.html | 46 ++++++++++--------- .../search-header.component.spec.ts | 44 ++++++++++++++++++ .../search-header/search-header.component.ts | 33 +++++++++---- 5 files changed, 113 insertions(+), 29 deletions(-) diff --git a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html index cd87689b4b..85aabc4adb 100644 --- a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html +++ b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html @@ -2,6 +2,7 @@ diff --git a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts index c903314fa0..65e5151542 100644 --- a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts @@ -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([ + { 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 = { operator: 'AND' }; component.ngOnInit(); diff --git a/lib/content-services/src/lib/search/components/search-header/search-header.component.html b/lib/content-services/src/lib/search/components/search-header/search-header.component.html index fa30b7ec6f..997ac27a28 100644 --- a/lib/content-services/src/lib/search/components/search-header/search-header.component.html +++ b/lib/content-services/src/lib/search/components/search-header/search-header.component.html @@ -3,36 +3,40 @@ - -
-
{{ category?.name | translate }}
- - + +
+
+
{{ category?.name | translate }}
+ + +
+ + + +
- - - -
diff --git a/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts b/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts index 1194e4d784..b072e8880e 100644 --- a/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts @@ -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); + }); + }); }); diff --git a/lib/content-services/src/lib/search/components/search-header/search-header.component.ts b/lib/content-services/src/lib/search/components/search-header/search-header.component.ts index b506b15497..a79bfba706 100644 --- a/lib/content-services/src/lib/search/components/search-header/search-header.component.ts +++ b/lib/content-services/src/lib/search/components/search-header/search-header.component.ts @@ -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(); 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; + } }