Всем привет!
В этой статье я бы хотел рассказать как реализовать доступное модальное окно, без использования атрибута «aria-modal».
«aria-modal» — это атрибут использующийся для того, чтобы сообщить вспомогательным технологиям (таким, как программы чтения с экрана), что веб-контент под текущим диалоговым окном недоступны для взаимодействия (инертен). Другими словами, ни один элемент, под модальным окном, не должен получить фокус при клике, навигации с использованием «TAB/SHIFT+TAB» или при свайпе на сенсорных устройствах.
Но почему мы не можем использовать «aria-modal» для модального окна?
Существуют несколько причин:
Перейдем к реализации.
Для того чтобы начать разработку, нам необходимо выделить свойства, которыми должно обладать доступное модальное окно:
Воспользуемся заготовкой, чтобы не тратить время на пошаговое описание создания модального окна.
HTML:
Стили:
JS:
Если открыть страницу и попробовать перейти на элементы, находящиеся за модальным окном, используя клавиши «TAB/SHIFT+TAB», то эти элементы получают фокус, как показано на прикрепленной картинке.
![image](https://habrastorage.org/r/w1560/webt/83/yp/le/83yple1rmzs5-wzbt93nia_jllw.png)
Чтобы решить эту проблему, нам необходимо присвоить всем интерактивным элементам атрибут 'tabindex' со значением минус единица.
1. Для дальнейшей работы создаем класс «modalWindow» cо следующими свойствами и методами:
2. Реализуем конструктор:
«interactiveElementsList» и «blockElementsList » нужны, чтобы содержать элементы страницы, которые были изменены в момент создания модального окна.
3. Создаем константу для хранения элементов, которые могут иметь фокус:
4. В методе 'create' выбираем все элементы подходящие по нашим селекторам и выставляем всем 'tabindex=-1' (игнорируем элементы, которые уже имеют данное значение)
Похожая проблема возникает когда мы используем специальные клавиши или жесты(в мобильных программах) для навигации, в этом случае мы можем навигироваться не только по интерактивным элементам, а также и по текстовым.
5. Здесь нам не нужно создавать массив для хранения селекторов, мы просто возьмем все дочерние элементы узла 'body'
6. Шестой шаг аналогичен шагу №4, только с использованием 'aria-hidden'
Завершенный метод «create»:
7. На седьмом шаге реализуем метод обратный 'create':
8. Чтобы это все заработало нам нужно создать экземпляр класса «modalWindow» и вызвать методы «create» и «remove»:
Полный код класса:
Если на мобильных устройствах не решены проблемы с навигацией по текстовым элементам, то можно использовать следующую выборку:
В этой статье я бы хотел рассказать как реализовать доступное модальное окно, без использования атрибута «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](https://habrastorage.org/webt/83/yp/le/83yple1rmzs5-wzbt93nia_jllw.png)
Чтобы решить эту проблему, нам необходимо присвоить всем интерактивным элементам атрибут '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());