Skip to content
Snippets Groups Projects
Commit 806780fa authored by Etienne LOUPIAS's avatar Etienne LOUPIAS
Browse files

fix(accessibility): annuaire Menu Button aria pattern

parent bdce4cbd
No related branches found
No related tags found
2 merge requests!907V3.2.0,!874fix(accessibility): annuaire Menu Button aria pattern
......@@ -5,14 +5,18 @@
<app-collapsable-filter
[label]="'Fonction'"
[expanded]="modalTypeOpened === TypeModal.jobs"
[id]="'modal' + TypeModal.jobs"
[active]="jobsFiltersActive"
(toggle)="openModal(TypeModal.jobs)"
(keyup)="onKeyboardNavOnFilters($event)"
/>
<app-collapsable-filter
[label]="'Employeur'"
[expanded]="modalTypeOpened === TypeModal.employers"
[id]="'modal' + TypeModal.employers"
[active]="employersFiltersActive"
(toggle)="openModal(TypeModal.employers)"
(keyup)="onKeyboardNavOnFilters($event)"
/>
<app-filter-modal
[modalType]="modalTypeOpened"
......
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { forkJoin, lastValueFrom } from 'rxjs';
import { SearchService } from '../../structure-list/services/search.service';
import { Utils } from '../../utils/utils';
import { TypeModal } from '../enums/TypeModal.enum';
import { SearchQuery } from '../models/searchQuery.model';
......@@ -14,6 +15,7 @@ export class AnnuaireHeaderComponent implements OnInit, OnChanges {
@Input() shouldResetFilters = 0;
@Output() searchEvent = new EventEmitter<SearchQuery>();
public utils = new Utils();
public modalTypeOpened: TypeModal;
public employersFiltersActive = false;
public jobsFiltersActive = false;
......@@ -24,11 +26,13 @@ export class AnnuaireHeaderComponent implements OnInit, OnChanges {
public searchInput = '';
public jobFilterChecked: string[] = [];
public employerFilterChecked: string[] = [];
public keyboardEvent = false;
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
public searchService: SearchService,
private elementRef: ElementRef,
) {}
async ngOnInit(): Promise<void> {
......@@ -142,9 +146,32 @@ export class AnnuaireHeaderComponent implements OnInit, OnChanges {
/** Open the modal and display the list according to the right filter button */
public openModal(modalType: TypeModal): void {
this.modalTypeOpened = this.modalTypeOpened === modalType ? undefined : modalType;
// Accessibility: when navigating with keyboard and opening a filter modal, send focus to the first focusable element of the opened modal
if (this.keyboardEvent) {
setTimeout(() => {
this.utils.setFocusOnElement(this.elementRef, `.modalContent .collapse-header, .modalContent input`);
}, 0);
}
}
// When filters and their modal are in the same component, we can remove onKeyboardNavOnFilters, setFocusOnOpenedModal, and setFocusOnFilters.
// because the focus will then flow normally between the filter and the modal
public onKeyboardNavOnFilters(event: KeyboardEvent): void {
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'Tab':
this.keyboardEvent = true;
break;
}
}
public closeModal(): void {
// Accessibility: when navigating with keyboard and closing a filter modal, send focus back to filters
if (this.keyboardEvent) {
this.utils.setFocusOnElement(this.elementRef, '#modal' + this.modalTypeOpened + ' button:first-of-type');
}
this.modalTypeOpened = undefined;
this.countCheckedFilters();
}
......
<div *ngIf="modalType" [ngClass]="['filterModal', getModalType()]">
<div
*ngIf="modalType"
role="menu"
cdkTrapFocus
[ngClass]="['filterModal', getModalType()]"
[cdkTrapFocusAutoCapture]="true"
>
<div class="filterModalContainer">
<div class="modalContent">
<app-label-checkbox
......
<button
type="button"
aria-haspopup="true"
[attr.aria-label]="label + 'Déplier les filtres : '"
[attr.aria-label]="'Déplier les filtres : ' + label"
[attr.aria-expanded]="expanded"
[ngClass]="{
expanded: expanded,
active: active
......
......@@ -189,7 +189,9 @@ export class StructureListSearchComponent implements OnInit {
// Accessibility: when navigating with keyboard and opening a filter modal, send focus to the first focusable element of the opened modal
if (this.keyboardEvent) {
this.setFocusOnOpenedModal();
setTimeout(() => {
this.utils.setFocusOnElement(this.elementRef, `.modalContent .collapse-header, .modalContent input`);
}, 0);
}
}
}
......@@ -206,32 +208,10 @@ export class StructureListSearchComponent implements OnInit {
}
}
private setFocusOnOpenedModal(): void {
setTimeout(() => {
const modalFirstFocusableElement = this.elementRef.nativeElement.querySelector(
`.modalContent .collapse-header, .modalContent input`,
);
if (modalFirstFocusableElement) {
const focusedElement = modalFirstFocusableElement as HTMLElement;
focusedElement.focus();
}
}, 0);
}
private setFocusOnFilter(): void {
const filterButton = this.elementRef.nativeElement.querySelector(
'#modal' + this.modalTypeOpened + ' button:first-of-type',
);
if (filterButton) {
const focusedElement = filterButton as HTMLElement;
focusedElement.focus();
}
}
public closeModal(): void {
// Accessibility: when navigating with keyboard and closing a filter modal, send focus back to filters
if (this.keyboardEvent) {
this.setFocusOnFilter();
this.utils.setFocusOnElement(this.elementRef, '#modal' + this.modalTypeOpened + ' button:first-of-type');
}
this.modalTypeOpened = undefined;
}
......
import { Injectable } from '@angular/core';
import { ElementRef, Injectable } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { Owner } from '../models/owner.model';
import { Structure } from '../models/structure.model';
......@@ -153,6 +153,14 @@ export class Utils {
return Boolean(structureForm.value.categories?.onlineProcedures.find((el) => el === 'autres'));
}
public setFocusOnElement(elementRef: ElementRef<any>, cssSelector: string): void {
const filterButton = elementRef.nativeElement.querySelector(cssSelector);
if (filterButton) {
const focusedElement = filterButton as HTMLElement;
focusedElement.focus();
}
}
/*
* Sanitize value to null if empty
*/
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment