Трапинг фокуса в диалоге

Трапинг фокуса (focus trapping) — это техника управления фокусом внутри интерактивного элемента, такого как модальное окно, диалог или всплывающее меню. Основная цель — ограничить навигацию с клавиатуры элементами внутри контейнера до момента его закрытия, предотвращая переход фокуса на элементы страницы за пределами контейнера. В JavaScript это критично для обеспечения доступности (accessibility) интерфейсов.

В DOM фокус определяется с помощью свойства document.activeElement. Все элементы с атрибутом tabindex или интерактивные элементы (button, input, a[href]) могут получать фокус. Для корректного трапинга необходимо отслеживать:

  • Вход фокуса внутрь контейнера.
  • Попытки выхода фокуса за пределы контейнера.
  • Клавишу Tab для циклической навигации.

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


Основные функции ally.js для фокуса

  1. tabbable(container) Возвращает массив всех элементов внутри container, которые могут быть достигнуты с помощью клавиши Tab. Элементы фильтруются по видимости и атрибуту tabindex.

    import { tabbable } from 'ally.js';
    
    const modal = document.querySelector('.modal');
    const focusableElements = tabbable(modal);

    Ключевой момент: порядок элементов в массиве соответствует порядку обхода Tab.

  2. focusFirstTabbable(container) и focusLastTabbable(container) Автоматически устанавливают фокус на первый или последний фокусируемый элемент внутри контейнера. Это удобно при открытии модального окна.

    import { focusFirstTabbable } from 'ally.js';
    
    focusFirstTabbable(modal);
  3. focusWithin(container) Проверяет, находится ли текущий фокус внутри контейнера. Используется для предотвращения выхода фокуса за пределы модального окна.

    import { focusWithin } from 'ally.js';
    
    if (!focusWithin(modal)) {
        focusFirstTabbable(modal);
    }

Реализация трапинга фокуса в модальном диалоге

Для полноценного трапинга необходима обработка события keydown, отслеживающего клавишу Tab. Основная логика:

  1. Получить все фокусируемые элементы внутри модального окна.
  2. Определить первый и последний элемент.
  3. При нажатии Tab на последнем элементе фокус возвращается на первый.
  4. При Shift+Tab на первом элементе фокус возвращается на последний.

Пример с использованием ally.js:

import { tabbable } from 'ally.js';

const modal = document.querySelector('.modal');

function trapFocus(event) {
    const focusable = tabbable(modal);
    const firstElement = focusable[0];
    const lastElement = focusable[focusable.length - 1];

    if (event.key === 'Tab') {
        if (event.shiftKey) {
            if (document.activeElement === firstElement) {
                event.preventDefault();
                lastElement.focus();
            }
        } else {
            if (document.activeElement === lastElement) {
                event.preventDefault();
                firstElement.focus();
            }
        }
    }
}

modal.addEventListener('keydown', trapFocus);

Особенности: ally.js исключает невидимые или отключённые элементы, поэтому массив focusable всегда корректен.


Автоматическое восстановление фокуса

После закрытия модального окна важно вернуть фокус на элемент, который инициировал открытие диалога. ally.js не управляет этим напрямую, но можно реализовать вручную:

const trigger = document.querySelector('#open-modal');

trigger.addEventListener('click', () => {
    modal.style.display = 'block';
    focusFirstTabbable(modal);
});

function closeModal() {
    modal.style.display = 'none';
    trigger.focus();
}

Ключевой момент: это улучшает доступность для пользователей клавиатуры и экранных читалок.


Обработка динамических элементов

Иногда фокусируемые элементы добавляются в контейнер динамически. ally.js позволяет получать актуальный список через tabbable(container) при каждом событии:

modal.addEventListener('keydown', (event) => {
    const focusable = tabbable(modal); // всегда актуально
});

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


Дополнительные утилиты ally.js для диалогов

  • nextFocus(element) / previousFocus(element) Возвращают ближайший элемент для навигации, что удобно при сложных интерфейсах с вложенными контейнерами.

  • focus(element) Безопасно устанавливает фокус на элемент, даже если он временно невидим или скрыт через CSS. Полезно для анимаций открытия модального окна.

  • isFocusable(element) Проверяет, может ли элемент принимать фокус, включая скрытые и disabled элементы.


Практические советы

  • Всегда хранить ссылку на элемент-триггер для восстановления фокуса.
  • Для вложенных модальных окон использовать рекурсивный подход: каждый контейнер управляет своим трапингом.
  • Не полагаться на стандартный tabindex без ally.js, так как порядок обхода Tab может быть нарушен из-за динамических элементов или CSS-свойства display:none.
  • ally.js совместим с большинством браузеров и учитывает особенности мобильной навигации клавиатурой.

Использование ally.js делает трапинг фокуса надежным, предотвращает “выход фокуса” из диалогов и обеспечивает соответствие стандартам доступности WCAG. Это особенно важно для сложных веб-приложений с модальными окнами, всплывающими меню и интерактивными компонентами.