Как написать доступный JavaScript

Вы завершаете веб-проект, и как только вы делаете последние штрихи, ваш менеджер проекта отправляет вам список ошибок специальных возможностей. Неадекватный цветовой контраст. Отсутствуют альтернативные теги. Этот интерактивный компонент должен быть доступен с клавиатуры.

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

Хотя доступность может расстраивать, вы можете настроить себя, свою команду и своего клиента на успех, спланировав доступность с самого начала. Вот четыре способа сэкономить ваше время и проблемы при создании доступных веб-сайтов и приложений с поддержкой JavaScript.

1. Используйте элемент <button> для всего, что пользователи нажимают.

В общем, использование семантических HTML-элементов будет благом для доступности вашего веб-проекта. При работе с интерактивностью <button> является семантическим тегом для вещей, которые пользователи нажимают, но не ссылок или других входных данных. Это семантический способ обозначить, что элемент является интерактивным и станет вашим новым лучшим другом.

Элемент HTML <button> представляет собой кнопку, на которую можно нажать.

- Сеть разработчиков Mozilla

Когда вы используете элемент <button> для элементов интерфейса, которые можно щелкнуть, и привязываете прослушиватели событий щелчка к этим кнопкам, вы получаете множество функций бесплатно.

Во-первых, кнопки автоматически настраиваются; они находятся в индексе вкладок страницы. Если пользователь заходит на ваш сайт и использует только клавиатуру, он может нажимать клавишу табуляции, чтобы циклически перемещаться по всем фокусируемым элементам, включая гиперссылки и кнопки, на вашей странице.

Во-вторых, программы чтения с экрана сообщат пользователю, что кнопка находится в фокусе. Пользователи программ чтения с экрана по умолчанию знают, что элементы кнопок интерактивны. Это делает особенно важным включение четкого и понятного текста в ваш <button>, чтобы все пользователи могли понять, что будет делать щелчок по нему. Есть также несколько полезных aria атрибутов, которые вы можете добавить к своей кнопке, но мы вернемся к этому позже.

В-третьих, когда вы добавляете прослушиватель событий клика к элементу <button>, вы получаете доступ к клавиатуре бесплатно. Это означает, что вы можете писать меньше JavaScript при использовании элемента <button>. Напротив, если вы добавите прослушиватель событий щелчка к div, вам также придется добавить прослушиватели клавиш для пробела и ввести ключи, чтобы сделать этот элемент доступным для клавиатур. Для элемента кнопки действия клавиатуры по умолчанию (пробел и ввод) и действия щелчка программы чтения с экрана запускают событие щелчка. Вам не нужно писать дополнительных слушателей клавиатуры.

Подводя итог: если пользователь нажимает на нее, а это не ссылка или какой-либо ввод, просто используйте <button>.

2. Планируйте общие взаимодействия с клавиатурой.

Для более сложных интерактивных компонентов, вероятно, в компоненте есть несколько интерактивных элементов. Обратите внимание на то, какие прослушиватели событий вы добавляете в DOM, и подумайте, могут ли эти действия запускаться с клавиатуры.

Например, есть ли на вашем компоненте кнопка закрытия или свертывания? Клавиша ESC, вероятно, также должна запускать закрытие. Есть ли какие-то действия с горизонтальной прокруткой или кнопки «Далее / Назад»? Рассмотрите возможность привязки событий к клавишам со стрелками.

Общие взаимодействия могут включать:

  1. Выход из текущего компонента
  2. Отправка
  3. Перемещение позиции / просмотр

Общие ключи для добавления действий:

  • введите (keyCode 13)
  • пробел (keyCode 32
  • клавиши со стрелками (37–40)
  • ESC (код клавиши 27)
  • вкладка (keyCode 9)

Как вы привязываете действия к определенным клавишам? Вы можете сделать это, добавив прослушиватель событий к событию keyup. Когда вы передаете событие в функцию обратного вызова, у вас есть доступ к свойству keyCode, и вы можете запускать действия в зависимости от keyCode. Мне трудно запомнить keyCodes, поэтому часто во время разработки я добавляю прослушиватель событий, который записывает все коды клавиш в консоль, чтобы я мог найти те, которые мне нужны:

document.addEventListener('keyup', (event) => {
    console.log(event.keyCode); 
});

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

document.addEventListener('keyup', (event) => {
    switch (event.keyCode) {
        // escape
        case 27:
            // exit
            break;
        // enter || spacebar
        case 13 || 32:
            // submit or something
            break;
        // left arrow
        case 37:
            // move back / previous
            break;
        // right arrow
        case 39:
            // move forward
            break;
        // up arrow
        case 38:
            // move up
            break;
        // down arrow
        case 40:
            // move down
            break;
       }
}

Я не использую все это в каждой ситуации, но я использую их чаще всего.

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

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

Вы можете перейти на другой уровень и проверить, удерживал ли пользователь нажатой клавишу Shift, проверив, event.shiftKey === true. Вы можете сделать это, если пытаетесь уловить фокус внутри модального окна и не хотите, чтобы пользователи SHIFT+TAB выходили из модального окна назад.

3. Управление состояниями ARIA.

В спецификации Доступность полнофункциональных интернет-приложений (WAI-ARIA или просто ARIA) Инициативы веб-доступности много чего, но когда вы начинаете работать с интерактивным JavaScript, вам действительно следует сосредоточиться на атрибуте aria-expanded.

Большая часть интерактивности сосредоточена на отображении или скрытии содержимого на странице. Свойство aria-expanded указывает, развернут или свернут элемент или другой элемент группировки, который он контролирует, согласно спецификации W3C.

Убедитесь, что ваш элемент отображается с соответствующим атрибутом aria-expanded: false, если элемент не развернут, и true, если элемент развернут. Этот атрибут следует применять к элементу, который управляет расширяемым элементом. Если группирующий элемент является дочерним по отношению к управляющему элементу, вам не нужно делать ничего особенного, но если у вас есть <button>, который будет управлять родственником <ul>, вам нужно будет указать, что кнопка управляет списком с помощью атрибут aria-controls (документация по aria-controls в W3C). Этот атрибут принимает идентификатор или список идентификаторов, которыми управляет интерактивный элемент. В нашем примере наша разметка будет выглядеть так:

<button class="list-expander" aria-expanded="false" aria-controls="expandable-list-1">Expand List</button>
<ul id="expandable-list-1">
    <li><a href="https://example.com">Sample Link</a></li>
    <li><a href="https://example.com">Sample Link 2</a></li>
    <li><a href="https://example.com">Sample Link 3</a></li>
</ul>

Теперь нам нужно переключить расширенное состояние. Обычно я это делаю с помощью метода setAttribute().

const listExpander = document.querySelector('.list-expander');
const list = document.querySelector('#expandable-list-1');
listExpander.addEventListener('click', (e) => {
    if(list.getAttribute('aria-expanded') === "true") {
        list.setAttribute('aria-expanded', 'false');
    } else {
        list.setAttribute('aria-expanded', 'true');
    }
});

Обратите внимание: когда я проверяю значение атрибута aria-expanded, я использую === "true". Это потому, что getAttribute возвращает либо строку "true", либо "false", а не истинное или ложное значение. (Сначала это меня сбило с толку).

Вы можете использовать тот же тип мышления с другими истинными / ложными атрибутами ARIA. Чаще всего я использую это с aria-hidden для отображения и скрытия модальных диалогов.

4. Управление фокусом

Последнее, что мы рассмотрим в этом руководстве, - это управление фокусом. Фокус относится к единственному элементу в браузере, с которым можно работать с клавиатуры. Элементы часто получают фокус, когда пользователь нажимает на них, использует клавишу TAB для циклического переключения фокусируемых элементов или использует программу чтения с экрана. На базовом уровне вы должны быть уверены, что пользователи могут в любой момент визуально определить, какой элемент находится в фокусе.

Чаще всего я управляю фокусом в модальных компонентах.

Вот пример проблемы, которую нам нужно решить. У нас есть страница с информацией о человеке и кнопка с надписью «Связаться с этим человеком». Эта кнопка открывает модальное окно, содержащее контактную форму. Но если форма не соответствует естественному порядку табуляции страницы (как это обычно бывает с модальными окнами), когда пользователь нажимает вкладку, его фокус клавиатуры находится за модальным окном. Пользователи клавиатуры и вспомогательных технологий часто зацикливаются и разочаровываются из-за плохо разработанных модальных окон.

Чтобы решить эту проблему, мы хотим сделать несколько вещей:

  1. Когда модальное окно откроется, переместите фокус на первый фокусируемый элемент внутри модального окна.
  2. Убедитесь, что пользователи могут легко закрыть модальное окно с клавиатуры, когда оно открыто.
  3. Когда модальное окно закрывается, верните фокус на элемент, который был активен при открытии модального окна.
  4. Если мы хотим быть очень осторожными, мы можем захватить TAB вперед и назад внутри модального окна, чтобы пользователи не могли уйти, пока они не закроют модальное окно.

Получите первый фокусируемый элемент.

У меня есть несколько вспомогательных методов, которые помогут мне определить все фокусируемые элементы и первый фокусируемый элемент в заданном контексте. Вот как я нахожу все элементы на странице, на которые можно сфокусироваться (ч / т Крис Фердинанди).

/** 
  * Get all focusable elements inside of the specifed context. 
  * 
  * @param {String} [context='document'] The DOM context you want to search in. 
  * @return {Array} Array of focusable elements 
  */ 
function getFocusable(context = 'document') { 
    let focusable = Array.from(context.querySelectorAll('button, [href], select, textarea, input:not([type="hidden"]), [tabindex]:not([tabindex="-1"])')); 
    return focusable; 
}

Эта функция использует querySelectorAll со списком селекторов, которые обычно являются фокусируемыми: <button>, ссылки с атрибутом href, входы и вещи, для которых установлен tabindex (не -1). Я также фильтрую селектор <input>, удаляя все скрытые входные данные, поскольку на них нельзя сфокусироваться. Я выполняю такую ​​же фильтрацию для элементов с атрибутом tabindex, установленным на -1, поскольку эти элементы должны быть доступны только с помощью метода JavaScript, а не в обычном индексе вкладки. Я использую Array.from для создания массива из списка узлов, возвращенного querySelectorAll.

Что мне нравится в этой функции, так это то, что я также могу передавать в контексте. По умолчанию для контекста установлено значение document, поэтому он найдет все элементы, на которые можно сфокусироваться, в документе. Но в нашем модальном примере выше вы можете передать сам модальный элемент в качестве контекста и получить список всех фокусируемых элементов в модальном 😎.

Найти первый элемент, на который можно сфокусироваться, теперь тривиально, нужно просто извлечь первый элемент в нашем массиве. Обычно у меня есть другая вспомогательная функция, чтобы получить первый фокусируемый элемент, и я не вызываю этот первый напрямую. Это так:

/**
 * Get all focusable elements inside of the specified context.
 *
 * @param  {String} [context='document'] The DOM context you want to search in.
 * @return {Array}  Array of focusable elements
 */
function getFocusable(context = 'document') {
    let focusable = Array.from(context.querySelectorAll('button, [href], select, textarea, input:not([type="hidden"]), [tabindex]:not([tabindex="-1"])'));
    return focusable;
}

Вы передаете контекст, и он вызывает нашу исходную функцию getFocusable() и возвращает первый элемент в массиве. Теперь мы можем вызвать focus() для этого элемента, чтобы программно сфокусироваться на первом фокусируемом элементе. Это выглядело бы так:

getFirstFocusable(modal).focus();

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

Мы частично обращались к этому ранее, когда обсуждали планирование общих взаимодействий с клавиатурой. Это прекрасный пример того, когда вы хотите, чтобы пользователь мог выйти из компонента.

Вы также можете добавить наложение между модальным содержимым и содержимым сайта, которое можно щелкнуть и сфокусировать с помощью событий щелчка, закрывающих модальное окно.

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

В нашем примере пользователь щелкнул кнопку, а затем его фокус перешел на модальное окно. Когда они закрывают модальное окно, мы хотим вернуть их фокус на кнопку, которая запустила модальное окно. На самом деле использование свойства document.activeElement довольно тривиально.

Когда мы обнаруживаем, что модальное окно должно открыться и до мы переносим фокус на это модальное окно, мы можем сохранить текущий активный элемент в такую ​​переменную:

let previousActiveElement = document.activeElement;

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

previousActiveElement.focus();

И теперь пользователь вернулся с того, с чего начал!

Захватите TAB и SHIFT + TAB внутри модального

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

Для этого нам нужно прослушивать событие keyup, пока модальное окно активно, и вот функция, которую я использую для захвата фокуса (это зависит от нашей getFocusable() функции сверху:

/**
 * Traps the tab key inside of the context, so the user can't accidentally get
 * stuck behind it.
 *
 * Note that this does not work for VoiceOver users who are navigating with
 * the VoiceOver commands, only for default tab actions. We would need to
 * implement something like the inert attribute for that (see https://github.com/WICG/inert)
 * @param  {object} e the Event object
 */
export function trapTabKey(e, context) {
    if (e.key !== 'Tab') return;

    let focusableItems = getFocusable(context);
    let focusedItem = document.activeElement;

    let focusedItemIndex = focusableItems.indexOf(focusedItem);

    if (e.shiftKey) {
        if (focusedItemIndex == 0) {
            focusableItems[focusableItems.length - 1].focus();
            e.preventDefault();
        }
    } else {
        if (focusedItemIndex == focusableItems.length - 1) {
            focusableItems[0].focus();
            e.preventDefault();
        }
    }
}

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

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

Если это была клавиша TAB, мы получаем все фокусируемые элементы в модальном окне и элемент, на котором они в данный момент сфокусированы. Когда у нас есть эти две вещи, мы можем использовать метод indexOf, чтобы определить, где находится пользователь в порядке табуляции в этом контексте.

Если они удерживали клавишу Shift (e.shiftKey === true), они двигались назад, поэтому мы хотим остановить их, когда они дойдут до первого объекта фокусировки в модальном окне, и сфокусироваться на последнем элементе, на который можно сфокусироваться: focusableItems[focusableItems.length - 1].focus().

Если они шли вперед и добрались до последнего элемента, на который можно сфокусироваться, в модальном окне (focusedItemIndex == focusableItems.length - 1), нам нужно сфокусироваться на первом элементе, на который можно сфокусироваться.

Нам нужно вызвать e.preventDefault() для обоих этих случаев, чтобы предотвратить запуск функции TAB по умолчанию. Однако для всех остальных случаев мы можем позволить им использовать TAB в обычном режиме.

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

Заключение

Мы многое здесь рассмотрели, но это должно стать для вас действительно хорошим началом разработки доступных интерактивных сайтов и приложений на JavaScript, а также даст вам основу для размышлений о том, как можно программировать другие виджеты и компоненты. Запомни:

  1. Используйте <button> для интерактивных элементов
  2. Планируйте общие действия с клавиатурой, такие как ESC, стрелки, ввод и TAB.
  3. Подумайте о любых соответствующих состояниях ARIA и управляйте ими.
  4. При необходимости управляйте фокусом.

Помня об этих методах с самого начала, вы сэкономите время и силы, и ваши пользователи будут вам благодарны!

А если вы хотите увидеть больше примеров того, как создавать доступные веб-сайты, подпишитесь на мой бесплатный курс электронной почты: 9 распространенных ошибок доступности веб-сайтов и способы их устранения. Получите доступ к курсу, зарегистрировавшись здесь!



Первоначально опубликовано на benrobertson.io.