# aria-activedescendant introuvable après un rendu différé par un composant

> NVDA annonce un silence total à la navigation dans une liste déroulante pourtant visible à l'écran. Le coupable : un attribut aria-activedescendant qui pointe vers un élément pas encore monté.

- Auteur : WordPress Développement
- Publié le : 2025-06-29
- Mis à jour le : 2025-06-29
- Catégorie : Accessibilité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/accessibilite/aria-activedescendant-rendu-differe-composant/

## L’essentiel

- aria-activedescendant échoue silencieusement si la cible n'existe pas encore
- Le rendu différé d'un composant décale l'apparition des options dans le DOM
- Le correctif consiste à synchroniser mise à jour de l'attribut et présence de la cible

« Élément introuvable. » Ce n'est pas un message que NVDA ou JAWS affichent littéralement, mais c'est le comportement observé : l'utilisateur descend dans une liste d'options avec les flèches du clavier, l'interface visuelle met bien en surbrillance l'option suivante, et pourtant le lecteur d'écran reste muet. Aucune annonce, aucune erreur dans la console, rien qui indique où chercher.

Ce scénario est devenu plus fréquent avec la généralisation de composants qui construisent leurs options à la volée — un champ de recherche avec suggestions, un sélecteur de produits filtré dynamiquement, un menu de commandes façon palette. Le mécanisme `aria-activedescendant` est parfaitement adapté à ce genre d'interface, à condition de respecter une contrainte simple mais facile à casser par inadvertance : l'élément ciblé doit exister dans le DOM au moment exact où l'attribut est mis à jour.

## Comprendre le mécanisme d'aria-activedescendant

Contrairement au focus classique, qui déplace réellement le curseur du clavier vers un élément, `aria-activedescendant` permet de garder le focus sur un conteneur (souvent un champ de texte avec `role="combobox"`) tout en indiquant au lecteur d'écran quel descendant est « virtuellement » actif. L'attribut est posé sur l'élément qui a le focus réel, et sa valeur est l'identifiant de l'option actuellement sélectionnée dans la liste, par exemple un élément portant `role="option"`.

Le lecteur d'écran surveille les changements de cet attribut et annonce le contenu de l'élément ciblé. Mais cette surveillance ne fonctionne que si l'élément existe déjà dans l'arbre d'accessibilité au moment où l'attribut change de valeur. Si l'identifiant pointe vers un élément qui sera créé une fraction de seconde plus tard, l'annonce est simplement perdue : le lecteur d'écran ne revient pas en arrière pour vérifier une deuxième fois.

## Où se cache le décalage de timing

Le scénario typique observé sur un composant de recherche instantanée : l'utilisateur tape une lettre, une requête part vers l'API, la liste de résultats se met à jour de façon asynchrone quelques dizaines de millisecondes plus tard. Le code de gestion du clavier, lui, réagit immédiatement à la flèche bas et met à jour `aria-activedescendant` vers l'identifiant du premier résultat attendu — sauf que ce résultat n'a pas encore été inséré dans le DOM, puisque la réponse de l'API n'est pas encore arrivée.

> L'essentiel à retenir : aria-activedescendant échoue silencieusement si la cible n'existe pas encore ; Le rendu différé d'un composant décale l'apparition des options dans le DOM ; Le correctif consiste à synchroniser mise à jour de l'attribut et présence de la cible

## Reproduire le bug de façon fiable

Pour confirmer ce diagnostic sans dépendre d'un lecteur d'écran à chaque test, il suffit d'inspecter la valeur de l'attribut au moment du changement :

```
document.querySelector('[role="combobox"]')
  .addEventListener('keydown', () => {
    setTimeout(() => {
      const id = document
        .querySelector('[role="combobox"]')
        .getAttribute('aria-activedescendant');
      console.log('cible pointée :', id);
      console.log('cible présente dans le DOM :', !!document.getElementById(id));
    }, 0);
  });
```

Si la console affiche régulièrement `false` pour la présence dans le DOM juste après une frappe rapide, le diagnostic est confirmé : l'attribut est mis à jour avant que la liste ne soit repeuplée.

## Le correctif : synchroniser mise à jour et présence

La correction ne consiste pas à ralentir le clavier, mais à inverser l'ordre des opérations : ne jamais poser `aria-activedescendant` sur un identifiant tant que l'élément correspondant n'est pas confirmé présent dans le DOM. Concrètement :

- Attendre la fin du rendu de la liste avant de calculer quelle option doit devenir active, plutôt que de l'anticiper au moment de la frappe.
- Si la liste est vide au moment de la navigation clavier, retirer purement et simplement l'attribut `aria-activedescendant` plutôt que de le laisser pointer vers une valeur obsolète.
- Vérifier, dans un test automatisé, que l'identifiant contenu dans `aria-activedescendant` correspond toujours à un élément existant après chaque interaction clavier simulée.

Ce découplage entre l'intention de l'utilisateur (appuyer sur une flèche) et l'état réel des données (résultats disponibles ou non) est le même principe qui gouverne la gestion des états de chargement en général : ne jamais représenter un état comme actif tant que les données qui le composent ne sont pas là.

## Prévenir la régression

Ce type de bug a la particularité de ne jamais apparaître dans un test manuel lent, où le développeur clique et attend, mais uniquement lors d'une navigation clavier rapide et répétée — exactement le mode d'usage d'un utilisateur expérimenté de lecteur d'écran, qui ne laisse jamais de temps mort entre deux touches. C'est aussi pour cela qu'il passe souvent inaperçu en recette interne.

Un test avec `@testing-library/user-event` qui simule des frappes rapides et vérifie l'attribut après chaque touche, sans `await` artificiellement long entre les actions, reproduit fidèlement ce mode d'usage et attrape la régression avant la mise en production.

## En résumé

Un attribut `aria-activedescendant` correctement posé mais mal synchronisé avec le rendu réel du DOM produit un silence total côté lecteur d'écran, sans aucun signal d'erreur visible. Le réflexe à adopter : ne jamais calculer la cible de cet attribut en avance de phase sur le rendu, et toujours vérifier son existence effective avant de l'assigner.
