Доступность в Angular c помощью CDK A11y на реальных кейсах с FocusTrap и FocusMonitor

Мы привыкли слышать, что Angular - это фреймворк, который решает массу задач из коробки: свой CLI, встроенная сборка приложений, автоматическая миграция на новые версии с помощью schematic, работа с HTTP, DI, реактивные формы, работа с состоянием - все это удобные инструменты для разработчика. Обычно я сравниваю его с коробкой автомат: сел и поехал, не отвлекаясь на переключение передач.

Но в мире веба мы всегда должны думать о пользователях. И один из разделов, который заботится о них, называется доступность (Accessibility, A11y в англоязычной среде). И тут Angular позаботился о нас и дал мощнейший инструмент из коробки под названием CDK A11y. Предлагаю ознакомиться с концепцией доступности и изучить применение этого инструмента в Angular.

Что такое доступность

Существует множество способов взаимодействия с сайтом: мышь, клавиатура, голосовое управление, touch-устройства и т.д. Доступным будет считаться тот сайт, который максимально покрывает все варианты взаимодействия с ним. Независимо от пользовательских предпочтений, возможность дойти от точки действия А к Б не будет являться для него проблемой. Попробуйте навигацию клавиатурой по Github - в нем неплохо учтены пользовательские кейсы доступности.

Помимо способов взаимодействия с сайтом, в A11y можно выделить отдельное направление про контент на сайте. Сюда будут входить хорошие шрифты, цвета (возможны высокие контрасты для людей с нарушением зрения), скрытый/вспомогательный контент.

Также к доступности можно отнести: скорость загрузки сайта (возможно, есть пользователи, у которых ограничена пропускная способность интернета). А также работу сайта на различных устройствах.

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

CDK A11y в Angular

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

Обычно выделяют следующие кейсы в интерфейсе для использования захвата фокуса:

  • навигация в модальных окнах;

  • навигация в меню;

  • навигация в datepicker;

  • различные пользовательские формы ввода, например, форма login;

Пример реализации захвата в календаре от Material.

Material Datepicker & FocusTrap
Material Datepicker & FocusTrap

Использовать cdkFocusTrap просто - достаточно обернуть необходимую область для захвата и при перемещении по tab мы не будем вываливаться за пределы области.

Area 1
<div cdkTrapFocus class="example-cdkTrapFocus">
  <button>button 1</button>
  <button>button 2</button>
  <button>button 3</button>
  <button>button 4</button>
</div>

Area 2
<div  class="example-cdkTrapFocus">
  <button>button 1</button>
  <button>button 2</button>
  <button>button 3</button>
  <button>button 4</button>
</div>

FocusMonitor - это сервис, который предоставляет работу с фокусом. Он дает две возможности:

1. С помощью Monitor можно подписаться на Observable и получать состояния DOM элемента. Он отдаёт события focus и blur и возвращает, с помощью чего был установлен фокус (мышь, клавиатура, touch-устройство или установка через код)

В каких кейсах можно использовать FocusMonitor? Где угодно! Например:

  • скрытие / открытие tooltip, пример из material;

  • выполнение логики для перерисовки визуальных частей компонента таблиц при взаимодействии с компонентами сортировки, пример из material;

  • мониторинг списка с выделением, пример из material;

Пример использования в коде:

export class FocusMonitorExample implements AfterContentInit, OnDestroy {
  ....
  constructor(
    private _element: ElementRef<HTMLElement>,
    private _focusMonitor: FocusMonitor
  ) {}

  ngAfterContentInit(): void {
    this._focusMonitor?.monitor(this._element)
    .pipe(takeUntil(this._destroyed))
    .subscribe(origin => {
      if (origin === 'keyboard' || origin === 'program') {
        // выполняем логику компонента
      }
    });
  }

  ngOnDestroy() {
    this._focusMonitor?.stopMonitoring(this._element);
  }
}

2. Он позволяет устанавливать фокус на указанные элементы через метод focusVia. При этом, в него можно передать параметр, с помощью чего был установлен фокус (мышь, клавиатура, через touch, через код)

В каких кейсах можно использовать FocusMonitor для установки фокуса:

  • кнопка в форме - кнопка, которая получает фокус. Такую кнопку можно нажать сразу, без перевода на неё фокуса с помощью клавиатуры. Отличный кейс, когда требуется показывать на сайте onboarding/tutorial с кнопкой «далее»;

  • поле ввода - при создании письма в gmail фокус, фокус сразу попадает в поле ввода почты получателя;

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

  • элементы меню;

Использование в коде:

export class FocusMonitorViaExample {
  ...
  @ViewChild('btn') buttonElement: ElementRef;
  constructor(
    private _focusMonitor: FocusMonitor
  ) {}

  focusElement() {
    this._focusMonitor.focusVia(this.buttonElement, 'program');
  }
}

Если всё так просто – зачем нам FocusMonitor?

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

  • Переиспользуемый код. Так или иначе мы бы сами сделали что-то подобное для своих приложений, чтобы не дублировать код.

  • Сервис работает в разном окружении, если приложение запущено за пределами браузера, эти кейсы предусмотрены (тесты, ssr и т.д.).

  • Обработка ShadowRoot. Если элемент будет находиться внутри ShadowRoot, то обработчики focus/blur нужно привязывать к его корню, иначе мы не получим события.

  • Оптимизация работы приложения. Используется runOutsideAngular для работы с нативными событиями addEventListener, чтобы лишний раз не тревожить Zone.js.

  • Предусматривает мониторинг дочерних элементов.

Поэтому CDK FocusMonitor очень полезен в ваших приложениях. Многие задачи, с которыми вы можете столкнуться, уже решены в нём.

Заключение

Мы рассмотрели понятие доступности. Узнали, какие инструменты существуют в Angular CDK. Подробно рассмотрели FocusMonitor и FocusTrap API для работы с доступностью. Но это далеко не весь функционал, который предоставляет нам CDK. Тема довольно обширная и выходит за рамки одной статьи. Если у вас есть опыт работы с остальным API доступности, напишите в комментариях, интересен ваш опыт и видение в этом направлении.