Всем привет!

В этой статье я бы хотел рассказать как реализовать доступное модальное окно, без использование аттрибута «aria-modal».

Немного теории!


«aria-modal» — это аттрибут использующийся для того, что бы сообщить вспомогательным технологиям (таким, как программы чтения с экрана), что веб-контент под текущим диалоговым окном недоступны для взаимодействия (инертен). Другими словами, ни один елемент под модальным окном не должен получить фокус при клике, навигации с использованием «TAB/SHIFT+TAB» или при свайпе на сенсерных устройствах.

Но почему мы не можем использовать «aria-modal» для модального окна?

Существуют несколько причин:

  • просто не поддерживаются программами чтения с экрана
  • игнорируется псевдо классами ":before/:after"

Перейдем к реализации.

Реализация


Для того что бы начать разработку, нам необходимо выделить свойста, которыми должно обладать доступное модальное окно:

  • все интрерактивные элементы, вне модального окна, должны быть заблокированы для манипуляции пользователем: клика, установки фокуса и так далее...
  • навигация должны быть доступна только по системным компонентам браузера и по контенту самого модального окана (весь контент вне модального окна должен быть проигнорирован)

Заготовка


Воспользуемся заготовкой, чтобы не тратить время на пошаговое описание создания модального окна.

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>

Стили:

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

Если открыть страницу и попробывать перейти на элементы, находящиеся за модальным окном, используя клавиши «TAB/SHIFT+TAB», то эти элементы получают фокус, как показано на прикрепленной картинке.

image

Чтобы решить эту проблему, нам необходимо присвоить всем интерактивным элементам атрибут 'tabindex' со значением минус единица.

1. Для дальнейшей работы создаем класс «modalWindow» cо следующими свойствами и методами:

  • doc — документ страницы. в котором строим модальное окно
  • modal — контейнер модального окна
  • interactiveElementsList — массив интерактивных элементов
  • blockElementsList- массив блочных элементов страницы
  • constructor — конструктор класса
  • create — метод используемый для создания модального окна
  • remove — метод используемы для удаления модального окна

2. Реализуем конструктор:

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

«interactiveElementsList» и «blockElementsList » нужны чтобы содержать элементы страницы, которые были изменены в момент создания модального окна.

3. Создаем константу, в которой будем хранить список все элементов, которые могут иметь фокус:

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

4. В методе 'create' выбираем все элементы подходящие по нашим селекторам и выставляем всем 'tabindex=-1' (игнорируем элементы, которые уже имеют данное значение)

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

Похожая проблема возникает когда мы используем специальные клавиши или жесты(в мобильных программах) для навигации, в этом случае мы можем навигироваться не только по интерактивным элементам, но и по текстовым. Чтобы исправить этот случай, нам нужно добавить

5. Здесь нам не нужно создавать массив для хранения селекторов, мы просто возьмем все дочерние элементы узла 'body'

let children = this.doc.body.children;

6. Четвертый шаг аналогичен шагу 2, только с использованием '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);
       }
    }
}

Завершенный метод «create»:

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. На шестом шаге реализуем метод обратный 'create':

 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. Что бы это все заработало нам нужно создать экземпляр класса «modalWindow» и вызвать методы «create» и «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();
    });

Полный код класса:

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

P.S.


Если на мобильных устройствах не решена проблемы с навигацией по текстовым элементам, то можно использовать следующую выборку:

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

Ссылки на полезные ресурсы