{ "index": 300, "slug": "editorial-2019-09-practice-accessibility-basics", "title": "Базовая доступность формы: имя, фокус и понятная ошибка", "excerpt": "Форма может работать мышью и всё равно оставлять пользователя без имени поля, видимого фокуса и объяснения ошибки. Разбираем короткий проверяемый маршрут и правки в нативном HTML.", "contentHtml": "

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

\n

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

\n

Цена ошибки растёт после первого релиза. Дефект повторяется на каждой форме с тем же компонентом. Поддержка получает вопросы о «неработающей кнопке». Команда добавляет случайные tabindex и ARIA-атрибуты, не понимая, какое состояние они меняют. Базовую доступность выгоднее проверять до распространения компонента: по одному действию, видимому симптому и повторяемому результату.

\n

Проверяем маршрут, а не набор атрибутов

\n

Для формы нужны три наблюдаемых свойства. Пользователь должен видеть, где находится фокус. Браузер должен вычислять для control имя, связанное с его назначением. Ошибка должна появляться текстом и быть связана с ошибочным полем. DOM помогает найти причину, но сам по себе не доказывает результат. Проверка начинается с живого маршрута клавиатуры, затем продолжается в панели Accessibility.

\n

Нативный HTML задаёт большую часть механизма. label связывает подпись с полем через пару for/id или через вложение control. button уже умеет получать фокус и активироваться клавишей. Браузер использует эти отношения, чтобы построить доступное представление текущей страницы. ARIA уточняет имя, описание и состояние, но не заменяет сломанный порядок DOM и не превращает произвольный div в готовую кнопку.

\n

Наблюдаемый пример: поле с подсказкой и ошибкой

\n

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

\n
<style>\n  input:focus, button:focus {\n    outline: 3px solid #005fcc;\n    outline-offset: 2px;\n  }\n</style>\n\n<form id=\"profile-form\" novalidate>\n  <label for=\"profile-email\">Рабочая почта</label>\n  <p id=\"profile-email-hint\">На этот адрес придёт ссылка.</p>\n  <input id=\"profile-email\" name=\"email\" type=\"email\" required\n    aria-describedby=\"profile-email-hint profile-email-error\"\n    aria-errormessage=\"profile-email-error\" aria-invalid=\"false\" />\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 value = email.value.trim();\n  const message = value === ''\n    ? 'Введите рабочую почту.'\n    : email.validity.valid\n      ? ''\n      : 'Проверьте формат: name@example.com';\n  setEmailError(message);\n  if (message) email.focus();\n});
\n

В разметке есть две разные связи. label даёт полю имя «Рабочая почта». aria-describedby перечисляет идентификаторы подсказки и места ошибки. Когда значение неверно, обработчик ставит aria-invalid=\"true\", показывает текст и возвращает фокус в поле. Пользователь получает причину и может сразу исправить значение.

\n

Порядок имеет значение. Ошибка не должна существовать только в цвете рамки: человек с нарушением цветового восприятия может её не заметить, а вспомогательная технология не получит объяснения. Текст должен назвать проблему и, если это уместно, ожидаемый формат. Если приложение рисует общий summary, он тоже должен вести к ошибочному полю. Не оставляйте пользователя в верхней части страницы без понятного следующего шага. Синий цвет в CSS ниже — пример; его контраст и видимость нужно проверить на реальном фоне.

\n

Симптомы и точечные проверки

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
После Tab не видно активный controlУбран outline или контраст фокуса слабыйПройти маршрут на реальном фоне и наблюдать активный элементВернуть заметный :focus-стиль и проверить его на светлой и тёмной поверхностях
Подпись видна, но имя поля пустоеРядом стоит span, либо for не совпадает с idОткрыть поле в панели Accessibility и сравнить name с видимой подписьюИспользовать связанный label; убрать дублирующие и расходящиеся имена
Кнопка доступна мышью, но не клавиатуройДействие повесили на div без полной клавиатурной моделиДойти до элемента Tab и активировать Enter или Space по правилам проектаЗаменить элемент на button или отдельно реализовать и проверить всю модель
После submit видна только красная рамкаОшибка не выведена текстом или состояние не синхронизированоПроверить текст, aria-invalid, описание и положение фокусаПоказать понятный текст, связать его с полем и выбрать один предсказуемый фокус
Tab прыгает в неожиданном порядкеПоложительные значения tabindex отделили фокус от DOMЗаписать порядок вперёд и назад, сравнить его с визуальным и смысловым порядкомВернуть логичный DOM и убрать положительные номера, если для них нет строгой причины
\n

Каждая строка отделяет факт от догадки. Если в панели нет имени, сначала исправляйте связь label и control. Если имя есть, но сообщение об ошибке не читается в выбранной вспомогательной технологии, одной правки HTML недостаточно: нужно проверить динамическое обновление и выбранный API. Не подменяйте один результат другим.

\n

Почему DOM не равен доступному представлению

\n

DOM отвечает на вопрос, какие узлы создал код и в каком порядке. Клавиатурный маршрут отвечает, к каким узлам пользователь может прийти. Панель Accessibility показывает, какое имя, роль, описание и состояние браузер вычислил для выбранного элемента. Эти слои связаны, но могут расходиться.

\n

Например, такой фрагмент виден на экране, но не создаёт формальную подпись:

\n
<span class=\"field-title\">Поиск</span>\n<input id=\"query\" type=\"search\" />
\n

Исправленный вариант задаёт связь явно:

\n
<label for=\"query-good\">Поиск</label>\n<input id=\"query-good\" type=\"search\" />
\n

Та же граница действует для интерактивных элементов. <div tabindex=\"0\">Сохранить</div> только добавляет узел в последовательность фокуса. Он не даёт автоматически роли кнопки, активации Space, корректного состояния или поведения формы. <button type=\"button\">Сохранить</button> передаёт браузеру больше нужной семантики. Чем меньше самодельных правил, тем короче маршрут проверки.

\n
\"Схема
Клавиатурный маршрут формы: фокус ведёт к названному полю, затем к действию; неудачная отправка показывает текстовую ошибку и возвращает фокус. Схема показывает порядок наблюдений, а не результат конкретного браузера.
\n

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

\n
  1. Выберите одну форму и назовите операцию: например, ввести рабочую почту и сохранить профиль. Зафиксируйте ожидаемый результат без общих слов.
  2. Откройте страницу в браузере из поддерживаемой матрицы. Начните с начала документа и не ставьте курсор мышью внутрь формы.
  3. Нажимайте Tab. Запишите последовательность: ссылка пропуска блока, поле, следующая часть формы, кнопка. На каждом шаге проверьте видимый фокус.
  4. Нажмите Shift+Tab из кнопки. Обратный маршрут должен возвращать к ожидаемому control, а не перескакивать через часть формы.
  5. Оставьте обязательное поле пустым. Активируйте submit клавиатурой. Запишите, куда попал фокус и какой текст объясняет ошибку.
  6. Откройте активное поле в панели Accessibility. Сверьте role, name, description и invalid state с тем, что видит пользователь.
  7. Исправьте одну причину: нативный элемент, связь label, порядок DOM, стиль фокуса или состояние ошибки. Не добавляйте ARIA наугад.
  8. Повторите тот же маршрут вперёд и назад. Если менялся динамический текст, отдельно проверьте его в выбранной связке браузера и вспомогательной технологии.
\n

Ограничения и отрицательный путь

\n

Этот маршрут закрывает узкий риск: базовое управление формой с клавиатуры, понятное имя control и текстовую ошибку. Он не измеряет контраст, масштабирование, жесты, язык страницы, ловушку фокуса в диалоге и все критерии WCAG. Для них нужны отдельные сценарии.

\n

Панель Accessibility зависит от браузера и версии. Её снимок показывает наблюдение конкретного user agent, а не универсальную гарантию для всех платформ. Наличие ARIA-атрибута тоже не доказывает полезный опыт. Вспомогательные технологии могут по-разному обрабатывать динамическую ошибку, поэтому «текст появился в DOM» и «сообщение было объявлено» — разные результаты.

\n

Если поле проходит DOM-проверку, но пользователь со screen reader не получает ожидаемое сообщение, отрицательный путь не закрыт. Повторите сценарий на согласованной связке браузера и вспомогательной технологии. Зафиксируйте, что именно прозвучало и куда переместился фокус. Если такого прогона нет, честный вывод ограничивается проверкой разметки и поведения клавиатуры.

\n

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

\n

Форму можно считать прошедшей этот базовый контроль, если один и тот же человек повторяет сценарий без мыши: видит каждый переход фокуса, проходит элементы в смысловом порядке, активирует отправку, получает текстовую причину ошибки, видит ошибочное поле и возвращается к нему предсказуемо. В панели Accessibility у поля есть ожидаемые role, name, description и invalid state. Браузер и версия записаны рядом с результатом.

\n

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

\n

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

\n" }