Cours Angular · Module 12: Sécurité et performances
Accessibilité (a11y)
Dans cette leçon5
L'accessibilité, c'est comme construire un château avec des portes pour tous les invités :
Les bases de l'accessibilité
1@Component({
2 template: `
3 <!-- HTML sémantique -->
4 <nav aria-label="Main navigation">
5 <ul>
6 @for (link of navLinks; track link.path) {
7 <li>
8 <a [routerLink]="link.path"
9 [attr.aria-current]="isActive(link.path) ? 'page' : null">
10 {{ link.label }}
11 </a>
12 </li>
13 }
14 </ul>
15 </nav>
16
17 <!-- Formulaires accessibles -->
18 <form>
19 <label for="samuraiName">Samurai name</label>
20 <input
21 id="samuraiName"
22 type="text"
23 [attr.aria-invalid]="nameControl.invalid"
24 [attr.aria-describedby]="nameControl.invalid ? 'nameError' : null"
25 >
26 @if (nameControl.invalid) {
27 <span id="nameError" role="alert">
28 Name is required
29 </span>
30 }
31 </form>
32
33 <!-- Boutons avec descriptions -->
34 <button
35 (click)="deleteSamurai()"
36 [attr.aria-label]="'Delete samurai ' + samurai.name">
37 <mat-icon>delete</mat-icon>
38 </button>
39
40 <!-- Zones live pour le contenu dynamique -->
41 <div aria-live="polite" class="sr-only">
42 {{ statusMessage() }}
43 </div>
44 `
45})
46export class AccessibleComponent {
47 // Classe réservée aux lecteurs d'écran
48 // .sr-only { position: absolute; width: 1px; height: 1px; ... }
49}Gestion du focus
1@Component({
2 template: `
3 <dialog #dialog>
4 <h2>Dialog Title</h2>
5 <button #closeBtn (click)="close()">Close</button>
6 </dialog>
7 `
8})
9export class DialogComponent implements AfterViewInit {
10 @ViewChild('dialog') dialog!: ElementRef<HTMLDialogElement>;
11 @ViewChild('closeBtn') closeBtn!: ElementRef<HTMLButtonElement>;
12
13 private previousFocus: Element | null = null;
14
15 open(): void {
16 this.previousFocus = document.activeElement;
17 this.dialog.nativeElement.showModal();
18 this.closeBtn.nativeElement.focus();
19 }
20
21 close(): void {
22 this.dialog.nativeElement.close();
23 (this.previousFocus as HTMLElement)?.focus();
24 }
25}Un piège à focus pour la fenêtre modale : cdkTrapFocus
Quand une fenêtre modale construite avec de simples éléments <div> est ouverte, la touche Tab peut faire sortir le focus de la fenêtre vers des éléments de la page que l'utilisateur ne voit pas en ce moment. Le paquet @angular/cdk propose pour cela une directive toute prête, cdkTrapFocus, du module A11yModule (@angular/cdk/a11y) : ajoute-la comme attribut de l'élément de la fenêtre, et Tab et Maj+Tab ne circuleront plus qu'entre les éléments qu'elle contient. Avec role="dialog", aria-modal="true" et une étiquette aria-label, le lecteur d'écran sait qu'il s'agit d'une boîte de dialogue et comment elle s'appelle.
1import { Component } from '@angular/core';
2import { A11yModule } from '@angular/cdk/a11y';
3
4@Component({
5 selector: 'app-confirm-dialog',
6 standalone: true,
7 imports: [A11yModule],
8 template: `
9 <div role="dialog" aria-modal="true" aria-label="Confirmation" cdkTrapFocus cdkTrapFocusAutoCapture>
10 <p>Veux-tu vraiment quitter le dojo ?</p>
11 <button (click)="confirm()">Oui</button>
12 <button (click)="cancel()">Non</button>
13 </div>
14 `
15})
16export class ConfirmDialogComponent {
17 confirm(): void {}
18 cancel(): void {}
19}cdkTrapFocusAutoCapture place le focus dans la fenêtre dès son ouverture et, à sa fermeture, le rend à l'élément qui était actif auparavant.
Navigation au clavier
1@Directive({
2 selector: '[appKeyboardNav]',
3 standalone: true
4})
5export class KeyboardNavDirective {
6 @HostListener('keydown', ['$event'])
7 onKeyDown(event: KeyboardEvent): void {
8 const items = this.getNavigableItems();
9 const currentIndex = items.indexOf(document.activeElement as HTMLElement);
10
11 switch (event.key) {
12 case 'ArrowDown':
13 event.preventDefault();
14 items[(currentIndex + 1) % items.length]?.focus();
15 break;
16 case 'ArrowUp':
17 event.preventDefault();
18 items[(currentIndex - 1 + items.length) % items.length]?.focus();
19 break;
20 case 'Home':
21 event.preventDefault();
22 items[0]?.focus();
23 break;
24 case 'End':
25 event.preventDefault();
26 items[items.length - 1]?.focus();
27 break;
28 }
29 }
30
31 private getNavigableItems(): HTMLElement[] {
32 return Array.from(
33 this.el.nativeElement.querySelectorAll('[tabindex="0"], button, a')
34 );
35 }
36}Tests d'accessibilité
1# Audit Lighthouse
2npm install -g lighthouse
3lighthouse http://localhost:4200 --only-categories=accessibility
4
5# axe-core dans les tests
6npm install axe-core @axe-core/playwrightTu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. À quoi sert l'attribut aria-live="polite" ?
2. Pourquoi ajoute-t-on aria-label à un bouton qui ne contient qu'une icône (par exemple <mat-icon>delete</mat-icon>) ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète la fenêtre modale accessible avec les attributs ARIA appropriés.
- Tri horizontal
Ordonne la balise d'ouverture de navigation avec un libellé pour le lecteur d'écran (depuis la gauche) :
- Tri vertical
Ordonne les étapes de la méthode open() qui gère le focus dans une boîte de dialogue (de haut en bas) :
- Clique dans l'ordre
Clique dans l'ordre pour construire l'attribut qui définit la sémantique d'une fenêtre de dialogue :