Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADF-5180] Add keyboard accessibility for filters #5868

Merged
merged 1 commit into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
}
}