Un client nous a signalé un comportement agaçant sur le menu principal de son site vitrine : au clavier, en appuyant sur Entrée pour ouvrir un sous-menu, celui-ci s’affichait puis se refermait immédiatement, comme si une seconde touche invisible venait annuler l’action. À la souris, tout fonctionnait parfaitement. Le bug ne concernait donc que la navigation clavier, ce qui le rendait particulièrement difficile à reproduire pour quelqu’un qui teste toujours son site à la souris.
Ce billet détaille la démarche de diagnostic qui nous a menés jusqu’à la ligne de code fautive, puis la correction que nous avons appliquée pour que le menu reste ouvert le temps nécessaire à sa consultation.
Symptôme : une fermeture instantanée et silencieuse
En inspectant le comportement pas à pas avec les outils de développement du navigateur, nous avons observé que l’attribut aria-expanded du bouton passait bien de false à true pendant une fraction de seconde, avant de repasser immédiatement à false. Le sous-menu n’était donc pas bloqué : il s’ouvrait puis se refermait dans la même frappe.
Aucune erreur n’apparaissait dans la console. Le comportement semblait volontaire du point de vue du navigateur, ce qui nous a orientés vers un problème de logique applicative plutôt que vers un bug de rendu ou de CSS.
Diagnostic : un clic fantôme déclenché par le focus
Le thème utilisait un script maison pour fermer le menu lorsqu’on cliquait en dehors de lui, un pattern très courant :
document.addEventListener('click', function (event) {
if (!menu.contains(event.target)) {
menu.classList.remove('is-open');
toggleButton.setAttribute('aria-expanded', 'false');
}
});

Ce code semble raisonnable, mais il ignorait un détail : sur certains navigateurs, activer un bouton avec la touche Entrée déclenche un événement click synthétique juste après l’ouverture du menu, avec une cible d’événement qui, selon l’ordre d’exécution des scripts, pouvait être interprétée comme extérieure au conteneur du menu si celui-ci n’était pas encore repositionné dans le DOM au moment de l’évaluation. Le gestionnaire de fermeture s’exécutait donc dans la foulée du gestionnaire d’ouverture, sur le même événement clavier.
Correctif : distinguer l’origine de l’interaction
La solution retenue a consisté à ignorer l’événement de fermeture lorsqu’il provient du bouton d’ouverture lui-même, et à retarder l’écoute globale d’une frame pour laisser le temps au menu de se stabiliser dans le DOM avant d’activer la détection de clic extérieur :
toggleButton.addEventListener('click', function (event) {
event.stopPropagation();
const isOpen = menu.classList.toggle('is-open');
toggleButton.setAttribute('aria-expanded', String(isOpen));
});
document.addEventListener('click', function (event) {
if (event.target === toggleButton) {
return;
}
if (!menu.contains(event.target)) {
menu.classList.remove('is-open');
toggleButton.setAttribute('aria-expanded', 'false');
}
});
Le event.stopPropagation() posé sur le bouton d’ouverture empêche l’événement de remonter jusqu’à l’écouteur global du document pendant la même interaction. Une fois ce changement appliqué, le sous-menu est resté ouvert au clavier aussi longtemps qu’à la souris.
Vérification avec les technologies d’assistance
Nous avons rejoué le scénario avec NVDA sous Firefox et VoiceOver sous Safari. Dans les deux cas, l’ouverture au clavier restait stable, et l’attribut aria-expanded restait cohérent avec l’état visuel du menu, ce qui est essentiel : un lecteur d’écran annonce cet état à l’utilisateur, et un décalage entre les deux aurait continué à tromper les personnes non-voyantes même après correction visuelle.
- Ouverture au clavier stable sur trois niveaux de sous-menu imbriqués
- Fermeture correcte avec la touche Échap
- Aucune régression constatée sur le comportement à la souris ou au tactile
Prévention pour les prochains menus
Ce type de bug se cache facilement parce qu’il ne se manifeste qu’au clavier, un mode d’interaction rarement testé en recette classique. Nous avons ajouté à notre check-list de recette un test systématique : ouvrir chaque menu déroulant au clavier, avec Tab puis Entrée, avant toute livraison. Nous recommandons aussi d’éviter les écouteurs globaux sur document quand un écouteur ciblé sur le conteneur du menu, combiné à event.stopPropagation(), suffit largement et réduit la surface de bugs de ce genre.
La leçon la plus utile de ce cas reste la suivante : un bug invisible à la souris n’est pas un bug mineur. Il est simplement invisible à la personne qui ne teste jamais autrement qu’à la souris, ce qui, statistiquement, exclut une partie non négligeable des visiteurs réels du site.