Библиотека Focus-trap предназначена для управления фокусом клавиатуры внутри определённого контейнера. Это особенно важно для модальных окон, всплывающих подсказок и сложных виджетов, где пользователь не должен покидать область интерфейса, пока элемент активен. Основная идея заключается в «замыкании» фокуса: при активации trap, клавиатурный фокус циклически проходит только через элементы внутри контейнера.
Ключевые моменты работы:
createFocusTrap(container, options) создаёт объект trap для
указанного DOM-контейнера.activate() и deactivate() включают и выключают
управление фокусом.initialFocus можно задать конкретный элемент для установки
фокуса при активации trap.returnFocusOnDeactivate управляет возвращением фокуса на
элемент, который был активен до активации trap.Focus-trap позволяет тонко настраивать поведение фокуса:
escapeDeactivates — разрешает закрытие
trap по клавише Esc.clickOutsideDeactivates — разрешает
деактивацию при клике вне контейнера.allowOutsideClick — позволяет
определённые клики за пределами trap без его деактивации.onActivate / onDeactivate
— колбэки для выполнения дополнительной логики при включении и
выключении trap.Эти параметры критически важны для корректного UX и предотвращения «залипания» фокуса вне видимой области интерфейса.
Unit-тестирование trap строится на симуляции поведения пользователя с клавиатурой и проверки состояния DOM:
activate/deactivate
доступны.initialFocus или
первый фокусируемый элемент контейнера).Tab и Shift+Tab ограничено рамками
контейнера.deactivate() фокус возвращается на элемент, который был
активен до активации trap.Esc, кликов за пределами контейнера, чтобы проверить
правильное срабатывание опций escapeDeactivates и
clickOutsideDeactivates.onActivate и onDeactivate, включая возможные
промисы и задержки.import { createFocusTrap } from 'focus-trap';
let trapContainer;
let trap;
let buttonOutside;
beforeEach(() => {
trapContainer = document.createElement('div');
const button1 = document.createElement('button');
const button2 = document.createElement('button');
buttonOutside = document.createElement('button');
trapContainer.appendChild(button1);
trapContainer.appendChild(button2);
document.body.appendChild(trapContainer);
document.body.appendChild(buttonOutside);
trap = createFocusTrap(trapContainer, {
escapeDeactivates: true,
clickOutsideDeactivates: true
});
});
afterEach(() => {
trap.deactivate();
document.body.removeChild(trapContainer);
document.body.removeChild(buttonOutside);
});
test('Фокус устанавливается на первый элемент внутри trap', () => {
trap.activate();
expect(document.activeElement).toBe(trapContainer.querySelector('button'));
});
test('Фокус не выходит за пределы trap при Tab', () => {
trap.activate();
const buttons = trapContainer.querySelectorAll('button');
buttons[0].focus();
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' });
document.activeElement.dispatchEvent(tabEvent);
expect(document.activeElement).toBe(buttons[1]);
});
test('Escape деактивирует trap', () => {
trap.activate();
const escEvent = new KeyboardEvent('keydown', { key: 'Escape' });
document.dispatchEvent(escEvent);
expect(document.activeElement).toBe(buttonOutside); // если returnFocusOnDeactivate = true
});
Focus-trap поддерживает асинхронные колбэки и возможность динамического изменения DOM после активации. Для unit-тестов важно учитывать:
setTimeout или
await Promise.resolve() для ожидания завершения
колбэков.test('Асинхронная деактивация с колбэком', async () => {
let deactivated = false;
trap = createFocusTrap(trapContainer, {
onDeactivate: () => new Promise(resolve => {
setTimeout(() => {
deactivated = true;
resolve();
}, 50);
})
});
trap.activate();
await trap.deactivate();
expect(deactivated).toBe(true);
});
document.body позволяет корректно проверять цикличность
фокуса и обработку событий клавиатуры.Unit-тестирование Focus-trap обеспечивает стабильность интерфейсов с модальными окнами и виджетами, предотвращая ошибки навигации клавиатурой и повышая доступность.