Библиотека Tabbable предназначена для управления фокусируемыми элементами в DOM и определения порядка навигации с помощью клавиши Tab. Одной из ключевых задач при работе с модальными окнами, всплывающими меню и интерактивными компонентами является корректное закрытие элемента при потере фокуса. Этот механизм обеспечивает интуитивное взаимодействие и предотвращает «залипание» фокуса внутри компонента.
Tabbable использует строгий критерий для определения, какие элементы считаются фокусируемыми:
tabindex ≥ 0<a> с href<button>, <input>,
<select>, <textarea>contenteditable="true"В библиотеке есть функция tabbable(element), которая
возвращает массив всех фокусируемых потомков указанного элемента. Этот
массив критически важен для управления потерей фокуса.
import tabbable from 'tabbable';
const modal = document.querySelector('.modal');
const focusableElements = tabbable(modal);
Чтобы корректно закрывать элемент при уходе фокуса, необходимо различать два типа событий:
blur — срабатывает на элементе, когда фокус уходит с
негоfocusout — срабатывает на родительском элементе,
включая все дочерние элементыИспользование focusout предпочтительно, так как оно
позволяет определить уход фокуса с любого потомка компонента, а не
только с конкретного элемента.
modal.addEventListener('focusout', (event) => {
const relatedTarget = event.relatedTarget;
if (!modal.contains(relatedTarget)) {
closeModal();
}
});
Ключевая задача — убедиться, что новый фокус не находится внутри
компонента. Это реализуется с помощью метода
Node.contains():
if (!modal.contains(document.activeElement)) {
closeModal();
}
Такой подход гарантирует, что закрытие произойдет только тогда, когда пользователь покидает область модального окна или меню.
Для более сложных случаев, когда требуется точное определение
фокусируемых элементов внутри компонента, можно использовать
tabbable() для проверки:
const focusableEls = tabbable(modal);
if (!focusableEls.includes(document.activeElement)) {
closeModal();
}
Это особенно важно для компонентов, где некоторые элементы динамически добавляются или удаляются.
Если внутри модального окна появляются новые фокусируемые элементы, массив элементов нужно обновлять:
function updateFocusableElements() {
focusableEls = tabbable(modal);
}
const observer = new MutationObserver(updateFocusableElements);
observer.observe(modal, { childList: true, subtree: true });
MutationObserver гарантирует, что при изменении DOM список фокусируемых элементов всегда актуален, а закрытие при уходе фокуса работает корректно.
Для поддержки клавиатурной навигации важно правильно обрабатывать клавишу Tab. Tabbable возвращает элементы в порядке их естественного табуляционного следования:
const firstFocusable = focusableEls[0];
const lastFocusable = focusableEls[focusableEls.length - 1];
modal.addEventListener('keydown', (event) => {
if (event.key === 'Tab') {
if (event.shiftKey && document.activeElement === firstFocusable) {
event.preventDefault();
lastFocusable.focus();
} else if (!event.shiftKey && document.activeElement === lastFocusable) {
event.preventDefault();
firstFocusable.focus();
}
}
});
Такой подход создаёт «фокусный цикл» внутри компонента и предотвращает случайный уход фокуса, при этом позволяя корректно закрывать компонент при действительном уходе.
Некоторые элементы, например <div> с
tabindex="0", также могут быть фокусируемыми. Tabbable
учитывает их автоматически, что упрощает управление потерей фокуса для
кастомных компонентов без стандартных интерактивных тегов.
tabbable() без необходимости,
особенно в больших DOM-структурах.MutationObserver для обновления списка
фокусируемых элементов только при реальных изменениях.focusout и
объединять их на общий контейнер.Эта архитектура позволяет создавать доступные, управляемые и предсказуемые интерфейсы, полностью контролируя поведение элементов при потере фокуса.