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

Многие интерактивные элементы пользовательского интерфейса должны автоматически закрываться, когда пользователь переключает внимание на другую часть страницы. Такое поведение характерно для выпадающих меню, модальных окон, всплывающих подсказок, контекстных панелей и диалогов. Корректная реализация закрытия при потере фокуса является важной частью доступности, поскольку пользователи клавиатуры и вспомогательных технологий перемещаются по интерфейсу иначе, чем пользователи мыши.

Библиотека ally.js предоставляет инструменты для корректного отслеживания состояния фокуса, определения его границ и управления поведением интерфейсных компонентов при изменении активного элемента.


Значение потери фокуса в доступных интерфейсах

В браузере фокус представляет собой текущий элемент, принимающий ввод пользователя. Для пользователей клавиатуры перемещение между элементами происходит с помощью клавиш Tab, Shift+Tab и стрелок.

Когда интерактивный компонент теряет фокус, он должен:

  • корректно завершить текущую сессию взаимодействия;
  • закрыться или скрыться;
  • освободить управление для следующего элемента;
  • не блокировать навигацию по странице.

Неправильная обработка потери фокуса приводит к нескольким проблемам:

Типичные ошибки реализации

  • меню остаётся открытым после перехода на другой элемент;
  • диалог закрывается при перемещении фокуса внутри него;
  • всплывающий элемент закрывается слишком рано;
  • фокус теряется и не возвращается ни к одному элементу.

Использование ally.js позволяет избежать подобных проблем благодаря специализированным утилитам управления фокусом.


События, связанные с потерей фокуса

В стандартном JavaScript существует несколько событий, реагирующих на изменение фокуса:

Событие Особенности
blur Срабатывает, когда элемент теряет фокус
focusout Аналог blur, но всплывает вверх по DOM
focusin Срабатывает при получении фокуса и всплывает
focus Срабатывает на элементе без всплытия

Для определения выхода фокуса из контейнера чаще всего используется комбинация focusin и focusout.

Однако эти события имеют ряд особенностей:

  • порядок их вызова отличается между браузерами;
  • некоторые события не всплывают;
  • сложно определить, покинул ли фокус контейнер полностью.

Библиотека ally.js упрощает решение этой задачи.


Определение активного элемента

Для отслеживания перемещения фокуса необходимо знать текущий активный элемент документа.

Стандартный способ:

document.activeElement

Однако в сложных интерфейсах используются:

  • iframe
  • Shadow DOM
  • вложенные компоненты

ally.js предоставляет функцию для более надёжного определения активного элемента.

ally.get.activeElement();

Она корректно обрабатывает:

  • вложенные документы
  • элементы внутри Shadow DOM
  • браузерные различия

Это особенно важно при проверке того, находится ли фокус внутри определённого контейнера.


Проверка принадлежности фокуса контейнеру

Закрытие интерфейсного элемента обычно происходит, когда фокус выходит за пределы его DOM-контейнера.

Пример контейнера:

<div class="dropdown" id="menu">
  <button>Пункт 1</button>
  <button>Пункт 2</button>
</div>

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

const container = document.getElementById('menu');
const active = ally.get.activeElement();

if (!container.contains(active)) {
  closeMenu();
}

Такой подход предотвращает закрытие меню при перемещении между его внутренними элементами.


Отслеживание выхода фокуса

Типичная схема реализации:

  1. компонент открывается;
  2. на документ добавляется обработчик событий;
  3. при изменении фокуса проверяется его принадлежность контейнеру;
  4. если фокус покинул контейнер — компонент закрывается.

Пример реализации:

function monitorFocus(container, onExit) {
  function handleFocusChange() {
    const active = ally.get.activeElement();

    if (!container.contains(active)) {
      onExit();
      document.removeEventListener('focusin', handleFocusChange);
    }
  }

  document.addEventListener('focusin', handleFocusChange);
}

Использование:

monitorFocus(menuElement, closeMenu);

Использование ally.maintain.tabFocus

Иногда интерфейс должен оставаться активным до тех пор, пока пользователь не покинет его полностью. В таких случаях используется управление областью фокуса.

Библиотека предоставляет механизм удержания фокуса внутри контейнера.

const handle = ally.maintain.tabFocus({
  context: dialogElement
});

В этом режиме:

  • клавиша Tab перемещает фокус только внутри диалога;
  • элементы страницы вне контейнера становятся недоступны;
  • при закрытии контейнера контроль фокуса снимается.

Завершение режима:

handle.disengage();

Это предотвращает преждевременное закрытие компонента при навигации внутри него.


Различие между кликом и фокусом

Многие реализации закрытия используют событие click вне элемента.

Пример:

document.addEventListener('click', (event) => {
  if (!menu.contains(event.target)) {
    closeMenu();
  }
});

Этот подход работает для мыши, но не для клавиатуры.

Проблемы такого метода:

  • пользователь клавиатуры может переместить фокус без клика;
  • вспомогательные технологии не генерируют события мыши;
  • закрытие компонента происходит неконсистентно.

Поэтому потеря фокуса является более универсальным сигналом завершения взаимодействия.


Комбинированный подход

На практике часто используется комбинация нескольких механизмов:

  • закрытие при клике вне элемента;
  • закрытие при потере фокуса;
  • закрытие по клавише Escape.

Пример:

function setupDismiss(container) {

  function close() {
    container.hidden = true;
    document.removeEventListener('focusin', onFocusChange);
    document.removeEventListener('keydown', onKeyDown);
  }

  function onFocusChange() {
    const active = ally.get.activeElement();

    if (!container.contains(active)) {
      close();
    }
  }

  function onKeyDown(event) {
    if (event.key === 'Escape') {
      close();
    }
  }

  document.addEventListener('focusin', onFocusChange);
  document.addEventListener('keydown', onKeyDown);
}

Такая схема обеспечивает:

  • поддержку мыши
  • поддержку клавиатуры
  • совместимость со скринридерами

Обработка временных переходов фокуса

Некоторые браузеры могут временно устанавливать фокус на body или document в процессе переключения между элементами. Это может привести к ложному закрытию интерфейса.

Для предотвращения подобных ситуаций применяется небольшая задержка проверки.

function handleFocusChange() {
  setTimeout(() => {
    const active = ally.get.activeElement();

    if (!container.contains(active)) {
      closeMenu();
    }
  }, 0);
}

Задержка позволяет дождаться завершения изменения фокуса.


Работа с модальными окнами

Модальные окна требуют особого поведения:

  • фокус должен оставаться внутри окна;
  • потеря фокуса наружу невозможна;
  • закрытие происходит по специальным действиям.

При использовании ally.js это реализуется комбинацией:

  • ally.maintain.tabFocus
  • ally.maintain.hidden

Пример:

const tabFocus = ally.maintain.tabFocus({
  context: dialog
});

const hidden = ally.maintain.hidden({
  filter: dialog
});

В результате:

  • элементы страницы становятся недоступны;
  • фокус перемещается только внутри модального окна;
  • закрытие происходит только по логике приложения.

Поведение для выпадающих меню

Выпадающие меню должны закрываться при следующих условиях:

  • пользователь перемещает фокус на другой элемент страницы;
  • происходит клик вне меню;
  • нажата клавиша Escape.

Минимальная логика:

function openMenu(button, menu) {

  menu.hidden = false;

  monitorFocus(menu, () => {
    menu.hidden = true;
  });

}

Это позволяет:

  • перемещаться по пунктам меню клавиатурой;
  • автоматически закрывать меню при выходе из него.

Влияние ARIA-атрибутов

ARIA-атрибуты помогают вспомогательным технологиям понимать структуру интерфейса.

Для элементов, закрывающихся при потере фокуса, часто используются:

aria-haspopup="true"
aria-expanded="true"
aria-controls="menu"

При закрытии элемента значение aria-expanded должно обновляться:

button.setAttribute('aria-expanded', 'false');

Это синхронизирует состояние интерфейса со скринридерами.


Типичные ошибки реализации

Закрытие при blur на контейнере

Событие blur не всплывает, поэтому обработчик не узнаёт о переходе фокуса на вложенный элемент.

Проверка event.target

При обработке focusout новый элемент фокуса находится в event.relatedTarget, а не в event.target.

Отсутствие обработки клавиатуры

Интерфейс закрывается только по клику, но остаётся открытым при навигации клавишами.

Потеря фокуса после закрытия

После скрытия компонента фокус может остаться на несуществующем элементе. В таких случаях его возвращают на кнопку, открывшую интерфейс.

button.focus();

Стратегия безопасного закрытия

Надёжная схема работы интерфейсного элемента включает несколько этапов:

  1. Открытие

    • отображение компонента
    • перемещение фокуса внутрь
  2. Отслеживание фокуса

    • мониторинг focusin
    • проверка принадлежности контейнеру
  3. Закрытие

    • скрытие элемента
    • очистка обработчиков
    • возврат фокуса на управляющий элемент
  4. Обновление ARIA-состояний

Такая архитектура обеспечивает стабильную работу как с мышью, так и с клавиатурой.


Роль ally.js в управлении потерей фокуса

Библиотека упрощает реализацию за счёт готовых механизмов:

Ключевые возможности

  • корректное определение активного элемента;
  • управление циклом табуляции;
  • скрытие элементов от фокуса;
  • работа с различиями браузеров;
  • безопасное управление областями фокуса.

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