La Cascade

Un peu de tout sur CSS, HTML, SVG et JS,traduit du web anglophone
Rechercher

Guide de l'accessibilité du clavier: JavaScript (2e partie)

par Cristian Diaz, 26 décembre 2022, javascript, accessibilite, semantique, article original paru le 21 novembre 2022 dans Smashing Magazine

Un ensemble d'outils JavaScript à utiliser dans différents composants pour améliorer l'expérience des utilisateurs du clavier.


Dans l'article précédent, nous avons vu comment améliorer l'accessibilité pour les utilisateurs de clavier grâce à HTML et CSS. Ces langages peuvent faire le job la plupart du temps, mais certaines contraintes de design et la nature même de certains composants nécessitent des interactions plus complexes, et c'est là que JavaScript entre en jeu.

Quand on parle d'accessibilité clavier, le travail se fait essentiellement avec des outils de base. Cet article couvre un ensemble d'outils que vous pouvez utiliser de pair avec différents composants afin d'améliorer l'accessibilité pour les utilisateurs du clavier.

Les bases

Notre travail avec JavaScript se fera généralement avec une poignée d'outils seulement, en particulier l'utilisation d'event listeners (écouteurs d'événements) et de certaines méthodes JavaScript de quelques API Web qui peuvent nous aider dans cette tâche.

Pour ajouter de l'interactivité à nos projets, l'un des outils les plus importants à notre disposition est l'événement, c'est-à-dire l'exécution d'une fonction qui se déclenche lorsque l'élément que nous contrôlons reçoit un changement.

Événement keydown

Un exemple d'événement que nous pouvons écouter avec cette API Web est l'événement keydown, qui vérifie quand une touche (key) est enfoncée.

Cet événement n'est pas utile pour ajouter l'accessibilité du clavier à des éléments tels que les boutons ou les liens car, par défaut, lorsque nous leur ajoutons un event listener de clic, celui-ci déclenche également l'événement lorsque nous utilisons les touches Entrée (pour les boutons et les liens) et Espace (pour les boutons uniquement). L'utilité de l'événement keydown apparaît lorsque nous devons ajouter une fonctionnalité à d'autres touches.

Pour prendre un exemple, revenons à l'infobulle que nous avons créée dans la première partie de cet article. J'ai mentionné que cette infobulle devait être fermée lorsque nous appuyons sur la touche Esc. Nous aurions besoin d'un event listener de touche enfoncée afin de vérifier si la touche enfoncée est Esc. Pour cela, nous devons détecter la touche enfoncée de l'événement et nous allons donc vérifier la propriété de la touche de l'événement.

Nous utiliserons keycode.info pour vérifier le dump d'événement pour cette touche. Si vous appuyez sur la touche Esc sur votre clavier, vous remarquerez que e.key (event.key) est égal à Escape.

Remarque : Il existe deux autres façons de détecter qu'une touche est enfoncée, ce sont la vérification de e.keyCode et e.which qui renvoient toutes deux un nombre. Dans le cas de la touche Esc, ce sera 27 (qui est affiché en gros dans la page). Mais gardez à l'esprit qu'il s'agit de solutions alternatives dépréciées, et que même si elles fonctionnent, e.key est aujourd'hui l'option préférée.

Avec cela, nous devons sélectionner nos boutons et ajouter l'event listener. Mon approche en la matière est d'utiliser cet event listener pour ajouter une classe au bouton et d'ajouter cette classe comme exception afin de l'afficher en utilisant la pseudo-classe de négation :not(). Commençons en modifiant un peu notre CSS :

button:not(.hide-tooltip):hover + [role='tooltip'],
button:not(.hide-tooltip):focus + [role='tooltip'],
[role='tooltip']:hover {
  display: block;
}

Cette exception étant ajoutée, créons maintenant notre event listener !

const buttons = [...document.querySelectorAll('button')]

buttons.forEach((element) => {
  element.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      element.classList.add('hide-tooltip')
    }
  })
})

Et voilà ! Avec juste un soupçon de JavaScript, nous avons ajouté une fonction d'accessibilité à notre infobulle. Et ce n'était que le début de ce que nous pouvons faire avec un event listener keydown. C'est un outil crucial pour améliorer l'accessibilité au clavier de plusieurs composants. Mais il existe encore un autre event listener que nous devons prendre en considération.

L'événement blur

Il ya un autre événement que nous allons utiliser souvent. Celui-ci détecte le moment où l'élément cesse de recevoir le focus. Cet event listener est important, la plupart du temps vous l'utiliserez pour annuler les éventuelles modifications que vous avez apportées avec l'event listener keydown.

Revenons à l'infobulle. Pour l'instant, elle présente un problème : si vous appuyez sur la touche Esc pour fermer l'infobulle, puis que vous mettez à nouveau le focus sur le même élément, l'infobulle n'apparaîtra pas. Pourquoi ? Parce que nous avons ajouté la classe hide-tooltip lorsque l'utilisateur appuie sur la touche Esc, mais nous n'avons jamais supprimé cette classe. C'est ici que le blur (flou) entre en jeu. Ajoutons un event listener pour rétablir cette fonctionnalité.

element.addEventListener('blur', (e) => {
  if (element.classList.contains('hide-tooltip')) {
    element.classList.remove('hide-tooltip')
  }
})

Autres Event Listeners (et pourquoi vous n'en avez peut-être pas besoin)

J'ai mentionné que nous aurions besoin de deux event listeners dans notre boîte à outils, mais il en existe d'autres que vous pourriez utiliser, comme focusout ou focus. Cependant, je pense que les cas d'utilisation pour eux sont assez rares. Mention spéciale pour focus toutefois car même si vous pouvez trouver de bons cas d'utilisation pour lui, vous devez être très prudent : si vous ne l'utilisez pas correctement, vous pouvez provoquer un changement de contexte.

Un changement de contexte est défini par les WCAG comme "des changements majeurs qui, s'ils sont effectués sans que l'utilisateur en soit conscient, peuvent désorienter les utilisateurs qui ne sont pas en mesure de visualiser la page entière simultanément." Voici quelques exemples de changements de contexte :

  • L'ouverture d'une nouvelle fenêtre ;
  • Une modification importante de la mise en page de votre site ;
  • Un déplacement du centre d'intérêt vers une autre partie du site.

Il est important de garder tout cela à l'esprit, car la création d'un changement de contexte au moment où l'on se concentre sur un élément constitue un manquement au critère 3.2.1 des WCAG :

Lorsqu'un composant de l'interface utilisateur reçoit le focus, il ne déclenche pas de changement de contexte.

Critère de réussite 3.2.1: Ordre de focalisation

Si vous ne faites pas attention, une mauvaise utilisation d'une fonction qui écoute l'événement focus peut créer un changement de contexte. Cela signifie-t-il que vous ne devriez pas l'utiliser ? Pas vraiment, mais pour être honnête, j'ai du mal à trouver une utilité à cet événement. La plupart du temps, vous utiliserez la pseudo-classe :focus pour créer des fonctionnalités similaires.

Cela dit, il existe tout de même au moins un modèle de composant qui peut bénéficier de cet écouteur d'événements dans certains cas, mais je le couvrirai plus tard lorsque je commencerai à parler des composants, alors mettons ce sujet en veilleuse pour l'instant.

Méthode focus()

Ah, voici une méthode que nous allons utiliser assez fréquemment ! Cette méthode de l'API HTMLElement nous permet d'attirer l'attention du clavier sur un élément particulier. Par défaut, elle dessine l'indicateur de focus dans l'élément et fait défiler la page jusqu'à l'emplacement de l'élément. Ce comportement peut être modifié à l'aide de quelques paramètres :

  • preventScroll :
    Lorsqu'il a la valeur true, le navigateur ne défile pas jusqu'à l'élément sur lequel le focus est programmé.
  • focusVisible :
    Lorsqu'il est réglé sur false, l'élément programmatiquement ciblé n'affiche pas son indicateur de ciblage. Cette propriété ne fonctionne pour l'instant que sur Firefox.

Gardez à l'esprit que pour mettre l'accent sur l'élément, celui-ci doit pouvoir être focusé ou tabulé. Si vous devez mettre l'accent sur un élément normalement non tabulable (comme une fenêtre de dialogue), vous devrez ajouter l'attribut tabindex avec un nombre entier négatif pour le rendre focalisable. Vous pouvez vérifier le fonctionnement de tabindex dans la première partie de ce guide.

<button id="openModal">Bring focus</button>
<div id="modal" role="dialog" tabindex="-1">
  <h2>Modal content</h2>
</div>

Puis nous ajouterons un écouteur d'événement de clic au bouton pour que la fenêtre de dialogue soit focalisée :

const button = document.querySelector('#openModal')
const modal = document.querySelector('#modal')

button.addEventListener('click', () => {
  modal.focus()
})

Et voilà ! Cette méthode sera très pratique dans de nombreux composants en tandem avec l'attribut keydown, il est donc crucial de comprendre comment les deux fonctionnent.

Modifier les attributs HTML avec JavaScript

Certains attributs HTML doivent être modifiés avec JavaScript pour créer l'accessibilité dans des modèles de composants complexes. Deux des plus importants pour l'accessibilité du clavier sont tabindex et le plus récemment ajouté inert. tabindex peut être modifié en utilisant setAttribute. Cet attribut requiert deux paramètres :

  • name
    Il vérifie le nom de l'attribut que vous souhaitez modifier.
  • value
    Il ajoute la chaîne de caractères que cet attribut requiert s'il ne requiert pas un attribut particulier (par exemple, si vous ajoutez les attributs hidden ou contenteditable, vous devrez utiliser une chaîne vide).

Voyons un exemple rapide de son utilisation :

const button = document.querySelector('button')

button.setAttribute('tabindex', '-1')

setAttribute est très utile pour l'accessibilité en général (je l'utilise souvent pour modifier les attributs ARIA en cas de besoin !). Mais, lorsque nous parlons d'accessibilité au clavier, tabindex est à peu près le seul attribut que vous modifierez avec cette méthode.

J'ai mentionné l'attribut inert auparavant, et celui-ci fonctionne un peu différemment car il possède sa propre propriété dans l'API Web HTMLElement. HTMLElement.inert est une valeur booléenne qui nous permet de basculer l'attribut inert.

Gardez à l'esprit deux choses avant d'utiliser cet attribut :

  • Vous aurez besoin d'un polyfill car il n'est pas entièrement implémenté dans tous les navigateurs et est encore assez récent. Ce polyfill créé par les ingénieurs de Chrome fonctionne plutôt bien dans les tests que j'ai effectués, donc si vous avez besoin de cette propriété, c'est une approche sûre, mais gardez à l'esprit qu'elle pourrait avoir des comportements inattendus.
  • Vous pouvez également utiliser setAttribute pour modifier cet attribut ! Les deux fonctionnent aussi bien, même avec un polyfill. C'est à vous de décider entre les deux.
const button = document.querySelector('button')

// Syntaxe avec HTMLElement.inert
button.inert = true

// Syntaxe avec Element.setAttribute()
button.setAttribute('inert', '')

Cette combinaison d'outils sera très pratique pour l'accessibilité du clavier. Commençons maintenant à les voir en action !

Patterns de composants

Toggletips

Un bouton avec focus clavier, et un message en-dessous disant 'Contenu personnalisé ici'
Info-bulle de Carbon Design System

Nous avons appris à créer une infobulle dans la partie précédente, et j'ai mentionné comment l'améliorer avec JavaScript, mais il existe un autre modèle pour ce type de composant appelé toggletip, qui est une infobulle fonctionnant avec un clic, plutôt qu'au survol.

Dressons une liste rapide de ce dont nous avons besoin pour que cela se produise :

  • Lorsque vous appuyez sur le bouton, les informations doivent être annoncées aux lecteurs d'écran. Cela devrait se produire lorsque vous appuyez à nouveau sur le bouton. Appuyer sur le bouton ne fermera pas le toggletip.
  • Le toggletip sera fermé lorsque vous cliquerez à l'extérieur du toggletip, que vous cesserez de focaliser le bouton ou que vous appuierez sur la touche Esc.

Je vais adopter l'approche de Heydon Pickering dont il parle dans son livre Inclusive Components. Commençons donc par le balisage :

<p>
  If you need to check more information, check here
  <span class="toggletip-container">
    <button class="toggletip-button">
      <span class="toggletip-icon" aria-hidden="true">?</span>
      <div class="sr-only">Más información</div>
    </button>
    <span role="status" class="toggletip-info"></span>
  </span>
</p>

L'idée est d'injecter le HTML nécessaire à l'intérieur de l'élément avec le role="status". Cela permettra aux lecteurs d'écran d'annoncer le contenu lorsque vous cliquez dessus. Nous utilisons un élément button pour le rendre tabulable. Maintenant, créons le script pour afficher le contenu !

toggletipButton.addEventListener('click', () => {
  toggletipInfo.innerHTML = ''
  setTimeout(() => {
    toggletipInfo.innerHTML = toggletipContent
  }, 100)
})

Ceci fait, il est temps d'ajouter l'accessibilité clavier à ce composant. Il n'est pas nécessaire d'afficher le contenu du toggletip lorsque vous appuyez sur le bouton, car une bonne sémantique HTML le fait déjà pour nous. Nous devons faire en sorte que le contenu du toggletip cesse de s'afficher lorsque vous appuyez sur la touche Esc et lorsque vous cessez le focus sur ce bouton. Cela fonctionne de manière très similaire à ce que nous avons fait pour les info-bulles dans la section précédente à titre d'exemple, alors commençons à travailler avec cela. Tout d'abord, nous allons utiliser l'écouteur d'événements keydown pour vérifier quand la touche Esc est enfoncée :

toggletipContainer.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    toggletipInfo.innerHTML = ''
  }
})

Et maintenant, nous devons vérifier l'événement blur pour faire la même chose. Celui-ci doit être sur l'élément bouton et non sur conteneur.

toggletipButton.addEventListener('blur', () => {
  toggletipInfo.innerHTML = ''
})

Et voilà le résultat !

voir Toggletip demo de smashingmag dans CodePen

Comme je l'ai mentionné, ça fonctionne de manière très similaire à l'infobulle que nous avons réalisée, mais je l'ai fait pour une bonne raison. À moins que vous ne fassiez quelque chose de très peu conventionnel, ces modèles se répéteront assez souvent.

Comme Stephanie Eckles le mentionne dans son article "4 tests obligatoires avant de livrer de nouvelles fonctionnalités", les utilisateurs du clavier s'attendent à certains comportements de la part des composants, comme la possibilité de fermer une fenêtre que vous venez d'ouvrir en appuyant sur la touche Esc, ou de naviguer dans un groupe d'options connexes à l'aide des touches fléchées.

Si vous gardez ces modèles à l'esprit, vous remarquerez des chevauchements dans les comportements de certains composants, et ça se répétera lorsque vous commencerez à créer du code JavaScript pour assurer l'accessibilité au clavier. Si vous gardez cette liste à l'esprit, vous comprendrez mieux les requis des composants que vous créez.

En parlant de ça, vérifions un autre modèle de composant commun.

Onglets

Deux groupes d'onglets: le premier a un texte gris et une bordure en bas, et l'onglet sélectionné a un texte plus foncé et une bordure bleue. Le second groupe d'onglets a un texte gris et un arrière-plan gris, tandis que l'onglet sélectionné a un arrière-plan plus clair et une bordure supérieure bleue.
Tabulation de Carbon Design System

tabindex itinérant

Les interfaces à onglets sont des modèles que vous pouvez encore voir de temps en temps. Elles présentent une fonctionnalité très intéressante pour la navigation au clavier : lorsque vous appuyez sur la touche Tab, vous accédez au panneau d'onglets actif. Pour naviguer dans la liste des onglets, vous devrez utiliser les touches fléchées. Il s'agit d'une technique appelée "tabindex itinérant" qui consiste à supprimer la capacité des éléments non actifs à être tabulables en ajoutant l'attribut tabindex="-1", puis à utiliser d'autres touches pour permettre la navigation entre ces éléments.

Avec les onglets, c'est le comportement attendu pour ceux-ci :

  • Lorsque vous appuyez sur les touches Flèche vers la gauche ou 'Flèche vers le haut`, cela déplace le focus du clavier sur l'onglet précédent. Si le focus est sur le premier onglet, le focus sera déplacé sur le dernier onglet.
  • Lorsque vous appuyez sur les touches Flèche vers la droiteouFlèche vers le bas, le focus du clavier est déplacé vers l'onglet suivant. Si le focus est sur le premier onglet, le focus est déplacé vers le dernier onglet. La création de cette fonctionnalité est un mélange de trois techniques que nous avons vues précédemment : la modification de tabindex avec setAttribute, l'écouteur d'événement keydownet la méthodefocus(). Commençons par vérifier le balisage de ce composant :
<ul role="tablist">
  <li role="presentation">
    <button id="tab1" role="tab" aria-selected="true">Tomato</button>
  </li>
  <li role="presentation">
    <button id="tab2" role="tab" tabindex="-1">Onion</button>
  </li>
  <li role="presentation">
    <button id="tab3" role="tab" tabindex="-1">Celery</button>
  </li>
  <li role="presentation">
    <button id="tab4" role="tab" tabindex="-1">Carrot</button>
  </li>
</ul>
<div class="tablist-container">
  <section
    role="tabpanel"
    aria-labelledby="tab1"
    tabindex="0"
  ></section>
  <section
    role="tabpanel"
    aria-labelledby="tab2"
    tabindex="0"
    hidden
  ></section>
  <section
    role="tabpanel"
    aria-labelledby="tab3"
    tabindex="0"
    hidden
  ></section>
  <section
    role="tabpanel"
    aria-labelledby="tab4"
    tabindex="0"
    hidden
  ></section>
</div>

Nous utilisons aria-selected="true" pour montrer quel est l'onglet actif, et nous ajoutons tabindex="-1" pour que les onglets non actifs ne puissent pas être sélectionnés avec la touche Tab. Les onglets doivent pouvoir être sélectionnés s'il n'y a pas d'élément tabulable à l'intérieur, c'est pourquoi j'ai ajouté l'attribut tabindex="0" et les onglets inactifs sont masqués à l'aide de l'attribut hidden.

Il est temps d'ajouter la navigation avec les touches fléchées. Pour cela, nous devrons créer un tableau avec les onglets et ensuite créer une fonction pour celui-ci. Notre prochaine étape consiste à vérifier quel est le premier et le dernier onglet de la liste. C'est important car l'action qui se produira lorsque vous appuierez sur une touche changera si le focus du clavier est sur l'un de ces éléments.

const TABS = [...TABLIST.querySelectorAll("[role='tab']")]

const createKeyboardNavigation = () => {
  const firstTab = TABS[0]
  const lastTab = TABS[TABS.length - 1]
}

Après cela, nous ajouterons un écouteur d'événement keydown à chaque onglet. Je vais commencer par ajouter la fonctionnalité des flèches gauche et haut.

// Previous code of the createKeyboardNavigation function
TABS.forEach((element) => {
  element.addEventListener("keydown", function (e) {
    if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
      e.preventDefault();
      if (element === firstTab) {
        lastTab.focus();
      } else {
        const focusableElement = TABS.indexOf(element) - 1;
        TABS[focusableElement].focus();
      }
    }
  }
}

Voici ce qui se passe ici :

  • Tout d'abord, nous vérifions que la touche pressée est la flèche Haut ou Gauche. Pour cela, nous vérifions l'event.key.
  • Si c'est vrai, nous devons empêcher ces touches de faire défiler la page car, rappelez-vous, par défaut, elles le font. Nous pouvons utiliser e.preventDefault() dans ce but.
  • Si la touche focalisée est le premier onglet, le focus clavier sera automatiquement porté sur le dernier onglet. Cela se fait en appelant la méthode focus() pour focaliser le dernier onglet (que nous stockons dans une variable).
  • Si ce n'est pas le cas, nous devons vérifier la position de l'onglet actif. Comme nous stockons les éléments de l'onglet dans un tableau, nous pouvons utiliser la méthode indexOf() pour vérifier sa position.
  • Comme nous essayons de naviguer vers l'onglet précédent, nous pouvons soustraire 1 du résultat de indexOf(), puis rechercher l'élément correspondant dans le tableau TABS et le focaliser de manière programmatique avec la méthode focus().

Nous devons maintenant effectuer un processus très similaire avec les touches Flèche vers le bas et Flèche vers la droite :

// Code précédent de la fonction createKeyboardNavigation

else if (e.key === "ArrowDown" || e.key === "ArrowRight") {
  e.preventDefault();
  if (element == lastTab) {
    firstTab.focus();
  } else {
    const focusableElement = TABS.indexOf(element) + 1;
    TABS[focusableElement].focus();
  }
}

Comme je l'ai mentionné, il s'agit d'un processus très similaire. Au lieu de soustraire 1 au résultat de indexOf(), nous ajoutons 1 car nous voulons amener le focus du clavier sur l'élément suivant.

Affichage du contenu et modification des attributs HTML

Nous avons créé la navigation, et maintenant nous devons afficher et masquer le contenu ainsi que manipuler les attributs aria-selected et tabindex. Rappelez-vous, nous devons faire en sorte que lorsque le focus du clavier est sur le panneau actif, et que vous appuyez sur Shift + Tab, le focus doit être dans l'onglet actif.

Tout d'abord, créons la fonction qui affiche le panneau :

const showActivePanel = (element) => {
  const selectedId = element.target.id
  TABPANELS.forEach((e) => {
    e.hidden = 'true'
  })
  const activePanel = document.querySelector(
    `[aria-labelledby="${selectedId}"]`
  )
  activePanel.removeAttribute('hidden')
}

Ce que nous faisons ici, c'est vérifier que l'id de l'onglet est pressé, puis masquer tous les panneaux d'onglets, et enfin rechercher le panneau d'onglet que nous voulons activer. Nous saurons qu'il s'agit de l'onglet car il possède l'attribut aria-labelledby et utilise la même valeur que l'id de l'onglet. Ensuite, nous l'affichons en supprimant l'attribut hidden.

Maintenant, nous devons créer une fonction pour modifier les attributs :

const handleSelectedTab = (element) => {
  const selectedId = element.target.id
  TABS.forEach((e) => {
    const id = e.getAttribute('id')
    if (id === selectedId) {
      e.removeAttribute('tabindex', '0')
      e.setAttribute('aria-selected', 'true')
    } else {
      e.setAttribute('tabindex', '-1')
      e.setAttribute('aria-selected', 'false')
    }
  })
}

Ce que nous faisons ici, c'est, encore une fois, vérifier l'attribut id et ensuite regarder chaque onglet. Nous allons vérifier si l'id de cet onglet correspond à l'id de l'élément pressé.

Si c'est le cas, nous le rendrons tabulable par le clavier soit en supprimant l'attribut tabindex (parce que c'est un bouton, donc par défaut il est tabulable par le clavier), soit en ajoutant l'attribut tabindex="0". En outre, nous ajouterons un indicateur aux utilisateurs de lecteurs d'écran qu'il s'agit de l'onglet actif en ajoutant l'attribut aria-selected="true".

Si cela ne correspond pas, tabindex et aria-selected seront respectivement définis à -1 et false.

Maintenant, il ne nous reste plus qu'à ajouter un écouteur d'événements de clics à chaque onglet pour gérer les deux fonctions.

TABS.forEach((element) => {
  element.addEventListener('click', (element) => {
    showActivePanel(element), handleSelectedTab(element)
  })
})

Et c'est tout ! Nous avons créé la fonctionnalité pour faire fonctionner les onglets, mais nous pouvons faire un petit quelque chose d'autre si nécessaire.

Activer l'onglet sur le focus

Vous vous souvenez de ce que j'ai souligné à propos de l'écouteur d'événement de focus ? Vous devez être prudent lorsque vous l'utilisez car il peut créer un changement de contexte par accident. Cependant, il a une certaine utilité, et ce composant est une occasion parfaite de l'utiliser !

Selon le guide des pratiques de création ARIA (APG), nous pouvons faire en sorte que le contenu affiché s'affiche lorsque vous mettez le focus sur l'onglet. Ce concept est souvent appelé "follow focus" (suivre le focus) et peut être utile aux utilisateurs de claviers et de lecteurs d'écran car il permet de naviguer plus facilement dans le contenu.

Toutefois, vous devez tenir compte de quelques considérations à ce sujet :

Si afficher le contenu signifie faire beaucoup de requêtes et, par extension, rendre le réseau plus lent, faire en sorte que le contenu affiché suive le focus n'est pas souhaité.
Si cela modifie la mise en page de manière significative, on peut le considérer comme un changement de contexte. Ça dépend du type de contenu que vous voulez afficher, et faire un changement de contexte sur le focus est un problème d'accessibilité, comme je l'ai expliqué précédemment.
Dans le cas présent, la quantité de contenu ne suppose pas un grand changement, ni au niveau du réseau, ni à celui de la mise en page, je vais donc faire en sorte que le contenu affiché suive le focus des onglets. C'est une tâche très simple avec l'écouteur d'événement focus. Nous pouvons littéralement copier et coller l'écouteur d'événements que nous avons créé et simplement changer le clic en focus.

TABS.forEach((element) => {
  element.addEventListener('click', (element) => {
    showActivePanel(element), handleSelectedTab(element)
  })

  element.addEventListener('focus', (element) => {
    showActivePanel(element), handleSelectedTab(element)
  })
})

Et voilà, c'est fait ! Maintenant, le contenu affiché fonctionnera sans qu'il soit nécessaire de cliquer sur l'onglet. Le choix vous revient (clic ou pas clic) et c'est étonnamment une question très nuancée. Personnellement, je m'en tiendrais à ce que cela s'affiche lorsque vous appuyez sur l'onglet car je pense que l'expérience de modifier l'attribut aria-selected en se concentrant simplement sur l'élément peut être légèrement déroutante. Mais ce n'est qu'une hypothèse de ma part, alors prenez ce que je dis avec un grain de sel et vérifiez toujours avec les utilisateurs.

Écouteurs d'événements keydown supplémentaires

Revenons un instant sur le createKeyboardNavigation. Il y a quelques touches que nous pouvons ajouter. Nous pouvons faire en sorte que les touches Home et End amènent le focus du clavier sur le premier et le dernier onglet, respectivement. C'est complètement facultatif, donc ce n'est pas grave si vous ne le faites pas, mais juste pour rappeler l'utilité d'un écouteur d'événements de touche, je vais le faire.

C'est une tâche très facile. Nous pouvons créer une autre paire d'instructions if pour vérifier si les touches Home et End sont enfoncées, et comme nous avons stocké le premier et le dernier onglet dans des variables, nous pouvons les mettre au point avec la méthode focus().

// Code précédent de la fonction createKeyboardNavigation, puis:

else if (e.key === "Home") {
  e.preventDefault();
  firstTab.focus()
} else if (e.key === "End") {
  e.preventDefault();
  lastTab.focus()
}

...et voilà le résultat !

voir Tab demo de smashingmag dans CodePen

Avec ce code, nous avons rendu ce composant accessible aux utilisateurs de clavier et de lecteur d'écran. Cela montre à quel point les concepts de base de l'écouteur d'événement keydown, de la méthode focus() et des modifications que nous pouvons apporter avec setAttribute sont utiles pour manipuler des composants complexes. Voyons-en un autre, très compliqué, et comment l'attribut inert peut nous aider à gérer cette tâche facilement.

Une modale ouverte, avec un texte de remplissage et deux boutons qui disent 'Annuler' et 'Enregistrer'
Modale de Carbon Design System

Ouvrir et fermer la modale

Les modales sont un pattern assez complexe quand on parle d'accessibilité au clavier, alors commençons par une tâche facile : ouvrir et fermer la modale.

C'est en effet facile, mais vous devez garder quelque chose à l'esprit : il est très probable que le bouton ouvre la modale, et la modale est loin dans le DOM. Vous devez donc gérer le focus de manière programmatique lorsque vous gérez ce composant. Il y a un petit piège ici : vous devez mémoriser quel élément a ouvert la modale pour pouvoir renvoyer le focus du clavier à cet élément au moment où nous le fermons.

Heureusement, il existe un moyen facile de le faire, mais commençons par créer le balisage de notre site :

<body>
  <header>
    <!-- Header's content -->
  </header>
  <main>
    <!-- Main's content -->
    <button id="openModal">Open modal</button>
  </main>
  <footer>
    <!-- Footer's content -->
  </footer>
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    hidden
    tabindex="-1"
  >
    <div class="dialog__overlay"></div>
    <div class="dialog__content">
      <h2 id="modal-title">Modal content</h2>
      <ul>
        <li><a href="#">Modal link 1</a></li>
        <li><a href="#">Modal link 2</a></li>
        <li><a href="#">Modal link 3</a></li>
      </ul>
      <button id="closeModal">Close modal</button>
    </div>
  </div>
</body>

Comme je l'ai mentionné, la modale et le bouton sont très éloignés l'un de l'autre dans le DOM. Il sera donc plus facile de créer un piège à focus plus tard, mais pour l'instant, vérifions la sémantique de la modale :

  • role="dialog" donnera à l'élément la sémantique requise pour les lecteurs d'écran. Il doit avoir une étiquette pour être reconnu comme une fenêtre de dialogue, nous utiliserons donc le titre de la modale comme étiquette en utilisant l'attribut aria-labelledby.
  • aria-modal="true" permet de faire en sorte qu'un utilisateur de lecteur d'écran ne puisse lire que le contenu des enfants de l'élément, ce qui bloque l'accès des lecteurs d'écran. Cependant, comme vous pouvez le voir sur la page aria-modal de a11ysupport.com, elle n'est pas entièrement prise en charge, donc vous ne pouvez pas vous fier uniquement à cela pour cette tâche. Elle sera utile pour les lecteurs d'écran qui la prennent en charge, mais vous verrez qu'il existe un autre moyen de s'assurer que les utilisateurs de lecteurs d'écran n'interagissent avec rien d'autre que la modale une fois qu'elle est ouverte.
  • Comme je l'ai indiqué, nous devons amener le focus du clavier sur notre modale, c'est pourquoi nous avons ajouté l'attribut tabindex="-1".

Avec cela en tête, nous devons créer la fonction pour ouvrir notre modale. Nous devons vérifier quel est l'élément qui l'a ouverte, et pour cela, nous pouvons utiliser la propriété document.activeElement pour vérifier quel est l'élément qui a le focus sur le clavier en ce moment et le stocker dans une variable. C'est mon approche pour cette tâche :

let focusedElementBeforeModal

const modal = document.querySelector("[role='dialog']")
const modalOpenButton = document.querySelector('#openModal')
const modalCloseButton = document.querySelector('#closeModal')

const openModal = () => {
  focusedElementBeforeModal = document.activeElement

  modal.hidden = false
  modal.focus()
}

C'est très simple :

  1. Nous stockons le bouton qui a ouvert la modale ;
  2. Puis nous l'affichons en supprimant l'attribut hidden ;
  3. Puis nous mettons le focus sur la modale avec la méthode focus().

Il est essentiel de stocker le bouton avant d'amener le focus sur la modale. Sinon, l'élément qui serait stocké dans ce cas serait la modale elle-même, et ce n'est pas ce que nous voulons.

Maintenant, nous devons créer la fonction pour fermer la modale :

const closeModal = () => {
  modal.hidden = true
  focusedElementBeforeModal.focus()
}

Voilà pourquoi il est important de stocker l'élément approprié. Lorsque nous fermerons la modale, nous ramènerons le focus clavier sur l'élément qui l'a ouverte. Avec ces fonctions créées, il ne nous reste plus qu'à ajouter les écouteurs d'événements pour ces fonctions ! N'oubliez pas que nous devons aussi faire en sorte que la modale se ferme lorsque nous appuyons sur la touche Esc.

modalOpenButton.addEventListener('click', () => openModal())
modalCloseButton.addEventListener('click', () => closeModal())
modal.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    closeModal()
  }
})

Pour l'instant, ça paraît très simple. Mais si c'était tout, les modales ne seraient pas considérées comme un modèle complexe pour l'accessibilité, n'est-ce pas ? C'est là que nous devons créer une tâche très importante pour ce composant, et nous avons deux façons de le faire.

Création d'un piège à focus

Un piège à focus garantit que le focus clavier ne peut pas s'échapper du composant. C'est crucial car si un utilisateur du clavier peut interagir avec quoi que ce soit en dehors d'une modale une fois qu'elle est ouverte, cela peut créer une expérience très déroutante. Nous disposons actuellement de deux moyens pour y parvenir.

L'une d'entre elles consiste à vérifier chaque élément qui peut être tabulé avec un clavier, puis à mémoriser lesquels sont le premier et le dernier, et à faire ceci :

  • Lorsque l'utilisateur appuie sur Shift + Tab et que le focus du clavier est sur le premier élément tabulable (rappelez-vous, vous pouvez vérifier cela avec document.activeElement), le focus ira sur le dernier élément tabulable.
  • Lorsque l'utilisateur appuie sur Tab, et que le focus du clavier est sur le dernier élément tabulable, le focus du clavier devrait aller sur le premier élément tabulable.

Normalement, je vous aurais montré comment faire ce code, mais je pense que les solutions A11y ont fait un très bon script pour créer un piège à focus. Il fonctionne en quelque sorte comme la navigation au clavier avec les touches fléchées que nous avons créées pour les éléments tabulables (comme je l'ai déjà mentionné, les motifs se répètent !), je vous invite donc à consulter cette page.

Je ne veux pas utiliser cette approche comme solution principale, car elle n'est pas exactement sans défaut. Il y a certaines situations que cette approche ne couvre pas.

La première est qu'elle ne prend pas en compte les lecteurs d'écran, notamment les lecteurs d'écran mobiles. Comme le mentionne Rahul Kumar dans son article "Focus Trapping for Accessibility (A11Y)", Talkback et Voiceover permettent à l'utilisateur de faire des gestes et des doubles tapotements pour naviguer vers l'élément focalisable suivant ou précédent, et ces gestes ne peuvent pas être détectés avec un écouteur d'événements car ces gestes, techniquement parlant, ne se produisent pas dans le navigateur. Il existe une solution à ce problème, mais je vais mettre ce sujet en suspens pendant un moment.

L'autre préoccupation est que cette approche de piège à focus peut conduire à des comportements bizarres si vous utilisez certaines combinaisons d'éléments tabulables. Prenez, par exemple, cette modale :

Une modale avec le titre 'Survey' et une question disant 'Combien de personnes compte votre équipe?' et trois options de réponse sous forme de boutons radio: 1 à 3 personnes, 4 à 10 personnes, et plus de 10 personnes. En-dessous, il y a un bouton avec un intitulé 'Envoyer la réponse'

Techniquement parlant, le premier élément tabulable est le premier input. Cependant, toutes les entrées de cet exemple doivent se concentrer sur le dernier élément tabulable (dans ce cas, l'élément bouton) lorsque l'utilisateur appuie sur les touches Shift + Tab. Sinon, cela pourrait provoquer un comportement bizarre si l'utilisateur appuie sur ces touches alors que le focus du clavier est sur le deuxième ou troisième input.

Si nous voulons créer une solution plus fiable, la meilleure approche consiste à utiliser l'attribut inert pour rendre le contenu externe inaccessible aux lecteurs d'écran et aux utilisateurs de clavier, en veillant à ce qu'ils puissent interagir uniquement avec le contenu de la modale. N'oubliez pas que cela nécessitera le polyfill inert pour ajouter plus de robustesse à cette technique.

Remarque : il est important de noter que malgré le fait qu'un piège à focus et l'utilisation d'inert contribuent en pratique à garantir l'accessibilité du clavier pour les modales, ils ne fonctionnent pas exactement de la même manière. La principale différence est que si vous définissez tous les documents, sauf la modale, comme inertes, vous pourrez toujours sortir du site Web et interagir avec les éléments du navigateur. C'est sans doute mieux pour des raisons de sécurité, mais c'est à vous de décider si vous voulez créer un piège à focus manuellement ou utiliser l'attribut inert.

Ce que nous allons faire d'abord, c'est sélectionner toutes les zones qui n'ont pas le rôle dialog. Comme l'attribut inert supprimera toute interaction du clavier et du lecteur d'écran avec les éléments et leurs enfants, nous devrons sélectionner uniquement les enfants directs de body. C'est pourquoi nous laissons le conteneur modal exister au même niveau que des balises comme main, header ou footer.

// Ce sélecteur fonctionne bien pour cette structure HTML spécifique. Adaptez-la selon votre projet.

const nonModalAreas = document.querySelectorAll(
  "body > *:not([role='dialog'])"
)

Nous devons maintenant revenir à la fonction openModal. Après avoir ouvert la modale, nous devons ajouter l'attribut inert à ces éléments. Ceci devrait être la dernière étape de la fonction :

const openModal = () => {
  // ici, le code ajouté précédemment

  nonModalAreas.forEach((element) => {
    element.inert = true
  })
}

Qu'en est-il lorsque vous fermez la modale ? Vous devez aller dans la fonction closeModal et supprimer cet attribut. Cela doit se faire avant l'exécution de tout le reste du code. Sinon, le navigateur ne sera pas en mesure de mettre le focus sur le bouton qui a ouvert cette modale.

const closeModal = () => {
  nonModalAreas.forEach((element) => {
    element.inert = false
  })

  // ici, le code ajouté précédemment
}

Voici le résultat :

voir Test modale de smashingmag dans CodePen

Supposons que vous ne vous sentiez pas à l'aise avec l'utilisation de l'attribut inert pour le moment et que vous souhaitiez créer un piège à focus manuellement, comme celui de A11y Solutions. Que pouvez-vous faire pour vous assurer que les utilisateurs de lecteurs d'écran ne puissent pas sortir de la modale ? aria-modal peut vous aider, mais n'oubliez pas que la prise en charge de cette propriété est assez fragile, notamment pour Talkback et VoiceOver pour iOS. La prochaine meilleure chose à faire est donc d'ajouter l'attribut aria-hidden="true" à tous les éléments qui ne sont pas la modale. Il s'agit d'un processus très similaire à celui que nous avons suivi pour l'attribut inert, et vous pouvez également utiliser les mêmes éléments dans le tableau que nous avons utilisé pour cette rubrique !

const openModal = () => {
  // ici, le code ajouté précédemment

  nonModalAreas.forEach((element) => {
    element.setAttribute('aria-hidden', 'true')
  })
}

const closeModal = () => {
  nonModalAreas.forEach((element) => {
    element.removeAttribute('aria-hidden')
  })

  // ici, le code ajouté précédemment
}

Ainsi, que vous décidiez d'utiliser l'attribut inert ou de créer manuellement un piège à focus, vous pouvez vous assurer que l'expérience utilisateur pour les utilisateurs de claviers et de lecteurs d'écran fonctionne au mieux.

Élément dialog

Vous avez peut-être remarqué le balisage que j'ai utilisé et que je n'ai pas utilisé le relativement nouvel élément <dialog>, et il y a une raison à cela. Oui, cet élément aide beaucoup en gérant le focus sur la modale et sur le bouton qui l'a ouverte facilement, mais, comme Scott O'Hara le souligne dans son article "Avoir un dialogue ouvert", il présente encore quelques problèmes d'accessibilité qui, même avec un polyfill, ne sont pas encore totalement résolus. J'ai donc décidé d'utiliser une approche plus robuste avec le balisage.

Si vous n'avez pas entendu parler de cet élément, il dispose de quelques fonctions pour ouvrir et fermer le dialogue, ainsi que de quelques nouvelles fonctionnalités qui seront pratiques lorsque nous créerons des modales. Si vous voulez vérifier comment il fonctionne, vous pouvez consulter la vidéo de Kevin Powell sur cet élément.

Cela ne veut pas dire que vous ne devez pas l'utiliser du tout. La situation de l'accessibilité concernant cet élément s'améliore, mais gardez à l'esprit que vous devez toujours prendre en considération certains détails pour vous assurer qu'il fonctionne correctement.

Autres modèles de composants

Je pourrais continuer avec de nombreux modèles de composants, mais pour être honnête, je pense que cela va devenir redondant car, en fait, ces modèles sont assez similaires entre les différents types de composants que vous pouvez faire. À moins que vous ne deviez fabriquer quelque chose de très peu conventionnel, les modèles que nous avons vus ici devraient suffire !

Ceci étant dit, comment pouvez-vous savoir ce que requerra un composant ? La réponse comporte de nombreuses nuances que cet article ne peut couvrir. Il existe certaines ressources comme le référentiel de composants accessibles de Scott O'Hara ou le système de design du gouvernement britannique, mais c'est une question qui n'a pas de réponse simple. La chose la plus importante à ce sujet est de toujours les tester avec des utilisateurs handicapés pour connaître les défauts qu'ils peuvent avoir en termes d'accessibilité.

Récapitulons

L'accessibilité du clavier peut être assez difficile, mais c'est réalisable une fois que vous comprenez comment les utilisateurs de clavier interagissent avec un site et quels principes vous devez garder à l'esprit. La plupart du temps, HTML et CSS font un excellent travail pour assurer l'accessibilité du clavier, mais parfois vous aurez besoin de JavaScript pour des modèles plus complexes.

C'est assez impressionnant tout ce qu'on peut faire pour l'accessibilité du clavier une fois qu'on a compris que la plupart du temps, le travail est fait avec les mêmes outils de base. Vous pouvez alors mélanger ces outils pour créer une expérience utilisateur formidable pour les utilisateurs de clavier !

Autres ressources externes

Articles de Cristian Diaz traduits dans La Cascade

Voir la page de Cristian Diaz et la liste de ses articles dans La Cascade.
Article original paru le 21 novembre 2022 dans Smashing Magazine
Traduit avec l'aimable autorisation de Smashing Magazine et de Cristian Diaz.
Copyright Smashing Magazine © 2022