Библиотека Tabbable в JavaScript предназначена для определения элементов на странице, которые могут получать фокус при навигации с помощью клавиши Tab. В её основе лежет анализ DOM-структуры и вычисление того, какие элементы являются интерактивными и видимыми. Ключевыми критериями для элемента, чтобы считаться “фокусируемым”, являются:
display: none, visibility: hidden или
opacity: 0 без возможности взаимодействия)., ,
…>).disabled).Эти условия позволяют создавать корректную модель навигации по странице без вмешательства в стандартное поведение браузера.
Реализация функции для поиска tabbable-элементов обычно включает несколько шагов:
Получение всех потенциально интерактивных элементов. Для этого можно использовать селекторы:
const focusableSelectors = `
a[href]:not([tabindex^="-"]),
area[href]:not([tabindex^="-"]),
input:not([disabled]):not([type="hidden"]),
select:not([disabled]),
textarea:not([disabled]),
button:not([disabled]),
iframe,
object,
embed,
[tabindex]:not([tabindex^="-"]),
[contenteditable]
`;
const candidates = Array.from(document.querySelectorAll(focusableSelectors));
Такой список включает элементы с естественным фокусом и элементы с
явным tabindex.
Фильтрация по видимости. Важно исключить элементы, которые не видны пользователю. Проверка обычно включает:
offsetParent не равен null.getClientRects().length > 0.visibility и display.function isVisible(element) {
return element.offsetParent !== null && element.getClientRects().length > 0;
}
const visibleCandidates = candidates.filter(isVisible);Учет порядка табуляции. Элементы с положительным
tabindex имеют приоритет над элементами с
tabindex=0. Для корректной последовательности можно
использовать сортировку:
visibleCandidates.sort((a, b) => {
const tabA = parseInt(a.getAttribute('tabindex') || '0', 10);
const tabB = parseInt(b.getAttribute('tabindex') || '0', 10);
return tabA - tabB;
});Обработка контекстов с Shadow DOM и фреймами.
Для комплексных интерфейсов важно учитывать, что tabbable-элементы могут
находиться внутри Shadow DOM или
. Нужно рекурсивно проходить все открытые
shadow root:
function getTabbableElements(root = document) {
let elements = Array.from(root.querySelectorAll(focusableSelectors)).filter(isVisible);
root.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) {
elements = elements.concat(getTabbableElements(el.shadowRoot));
}
});
return elements.sort((a, b) => {
const tabA = parseInt(a.getAttribute('tabindex') || '0', 10);
const tabB = parseInt(b.getAttribute('tabindex') || '0', 10);
return tabA - tabB;
});
} и
должны иметь href, иначе
они не считаются фокусируемыми.contenteditable всегда
доступны для фокуса, даже если они не интерактивны по умолчанию.
исключаются, так как невидимые поля не должны получать фокус.disabled (включая
) должны быть полностью
исключены.Эти правила повторяют поведение нативного браузера и обеспечивают консистентность.
opacity и
clip-path: современные интерфейсы иногда скрывают
элементы визуально, но оставляют их в DOM. Для более точного анализа
можно проверять getComputedStyle(element).opacity !== "0" и
пересечение с viewport через
element.getBoundingClientRect().tabindex="0" для корректного включения в
последовательность табуляции.Ниже пример реализации, объединяющий все основные аспекты:
function tabbable(root = document) {
const focusableSelectors = `
a[href]:not([tabindex^="-"]),
area[href]:not([tabindex^="-"]),
input:not([disabled]):not([type="hidden"]),
select:not([disabled]),
textarea:not([disabled]),
button:not([disabled]),
iframe,
object,
embed,
[tabindex]:not([tabindex^="-"]),
[contenteditable]
`;
function isVisible(el) {
const style = window.getComputedStyle(el);
return el.offsetParent !== null &&
el.getClientRects().length > 0 &&
style.visibility !== 'hidden' &&
style.display !== 'none' &&
parseFloat(style.opacity) !== 0;
}
function collect(root) {
let elements = Array.from(root.querySelectorAll(focusableSelectors)).filter(isVisible);
root.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) {
elements = elements.concat(collect(el.shadowRoot));
}
});
return elements;
}
const result = collect(root);
return result.sort((a, b) => {
const tabA = parseInt(a.getAttribute('tabindex') || '0', 10);
const tabB = parseInt(b.getAttribute('tabindex') || '0', 10);
return tabA - tabB;
});
}
Эта реализация полностью повторяет поведение библиотеки
Tabbable, поддерживает сложные интерфейсы, shadow DOM и
корректную сортировку элементов по tabindex.