Многие интерактивные элементы пользовательского интерфейса должны автоматически закрываться, когда пользователь переключает внимание на другую часть страницы. Такое поведение характерно для выпадающих меню, модальных окон, всплывающих подсказок, контекстных панелей и диалогов. Корректная реализация закрытия при потере фокуса является важной частью доступности, поскольку пользователи клавиатуры и вспомогательных технологий перемещаются по интерфейсу иначе, чем пользователи мыши.
Библиотека ally.js предоставляет инструменты для корректного отслеживания состояния фокуса, определения его границ и управления поведением интерфейсных компонентов при изменении активного элемента.
В браузере фокус представляет собой текущий элемент, принимающий ввод
пользователя. Для пользователей клавиатуры перемещение между элементами
происходит с помощью клавиш Tab, Shift+Tab и
стрелок.
Когда интерактивный компонент теряет фокус, он должен:
Неправильная обработка потери фокуса приводит к нескольким проблемам:
Типичные ошибки реализации
Использование ally.js позволяет избежать подобных проблем благодаря специализированным утилитам управления фокусом.
В стандартном JavaScript существует несколько событий, реагирующих на изменение фокуса:
| Событие | Особенности |
|---|---|
blur |
Срабатывает, когда элемент теряет фокус |
focusout |
Аналог blur, но всплывает вверх по DOM |
focusin |
Срабатывает при получении фокуса и всплывает |
focus |
Срабатывает на элементе без всплытия |
Для определения выхода фокуса из контейнера чаще всего используется
комбинация focusin и focusout.
Однако эти события имеют ряд особенностей:
Библиотека ally.js упрощает решение этой задачи.
Для отслеживания перемещения фокуса необходимо знать текущий активный элемент документа.
Стандартный способ:
document.activeElement
Однако в сложных интерфейсах используются:
iframeally.js предоставляет функцию для более надёжного определения активного элемента.
ally.get.activeElement();
Она корректно обрабатывает:
Это особенно важно при проверке того, находится ли фокус внутри определённого контейнера.
Закрытие интерфейсного элемента обычно происходит, когда фокус выходит за пределы его 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();
}
Такой подход предотвращает закрытие меню при перемещении между его внутренними элементами.
Типичная схема реализации:
Пример реализации:
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);
Иногда интерфейс должен оставаться активным до тех пор, пока пользователь не покинет его полностью. В таких случаях используется управление областью фокуса.
Библиотека предоставляет механизм удержания фокуса внутри контейнера.
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.tabFocusally.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-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();
Надёжная схема работы интерфейсного элемента включает несколько этапов:
Открытие
Отслеживание фокуса
focusinЗакрытие
Обновление ARIA-состояний
Такая архитектура обеспечивает стабильную работу как с мышью, так и с клавиатурой.
Библиотека упрощает реализацию за счёт готовых механизмов:
Ключевые возможности
В результате разработка интерфейсов, закрывающихся при потере фокуса, становится более предсказуемой и совместимой с требованиями доступности.