Перевод статьи «HTML Roving tabindex Attribute Explained with Examples».
Вам когда-нибудь случалось использовать CSS-свойства order или direction? Возможно, вы пользовались ими десятки раз. Но осознавали ли вы, что эти свойства приводят к отключению того, что отображается на экране, от того, что у вас на самом деле в DOM?
«Использование свойства order
отсоединяет визуальное представление контента от порядка, определенного в DOM», — документация MDN.
При создании «Twitter Customize Theme UI» я столкнулся с этим, когда изменил направление элемента при помощи dir="rtl"
.
Элемент изменяет свое направление лишь виртуально, HTML остается тем же. В результате моя клавиатурная навигация стала начинаться с конца.
Мы можем исправить отключение от порядка, определенного в DOM, при помощи tabindex
. Но использование tabindex
со значениями, отличными от 0, настоятельно не рекомендуется и осуждается многими разработчиками (включая меня).
Поэтому мы воспользуемся приемом под названием «блуждающий tabindex» (англ. roving tabindex). Но сперва давайте освежим свои знания по части tabindex
как такового.
HTML-атрибут tabindex
HTML-атрибут tabindex
используется для указания tab-позиции элементов. Обычно с его помощью задается порядок перехода по элементам при помощи клавиши Tab.
В качестве значений tabindex
принимает только целые числа от 0 до 32767. Если вы не определите значение, по умолчанию будет использоваться 0.
tabindex="0"
задает любому элементу естественный порядок табуляции:
<span tabindex="0">Теперь я следую естественному порядку табуляции<span>
tabindex="-1"
удаляет элемент из естественного порядка табуляции:
<button tabindex="-1">Я больше не являюсь частью естественного порядка табуляции</button>
Любой tabindex
, больший 0, задает порядковый номер элемента при последовательном нажатии клавиши Tab. Элемент с tabindex="4"
получит фокус раньше элементов с tabindex="5"
и tabindex="0"
, но после элемента с tabindex="3"
.
Если вы не знакомы с атрибутом tabindex
, почитайте документацию.
Что такое блуждающий tabindex?
Теперь, вспомнив, что такое tabindex
вообще, давайте рассмотрим применение блуждающего tabindex
.
Блуждающий tabindex
— это когда значение атрибута tabindex
устанавливается в -1 для всех потомков элемента за исключением того, который в настоящее время находится в фокусе.
<div class"btns js-btns"> <button tabindex="0"> currently focused </button> <button tabindex="-1"> next button </button> ... </div>
Затем при помощи EventListener
мы можем определить, какая кнопка в настоящее время получила фокус. По окончании события tabindex
элемента, который больше не в фокусе, устанавливается в -1. А tabindex
следующего элемента устанавливается в 0, после чего для него вызывается метод focus()
. Все эти действия повторяются, пока не будет достигнут последний элемент.
Более подробное руководство можно почитать здесь.
Как использовать блуждающий tabindex
Давайте применим этот прием для исправления поведения, которое мы получили из-за использования dir="rtl"
.
Атрибут dir="rtl"
мы применили для визуального разворота порядка (order) HTML-кода, приведенного ниже. (Это эквивалент использования CSS-свойства direction
).
Если вы недостаточно знакомы с HTML-атрибутом dir
, почитать о нем можно здесь.
<div class="btns js-btns" dir="rtl"> <button="btn js-btn" aria-label="button name"> button </button> <button="btn js-btn" aria-label="button name"> button </button> <button="btn js-btn" aria-label="button name"> button </button> <button="btn js-btn" aria-label="button name"> button </button> <button="btn js-btn" aria-label="button name"> button </button> </div>
Для начала мы добавим ко всем кнопкам атрибут tabindex
и дадим ему значение -1:
let btns = document.querySelectorAll("button"); btns.forEach((btn, index) => { // Устанавливаем tabindex первой кнопки в 0 // а tabindex всех остальных кнопок в -1 if (index == btns.length - 1) { btn.setAttribute("tabindex", 0); } else { btn.setAttribute("tabindex", -1); }
Обратите внимание, что tabindex
последнего элемента устанавливается в 0 первым (if (index == btns.length - 1)
). Визуально это первый элемент, который видит пользователь, потому что мы установили в HTML-коде атрибут dir="rtl"
.
<div class="btns js-btns" dir="rtl"> //buttons </div>
Затем мы добавляем EventListener
, где устанавливаем значение -1 для tabindex
кнопки, находящейся в фокусе. Мы продолжаем устанавливать в 0 tabindex
каждого следующего потомка, пока не достигнем последнего. Затем фокус возвращается обратно к первому.
// add an event listener when tab key is pressed btn.addEventListener("keydown", (e) => { if (e.keyCode == 9) { // prevent the default behaviour e.preventDefault(); // set current button tabindex to 0 btn.setAttribute("tabindex", -1); // if not last button keep setting tabindex to 0 if (btn.previousElementSibling != null) { let nextEl = btn.previousElementSibling; nextEl.setAttribute("tabindex", 0); nextEl.focus(); } else { // when we get to last element set first element to tabindex 0 // and call focus method on it. // note the .lastElementChild the last element becomes our first // that's because of the direction we changed let firstEl = document.querySelector(".js-btns").lastElementChild; firstEl.setAttribute("tabindex", 0); firstEl.focus(); } } }); });
После всего этого код выглядит следующим образом:
let btns = document.querySelectorAll("button"); btns.forEach((btn, index) => { // set first button tabindex to 0 // and set every other button tabindex to -1 if (index == btns.length - 1) { btn.setAttribute("tabindex", 0); } else { btn.setAttribute("tabindex", -1); } // add an event listener when tab key is pressed btn.addEventListener("keydown", (e) => { if (e.keyCode == 9) { // prevent the default behaviour e.preventDefault(); // set current button tabindex to 0 btn.setAttribute("tabindex", -1); // if not last button keep setting tabindex to 0 if (btn.previousElementSibling != null) { let nextEl = btn.previousElementSibling; nextEl.setAttribute("tabindex", 0); nextEl.focus(); } else { // when we get to last element set first element to tabindex 0 // and call focus method on it. // note the .lastElementChild the last element becomes our first // that's because of the direction we changed let firstEl = document.querySelector(".js-btns").lastElementChild; firstEl.setAttribute("tabindex", 0); firstEl.focus(); } } }); });
Рабочую версию можно посмотреть на Codepen (попробуйте перемещаться по элементам при помощи клавиши Tab).
See the Pen fixed tab order with rovering tabindex by iamspruce (@Spruce_khalifa) on CodePen.
Но прежде чем использовать этот прием, задумайтесь на секунду. Практически любое решение по части проектирования имеет свою цену. Так что давайте рассмотрим плюсы и минусы блуждающего tabindex
.
Преимущества использования блуждающего tabindex:
- Устранение проблем с перемещением по элементам при помощи клавиш.
- Исправление проблемы отключения от определенного в DOM порядка, возникшей из-за смены направления.
Недостатки использования блуждающего tabindex:
- Полная зависимость от JavaScript. Если пользователь по каким-то причинам отключит JS, перемещение по элементам при помощи клавиш снова будет вести себя странно.
- Нет поддержки для технологий обеспечения доступности.
Итоги
Использование блуждающего tabindex
не связано с какими-то конкретными подходами и не имеет предпочтительных способов реализации. Поэтому, как бы вы ни написали свой код, он будет хорош, если вы будете придерживаться следующего процесса:
- Установить значение -1 для
tabindex
всех элементов за исключением первого. - Добавить прослушиватель событий клавиатуры для определения, какой элемент находится в фокусе.
- Установить значение -1 для
tabindex
элемента, находившегося в фокусе до этого. - Установить значение 0 для
tabindex
следующего потомка. - Вызвать для этого элемента метод
focus()
.
[customscript]techrocks_custom_after_post_html[/customscript]
[customscript]techrocks_custom_script[/customscript]