Catégories: Accessibilité, Code, WordPress

Développer des sous-menus déroulants accessibles dans un thème WordPress

Les sous-menus déroulants font partie de la navigation de nombreux sites web. Sur un site WordPress, leur accessibilité dépend en grande partie du thème utilisé. Je partage dans ce billet mes réflexions et mes recherches concernant les bonnes pratiques à appliquer pour rendre les sous-menus déroulants accessibles lorsqu’on développe un thème WordPress sur mesure.

C’est quoi, un sous-menu déroulant accessible ?

Les sous-menus déroulants sont un mécanisme de navigation très pratique, grâce auquel on peut intégrer un grand nombre de liens dans le menu principal d’un site, sans que ça occupe trop de place. Ils permettent également d’organiser le contenu de manière hiérarchique, et de faciliter la navigation dans les différentes rubriques d’un site. Il s’agit donc d’un design pattern très répandu sur le web.

Lorsqu’on navigue sur un site à l’aide d’un écran et d’une souris, les sous-menus déroulants sont en général faciles et intuitifs à utiliser. Il suffit de survoler avec la souris l’élément de navigation de premier niveau pour faire apparaître le sous-menu qui lui est associé.

L’expérience peut toutefois se révéler bien plus compliquée pour les personnes handicapées, en particulier pour celles qui naviguent à l’aide du clavier ou d’un lecteur d’écran.

Accessibilité pour la navigation au clavier

Une partie des internautes ne se sert pas de la souris pour naviguer sur le web, en raison d’un handicap moteur ou visuel. Certains d’entre eux peuvent utiliser un clavier, et se servent donc de différentes touches pour faire défiler la page, se déplacer entre les les éléments interactifs (par exemple les boutons, les liens, ou les champs de formulaires) et les activer. D’autres ne peuvent pas du tout se servir de leurs mains, et ont recours à des technologies d’assistance qui leur permettent de naviguer sur le web à l’aide de leurs pieds ou de leur tête. Ces outils utilisent également les fonctionnalités prévues pour la navigation au clavier.

Les Web Content Accessibility Guidelines (WCAG) stipulent donc que toutes les fonctionnalités d’un site doivent être accessibles à l’aide du clavier 1. Dans le cas d’un sous-menu déroulant, cela implique qu’un mécanisme technique permette de l’afficher également sans la souris et qu’on puisse activer avec le clavier tous les liens qu’ils contient.

Lorsqu’on se déplace entre les éléments du menu à l’aide des touches TAB (pour avancer) et MAJ + TAB (pour revenir en arrière), un indicateur visuel doit être présent pour qu’on sache quel élément reçoit le focus – c’est à dire sur lequel on se trouve. Il s’agit habituellement d’une bordure qui apparaît à la tabulation.

Accessibilité avec un lecteur d’écran

L’utilisation des sous-menus peut aussi représenter un challenge pour les personnes aveugles ou malvoyantes, qui utilisent un logiciel appelé « lecteur d’écran ». Cet outil lit le contenu d’une page, et le restitue oralement. Les balises HTML présentes dans le code de la page lui permettent de comprendre la structure du contenu et d’identifier certains éléments de mise en forme, par exemple les titres ou les listes.

On formate habituellement les menus de navigation avec les éléments HTML <ul> qui indique une liste non numérottée, et <li> pour chaque élément de la liste. Ainsi, les lecteurs d’écran annoncent dès le début du menu le nombre d’éléments qu’il contient, ce qui est très utile pour les personnes qui ne peuvent pas voir l’écran. Pour les sous-menus, on utilise simplement une liste imbriquée à l’intérieur de l’élément de menu parent.

Sans aucun styles, un menu de navigation avec sous-menus doit donc ressembler à ceci:

  • Accueil
  • À propos
    • Notre équipe
    • Références
  • Prestations
  • Blog
  • Contact

Afin d’améliorer l’accessibilité des menus et surtout des sous-menus déroulants, on peut utiliser des attributs de l’API WAI-ARIA. Ceux-ci permettent de communiquer des informations spéficiques aux lecteurs d’écran, afin de facilier l’utilisation de certains composants complexes pour les personnes qui ont un handicap visuel.

Attention toutefois: le W3C stipule que « No ARIA is better than bad ARIA » (« Pas d’ARIA vaut mieux que du mauvais ARIA »). Si on utilise les attributs et rôles ARIA à mauvais escient, cela peut produire l’effet inverse de celui escompté: un attribut mal placé suffit parfois à rendre un composant – voire un site entier – totalement inutilisable 2.


État des lieux des sous-menus déroulants natifs de WordPress

Par défaut, la structure HTML des menus correspond aux normes de formatage décrites précédemment:

  • une liste principale pour les éléments de menu de premier niveau;
  • une liste imbriquée à l’intérieur de l’élément de menu parent – par exemple « À propos » – pour le sous-menu.

L’élément de menu parent contient forcément un lien de premier niveau, qui apparait dans la barre de navigation. Dans les grandes lignes, ça ressemble à ceci:

<nav class="site-menu" 
     role="navigation"
     aria-label="Navigation principale">

  <ul class="nav">
    <!-- 1er niveau de navigation -->
    <li class="menu-item"><a href="/">Accueil</a></li>
    <li class="menu-item menu-item-has-children">
      <a href="#">À propos</a>
      <!-- sous-menu -->
      <ul class="sub-menu">
        <li class="menu-item"><a href="notre-equipe">Notre équipe</a></li>
        <li class="menu-item"><a href="references">Références</a></li>
      </ul>
    </li>
    <li class="menu-item"><a href="prestations">Prestations</a></li>
    <li class="menu-item menu-item-has-children">
      <a href="blog">Blog</a>
      <!-- sous-menu -->
      <ul class="sub-menu">
        <li class="menu-item"><a href="categorie-1">Catégorie 1</a></li>
        <li class="menu-item"><a href="categorie-2">Catégorie 2</a></li>
      </ul>
    </li>
    <li class="menu-item"><a href="contact">Contact</a></li>
  </ul>

</nav>

Il s’agit d’une structure classique pour un menu de navigation, et parfaitement sémantique.

Deux problèmes apparaissent néanmoins:

  1. Que faire de l’élément de la navigation principale qui sert à afficher le menu déroulant – dans mon exemple, les liens « Blog » et « À propos » ?
    • Pointe-t-il vers une page spécifique – comme c’est le cas du lien « Blog » ?
    • Quel est le rôle de cette page par rapport à celles qui figurent dans le menu déroulant ?
    • Si on ne souhaite pas créer de page dédiée, on peut utiliser un lien personnalisé avec un href="#" – comme le lien « À propos ». Mais dans ce cas, un élément <button> ne serait-il pas plus adapté qu’un lien ? 3
  2. Comment rendre ces menus déroulants accessibles, à la fois pour les lecteurs d’écran et lorsqu’on navigue avec le clavier ?

La manière dont les menus déroulants sont affichés sur le site, ainsi que leur accessibilité pour la navigation au clavier et pour les lecteur d’écran, dépend entièrement du thème utilisé. Dans certains cas, la question de l’accessibilité n’est tout simplement pas prise en compte; c’est notamment le cas de gros thèmes très populaires, tels que Divi dont les menus déroulants sont inateignables au clavier.

La vidéo ci-dessous compare l’accessibilité des menus de 10 thèmes WordPress très répandus. Le verdict n’est pas très réjouissant, la plupart d’entre eux ne permettent malheureusement pas une navigation fluide pour les internautes handicapés.


Première solution: CSS uniquement

Une solution simple, qui requiert seulement un peu de CSS, est d’utiliser l’état :focus-within pour faire apparaître le menu déroulant non seulement lorsqu’on survole l’élément de navigation principal avec la souris, mais aussi lorsqu’un des liens qu’il contient reçoit le focus.

ul.sub-menu {
  /* On cache le sous-menu en le positionnant hors du viewport */
  position: absolute;
  left: -3333rem;
  top: 1.75em;
}

/*
 * Affichage du sous-menu:
 * :hover pour l'affichage à la souris
 * :focus-within pour l'affichage à la tabulation
 */

.menu-item.has-children:hover > ul.sub-menu,
.menu-item.has-children:focus-within > ul.sub-menu {
  left: 0;
}
Démonstration du fonctionnement des sous-menus déroulant sur mon site: lorsque je tabule après le lien « Blog », j’entre automatiquement dans le sous-menu qui contient les catégories.

C’est l’option que j’ai choisie sur mon propre site, pour le sous-menu de l’onglet « blog »; elle fonctionne bien dans un cas comme celui-ci, avec un seul sous-menu qui contient peu de liens. C’est en revanche une mauvaise pratique sur un site qui comprend plusieurs sous-menus, surtout s’ils sont très remplis: on se retrouve alors à devoir tabuler sur des dizaines de liens, sans option plus rapide pour atteindre ceux qui se trouvent à la fin du menu.

Cette solution très basique reste pratique pour un correctif rapide (et idéalement provisoire), ou si on ne peut pas intervenir de manière plus poussée dans le code du thème. C’est également un bon fallback en cas de problème avec Javascript. Mais si on en a la possibilité, il vaut mieux mettre en place un mécanisme plus complet, qui réponde au motif de conception ARIA Disclosure.

Regardons ce qu’il en est des deux derniers thèmes de base de WordPress, Twenty Twenty-Three et Twenty Twenty-Four. Lorsqu’un élément de navigation principal contient un sous-menu, un bouton est ajouté après le lien principal. C’est en cliquant sur ce bouton qu’on peut afficher ou masquer le menu déroulant. On progresse donc, car cette méthode permet de tabuler rapidement à travers la navigation principale, sans la contrainte d’ouvrir tous les sous-menus.

Un problème persiste toutefois, dans le cas où le lien principal contient un href="#". Au clic sur le lien, rien ne se passe, ce qui est plutôt une bonne chose (l’alternative étant de recharger la page avec /# ajouté à la fin de l’URL). Cela peut toutefois déstabiliser l’utilisateur, qui se demandera peut-être s’il y a eu un problème. Et surtout, les lecteurs d’écran énoceront son intitulé, par exemple « lien, Blog », sans préciser qu’il n’y a pas d’URL et qu’il ne se passera rien au moment du clic.

Il faudrait donc pouvoir différencier les cas où l’élément de navigation principal contient un vrai lien, et ceux où il contient un placeholder href="#". Dans ce deuxième cas, on pourrait alors remplacer le lien par un bouton.


Deuxième solution: PHP, CSS et Javascript

Cette deuxième solution, un peu plus complexe que la précédente, nécessite de pouvoir modifier librement les fichiers du thème ou du thème enfant 4. Elle requiert aussi une bonne compréhension des trois langages utilisés, ainsi que du développement de thèmes WordPress.

Par ailleurs, cette solution concerne le développement de thème dits « classiques »; les sous-menus des thèmes basés sur le système Full Site Edit (FSE) sont par défaut conçus de manière accessible – à l’exception du même problème avec les liens href="#" que dans Twenty Twenty-Four.

Grâce à PHP, nous allons pouvoir modifier un peu la structure HTML du menu, afin de l’optimiser pour l’accessibilité. On utilisera ensuite Javascript pour se débarasser des fameux liens href="#", et pour gérer l’affichage des sous-menus avec le clavier. L’affichage à la souris continue en revanche de fonctionner grâce à CSS.

Voici la méthode que j’ai choisi de développer:

  • Lorsqu’on navigue à la souris, rien ne change, le menu déroulant s’affiche automatiquement au survol (:hover).
  • Pour tous les éléments de navigation qui contiennent un sous-menu, j’ajoute grâce à une fonction PHP un bouton .toggle-sub-menu, situé directement après le lien principal. Ce bouton permettra d’afficher et de masquer les sous-menus avec le clavier. Il doit obligatoirement être un élément <button> 5.
  • Si le lien principal contient uniquement le caractère #, je supprime le bouton précédent, et je remplace grâce à javascript l’ensemble du lien vide par un nouveau bouton à qui je donne aussi la classe .toggle-sub-menu.
  • Lorsqu’on actionne le bouton .toggle-sub-menu avec les touches entrée ou espace du clavier, le sous-menu correspondant est affiché ou masqué.
  • Lorsqu’on tabule à l’extérieur du menu déroulant, il est automatiquement masqué. Il faut activer une nouvelle fois sur le bouton pour le ré-afficher.
Démonstration du fonctionnement des sous-menus déroulants avec le clavier.

Attributs ARIA

Comme on crée un mécanisme non natif avec du javascript, les attributs ARIA nous permettent de communiquer des informations supplémentaires aux lecteurs d’écran. Cela facilitera l’utilisation du menu déroulant pour les personnes qui utilisent ce type de logiciels.

  • Sur chaque sous-menu <ul class="sub-menu"> j’ajoute un id unique, lié à l’id de l’élément de navigation principal. Par exemple, si l’id de l’élément principal est .menu-item-2, l’id de son sous-menu sera .menu-item-2-sub-menu. Il est nécessaire de faire cela nous-mêmes, car WordPress n’ajoute pas cet id automatiquement sur les sous-menus.
  • Sur chaque bouton .toggle-sub-menu, j’ajoute les attributs suivants:
    • aria-controls indique l’élément que le bouton contrôle. La valeur doit être l’id du sous-menu correspondant. Par exemple aria-controls="menu-item-2-sub-menu".
    • aria-expanded="false" lorsque le sous-menu est masqué. La valeur de l’attribut doit passer à true lorsque le sous-menu est affiché.
    • aria-label permet de préciser la fonction du bouton pour les lecteurs d’écran. Par exemple aria-label="Sous-menu À propos".

PHP: modification de la structure HTML des sous-menus

WordPress inclut par défaut une fonctionnalité qui nous permet de modifier la structure des menus de navigation, que nous allons utiliser pour ajouter des attributs ARIA. On pourrait sans problème le faire en javascript – ça serait même plus simple –, mais ainsi ils se trouvent directement dans le HTML de la page.

Par défaut, la class PHP Walker_Nav_Menu produit le HTML des menus de navigation. Il est possible de déclarer une class walker enfant, qui surcharge la class de base. Dans notre cas, on va modifier uniquement le markup des sous-menus, grâce aux fonctions start_lvl() et end_lvl().

Voici le code que j’ajoute dans le fichier functions.php du thème WordPress:

// On déclare des variables globales qui nous permettront de récupérer l'id et le nom de l'élément de menu principal

function add_menu_items( $item_output, $item, $depth, $args ) {
  global $menu_title;
  $menu_title = $item->title;

  global $menu_item_id;
  $menu_item_id = $item->ID;

  return $item_output;
}
add_filter( 'walker_nav_menu_start_el', 'add_menu_items', 10, 4);

// Déclaration de la nouvelle class Walker

class A11y_Walker_Child_Menu extends Walker_Nav_Menu {

  // Début du sous-menu
  public function start_lvl(&$output, $depth = 0, $args = array()) {
    global $menu_title;
    global $menu_item_id;

    $indent = str_repeat('\t', $depth);
    
    // Attributs du sous-menu (classes + id)
    $classes = array( 'sub-menu' );
    $class_names = implode( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
		
    $sub_menu_id = 'menu-item-' . $menu_item_id . '-sub-menu';
		
    $atts = array();
    $atts['class'] = ! empty( $class_names ) ? $class_names : '';
    $atts['id'] = $sub_menu_id;
		
    $atts = apply_filters( 'nav_menu_submenu_attributes', $atts, $args, $depth );
    $attributes = $this->build_atts( $atts );

    // Attributs + contenu du bouton (classe, icône, label)
    $btn_class = 'expand-sub-menu';
    $icon = '<span class="icon" aria-hidden="true"></span>';
    $label = '<span class="label sr-only">sub-menu ' . $menu_title . '</span>';

    // Rendu du bouton et ouverture du <ul>
    $output .= sprintf( '%1$s<button class="%2$s" aria-controls="%3$s" aria-expanded="false">%4$s%5$s</button><ul%6$s>',
      $indent,
      $btn_class,
      $sub_menu_id,
      $icon,
      $label,
      $attributes
    );
  }

  // Fin du sous-menu (fermeture du <ul>)
  public function end_lvl(&$output, $depth = 0, $args = array()) {
    $indent = str_repeat("\t", $depth);
    $output .= "$indent</ul>\n";
  }
}

On va ensuite utiliser le paramètre walker de la fonction wp_nav_menu() pour dire à WordPress d’utiliser la structure HTML que nous avons définie dans A11y_Walker_Child_Menu() à la place de la structure de base. Je place le code suivant dans le fichier du thème où le menu doit se trouver, par exemple header.php.

<nav class="site-menu" role="navigation" aria-label="<?php _e( 'Primary Menu', 'textdomain' ); ?>">
  <?php wp_nav_menu( array(
    'theme_location' => 'nav-primary',
    'container' => false,
    'menu_class' => 'nav',
    'link_before' => '<span>',
    'link_after' => '</span>',
    'walker' => new A11y_Walker_Child_Menu()
  ) ); ?>
</nav>

CSS: afficher / masquer les sous-menus avec la souris

Pour l’affichage des sous-menus à la souris, j’utilise les mêmes propriétés CSS que dans la première solution proposée.

Je conserve également l’affichage au clavier grâce à :focus-within, pour qu’il reste accessible s’il y a un problème avec javascript. Pour m’assurer que la méthode en CSS n’est utilisée que si javascript n’est pas actif, je précise dans le sélecteur que le sous-menu doit s’afficher uniquement s’il n’a pas la classe .hidden – qui ne se trouve pas dans le HTML natif et est ajoutée à l’aide de javascript.

ul.sub-menu {
  /* On cache le sous-menu en le positionnant hors du viewport */
  position: absolute;
  left: -3333rem;
  top: 1.75em;
}

/*
 * Affichage du sous-menu:
 * :hover pour l'affichage à la souris
 * :focus-within pour l'affichage à la tabulation
 */

.menu-item.has-children:hover > ul.sub-menu,
.menu-item.has-children:focus-within > ul.sub-menu:not(.hidden) {
  left: 0;
}

Javascript: remplacement des liens href="#" par un bouton

On passe ensuite au javascript. Pour cette démo, j’utilise un mélange de javascript natif et de jQuery, car jQuery est de toute façon chargé dans mes projets WordPress, la vie est courte et je ne suis pas là pour souffrir 6. Vous pouvez évidemment reproduire le même mécanisme uniquement en VanillaJS, ou avec la bibliothèque ou framework qui fait battre votre cœur.

La première étape sera de détecter les éléments de navigation de niveau principal qui contiennent 1) un sous-menu et 2) un lien href="#". Lorsqu’on rencontre un tel cas, on supprime le bouton .toggle-sub-menu initial (celui qu’on a ajouté en PHP), et on crée un nouveau bouton qui remplacera complètement le lien #.

Le code suivant dois aller dans un fichier javascript de votre thème, par exemple site-functions.js.

function transformEmptyNavLinksIntoButtons() {

  const emptyNavLinks = $('.menu-item-has-children a[href="#"]');

  emptyNavLinks.each(function(){
    const link = $(this),
          linkHTML = link[0].innerHTML,
          linkLabel = link.text(),
          linkDropdown = link.siblings('.sub-menu'),
          linkMenuItemID = link.parent('.menu-item').attr('id'),
          subMenuID = linkMenuItemID + '-sub-menu',
          buttonAriaLabel = 'Sub-menu ' + linkLabel;

    // Suppression du bouton 'toggle-sub-menu' initial
    link.siblings('.toggle-sub-menu').remove();

    // Création du nouveau bouton
    const button = document.createElement('button');
          button.innerHTML = linkHTML + '<span class="icon fa-solid fa-angle-down" aria-hidden="true"></span>';
          button.className = 'btn toggle-sub-menu';
          button.setAttribute('aria-controls', subMenuID);
          button.setAttribute('aria-expanded', false);
          button.setAttribute('aria-label', buttonAriaLabel);

    // Remplacement du lien # par le bouton
    link.replaceWith(button);

  });

} transformEmptyNavLinksIntoButtons();

Javascript: afficher / masquer les sous-menus avec le clavier

Toujours en javascript, on termine avec le mécanisme qui permettra d’afficher et de masquer les sous-menus déroulants à l’aide des touches entrée et espace du clavier:

function toggleSubMenu(){
  const menuItemsWithChildren = $(".menu-item-has-children");
  const subMenus = $(".menu-item-has-children .sub-menu");
  const subMenuToggles = $(".menu-item-has-children .toggle-sub-menu");
    
  // Ajout de classes utilitaires sur .sub-menu et .menu-item-has-children
  menuItemsWithChildren.addClass("has-dropdown-button");
  subMenus.addClass("hidden");
    
  // Fonction pour afficher / masquer les sous-menus
  subMenuToggles.click(function(){
    const dropdown = $(this).siblings('.sub-menu');
    const parentSiblings = $(this).parents('.menu-item').siblings('.menu-item-has-children.has-dropdown-button');

    // Ajout ou suppression des classes 'show' et 'hidden'
    dropdown.toggleClass("show")
            .toggleClass("hidden");
    
    // Modification de l'attribut aria-expanded
    $(this).attr('aria-expanded', 
      $(this).attr('aria-expanded') == 'false' ? 'true' : 'false'
    );
    // Ajout ou suppression de la classe "close"
    $(this).toggleClass("close");

    // Fermeture des autres menus déroulants
    parentSiblings.find('.sub-menu')
                  .removeClass('show')
                  .addClass('hidden');
    parentSiblings.find('.toggle-sub-menu')
                  .removeClass("close");
  });

  // Fermeture du menu déroulant quand on tabule sur un autre élément de menu de premier niveau
  const firstLevelMenuLinks = $(".site-menu .nav > .menu-item").children("a, .toggle-sub-menu");

  firstLevelMenuLinks.on( "focus", function() {
    var siblingsSubMenu = $(this).parents('.menu-item').siblings('.menu-item-has-children.has-dropdown-button').find('.sub-menu');

    siblingsSubMenu.removeClass('show')
                   .addClass('hidden');
    siblingsSubMenu.siblings('.toggle-sub-menu')
                   .attr('aria-expanded','false')
                   .removeClass('close');
  });

} toggleSubMenu();

Si vous avez créé un nouveau fichier javascript, n’oubliez pas de l’ajouter à votre thème grâce à la fonction wp_enqueue_script() que vous placez dans le fichier functions.php.

function add_custom_scripts() {
  wp_enqueue_script( 'site-functions', get_template_directory_uri() . '/path/to/site-functions.js', array( 'jquery' ), '' , true );
}
add_action( 'wp_enqueue_scripts', 'add_custom_scripts' );

Et voilà ! Notre menu de navigation principal est désormais parfaitement fonctionnel pour une utilisation avec la souris, le clavier ou un lecteur d’écran.

Vous trouverez ci-dessous une démonstration réalisée sur CodePen.

Ressources

Notes et références

  1. Règle n°2.1: accessibilité au clavier (en anglais) ↩︎
  2. Un exemple pas très drôle de mauvaise utilisation d’ARIA: sur booking.com, qui est un site de réservation d’hôtels, les prix des chambres se trouvent à l’intérieur d’un <div> sur lequel figure l’attribut aria-hidden="true". À cause de cela, les lecteurs d’écran ignorent totalement le contenu de ce <div> et n’énoncent pas du tout les prix. Cette erreur de code rend l’ensemble du site inutilisable de manière autonome par une personne aveugle. Une démonstration de ce problème est présente dans l’excellente conférence « Bien doser l’utilisation d’ARIA » ↩︎
  3. La réponse à cette question est « oui »: dans la majorité des cas, l’utilisation de href="#" sur un lien, surtout lorsqu’il est couplé à la méthode javascript event.preventDefault(); est un bon indicateur qu’un bouton serait sans doute préférable. ↩︎
  4. On appelle « thème enfant » une sorte de sous-thème, dépendant d’un thème principal. La création d’un thème enfant permet de modifier des éléments de design ou d’ajouter des fonctionnalités sans toucher aux fichiers du thème principal. ↩︎
  5. Rappel réglementaire qu’on ne fait pas des boutons avec d’autres éléments HTML que <button> qui est prévu exactement pour ça. ↩︎
  6. Il se trouve que oui, écrire 12 lignes de code pour obtenir le même résultat qu’avec une fonction prédéfinie dans jQuery me fait souffrir, et surtout me tape méchamment sur les nerfs. Chacun·e ses problèmes 😅 ↩︎

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser les balises HTML suivantes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>