Блуждающий tabindex: разбираем HTML-атрибут на примерах

Перевод статьи «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:

  1. Устранение проблем с перемещением по элементам при помощи клавиш.
  2. Исправление проблемы отключения от определенного в DOM порядка, возникшей из-за смены направления.

Недостатки использования блуждающего tabindex:

  1. Полная зависимость от JavaScript. Если пользователь по каким-то причинам отключит JS, перемещение по элементам при помощи клавиш снова будет вести себя странно.
  2. Нет поддержки для технологий обеспечения доступности.

Итоги

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

  1. Установить значение -1 для tabindex всех элементов за исключением первого.
  2. Добавить прослушиватель событий клавиатуры для определения, какой элемент находится в фокусе.
  3. Установить значение -1 для tabindex элемента, находившегося в фокусе до этого.
  4. Установить значение 0 для tabindex следующего потомка.
  5. Вызвать для этого элемента метод focus().

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

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

Прокрутить вверх