Neste artigo, gostaria de mostrar como implementar um modal acessível sem usar o atributo "aria-modal" .
Um pouco de teoria!
"Aria-modal" é um atributo usado para informar às tecnologias assistivas (como leitores de tela) que o conteúdo da web no diálogo atual não é interoperável (inerte). Em outras palavras, nenhum elemento abaixo do modal deve receber foco no clique, navegação TAB / SHIFT + TAB ou deslizar nos dispositivos sensores.
Mas por que não podemos usar "ária-modal" para a janela modal?
Existem vários motivos:
- simplesmente não é compatível com leitores de tela
- ignorado por pseudo classes ": antes /: depois"
Vamos prosseguir para a implementação.
Implementação
Para iniciar o desenvolvimento, precisamos selecionar as propriedades que a janela modal disponível deve ter :
- todos os elementos interativos, fora da janela modal, devem ser bloqueados para manipulação do usuário: clique, foco e assim por diante ...
- a navegação deve estar disponível apenas por meio dos componentes do sistema do navegador e do conteúdo do modal em si (todo o conteúdo fora do modal deve ser ignorado)
Em branco
Usaremos um template para não perder tempo com uma descrição passo a passo da criação de uma janela 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');
});
Se você abrir a página e tentar navegar para os elementos atrás da janela modal usando as teclas "TAB / SHIFT + TAB", então esses elementos recebem o foco, conforme mostrado na figura anexa.

Para resolver este problema, precisamos atribuir a todos os elementos interativos o atributo 'tabindex' com um valor de menos um.
1. Para trabalho posterior, crie uma classe "modalWindow" com as seguintes propriedades e métodos:
- doc - documento da página. em que construímos uma janela modal
- modal - o contêiner para a janela modal
- interativoElementsList - uma matriz de elementos interativos
- blockElementsList - uma matriz de elementos de bloco de página
- construtor - o construtor da classe
- criar - o método usado para criar a janela modal
- remove - o método usado para remover o modal
2. Vamos implementar o construtor:
constructor(doc, modal) {
this.doc = doc;
this.modal = modal;
this.interactiveElementsList = [];
this.blockElementsList = [];
}
"InteractiveElementsList" e "blockElementsList" são necessários para conter os elementos da página que foram alterados quando o modal foi criado.
3. Crie uma constante na qual armazenaremos uma lista de todos os elementos que podem ter foco:
const INTERECTIVE_SELECTORS = ['a', 'button', 'input', 'textarea', '[tabindex]'];
4. No método 'criar', selecione todos os elementos que correspondem aos nossos seletores e defina todos 'tabindex = -1' (ignore os elementos que já têm 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);
}
}
}
Um problema semelhante surge quando usamos teclas especiais ou gestos (em programas móveis) para navegação, neste caso podemos navegar não apenas por meio de elementos interativos, mas também por meio de texto. Para corrigir isso, precisamos adicionar
5. Aqui não precisamos criar um array para conter os seletores, apenas pegamos todos os filhos do nó 'corpo'
let children = this.doc.body.children;
6. A quarta etapa é semelhante à etapa 2, usando apenas '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 de "criação" concluído:
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. Na sexta etapa, implementamos o método reverso de 'criar':
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 isso funcione, precisamos criar uma instância da classe "modalWindow" e chamar os métodos "criar" e "remover":
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 aula 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');
}
}
PS
Se os problemas de navegação em elementos de texto não forem resolvidos em dispositivos móveis, a seguinte seleção pode ser usada:
const BLOCKS_SELECTORS = ['div', 'header', 'main', 'section', 'footer'];
let children = this.doc.querySelectorAll(BLOCKS_SELECTORS .toString());