Клавиатурная навигация

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

Библиотека iziToast по умолчанию ориентирована на визуальное взаимодействие, однако поддержка клавиатуры может быть реализована за счёт правильной настройки параметров и добавления обработчиков событий.


Управление фокусом

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

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

iziToast.show({
    title: 'Уведомление',
    message: 'Доступно с клавиатуры',
    buttons: [
        ['<button>ОК</button>', function (instance, toast, button) {
            instance.hide({ transitionOut: 'fadeOut' }, toast);
        }]
    ],
    onOpened: function (instance, toast) {
        const btn = toast.querySelector('button');
        if (btn) btn.focus();
    }
});

Ключевой момент — использование обработчика onOpened, который срабатывает после добавления уведомления в DOM.


Закрытие уведомлений с клавиатуры

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

Добавление глобального обработчика:

document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') {
        document.querySelectorAll('.iziToast').forEach(toast => {
            iziToast.hide({}, toast);
        });
    }
});

Особенности:

  • закрываются все активные уведомления
  • можно модифицировать поведение, закрывая только последнее уведомление

Навигация между элементами внутри уведомления

Если уведомление содержит несколько интерактивных элементов (кнопки, ссылки), важно обеспечить корректный порядок табуляции.

iziToast использует обычную DOM-структуру, поэтому порядок определяется:

  • последовательностью элементов в HTML
  • значением tabindex

Пример:

buttons: [
    ['<button tabindex="0">Подтвердить</button>', handler],
    ['<button tabindex="0">Отмена</button>', handler]
]

Если необходимо исключить элемент из табуляции:

<button tabindex="-1">Скрытая кнопка</button>

Ограничение фокуса внутри уведомления (Focus Trap)

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

Пример простой реализации:

function trapFocus(toast) {
    const focusable = toast.querySelectorAll('button, a, input');
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    toast.addEventListener('keydown', function (e) {
        if (e.key === 'Tab') {
            if (e.shiftKey && document.activeElement === first) {
                e.preventDefault();
                last.focus();
            } else if (!e.shiftKey && document.activeElement === last) {
                e.preventDefault();
                first.focus();
            }
        }
    });
}

Подключение:

onOpened: function (instance, toast) {
    trapFocus(toast);
}

Доступность и ARIA-атрибуты

Для корректной работы экранных читалок необходимо добавить ARIA-атрибуты.

Пример:

iziToast.show({
    title: 'Ошибка',
    message: 'Произошла проблема',
    attributes: {
        role: 'alert',
        'aria-live': 'assertive'
    }
});

Основные значения:

  • role="alert" — сообщает о важном уведомлении
  • aria-live="polite" — мягкое уведомление
  • aria-live="assertive" — немедленное озвучивание

Управление временем жизни уведомления

Автоматическое исчезновение уведомлений может мешать клавиатурной навигации. Для таких случаев рекомендуется отключить таймер:

iziToast.show({
    timeout: false
});

Или увеличивать время:

timeout: 10000

Обработка событий клавиатуры внутри уведомления

Можно добавлять кастомные горячие клавиши для действий:

onOpened: function (instance, toast) {
    toast.addEventListener('keydown', function (e) {
        if (e.key === 'Enter') {
            const btn = toast.querySelector('button');
            if (btn) btn.click();
        }
    });
}

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

  • активировать основное действие через Enter
  • создавать быстрые сценарии взаимодействия

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

При наличии нескольких уведомлений важно определить:

  • какое получает фокус первым
  • как переключаться между ними

Рекомендуемый подход:

  • фокус устанавливается только на последнее уведомление
  • предыдущие остаются доступными через Shift+Tab

Пример:

onOpened: function (instance, toast) {
    const toasts = document.querySelectorAll('.iziToast');
    const lastToast = toasts[toasts.length - 1];

    if (toast === lastToast) {
        const btn = toast.querySelector('button');
        if (btn) btn.focus();
    }
}

Интеграция с глобальной системой навигации

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

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

Пример возврата фокуса:

let lastFocused;

document.addEventListener('focusin', (e) => {
    lastFocused = e.target;
});

iziToast.show({
    onClosed: function () {
        if (lastFocused) lastFocused.focus();
    }
});

Частые проблемы и решения

Фокус не устанавливается

  • элемент ещё не отрисован → использовать onOpened

Tab пропускает элементы

  • отсутствует tabindex
  • элементы скрыты или disabled

Escape не работает

  • событие перехватывается другим обработчиком
  • требуется stopPropagation или изменение порядка подписки

Невозможно выйти из уведомления

  • неправильно реализован focus trap

Практические рекомендации

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

Расширенная конфигурация доступности

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

iziToast.show({
    title: 'Действие требуется',
    message: 'Подтвердите операцию',
    timeout: false,
    close: false,
    overlay: true,
    buttons: [
        ['<button>Да</button>', function (instance, toast) {
            instance.hide({}, toast);
        }, true],
        ['<button>Нет</button>', function (instance, toast) {
            instance.hide({}, toast);
        }]
    ],
    onOpened: function (instance, toast) {
        trapFocus(toast);
        const btn = toast.querySelector('button');
        if (btn) btn.focus();
    },
    onClosed: function () {
        document.body.focus();
    },
    attributes: {
        role: 'alertdialog',
        'aria-modal': 'true'
    }
});

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