Skip to content
Snippets Groups Projects
Commit 29f18473 authored by Marlène SIMONDANT's avatar Marlène SIMONDANT
Browse files

Merge branch '245-filters' into 'dev'

fix(Accessibility): filters

See merge request !827
parents e7e43fb0 9bbb4cad
No related branches found
No related tags found
2 merge requests!846V3.1.0 (sans impression),!827fix(Accessibility): filters
Showing
with 98 additions and 16 deletions
......@@ -12,7 +12,12 @@
</div>
<div class="modalFooter">
<app-button [variant]="'secondary'" [label]="'Effacer'" (action)="clearFilters()" />
<app-button
[variant]="'secondary'"
[label]="'Effacer'"
[ariaLabel]="'Effacer et fermer'"
(action)="clearFilters()"
/>
<app-button [variant]="'primary'" [label]="'Appliquer'" (action)="onSubmitFilters()" />
</div>
</div>
......
......@@ -35,9 +35,9 @@ export class ResultListComponent implements OnChanges, AfterViewInit {
public nextChildIndex: number;
ngAfterViewInit(): void {
requestAnimationFrame(() => {
setTimeout(() => {
document.getElementById('app-body')?.scrollTo({ top: this.windowScrollService.scrollYToPreserve.value });
});
}, 10);
}
ngOnChanges(changes: SimpleChanges): void {
......@@ -48,9 +48,9 @@ export class ResultListComponent implements OnChanges, AfterViewInit {
// Accessibility: after click on "view more" button, send focus to the first newly displayed member card
if (this.keyboardEvent && this.nextChildIndex) {
requestAnimationFrame(() => {
setTimeout(() => {
this.setFocusOnFirstNewMemberCard();
});
}, 0);
}
}
public goToUser(userId: string): void {
......
......@@ -13,6 +13,7 @@
<app-checkbox-form
*ngFor="let module of accessModality.modules"
[isChecked]="isInArray('accessModality', module.id)"
[id]="module.id"
[text]="module.name"
[iconSvg]="module.id"
(checkEvent)="onCheckChange($event, 'categories.accessModality', module.id)"
......
......@@ -8,6 +8,7 @@
</div>
<app-checkbox-form
text="J’accepte que mes informations soient enregistrées"
id="acceptDataBeSaved"
(checkEvent)="acceptDataBeSaved($event)"
/>
</div>
......@@ -24,6 +25,7 @@
<app-checkbox-form
*ngIf="!isEditMode"
text="J’accepte de partager les données de ma structure"
[id]="'acceptOpenData'"
[isChecked]="false"
(checkEvent)="acceptOpenData($event)"
/>
......
......@@ -8,6 +8,7 @@
<app-checkbox-form
*ngFor="let module of labelsQualifications.modules.sort()"
[isChecked]="isInArray(module.id, 'labelsQualifications')"
[id]="module.id"
[text]="module.name"
[iconSvg]="module.id"
[iconType]="'labels'"
......
<div class="checkbox" tabindex="-1" [ngClass]="{ selected: isChecked }" (click)="clicked()" (keydown.enter)="clicked()">
<app-checkbox [checked]="isChecked" />
<app-checkbox [checked]="isChecked" [id]="id" />
<svg *ngIf="iconSvg" aria-hidden="true" class="icon" [ngClass]="iconType">
<use [attr.xlink:href]="'assets/form/sprite.svg#' + iconSvg" />
</svg>
<p id="checkboxLabel">{{ text }}</p>
<label for="{{ id }}">{{ text }}</label>
</div>
......@@ -22,7 +22,7 @@ div.checkbox {
min-width: 44px;
}
p {
label {
@include font-bold-16;
color: $grey-1;
text-align: left;
......
......@@ -6,6 +6,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
styleUrls: ['./checkbox-form.component.scss'],
})
export class CheckboxFormComponent {
@Input({ required: true }) public id: string;
@Input() public isChecked = false;
@Input() public text: string;
@Input() public iconSvg: string;
......
......@@ -5,6 +5,5 @@
[checked]="checked"
[indeterminate]="indeterminate"
[disabled]="disabled"
[attr.aria-labelledby]="'checkboxLabel'"
(click)="action.emit($event)"
/>
......@@ -7,7 +7,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
})
export class CheckboxComponent {
/** HTML id associated with for */
@Input() id: string;
@Input({ required: true }) id: string;
/** Checked ? */
@Input({ required: true }) checked: boolean;
......
<button
type="button"
[attr.aria-label]="'Déplier les filtres : ' + label"
aria-haspopup="true"
[attr.aria-label]="label + 'Déplier les filtres : '"
[ngClass]="{
expanded: expanded,
active: active
......
......@@ -5,6 +5,7 @@ import { Component, EventEmitter, Output } from '@angular/core';
template: `<div
class="collapse-header"
role="button"
aria-haspopup="true"
tabindex="0"
[ngClass]="size"
(click)="toggle.emit()"
......
......@@ -4,10 +4,16 @@
<div class="collapseHeader">
<app-checkbox
[checked]="getCategoryCheckboxStatus(category) === 'checked'"
[id]="category.id"
[indeterminate]="getCategoryCheckboxStatus(category) === 'halfChecked'"
(action)="pickAllCategory(category); $event.stopPropagation()"
/>
<span class="titleCollapse">{{ category.name }}</span>
<label
class="titleCollapse"
for="{{ category.id }}"
[attr.aria-label]="category.name + '. Cocher pour tout sélectionner'"
>{{ category.name }}</label
>
</div>
</app-collapse-header>
<app-collapse-content>
......
......@@ -33,11 +33,12 @@
<div class="collapseHeader">
<app-checkbox
[size]="'medium'"
[id]="c.id"
[checked]="getCategoryCheckboxStatus(c) === 'checked'"
[indeterminate]="getCategoryCheckboxStatus(c) === 'halfChecked'"
(action)="handleCategoryCheckBox($event, c); $event.stopPropagation()"
/>
<span>{{ c.name }}</span>
<label for="{{ c.id }}" [attr.aria-label]="c.name + '. Cocher pour tout sélectionner'">{{ c.name }}</label>
</div>
</app-collapse-header>
<app-collapse-content>
......@@ -55,7 +56,12 @@
</app-collapse>
</div>
<div class="modalFooter">
<app-button [variant]="'secondary'" [label]="'Effacer'" (action)="clearFilters()" />
<app-button
[variant]="'secondary'"
[label]="'Effacer'"
[ariaLabel]="'Effacer et fermer'"
(action)="clearFilters()"
/>
<app-button [variant]="'primary'" [label]="'Appliquer'" (action)="emitModules(checkedModules)" />
</div>
</div>
......
......@@ -5,26 +5,34 @@
<app-collapsable-filter
[label]="'Démarches en ligne'"
[expanded]="modalTypeOpened === TypeModal.accompaniment"
[id]="'modal' + TypeModal.accompaniment"
[active]="numberAccompanimentChecked > 0"
(toggle)="openModal(TypeModal.accompaniment)"
(keyup)="onKeyboardNavOnFilters($event)"
/>
<app-collapsable-filter
[label]="'Compétences numériques'"
[expanded]="modalTypeOpened === TypeModal.training"
[id]="'modal' + TypeModal.training"
[active]="numberTrainingChecked > 0"
(toggle)="openModal(TypeModal.training)"
(keyup)="onKeyboardNavOnFilters($event)"
/>
<app-collapsable-filter
[label]="'Public'"
[expanded]="modalTypeOpened === TypeModal.public"
[id]="'modal' + TypeModal.public"
[active]="numberPublicChecked > 0"
(toggle)="openModal(TypeModal.public)"
(keyup)="onKeyboardNavOnFilters($event)"
/>
<app-collapsable-filter
[label]="'Matériel & wifi'"
[expanded]="modalTypeOpened === TypeModal.equipments"
[id]="'modal' + TypeModal.equipments"
[active]="numberEquipmentChecked > 0"
(toggle)="openModal(TypeModal.equipments)"
(keyup)="onKeyboardNavOnFilters($event)"
/>
<app-checkbox-filter
[module]="{
......@@ -43,7 +51,12 @@
[checked]="searchService.getIndex(checkedModulesFilter, 'accesLibre', 'accessModality') > -1"
(toggle)="externalCheckboxCheck($event)"
/>
<app-button [variant]="'tertiary'" [label]="'Plus de filtres'" (action)="openModal(TypeModal.moreFilters)" />
<app-button
[variant]="'tertiary'"
[label]="'Plus de filtres'"
[id]="'modal' + TypeModal.moreFilters"
(action)="openModal(TypeModal.moreFilters)"
/>
<div *ngIf="modalTypeOpened">
<app-more-filters
[modalType]="modalTypeOpened"
......@@ -51,6 +64,7 @@
[modules]="checkedModulesFilter"
(searchEvent)="fetchResults($event)"
(closeEvent)="closeModal()"
(keyup)="onKeyboardNavOnFilters($event)"
/>
</div>
</div>
......
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Utils } from '../../../utils/utils';
import { Theme } from '../../enum/themes.enum';
......@@ -38,12 +38,14 @@ export class StructureListSearchComponent implements OnInit {
public prmAdded = false;
public hasUrlParams = false;
public keyboardEvent = false;
constructor(
public searchService: SearchService,
private activatedRoute: ActivatedRoute,
private route: ActivatedRoute,
private router: Router,
private elementRef: ElementRef,
) {}
ngOnInit(): void {
this.searchInput = this.activatedRoute.snapshot.queryParamMap.get('query');
......@@ -177,10 +179,53 @@ export class StructureListSearchComponent implements OnInit {
this.closeModal();
} else if (this.modalTypeOpened !== modalType) {
this.modalTypeOpened = 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) {
this.setFocusOnOpenedModal();
}
}
}
// 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;
}
}
private setFocusOnOpenedModal(): void {
setTimeout(() => {
const modalFirstFocusableElement = this.elementRef.nativeElement.querySelector(
`.modalContent input, .modalContent button`,
);
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.modalTypeOpened = undefined;
}
......
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