Cours Angular · Module 12: Sécurité et performances

Accessibilité (a11y)

3 min de lecture
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.

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/playwright

Tu 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. 1. À quoi sert l'attribut aria-live="polite" ?

  2. 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 :

Articles utiles