DarkRiDDeR14 мин

Разбор: красная ошибка формы не становится контрактом сама

ДоступностьFrontendПолевой разбор

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

Разбирать такой случай полезнее как контракт, а не как выбор цвета. Валидация должна назвать ошибку текстом, control должен получить актуальное состояние, пользователь должен понять, где продолжить путь, а браузер должен показать эти отношения в живой странице. Нельзя заменить эту цепочку одной проверкой DOM: в нём может лежать правильный p с сообщением, пока он hidden, а фокус и вычисленное описание остаются другими.

Картина до правки: четыре вопроса к одной ошибке

Возьмём поле «Почта» в регистрации. Серверная проверка и формат могут быть шире, чем type=email, но для клиентского примера достаточно пустого или заведомо неверного значения. Важнее не валидатор, а последствия его результата. После неудачной отправки нужно ответить: какое поле неверно, есть ли объяснение в тексте, как это отражено в состоянии control и какой следующий клавиатурный шаг ожидается.

Красный border отвечает только на часть вопроса «есть ли проблема». Он не называет поле в тексте, не объясняет формат и может исчезнуть в пользовательской теме. WCAG 2.1 для автоматически определённой input error требует идентифицировать ошибочный элемент и описать ошибку пользователю текстом. Поэтому первый технический артефакт — не палитра, а связанная строка «Укажите почту в формате name@example.com».

Контракт ошибки поля почты
НаблюдениеПричина разрываПроверка в браузереИсправление
Красная рамка есть, текста нетсостояние выражено только стилемпосле submit найти видимое текстовое сообщение рядом с полемдобавить короткое описание ошибки и не прятать его, пока поле не исправлено
Текст существует, но control не отмеченошибка рендерится отдельным компонентом без синхронизации состоянияу активного input нет invalid state в Accessibility treeменять aria-invalid в той же функции, что выводит текст
Фокус остаётся на submitвалидация не определила следующий шаг для клавиатурыпосле неудачного submit записать document.activeElement и видимый индикаторсогласованно перевести фокус к первому ошибочному control или к error summary
Подсказка и ошибка спорят друг с другомaria-describedby ссылается не на те ID либо текст заменён без контекстасверить name, description и текущий текст рядом с полемоставить понятную подсказку и добавить конкретную ошибку без дублирования

Таблица не диктует единственный UX. На длинной форме команда может выбрать error summary в начале, на короткой — первое ошибочное поле. В обоих вариантах нужно зафиксировать маршрут и не менять фокус при каждом символе. Фокус после submit — реакция на завершённую операцию; во время набора внезапный прыжок мешает исправлять значение.

Один владелец состояния ошибки

Ниже валидация и представление ошибки собраны в одной функции. Она принимает текст, выводит его, меняет hidden и синхронизирует aria-invalid. При ошибке submit фокус возвращается в поле. Такой код не решает серверную валидацию и не доказывает озвучивание сообщения, но не оставляет четыре независимые точки, которые могут разойтись при следующем изменении компонента.

<form id="registration" novalidate>
  <label for="registration-email">Почта</label>
  <input id="registration-email" type="email" required
    aria-describedby="registration-email-hint registration-email-error"
    aria-errormessage="registration-email-error" aria-invalid="false" />
  <p id="registration-email-hint">Ссылка для входа придёт на этот адрес.</p>
  <p id="registration-email-error" hidden></p>
  <button type="submit">Создать аккаунт</button>
</form>

const input = document.querySelector("#registration-email");
const message = document.querySelector("#registration-email-error");

function renderEmailError(text) {
  const hasError = Boolean(text);
  input.setAttribute("aria-invalid", String(hasError));
  message.hidden = !hasError;
  message.textContent = text;
}

document.querySelector("#registration").addEventListener("submit", (event) => {
  event.preventDefault();
  const text = input.validity.valid ? "" : "Укажите почту в формате name@example.com";
  renderEmailError(text);
  if (text) input.focus();
});

В этом примере aria-describedby соединяет field с подсказкой и контейнером ошибки. WAI-ARIA описывает его как ID reference list для полного description. aria-errormessage дополнительно указывает на custom error message и используется вместе с aria-invalid. При валидном значении сообщения нет в действии; при ошибке текст должен стать доступен пользователю. Эти отношения стоит проверять вместе, а не искать в JSX отдельные атрибуты.

Тип сообщения тоже имеет значение. «Неверно» не даёт следующего шага. «Укажите почту в формате name@example.com» называет поле и ожидаемое исправление. Если формат определяется сервером или бизнес-правилом, текст не должен обещать больше, чем реально проверяет клиент. В тикете удобно отдельно записать: клиент ловит пустое значение и базовый формат, сервер остаётся источником окончательного решения.

Фокус после submit: не прятать выбор в обработчике

Обработчик submit уже знает, что пользователь попытался завершить форму. В этот момент допустимо выбрать следующий фокусируемый объект и сделать его видимым. Но выбор должен быть частью контракта страницы. Если фокус идёт на первое ошибочное поле, там должен быть виден индикатор и доступен текст причины. Если фокус идёт на summary, summary должна содержать ссылки или понятный порядок к ошибкам.

Не стоит автоматически фокусировать каждое поле в момент изменения value. Такая правка обрывает набор, особенно когда валидатор срабатывает на input. Сначала определяем событие: submit, blur или явная проверка. Затем для каждого события задаём предсказуемое поведение. В нашем сценарии проверяем именно submit: его легко повторить с клавиатуры и сравнить до/после.

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

Ручной сценарий клавиатуры и план дерева доступности

Ниже не отчёт о выполненном браузерном или assistive-technology прогоне. Это безопасный сценарий, который команда запускает на локальном стенде до релиза. Он не требует отправлять персональные данные: используется тестовый адрес, а submit может быть перехвачен. Результат нужно хранить как пару «ожидание — факт» с браузером и версией.

  1. Открыть форму в исходном состоянии, перейти Tab до поля почты и удостовериться, что его фокус различим. В DevTools записать role, name и description этого control, не подменяя запись скриншотом исходного HTML.
  2. Оставить поле пустым или ввести тестовое неверное значение. Перейти Tab до submit и активировать его с клавиатуры, не касаясь мыши.
  3. Проверить, что неверное поле имеет видимый текст ошибки, не только изменение border. Сверить, что текст называет исправление, а не просто сообщает о неуспехе.
  4. Записать активный элемент после submit. Если продукт выбрал фокус на первом неверном поле, он должен быть там; если выбран summary, следующий Tab должен вести к понятному способу исправления.
  5. Открыть дерево доступности для ошибочного input и сверить name, description, invalid state и видимость связанного сообщения в этом браузере. Если вывод отличается от ожидания, приложить факт, а не угадывать причину.
  6. Запланировать отдельный повтор со screen reader из поддерживаемой матрицы. Только после выполненного прогона можно фиксировать, как динамическое изменение воспринимается выбранной технологией.

Почему дерево браузера нужно вместе с клавиатурой

Клавиатура показывает порядок и видимый фокус. Accessibility tree показывает, какую семантику браузер собрал для активного элемента. По отдельности проверки неполны: можно увидеть хорошую рамку, но пустое name; можно увидеть aria-invalid в панели, но не заметить, что Tab после submit оставил пользователя на кнопке. Сценарий связывает эти два факта одной операцией.

Для базовой формы не нужно строить собственную систему ролей. Нативные label, input и button уже дают браузеру значительную часть поведения. Работы ARIA здесь ровно столько, сколько нужно для отношений с подсказкой и error message. Чем меньше кастомной интерактивности вокруг обязательного поля, тем проще сопоставить код, фокус и представление браузера при регрессии.

Проверка перед выпуском и известные границы

После правки полезно добавить короткий регрессионный тест на DOM-состояние: при неудачном submit поле получает aria-invalid=true, текст ошибки перестаёт быть hidden, а обработчик выбирает ожидаемый фокус. Такой тест не заменит живой browser route, но не даст случайной правке удалить ID-связь или поменять текстовую ошибку на одну рамку. Ручной сценарий остаётся выпускной проверкой для фокуса и дерева браузера.

  • Статья не утверждает соответствие всей страницы WCAG 2.1. Проверяется один контракт ошибки в одной форме; контраст, язык, масштабирование, сложные виджеты и остальные экраны требуют своих сценариев.
  • aria-errormessage и aria-invalid не заменяют видимый текст. Если error element скрыт или сообщение не объясняет исправление, формальная ссылка не помогает пользователю.
  • Автор не запускал в этом пакете browser automation, screen reader или реальное серверное API. Утверждения о них заменены воспроизводимым планом проверки.
  • Если в продукте есть asynchronous validation, нужно отдельно описать отмену старого ответа, состояние ожидания и момент, когда ошибка становится актуальной. Этот базовый пример намеренно не имитирует сеть.

Итог: ошибка становится частью маршрута

В форме доступная ошибка начинается не с ARIA и не с красного класса. Сначала определяем пользовательский сбой, затем делаем текст ошибки видимым, связываем его с control, обновляем invalid state и выбираем предсказуемый фокус после submit. После этого идём по странице клавиатурой и смотрим конкретный input в дереве браузера. Такой контракт можно повторить на следующей форме и проверить без догадок.

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

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