Dans cet article, je voudrais vous dire comment implémenter un modal accessible sans utiliser l'attribut "aria-modal" .
Un peu de théorie!
«Aria-modal» est un attribut utilisé pour indiquer aux technologies d'assistance (telles que les lecteurs d'écran) que le contenu Web sous la boîte de dialogue actuelle n'est pas interopérable (inerte). En d'autres termes, aucun élément sous le modal ne doit recevoir le focus sur le clic, la navigation TAB / SHIFT + TAB ou le balayage sur les capteurs.
Mais pourquoi ne pouvons-nous pas utiliser "aria-modal" pour la fenêtre modale?
Il existe plusieurs raisons:
- tout simplement pas pris en charge par les lecteurs d'écran
- ignoré par les pseudo-classes ": avant /: après"
Passons à la mise en œuvre.
la mise en oeuvre
Afin de démarrer le développement, nous devons sélectionner les propriétés que la fenêtre modale disponible doit avoir :
- tous les éléments interactifs, en dehors de la fenêtre modale, doivent être bloqués pour la manipulation de l'utilisateur: clic, focus, etc.
- la navigation ne doit être disponible que via les composants système du navigateur et via le contenu du modal lui-même (tout le contenu en dehors de la fenêtre modale doit être ignoré)
Vide
Nous utiliserons un modèle pour ne pas perdre de temps sur une description étape par étape de la création d'une fenêtre modale.
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<button type="button" id="infoBtn" class="btn"> Standart button </button>
<button type="button" id="openBtn"> Open modal window</button>
<div role="button" tabindex="0" id="infoBtn" class="btn"> Custom button </button>
</div>
<div>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Deserunt maxime tenetur sint porro tempore aperiam! Eaque tempore repudiandae culpa omnis placeat, fugit nostrum quisquam in ipsa odit accusamus illum velit?
</div>
<div id="modalWindow" class="modal">
<div>
<button type="button" id="closeBtn" class="btn-close">Close</button>
<h2>Modal window</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, doloribus.</p>
</div>
</div>
</body>
</html>
Modes:
.modal {
position: fixed;
font-family: Arial, Helvetica, sans-serif;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
z-index: 99999;
transition: opacity 400ms ease-in;
display: none;
pointer-events: none;
}
.active{
display: block;
pointer-events: auto;
}
.modal > div {
width: 400px;
position: relative;
margin: 10% auto;
padding: 5px 20px 13px 20px;
border-radius: 10px;
background: #fff;
}
.btn-close {
padding: 5px;
position: absolute;
right: 10px;
border: none;
background: red;
color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
}
.btn {
display: inline-block;
border: 1px solid #222;
padding: 3px 10px;
background: #ddd;
box-sizing: border-box;
}
JS:
let modaWindow = document.getElementById('modalWindow');
document.getElementById('openBtn').addEventListener('click', function() {
modaWindow.classList.add('active');
});
document.getElementById('closeBtn').addEventListener('click', function() {
modaWindow.classList.remove('active');
});
Si vous ouvrez la page et essayez d'accéder aux éléments derrière la fenêtre modale à l'aide des touches «TAB / SHIFT + TAB», ces éléments reçoivent le focus, comme illustré dans l'image jointe.
Pour résoudre ce problème, nous devons attribuer à tous les éléments interactifs l'attribut 'tabindex' avec une valeur de moins un.
1. Pour plus de travail, créez une classe "modalWindow" avec les propriétés et méthodes suivantes:
- doc - document de page. dans laquelle nous construisons une fenêtre modale
- modal - le conteneur de la fenêtre modale
- interactiveElementsList - un tableau d'éléments interactifs
- blockElementsList - un tableau d'éléments de bloc de page
- constructeur - le constructeur de la classe
- create - la méthode utilisée pour créer la fenêtre modale
- remove - la méthode utilisée pour supprimer le modal
2. Implémentons le constructeur:
constructor(doc, modal) {
this.doc = doc;
this.modal = modal;
this.interactiveElementsList = [];
this.blockElementsList = [];
}
"InteractiveElementsList" et "blockElementsList" sont nécessaires pour contenir les éléments de page qui ont été modifiés lors de la création du modal.
3. Créez une constante dans laquelle nous allons stocker une liste de tous les éléments qui peuvent avoir le focus:
const INTERECTIVE_SELECTORS = ['a', 'button', 'input', 'textarea', '[tabindex]'];
4. Dans la méthode 'create', sélectionnez tous les éléments correspondant à nos sélecteurs et définissez tous 'tabindex = -1' (ignorez les éléments qui ont déjà cette valeur)
let elements = this.doc.querySelectorAll(INTERECTIVE_SELECTORS.toString());
let element;
for (let i = 0; i < elements.length; i++) {
element = elements[i];
if (!this.modal.contains(element)) {
if (element.getAttribute('tabindex') !== '-1') {
element.setAttribute('tabindex', '-1');
this.interactiveElementsList.push(element);
}
}
}
Un problème similaire se pose lorsque nous utilisons des touches ou des gestes spéciaux (dans les programmes mobiles) pour la navigation, dans ce cas, nous pouvons naviguer non seulement à travers des éléments interactifs, mais également à travers du texte. Pour résoudre ce problème, nous devons ajouter
5. Ici, nous n'avons pas besoin de créer un tableau pour contenir les sélecteurs, nous prenons simplement tous les enfants du nœud 'body'
let children = this.doc.body.children;
6. La quatrième étape est similaire à l'étape 2, en utilisant uniquement «aria-hidden»
for (let i = 0; i < children.length; i++) {
element = children[i];
if (!this.modal.contains(element)) {
if (element.getAttribute('aria-hidden') !== 'true') {
element.setAttribute('aria-hidden', 'true');
this.blockElementsList.push(element);
}
}
}
Méthode de "création" terminée:
create() {
let elements = this.doc.querySelectorAll(INTERECTIVE_SELECTORS.toString());
let element;
for (let i = 0; i < elements.length; i++) {
element = elements[i];
if (!this.modal.contains(element)) {
if (element.getAttribute('tabindex') !== '-1') {
element.setAttribute('tabindex', '-1');
this.interactiveElementsList.push(element);
}
}
}
let children = this.doc.body.children;
for (let i = 0; i < children.length; i++) {
element = children[i];
if (!this.modal.contains(element)) {
if (element.getAttribute('aria-hidden') !== 'true') {
element.setAttribute('aria-hidden', 'true');
this.blockElementsList.push(element);
}
}
}
}
7. À la sixième étape, nous implémentons la méthode inverse de «création»:
remove() {
let element;
while(this.interactiveElementsList.length !== 0) {
element = this.interactiveElementsList.pop();
element.setAttribute('tabindex', '0');
}
while(this.interactiveElementsList.length !== 0) {
element = this.interactiveElementsList.pop();
element.setAttribute('aria-gidden', 'false');
}
}
8. Pour que tout fonctionne, nous devons créer une instance de la classe "modalWindow" et appeler les méthodes "create" et "remove":
let modaWindow = document.getElementById('modalWindow');
const modal = new modalWindow(document, modaWindow);
document.getElementById('openBtn').addEventListener('click', function() {
modaWindow.classList.add('active');
// modal.create();
});
document.getElementById('closeBtn').addEventListener('click', function() {
modaWindow.classList.remove('active');
// modal.remove();
});
Code de classe complet:
class modalWindow{
constructor(doc, modal) {
this.doc = doc;
this.modal = modal;
this.interactiveElementsList = [];
this.blockElementsList = [];
}
create() {
let elements = this.doc.querySelectorAll(INTERECTIVE_SELECTORS.toString());
let element;
for (let i = 0; i < elements.length; i++) {
element = elements[i];
if (!this.modal.contains(element)) {
if (element.getAttribute('tabindex') !== '-1') {
element.setAttribute('tabindex', '-1');
this.interactiveElementsList.push(element);
}
}
}
let children = this.doc.body.children;
for (let i = 0; i < children.length; i++) {
element = children[i];
if (!this.modal.contains(element)) {
if (element.getAttribute('aria-hidden') !== 'true') {
element.setAttribute('aria-hidden', 'true');
this.blockElementsList.push(element);
}
}
}
}
remove() {
let element;
while(this.interactiveElementsList.length !== 0) {
element = this.interactiveElementsList.pop();
element.setAttribute('tabindex', '0');
}
while(this.interactiveElementsList.length !== 0) {
element = this.interactiveElementsList.pop();
element.setAttribute('aria-gidden', 'false');
}
}
PS
Si les problèmes de navigation sur les éléments de texte ne sont pas résolus sur les appareils mobiles, la sélection suivante peut être utilisée:
const BLOCKS_SELECTORS = ['div', 'header', 'main', 'section', 'footer'];
let children = this.doc.querySelectorAll(BLOCKS_SELECTORS .toString());