Pull to refresh

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

Reading time 6 min
Views 3.5K
Всем привет!

В этой статье я бы хотел рассказать как реализовать доступное модальное окно, без использования атрибута «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. Шестой шаг аналогичен шагу №4, только с использованием '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());

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


Tags:
Hubs:
+12
Comments 8
Comments Comments 8

Articles