diff --git a/src/app/annuaire/annuaire-header/annuaire-header.component.html b/src/app/annuaire/annuaire-header/annuaire-header.component.html index dda02e28cc70e77c8904a9bd2abc3e9a31cc35f9..48e53f05d786353eccefb0e1ed4509af712af07b 100644 --- a/src/app/annuaire/annuaire-header/annuaire-header.component.html +++ b/src/app/annuaire/annuaire-header/annuaire-header.component.html @@ -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" diff --git a/src/app/annuaire/annuaire-header/annuaire-header.component.ts b/src/app/annuaire/annuaire-header/annuaire-header.component.ts index 2d18ac5300f2e04a12eec21a00f9900ef5f5ef1f..2660979def5cf004e9300831e36066751d76e028 100644 --- a/src/app/annuaire/annuaire-header/annuaire-header.component.ts +++ b/src/app/annuaire/annuaire-header/annuaire-header.component.ts @@ -1,7 +1,8 @@ -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(); } diff --git a/src/app/annuaire/filter-modal/filter-modal.component.html b/src/app/annuaire/filter-modal/filter-modal.component.html index 82d4be520f402fd52b9a8e2467a88dda498f9d39..2743408cd8b1597d221c3c91e9d79f7725e8c416 100644 --- a/src/app/annuaire/filter-modal/filter-modal.component.html +++ b/src/app/annuaire/filter-modal/filter-modal.component.html @@ -1,4 +1,10 @@ -<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 diff --git a/src/app/shared/components/collapsable-filter/collapsable-filter.component.html b/src/app/shared/components/collapsable-filter/collapsable-filter.component.html index 21e02b156392bc40b7e33a413c220171c9196eb3..31f08faf385cdaa3d3d6f332e11fe5436dce2fd4 100644 --- a/src/app/shared/components/collapsable-filter/collapsable-filter.component.html +++ b/src/app/shared/components/collapsable-filter/collapsable-filter.component.html @@ -1,7 +1,8 @@ <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 diff --git a/src/app/structure-list/components/structure-list-search/structure-list-search.component.ts b/src/app/structure-list/components/structure-list-search/structure-list-search.component.ts index 3ff2728f001cd84fe850f84025806596f5fc2978..ee547e67a4428ea077212e27a2223ee082ee3aa9 100644 --- a/src/app/structure-list/components/structure-list-search/structure-list-search.component.ts +++ b/src/app/structure-list/components/structure-list-search/structure-list-search.component.ts @@ -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; } diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index e4d85206e6fc8b29460399f6c5140594a353cabe..9753d60b90c57ad53ab9dc81210a40bcec805472 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -1,4 +1,4 @@ -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 */