Закрытие при потере фокуса

Основные принципы

Библиотека 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

Для более сложных случаев, когда требуется точное определение фокусируемых элементов внутри компонента, можно использовать 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 список фокусируемых элементов всегда актуален, а закрытие при уходе фокуса работает корректно.

Практические сценарии использования

  1. Модальные окна — закрытие при клике за пределами модального окна или при переходе фокуса на другой элемент.
  2. Выпадающие меню — автоматическое скрытие меню при уходе фокуса на другой компонент.
  3. Табы и панели — управление переключением фокуса между интерактивными панелями без «залипания».

Особенности взаимодействия с клавиатурой

Для поддержки клавиатурной навигации важно правильно обрабатывать клавишу 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 и объединять их на общий контейнер.

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