Guide de l'accessibilité du clavier: JavaScript (2e partie)
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 :
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 valeurtrue
, le navigateur ne défile pas jusqu'à l'élément sur lequel le focus est programmé.focusVisible
:
Lorsqu'il est réglé surfalse
, 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 attributshidden
oucontenteditable
, 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
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 !
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
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 droite
ouFlè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 detabindex
avecsetAttribute
, l'écouteur d'événementkeydown
et 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 tableauTABS
et le focaliser de manière programmatique avec la méthodefocus()
.
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 !
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.
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'attributaria-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 :
- Nous stockons le bouton qui a ouvert la modale ;
- Puis nous l'affichons en supprimant l'attribut
hidden
; - 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 avecdocument.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 :
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 :
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 !