Resolver el problema de hacer accesible la ventana modal para personas con discapacidad

¡Hola!



En este artículo me gustaría mostrarle cómo implementar un modal accesible sin usar el atributo "aria-modal" .



¡Un poco de teoría!



"Aria-modal" es un atributo que se utiliza para indicar a las tecnologías de asistencia (como los lectores de pantalla) que el contenido web del cuadro de diálogo actual no es interoperable (inerte). En otras palabras, ningún elemento debajo del modal debería recibir el foco al hacer clic, TAB / SHIFT + TAB navegación o deslizar los dispositivos sensores.



Pero, ¿por qué no podemos usar "aria-modal" para la ventana modal?



Hay varias razones:



  • simplemente no es compatible con lectores de pantalla
  • ignorado por pseudo clases ": antes /: después"


Pasemos a la implementación.



Implementación



Para comenzar el desarrollo, debemos seleccionar las propiedades que debe tener la ventana modal disponible :



  • todos los elementos interactivos, fuera de la ventana modal, deben estar bloqueados para la manipulación del usuario: hacer clic, enfocar, etc.
  • la navegación debe estar disponible solo a través de los componentes del sistema del navegador y a través del contenido del modal en sí (todo el contenido fuera de la ventana modal debe ignorarse)


Blanco



Usaremos una plantilla para no perder tiempo en una descripción paso a paso de la creación de una ventana modal.



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>


Estilos:



    .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 abre la página e intenta navegar a los elementos detrás de la ventana modal usando las teclas "TAB / SHIFT + TAB", estos elementos reciben el foco, como se muestra en la imagen adjunta.



imagen



Para resolver este problema, debemos asignar a todos los elementos interactivos el atributo 'tabindex' con un valor de menos uno.



1. Para seguir trabajando, cree una clase "modalWindow" con las siguientes propiedades y métodos:



  • doc - documento de página. en el que construimos una ventana modal
  • modal - el contenedor de la ventana modal
  • interactiveElementsList: una serie de elementos interactivos
  • blockElementsList: una matriz de elementos de bloque de página
  • constructor: el constructor de la clase
  • crear: el método utilizado para crear la ventana modal
  • eliminar: el método utilizado para eliminar el modal


2. Implementemos el constructor:



constructor(doc, modal) {
    this.doc = doc;
    this.modal = modal;
    this.interactiveElementsList = [];
    this.blockElementsList = [];
}


"InteractiveElementsList" y "blockElementsList" son necesarios para contener los elementos de la página que se cambiaron cuando se creó el modal.



3. Cree una constante en la que almacenaremos una lista de todos los elementos que pueden tener foco:



const INTERECTIVE_SELECTORS = ['a', 'button', 'input', 'textarea', '[tabindex]'];


4. En el método 'crear', seleccione todos los elementos que coincidan con nuestros selectores y configure todos 'tabindex = -1' (ignore los elementos que ya tienen este valor)



 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 problema similar surge cuando usamos teclas especiales o gestos (en programas móviles) para la navegación, en este caso podemos navegar no solo a través de elementos interactivos, sino también a través del texto. Para solucionar esto, necesitamos agregar



5. No necesitamos crear una matriz para contener los selectores aquí, solo tomamos todos los hijos del nodo 'cuerpo'



let children = this.doc.body.children;


6. El cuarto paso es similar al paso 2, solo se usa '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étodo "crear" completado:



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. En el sexto paso, implementamos el método de "creación" inverso:



 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. Para que todo esto funcione, necesitamos crear una instancia de la clase "modalWindow" y llamar a los métodos "create" y "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();
    });


Código de clase completo:



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');
        }
    }


PD



Si los problemas con la navegación en elementos de texto no se resuelven en dispositivos móviles, se puede utilizar la siguiente selección:



  const BLOCKS_SELECTORS = ['div', 'header', 'main', 'section', 'footer'];
  let children = this.doc.querySelectorAll(BLOCKS_SELECTORS .toString());


Enlaces a recursos útiles






All Articles