Пространственная навигация (фокус на клавишах со стрелками)

Веб-функции: scrollIntoView()

Навигация с помощью клавиш со стрелками или D-Pad позволяет пользователям, которые работают только с клавиатурой или альтернативными устройствами ввода, перемещаться по интерактивным элементам на основе их визуального макета. Это очень полезно для пространственных пользовательских интерфейсов (таких как медиапанели, панели управления или вложенные каталоги), но должно быть реализовано осторожно, чтобы не нарушать нативное поведение браузера, например прокрутку области просмотра.

В этом руководстве рассказывается, как создать эффективную и доступную двумерную навигацию внутри составных виджетов.


Основные принципы архитектуры

При реализации пользовательского пространственного фокуса с помощью клавиш со стрелками придерживайтесь следующих основных правил:

  1. Ограничьте использование клавиш со стрелками составными виджетами. Не переопределяйте клавиши со стрелками в обычном тексте документа или простых вертикальных списках, где ожидается навигация по умолчанию с помощью клавиши Tab. Общие рекомендации по структуре документа приведены в руководстве по программированию специальных возможностей.
  2. Сохранять прокрутку границ нативных элементов. Если в направлении нажатия нет элемента, на который можно установить фокус, не запрещать поведение браузера по умолчанию. Разрешите всплытие события, чтобы браузер мог прокручивать область просмотра. Переопределение клавиш со стрелками нарушает специальные возможности (WCAG 1.4.10).
  3. Обеспечьте видимость и выравнивание фокуса. При изменении фокуса убедитесь, что новый элемент, на который он перемещается, прокручивается в область видимости (например, с помощью scrollIntoView({ block: 'nearest', inline: 'nearest' })), чтобы он оставался полностью видимым.
  4. Координация с системой макета. Определяйте позиции кандидатов на фокус по текущим ограничивающим геометриям (getBoundingClientRect()) при обработке каждого нажатия клавиши. Не предполагайте, что порядок DOM или фиксированная структура сетки представляют визуальный макет. Рекомендации по современным макетам можно найти в статье Макеты CSS и адаптивный дизайн.

Управление фокусом для специальных возможностей

Пространственный контейнер должен предоставлять одну позицию табуляции для последовательной навигации по вкладкам страницы. Для управления фокусом клавиатуры внутри виджета используйте шаблон Roving Tabindex:

  • Активный объект содержит параметр tabindex="0".
  • Все неактивные объекты имеют статус tabindex="-1".
  • При навигации перемещайте фокус программно, изменяя атрибут tabindex="0" и вызывая focus().

Визуальный выбор кандидатов

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

  • Кандидаты. Используйте дочерние элементы контейнера виджета, которые могут получать фокус и передавать checkVisibility(). Скрытые элементы могут иметь прямоугольник нулевого размера в (0, 0) и не должны становиться случайными целями up или left.
  • Геометрия. Чтение getBoundingClientRect() для элемента в фокусе и каждого кандидата при обработке нажатия клавиши. Не кешируйте эти прямоугольники глобально: макет может измениться по многим причинам, а небольшое количество считываний геометрии обходится дешево при обработке ключевого события, заданного пользователем.
  • Фильтр направления. Кандидат находится в нажатом направлении, только когда его ближний край находится на дальнем краю текущего элемента или за ним на этой оси, что допускает небольшой допуск для субпиксельного округления. Например, кандидат на должность right должен иметь candidate.left >= current.right - tolerance. Не сравнивайте центры, чтобы определить, находится ли кандидат в направлении.
  • Сначала выровненные кандидаты. Среди кандидатов, прошедших фильтр направления, предпочтение отдается тем, чья протяженность по перпендикулярной оси перекрывает протяженность текущего элемента. Выбирается наименьший зазор на оси навигации, а связи разрываются с наименьшим перпендикулярным смещением центра. Это позволяет сохранить навигацию в соседней строке или столбце, даже если элемент необычно широкий.
  • Резервная оценка. Если ни один из кандидатов не перекрывается по перпендикулярной оси, оцените оставшихся кандидатов как gap + w * perpendicularCentreOffset и выберите минимальное значение. Используйте вес w >= 2 в качестве отправной точки, а затем настройте его для макета. Больший вес отдает предпочтение кандидатам, которые более тесно выровнены.
  • No candidate (Нет кандидата). Ничего не возвращать. Обработчик событий не должен вызывать preventDefault(), чтобы браузер мог выполнять прокрутку.

Точная реализация может отличаться, но сохраняйте проверку направления на основе ребер и применяйте предпочтение перекрытия перед взвешенным резервным вариантом. Это суть алгоритма. Не представляйте кеш или фиксированный вес как универсально правильный.


Интеграция нажатия клавиш и взаимодействие с прокруткой

Маршрутизатор событий должен предотвращать стандартную прокрутку области просмотра только при допустимом переходе пространственного фокуса:

  • Не перехватывайте событие, если нажата клавиша-модификатор, если e.defaultPrevented уже имеет значение true или если клавиши со стрелками используются для управления самым внутренним целевым объектом события. Используйте e.composedPath()[0] вместо e.target, поскольку e.target может быть перенаправлен на теневой хост. Практическая проверка редактируемости/управления – target.matches(':read-write, select, input[type="range"], [role="slider"]'); закрытые теневые корни проверить невозможно.
  • Ограничьте область действия прослушивателя контейнером виджета, чтобы на странице могло сосуществовать несколько пространственных виджетов.
  • Выполнять действие, только если выбранный элемент сам является кандидатом. Если элементы содержат собственные элементы управления, на которых можно фокусироваться, сделайте эти элементы управления кандидатами или оставьте их в покое. Не разрешайте родительский элемент с помощью closest().
  • Когда кандидат найден, вызовите preventDefault(), обновите перемещающийся tabindex, вызовите focus() и используйте scrollIntoView({ block: 'nearest', inline: 'nearest' }).
  • Если подходящий элемент не найден, вернитесь, не вызывая preventDefault(), чтобы продолжить нативную прокрутку.

Не добавляйте односимвольные быстрые клавиши, такие как h/j/k/l или W/A/S/D, для пространственной навигации. Клавиши со стрелками не являются символьными, и сохранение нативной прокрутки при отсутствии кандидата позволяет избежать ненужной блокировки перемещения области просмотра.