Перейти к содержанию

FocusTrap удерживает клавиатурный фокус в пределах заданного DOM-контейнера. По умолчанию, пока ловушка активна, Tab и Shift+Tab перемещают фокус только между элементами этого контейнера, доступными для перехода с клавиатуры.

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

В Bitrix Framework за ловушку фокуса отвечает расширение ui.a11y. В нем доступны класс FocusTrap и директива FocusTrapDirective для Vue.

Подключить расширение

Если вы подключаете компонент из PHP, загрузите расширение ui.a11y.

\Bitrix\Main\UI\Extension::load('ui.a11y');

Если вы работаете в модульном JavaScript, импортируйте FocusTrap из ui.a11y.

import { FocusTrap } from 'ui.a11y';

Активировать и деактивировать

Создайте экземпляр FocusTrap, передав DOM-контейнер, и управляйте жизненным циклом ловушки через три метода. В конструктор нужно передать существующий HTMLElement. Если вы ищете контейнер через querySelector(), сначала проверьте результат.

  • activate(options?) — активирует ловушку и устанавливает начальный фокус. Передайте { initialFocus: false }, если при активации не нужно применять начальный фокус.

  • deactivate() — деактивирует ловушку и возвращает фокус на элемент, который был активен до вызова activate().

  • destroy() — деактивирует ловушку и убирает служебные элементы из разметки. Вызывайте при удалении контейнера из DOM.

import { FocusTrap } from 'ui.a11y';

const container = document.querySelector('#modal');
if (!container)
{
    throw new Error('Modal container was not found.');
}

const trap = new FocusTrap(container);

// Открытие: фокус перемещается на первый элемент внутри контейнера, доступный для перехода по `Tab`.
trap.activate();

// Закрытие: фокус возвращается на элемент, который был активен до вызова activate().
trap.deactivate();

// Удаление из DOM: деактивирует ловушку и убирает служебные элементы из разметки.
trap.destroy();

Если ловушка уже активна, повторный вызов activate() не имеет эффекта.

Передать параметры

Конструктор FocusTrap принимает необязательный объект FocusTrapOptions. Полный состав объекта нужен, если вы описываете параметры в TypeScript.

type FocusTrapOptions = {
    initialFocus?: InitialFocus | InitialFocus[];
    forceInitialFocus?: boolean;
    restoreFocus?: RestoreFocus;
    preventScroll?: boolean;
    suppressFocusOnRestore?: boolean;
    looped?: boolean;
    isolateOutside?: boolean;
    outsideExceptionSelectors?: string[];
    startBoundary?: FocusBoundaryTarget;
    endBoundary?: FocusBoundaryTarget;
};

type InitialFocus = 'first-tabbable' | 'container' | string | boolean | (() => HTMLElement | null);
type RestoreFocus = boolean | string | HTMLElement | (() => HTMLElement | null);
type FocusBoundaryTarget = string | HTMLElement | (() => HTMLElement | null);

В initialFocus можно передать одно значение InitialFocus или массив таких значений.

Параметр Тип Описание
initialFocus InitialFocus | InitialFocus[] Определяет, какой элемент получит фокус при активации. Не выполняется, если внутри контейнера уже есть активный элемент. По умолчанию true.
forceInitialFocus boolean Применяет initialFocus при активации, даже если один из элементов контейнера уже находится в фокусе. По умолчанию false.
restoreFocus RestoreFocus Определяет, куда вернется фокус при деактивации. По умолчанию true.
preventScroll boolean Запрещает прокрутку страницы при программном перемещении фокуса. По умолчанию true.
suppressFocusOnRestore boolean Временно снимает обработчики focus при возврате фокуса. Нужен для старых виджетов, где возврат фокуса вызывает нежелательное поведение.
looped boolean Включает циклический обход Tab от последнего элемента к первому и обратно. По умолчанию true.
isolateOutside boolean Добавляет атрибут inert всем элементам за пределами контейнера на время активности ловушки. По умолчанию false.
outsideExceptionSelectors string[] Задает CSS-селекторы элементов вне контейнера, которые не получают inert при isolateOutside: true.
startBoundary FocusBoundaryTarget Задает элемент, который получает фокус при нажатии Shift+Tab на первом элементе контейнера. Элемент должен быть доступен для фокуса; если элемент не найден или не может получить фокус, фокус переходит на последний элемент контейнера.
endBoundary FocusBoundaryTarget Задает элемент, который получает фокус при нажатии Tab на последнем элементе контейнера. Элемент должен быть доступен для фокуса; если элемент не найден или не может получить фокус, фокус переходит на первый элемент контейнера.

Управлять начальным фокусом

Параметр initialFocus определяет, какой элемент получит фокус при вызове activate(). Он принимает одно значение или массив значений — ловушка перебирает кандидатов по порядку и устанавливает фокус на первом подходящем.

Если фокус уже находится внутри контейнера, initialFocus не применяется. Чтобы проигнорировать текущий фокус и все равно выполнить initialFocus, передайте forceInitialFocus: true.

|

|| Значение | Поведение || || true или 'first-tabbable' | Фокус на первом элементе внутри контейнера, доступном для перехода по Tab. Поведение по умолчанию. || || 'container' | Фокус на самом контейнере. || || CSS-селектор | Фокус на первом элементе, совпадающем с селектором. || || false | Фокус не перемещается при активации. || || Функция | Фокус на элементе, который возвращает функция, если он доступен для фокуса. || |#

Если в initialFocus передан CSS-селектор, используйте валидный селектор. Если селектор валидный, но элемент не найден или не может получить фокус, ловушка переходит к следующему кандидату.

import { FocusTrap } from 'ui.a11y';

const container = document.querySelector('#dialog');
if (!container)
{
    throw new Error('Dialog container was not found.');
}

const trap = new FocusTrap(container, {
    // Попробовать сфокусировать кнопку подтверждения,
    // если ее нет — первый элемент, доступный для перехода по `Tab`.
    initialFocus: ['#confirm-button', true],
});

trap.activate();

Если ни один кандидат не подошел или в контейнере нет элементов, доступных для перехода по Tab, фокус перемещается на сам контейнер. В этом случае FocusTrap добавляет контейнеру tabindex="-1", если атрибут не был задан.

Настроить возврат фокуса

Параметр restoreFocus определяет, куда вернется фокус после deactivate().

|

|| Значение | Поведение || || true | Фокус возвращается на элемент, который был активен в момент вызова activate(). Поведение по умолчанию. || || false | Фокус не восстанавливается. || || CSS-селектор | Фокус на первом элементе, совпадающем с селектором. || || HTMLElement | Фокус на переданном элементе. || || Функция | Фокус на элементе, который возвращает функция, если он доступен для фокуса. || |#

Передавайте в restoreFocus валидный CSS-селектор или элемент, который может получить фокус. Если селектор невалидный или элемент не найден, ловушка возвращает фокус на элемент, который был активен при вызове activate().

import { FocusTrap } from 'ui.a11y';

const openButton = document.querySelector('#open-button');
const container = document.querySelector('#dialog');
if (!openButton || !container)
{
    throw new Error('Focus target or dialog container was not found.');
}

const trap = new FocusTrap(container, {
    // Явно задать элемент для возврата фокуса.
    restoreFocus: openButton,
});

trap.activate();

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

  • setRestoreFocus(restore) — изменяет стратегию возврата фокуса. Принимает те же типы, что параметр restoreFocus конструктора.

  • setLastFocusedElement(el) — вручную задает элемент, на который вернется фокус при деактивации. Элемент должен находиться вне контейнера и не быть body.

Если ловушку активируют без действия пользователя, например после загрузки страницы или по событию приложения, автоматически сохраненный элемент для возврата фокуса может не подходить. В этом случае задайте целевой элемент через setLastFocusedElement(el).

Изолировать содержимое за пределами ловушки

При isolateOutside: true ловушка добавляет атрибут inert всем элементам вне контейнера. Элементы вне контейнера не получают клики и фокус, а скринридер исключает их из чтения.

Если часть страницы должна остаться доступной, передайте ее CSS-селектор в outsideExceptionSelectors. Селекторы должны быть валидными: если список нельзя обработать, исключения не применятся.

import { FocusTrap } from 'ui.a11y';

const container = document.querySelector('#modal');
if (!container)
{
    throw new Error('Modal container was not found.');
}

const trap = new FocusTrap(container, {
    isolateOutside: true,
    // Тултип показывается поверх модального окна, поэтому исключаем его из inert.
    outsideExceptionSelectors: ['#global-tooltip'],
});

trap.activate();

При деактивации атрибут inert снимается. Если элемент уже имел inert до активации ловушки, он остается inert после деактивации.

Если внешний элемент не должен получать inert при обработке соседних элементов, добавьте ему атрибут data-a11y-ignore-inert. Для обычных исключений используйте outsideExceptionSelectors: этот параметр явно связывает исключение с конкретной ловушкой.

Перемещать фокус программно

FocusTrap предоставляет методы для программного управления фокусом внутри контейнера. Методы возвращают null, если подходящий элемент не найден, кроме focusContainer(): он всегда возвращает контейнер.

Метод Описание Возвращает
focusFirst(options?) Перемещает фокус на первый элемент в контейнере, доступный для перехода по Tab. HTMLElement | null
focusLast(options?) Перемещает фокус на последний элемент в контейнере, доступный для перехода по Tab. HTMLElement | null
focusNext(options?) Перемещает фокус на следующий элемент, доступный для перехода по Tab, относительно текущего. HTMLElement | null
focusPrevious(options?) Перемещает фокус на предыдущий элемент, доступный для перехода по Tab, относительно текущего. HTMLElement | null
focusContainer(options?) Перемещает фокус на контейнер. HTMLElement
focusBySelector(selector, options?) Перемещает фокус на первый элемент, совпадающий с CSS-селектором внутри контейнера. HTMLElement | null
applyInitialFocus() Применяет логику initialFocus вручную.

Все методы принимают необязательный объект FocusNavigatorOptions. Его параметр preventScroll управляет только конкретным вызовом и не связан с одноименным параметром конструктора. Передайте { preventScroll: false }, чтобы разрешить прокрутку в отдельном вызове.

В focusBySelector(selector, options?) передавайте валидный CSS-селектор. Если элемент не найден или не может получить фокус, метод возвращает null.

import { FocusTrap } from 'ui.a11y';

const container = document.querySelector('#panel');
if (!container)
{
    throw new Error('Panel container was not found.');
}

const trap = new FocusTrap(container, {
    initialFocus: false,
});

trap.activate();

// Переместить фокус вручную после загрузки данных.
fetch('/api/data').then(() => {
    trap.focusBySelector('#first-input');
});

Проверить и изменить состояние ловушки

|

|| Метод | Описание || || isActive() | Возвращает true, если ловушка активна. || || isLooped() | Возвращает true, если включен циклический обход Tab. || || contains(el) | Возвращает true, если элемент находится внутри контейнера. || || getId() | Возвращает уникальный идентификатор ловушки. || || setLooped(flag) | Включает или отключает циклический обход Tab. || || setPreventScroll(flag) | Включает или отключает блокировку прокрутки при программном перемещении фокуса. || |#

Проверить работу ловушки

После подключения FocusTrap проверьте основные сценарии клавиатурой и из кода.

  • При открытии контейнера фокус переходит на ожидаемый элемент. Если внутри контейнера уже есть активный элемент, initialFocus не применяется без forceInitialFocus: true.

  • При looped: true клавиша Tab на последнем элементе возвращает фокус к первому, а Shift+Tab на первом элементе — к последнему.

  • При looped: false фокус не зацикливается на границах контейнера.

  • При deactivate() фокус возвращается на элемент, заданный в restoreFocus, или на элемент, который был активен перед activate().

  • Если restoreFocus, initialFocus, startBoundary или endBoundary указывают на несуществующий или нефокусируемый элемент, ловушка использует запасное поведение. Проверить фокусируемость элемента можно через InteractivityChecker.isFocusable().

  • При isolateOutside: true элементы вне контейнера получают inert, кроме элементов из outsideExceptionSelectors и элементов с data-a11y-ignore-inert. После deactivate() атрибут снимается, если он не был задан до активации ловушки.

Использовать с Vue

Для Vue-компонентов расширение экспортирует директиву FocusTrapDirective. Зарегистрируйте ее в приложении и используйте как v-focus-trap.

import { createApp } from 'ui.vue3';
import { FocusTrapDirective } from 'ui.a11y';

const app = createApp(MyComponent);
app.directive('focus-trap', FocusTrapDirective);
app.mount('#app');

Директива принимает boolean или объект с полями active и options.

<!-- Активировать ловушку: -->
<div v-focus-trap="true">...</div>

<!-- Передать параметры: -->
<div v-focus-trap="{ active: isModalOpen, options: { isolateOutside: true } }">...</div>

Директива создает FocusTrap при монтировании, активирует или деактивирует при обновлении значения и вызывает destroy() при размонтировании. Если объект options изменился, директива уничтожает старую ловушку и создает новую.

Включить логирование

FocusTrap поддерживает логирование событий фокуса через статические методы.

import { FocusTrap } from 'ui.a11y';

// Включить вывод событий в консоль.
FocusTrap.enableDebug();

// Отключить.
FocusTrap.disableDebug();

Включайте логирование на время отладки сценария с фокусом и отключайте после проверки.

Связанные материалы