{ "index": 299, "slug": "editorial-2019-09-mechanism-accessibility-basics", "title": "Почему DOM не доказывает доступность: имя, роль и фокус", "excerpt": "В разметке есть input и кнопка, но пользователь всё равно может потерять фокус или не понять ошибку. Разбираем, как браузер превращает HTML в доступное представление и что проверить на живой форме.", "contentHtml": "

Симптом обычно появляется на готовом экране. Мышью форма работает: поле принимает текст, кнопка отправляет данные, красная рамка показывает ошибку. После перехода клавишей Tab фокус пропадает на фоне. У поля «Почта» нет понятного имени. После submit остаётся только цвет, а текст причины не связан с input. Цена ошибки — сорванная операция для пользователя и дорогое исправление общего компонента после того, как его скопировали на несколько страниц.

\n

Тезис простой: DOM — это вход для механизма доступности, а не его итог. Браузер читает порядок узлов, нативные элементы, подписи и ARIA-атрибуты. Затем строит представление с ролью, именем, описанием и состояниями. Пользователь и вспомогательная технология взаимодействуют с этим представлением через браузер. Поэтому наличие узла в инспекторе не доказывает, что control получил имя, доступен с клавиатуры или объявляет актуальную ошибку.

\n

Как работает механизм

\n

У механизма есть несколько последовательных слоёв. HTML создаёт элементы и связывает их отношениями. Браузер определяет, какие элементы интерактивны, в каком порядке они получают фокус и какую нативную роль имеют. Текущее состояние страницы меняет доступное представление: поле может стать недействительным, ошибка может появиться или исчезнуть, фокус может перейти к другому control. Затем браузер передаёт эти сведения платформенному accessibility API.

\n

Разрыв возникает, когда проверяют только первый слой. Разработчик видит input с id и текст рядом с ним. Пользователь видит поле без подписи, если рядом стоит обычный span, а не связанный label. Разработчик видит обработчик click на div. Пользователь клавиатуры не получает нативную активацию, предсказуемый фокус и обязательное поведение кнопки. Разработчик видит элемент с aria-describedby. Пользователь не получает описание, если ссылка указывает на несуществующий ID или текст остаётся скрытым.

\n
Схема преобразования HTML в доступное представление: связанный label и input дают проверяемые имя, роль, описание и состояние, а несвязанный текст оставляет имя пустым
DOM задаёт входные данные. Проверять нужно вычисленное представление выбранного control и его поведение в клавиатурном маршруте.
\n

Имя начинается с HTML-связи

\n

Видимая подпись и программное имя должны описывать один и тот же control. Для обычного поля начните с нативного HTML: у label должен быть атрибут for, равный id поля, либо control должен быть вложен в label. Соседний span не создаёт такую связь. Уникальный id тоже не создаёт имя сам по себе.

\n
<form id='profile-form' novalidate>\n  <label for='profile-email'>Рабочая почта</label>\n  <p id='profile-email-hint'>Ссылка для входа придёт на этот адрес.</p>\n  <input\n    id='profile-email'\n    name='email'\n    type='email'\n    autocomplete='email'\n    required\n    aria-describedby='profile-email-hint profile-email-error'\n    aria-errormessage='profile-email-error'\n    aria-invalid='false'\n  />\n  <p id='profile-email-error' hidden></p>\n  <button type='submit'>Сохранить</button>\n</form>\n\nconst form = document.querySelector('#profile-form');\nconst email = document.querySelector('#profile-email');\nconst error = document.querySelector('#profile-email-error');\n\nfunction setEmailError(message) {\n  const invalid = Boolean(message);\n  email.setAttribute('aria-invalid', String(invalid));\n  error.hidden = !invalid;\n  error.textContent = message;\n}\n\nform.addEventListener('submit', (event) => {\n  event.preventDefault();\n  const message = email.validity.valid\n    ? ''\n    : 'Введите адрес в формате name@example.com';\n  setEmailError(message);\n  if (message) email.focus();\n});
\n

Пример учебный. Он не выполняет серверную проверку, не имитирует сеть и не доказывает, что конкретная вспомогательная технология произнесёт сообщение. Его задача — показать единый переход состояния. При ошибке меняются текст, видимость и aria-invalid. Фокус возвращается в поле после неудачной отправки. В реальном продукте сервер остаётся источником окончательного решения, а клиентский текст не должен обещать проверку, которой он не выполняет.

\n

aria-describedby связывает control с подсказкой и текстом ошибки. Все ID должны существовать, а текст должен быть понятен без цвета. aria-errormessage указывает на сообщение об ошибке и используется вместе с aria-invalid. Атрибуты не создают содержимое и не исправляют неверную логику фокуса. Если error-элемент остаётся скрытым после ошибки или содержит только «Неверно», формальная связь не даёт следующего действия.

\n

Симптом → причина → проверка → действие

\n
Минимальная диагностика одной формы
СимптомПричинаПроверкаДействие
После Tab не видно активный элементoutline отключён без равноценного индикатораПройти маршрут на настоящем фоне и записать activeElementВернуть заметный :focus-стиль и проверить контраст индикатора
У поля нет понятного имениРядом стоит span или for указывает не на этот idСверить label/for/id и имя поля в Accessibility treeИсправить нативную связь; не добавлять случайный aria-label
Кнопка работает только мышьюДействие повесили на div или spanАктивировать Tab, Enter и Space в поддерживаемом браузереИспользовать button или отдельно реализовать весь keyboard-контракт
Есть красная рамка, но нет объясненияОшибка выражена только CSS-классомОтправить пустую форму и найти видимый текст рядом с полемВывести причину, связать её с control и обновить invalid state
Порядок прыгает между блокамиПоложительный tabindex отделил фокус от DOM-порядкаСравнить Tab и Shift+Tab с визуальным порядком страницыСобрать логичный DOM и убрать положительные значения tabindex
\n

Таблица помогает выбрать малую проверку, но не заменяет полный аудит. Она не измеряет контраст всего интерфейса, масштабирование текста, жесты, язык страницы или сложное модальное окно. Дерево доступности также не заменяет прогон с screen reader. Сначала фиксируйте наблюдение: браузер, версия, шаг, ожидаемое и фактическое состояние. Не записывайте «ошибка озвучивается», пока вы действительно не прошли этот сценарий с выбранной связкой браузера и вспомогательной технологии.

\n

Фокус связывает смысл и действие

\n

Порядок фокуса — это порядок выполнения операции. Если пользователь после поля попадает на декоративный узел, скрытую кнопку или другой блок, он теряет модель страницы. WCAG требует сохранять смысл и работоспособность последовательной навигации. Поэтому сначала проверяйте порядок документа и нативные элементы. Положительный tabindex создаёт отдельный порядок и быстро ломается после добавления нового control.

\n

Видимый фокус тоже функционален. CSS вроде outline: none допустим только вместе с равноценным индикатором. Цвет рамки должен отличаться от фона и соседних состояний. На длинной форме проверьте, что sticky-панель или прокрутка не закрывают сфокусированный элемент. Это отдельная проверка: наличие фокуса в DOM ещё не означает, что его можно увидеть.

\n

Порядок действий

\n
  1. Выберите один реальный сценарий: поиск, регистрация или сохранение профиля. Назовите начальное действие, поля, кнопку и ожидаемый результат.
  2. Прочитайте разметку только для первой гипотезы. Проверьте нативные label, input, button, существование ID и отсутствие положительных tabindex.
  3. Откройте страницу в поддерживаемом браузере и пройдите Tab без мыши. Запишите порядок, видимость фокуса и возможность активировать действие с клавиатуры.
  4. На каждом спорном control откройте панель Accessibility. Сверьте role, name, description и состояния с текстом и состоянием страницы.
  5. Вызовите отрицательный путь: оставьте обязательное поле пустым или введите учебное неверное значение. Проверьте текст ошибки, связь по ID, aria-invalid и решение о следующем фокусе.
  6. Сделайте одну минимальную правку и повторите маршрут с чистого состояния. В результат добавьте браузер, версию, шаги и фактическое наблюдение.
\n

Что проверка не обещает

\n

Нативный HTML не делает сложный виджет доступным автоматически. Диалог, комбобокс, вкладки и drag-and-drop имеют дополнительные клавиатурные правила и состояния. Их нельзя закрыть копированием атрибутов из примера формы.

\n

ARIA не заменяет нативную семантику. Добавленный role='button' не превращает div в полноценную кнопку: автору всё равно нужно обеспечить фокус, активацию Enter и Space, состояние и доступное имя. Там, где подходит button, он уменьшает количество правил и поверхность ошибки.

\n

Проверка одного браузера не доказывает совместимость со всеми браузерами и технологиями. Панель Accessibility показывает представление конкретной среды. Screen reader может иметь собственные особенности. Проверяйте поддерживаемые комбинации из матрицы проекта и разделяйте факты от предположений.

\n

Критерий готовности

\n

Форма готова к этому уровню проверки, если один человек может пройти выбранный сценарий без мыши и без угадывания. На каждом шаге виден фокус. У каждого control есть ожидаемые role и name. Подсказка и ошибка имеют понятный текст и действующие ID-связи. После неудачного submit пользователь попадает в заранее выбранное место и понимает следующий шаг. Эти факты записаны для конкретного браузера. Отдельно отмечено, какие проверки со screen reader ещё не выполнены.

\n

Такой критерий не означает соответствие всей странице WCAG. Он закрывает один проверяемый контракт формы: HTML задаёт семантику, браузер строит доступное представление, клавиатура проверяет маршрут, а отрицательный путь проверяет состояние ошибки. Если любой из этих фактов не подтверждён, задача ещё не готова.

\n

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

\n" }