DarkRiDDeR13 мин

Под капотом: почему DOM не доказывает имя, роль и фокус

ДоступностьFrontendРазбор механизма

Симптом коварен именно потому, что в код-ревью всё выглядит живым: input отрисован, обработчик клика срабатывает, классы ошибок меняются. В браузере пользователь проходит Tab и получает control без понятного имени либо теряет ожидаемый порядок. Цена такого дефекта — не только недоступный экран. Команда начинает добавлять role, tabindex и обработчики наугад, а затем уже не понимает, какое из дополнений отвечает за поведение.

Разберём границу без мистики. DOM хранит узлы и атрибуты. HTML задаёт нативные правила для form controls и интерактивных элементов. Браузер применяет эти правила к текущей странице и предоставляет представление, которое удобно проверять в accessibility tree. Поэтому строка разметки и фактическое имя control — связанные, но не одинаковые объекты. В 2019-м для такой диагностики достаточно одной формы, браузера и дисциплины: смотреть реальный фокус до того, как менять API.

Три слоя, которые нельзя смешивать

Первый слой — DOM: порядок узлов, id, текст и атрибуты. Второй — поведение браузера: какой узел участвует в последовательной фокусировке, что происходит при Enter и Space, какую роль даёт нативный HTML. Третий — доступное представление: имя, роль, описание и состояние, которые пользовательские технологии получают через браузер и платформенный API. Ошибка появляется, когда разработчик проверил первый слой и по нему объявил третий готовым.

Например, span со словом «Поиск» рядом с input виден в DOM и на макете. Но HTML связывает caption с конкретным form control через label: for должен указывать на id labelable element, либо control должен быть вложен в label. Внешний span не становится связанной подписью только потому, что стоит близко. У этого случая есть точная проверка: открыть живой input в панели Accessibility и сравнить вычисленное name с текстом, который видит человек.

Так же устроена и роль. Нативная button уже несёт знакомое браузеру поведение. div с click-обработчиком заставляет автора отдельно продумать доступность с клавиатуры, фокус, активацию и состояние. Иногда компоненту действительно нужна собственная модель, но базовый экран не должен получать её случайно только ради внешнего вида. Чем больше самодельных правил, тем больше маршрутов придётся проверять.

Схема преобразования семантики: подпись label и input с id проходят через браузер и дают проверяемые role, name, description и state; рядом показан input с несвязанным span и пустым именем
DOM — вход для браузера, а не финальный отчёт о доступности. Проверяем вычисленное представление выбранного control.

Имя начинается с нативной связи

Ниже намеренно показаны две пары похожей разметки. В плохом варианте текст рядом с input и div-кнопка могут выглядеть правильно. В лучшем варианте label связан с control, а действие выражено button. Это не запрет на компонентный UI. Это точка, с которой удобно начинать: сначала даём браузеру честную семантику, потом добавляем компонентные детали только там, где задача этого требует.

<!-- Видимое слово не становится именем само по себе. -->
<span class="field-title">Поиск</span>
<input id="query" type="search" />

<!-- У label есть формальная связь с control. -->
<label for="query-good">Поиск</label>
<input id="query-good" type="search" />

<!-- Обычное действие лучше выражать нативной кнопкой. -->
<div class="save" tabindex="0">Сохранить</div>
<button type="button">Сохранить</button>

<!-- Положительные tabindex меняют порядок относительно документа. -->
<a href="/account" tabindex="3">Профиль</a>
<input id="phone" type="tel" />

Строка aria-label не является универсальной заменой label. Она может дать имя элементу, но видимая подпись и вычисленное имя способны разойтись. Для поля формы label одновременно объясняет действие на экране и создаёт установленную HTML-связь. Если имя строится из нескольких фрагментов или контроль нетипичен, решение нужно проверить на конкретной реализации, а не переносить механически из другого компонента.

WCAG 2.1 формулирует для компонентов понятную цель: их name, role и значения или состояния должны быть программно определимы. В статье это не превращается в обещание «одного aria-атрибута». Сначала задаём role нативным элементом, name — текстом и label, state — актуальной разметкой. Затем смотрим, что вышло в браузере.

Порядок фокуса — это порядок исполнения

Tab не должен быть вторым пользовательским сценарием после мыши. Для части людей это основной способ пройти форму, меню или модальное окно. При последовательной навигации WCAG требует порядок, который сохраняет смысл и работоспособность. Это означает не «сделать красиво по документу», а проверить фактическую цепочку: после текущего поля куда попадёт фокус и может ли пользователь продолжить действие без угадывания.

Положительный tabindex меняет порядок относительно других элементов. В небольшом фрагменте число 3 кажется прямым решением: оно помещает ссылку раньше. Но порядок начинает жить отдельно от разметки, а следующая правка компонента легко создаёт прыжок. Для обычной страницы лучше собрать DOM в логическом порядке, оставить нативные focusable elements и пользоваться tabindex=0 только там, где элементу действительно нужна включённость в естественную последовательность.

Что проверять на каждом слое
СлойНедостаточное доказательствоПроверяемый фактДействие
DOMу input есть id, а возле него есть текстlabel связан с этим control через for/id или вложениеисправить структуру, не добавлять случайный aria-label
Фокусв разметке есть tabindex или обработчик keydownTab и Shift+Tab дают ожидаемый порядок, а фокус виденубрать положительные tabindex, восстановить порядок документа и :focus-стиль
Доступное представлениеатрибут aria-describedby присутствуетв выбранном браузере у активного control есть ожидаемые role, name, description и invalid stateсверить ID-ссылки и живое состояние после ошибки
Динамикав DOM появился div с текстом ошибкиошибка видима пользователю и имеет связь с ошибочным полемсинхронизировать текст, hidden и aria-invalid; запланировать проверку со вспомогательной технологией

ARIA добавляет связь, но не отменяет HTML

aria-describedby создаёт список ссылок на элементы, составляющие полное описание. Это подходит для короткой подсказки рядом с полем и для актуального текста ошибки, если связи действительно существуют. Но атрибут не создаёт текст из воздуха: referenced element должен содержать понятные слова и быть в ожидаемом состоянии. Если id опечатан или ошибка остаётся скрытой, формальная строка не даст пользователю полезного объяснения.

aria-errormessage имеет более узкую роль. WAI-ARIA 1.1 требует использовать его вместе с aria-invalid; пока поле валидно, сообщение не применимо, а когда ошибка есть — соответствующий текст не должен быть скрыт. Это причина не ставить атрибут в шаблон и забывать о состоянии. Нужен один владелец: функция валидации меняет visible text и aria-invalid в одном месте, а тест повторяет ошибочный submit.

Не стоит назначать ARIA-роль нативному input или button, если она совпадает с их поведением. Лишняя роль создаёт ещё одно место для ошибки и не делает интерфейс «более доступным». Для нестандартного виджета потребуются отдельные keyboard interactions и состояние; в базовом ремонте формы выгоднее уменьшить эту поверхность, а не расширять её.

Маршрут технической проверки

  1. Взять один реальный экран и назвать операцию: поиск, сохранение профиля или регистрация. Выписать controls в том порядке, в каком пользователь должен пройти их с клавиатуры.
  2. Прочитать разметку только для первой гипотезы: проверить label/for/id, нативные button и link, hidden-элементы, положительные tabindex и обработчики submit.
  3. Открыть страницу в браузере, пройти Tab и Shift+Tab без мыши, записать active element, видимый фокус и факт активации Enter или Space там, где он предусмотрен.
  4. На каждом спорном control открыть Accessibility tree этого браузера. Сверить role, name, description и состояния с тем, что должно быть видно пользователю на этом шаге.
  5. Вызвать ошибку формы, проверить текст, связь с полем и предсказуемый фокус. Не записывать «сообщение объявлено», пока тот же путь не выполнен с согласованной вспомогательной технологией.
  6. Сделать одну минимальную правку, повторить маршрут с чистого состояния и приложить результат: браузер, версия, шаги, ожидаемое и фактическое поведение.

Что browser tree подтверждает, а что нет

Дерево доступности полезно как прибор диагностики. Оно позволяет поймать пустое name, неожиданную role, потерянное description или неактуальный invalid state до ручной проверки со screen reader. Но у него есть границы. В нём нельзя заменить пользовательский опыт чтения длинного текста, последовательность озвучивания динамических изменений или матрицу поддержки конкретной связки браузер плюс технология.

Поэтому в отчёте лучше разделить результаты. «В Firefox версии N на поле после неудачного submit видны name, description и invalid=true» — наблюдение браузера. «Пользователь услышал ошибку в выбранном screen reader» — другой эксперимент, который нужно действительно провести. Такое разделение не ослабляет статью; оно не даёт команде принять догадку за готовность.

Итог: проверяем смысл элемента на живом пути

DOM остаётся исходным материалом, но проверка заканчивается не на нём. Делаем нативный HTML носителем роли и имени, держим логический порядок фокуса, связываем описание и ошибку с control, после чего идём по странице клавиатурой и смотрим вычисленное представление в браузере. Это даёт короткий, воспроизводимый диагноз вместо накопления случайных ARIA-правок.

Проверяемые источники

  • WCAG 2.1: 2.4.3 Focus Order — при последовательной навигации порядок фокуса должен сохранять смысл и работоспособность операции
  • WCAG 2.1: 2.4.7 Focus Visible — для управляемого с клавиатуры интерфейса нужен режим с видимым индикатором фокуса
  • WCAG 2.1: 4.1.2 Name, Role, Value — компонентам интерфейса требуются программно определимые имя, роль и доступные состояния или значения
  • HTML Standard: label element — label связывается с form control через for/id или вложением самого control
  • HTML Standard: tabindex — положительный tabindex создаёт отдельный относительный порядок последовательной фокусировки
  • WAI-ARIA 1.1: aria-describedby — список ID связывает элемент с полной текстовой description
  • WAI-ARIA 1.1: aria-errormessage — ссылка на пользовательское сообщение об ошибке используется вместе с aria-invalid; актуальный текст ошибки не должен быть скрыт