8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 298,
|
||
"slug": "editorial-2019-09-field-accessibility-basics",
|
||
"title": "Ошибка поля формы: текст, состояние и фокус должны быть одной системой",
|
||
"excerpt": "Красная рамка не объясняет ошибку и не возвращает пользователя к месту исправления. Разбираем контракт поля после submit: label, текст ошибки, aria-invalid, фокус и проверка в браузере.",
|
||
"contentHtml": "<p>После отправки формы у поля появляется красная рамка, но фокус остаётся на кнопке. Пользователь клавиатуры не знает, какое поле исправлять. При этом текст ошибки может быть в DOM, но скрыт, не связан с input или повторяет только слово «Ошибка». Цена такого дефекта — сорванная регистрация, повторный ввод и новая переделка общего компонента формы.</p>\n<p>Тезис простой: ошибка поля — это не цвет, а согласованный маршрут. Валидатор определяет сбой, интерфейс называет его текстом, поле получает состояние, а фокус указывает следующий шаг. Если одна часть цепочки отстаёт, мышь ещё может скрыть проблему, но клавиатурный путь и вспомогательная технология теряют контекст.</p>\n<h2>Механизм: что должно измениться после submit</h2>\n<p>Возьмём поле «Почта». До отправки у него есть доступное имя из связанного <code>label</code>, подсказка о формате и обычное состояние. После неудачного <code>submit</code> приложение добавляет конкретное сообщение, ставит <code>aria-invalid=\"true\"</code> и выбирает точку фокуса. Эти изменения должны произойти из одного результата валидации.</p>\n<p>HTML связывает <code>label</code> и control через уникальную пару <code>for</code>/<code>id</code> или через вложение input в label. Текст рядом с полем без этой связи остаётся визуальной подписью. Placeholder тоже не заменяет label: он исчезает при вводе и не должен нести имя control.</p>\n<p><code>aria-describedby</code> связывает input с подсказкой и сообщением об ошибке по их идентификаторам. Список ссылок должен указывать на существующие элементы с актуальным текстом. <code>aria-invalid</code> помечает значение как невалидное. Если вместо <code>aria-describedby</code> выбран <code>aria-errormessage</code>, его применяют вместе с <code>aria-invalid=\"true\"</code> и оставляют сообщение доступным пользователю. Ни один из этих атрибутов не заменяет видимый текст.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика ошибки одного поля после submit</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Есть только красная рамка</td><td>Состояние выражено цветом</td><td>Отключить CSS и найти текстовое сообщение</td><td>Вывести объяснение и способ исправления</td></tr><tr><td>Текст есть, input его не получает</td><td>Нет связи по ID или ссылка устарела</td><td>Сверить <code>aria-describedby</code> с ID видимого сообщения</td><td>Синхронизировать атрибут и рендер сообщения</td></tr><tr><td>Фокус остаётся на submit</td><td>Обработчик не выбрал следующий шаг</td><td>Записать <code>document.activeElement</code> сразу после submit</td><td>Перевести фокус на первое ошибочное поле или summary</td></tr><tr><td>Поле объявлено ошибочным слишком рано</td><td>Валидатор срабатывает на каждый символ</td><td>Открыть пустую форму и проверить начальное состояние</td><td>Ставить <code>aria-invalid</code> после заданного события, например submit</td></tr><tr><td>Ошибка исчезла, но старый ID остался</td><td>Текст и состояние обновляются разными ветками</td><td>Исправить значение и проверить DOM и Accessibility tree</td><td>Удалить сообщение, ссылку и invalid state одной операцией</td></tr></tbody></table></div>\n<p>Таблица описывает диагностику, а не готовый UX для любой формы. Для длинной анкеты можно сначала сфокусировать summary со ссылками на ошибки. Для короткой формы быстрее отправить фокус в первое неверное поле. Решение нужно выбрать один раз и проверить на всей странице. Фокус не следует перемещать при каждом нажатии клавиши: это прерывает ввод.</p>\n<h2>Рабочий пример: один владелец состояния</h2>\n<p>Ниже учебный пример для локальной формы. Он не проверяет серверный API и не доказывает озвучивание сообщения конкретным screen reader. Его задача — показать, как связать валидатор, текст, состояние и фокус. Значения формата и выбор первого поля — проектные решения.</p>\n<pre><code>const form = document.querySelector('[data-signup]');\nconst email = document.querySelector('#email');\nconst error = document.querySelector('#email-error');\n\nfunction setEmailError(message) {\n const invalid = Boolean(message);\n\n error.textContent = message || '';\n error.hidden = !invalid;\n email.setAttribute('aria-invalid', String(invalid));\n email.setAttribute(\n 'aria-describedby',\n invalid ? 'email-hint email-error' : 'email-hint',\n );\n}\n\nform.addEventListener('submit', (event) => {\n event.preventDefault();\n const value = email.value.trim();\n\n if (!value || !value.includes('@')) {\n setEmailError('Укажите почту в формате name@example.com.');\n email.focus();\n return;\n }\n\n setEmailError('');\n // Учебный пример: здесь могла бы начаться отправка формы.\n});</code></pre>\n<p>Разметка для этого обработчика должна содержать уникальный ID и связанную подпись:</p>\n<pre><code><form data-signup novalidate>\n <label for=\"email\">Почта</label>\n <input id=\"email\" name=\"email\" type=\"email\"\n aria-describedby=\"email-hint\">\n <p id=\"email-hint\">Например, name@example.com</p>\n <p id=\"email-error\" hidden></p>\n <button type=\"submit\">Продолжить</button>\n</form></code></pre>\n<p>Атрибут <code>novalidate</code> здесь отключает встроенный браузерный экран ошибки, чтобы весь учебный сценарий проходил через один обработчик. Это не рекомендация отказаться от нативной constraint validation: в рабочей форме нужно отдельно решить, какой слой владеет проверкой и как серверные ошибки попадают в тот же контракт.</p>\n<p>Валидация здесь намеренно грубая. Проверка символа <code>@</code> не является полной проверкой адреса, а сервер всё равно остаётся источником окончательного решения. В рабочем коде нельзя считать запрос успешным только потому, что клиентская ветка прошла. Серверная ошибка должна попасть в тот же контракт: текст, состояние поля и предсказуемый фокус.</p>\n<p>В примере нет <code>role=\"alert\"</code>: после ошибки фокус переходит на input, а <code>aria-describedby</code> уже включает видимый текст. Так у пользователя есть один основной маршрут объявления. Если продукт оставляет фокус на summary или поле не получает фокус, может понадобиться live region, но это решение нужно проверить в поддерживаемых браузерах и screen reader, чтобы не объявлять одну ошибку дважды.</p>\n<h2>Иллюстрация маршрута</h2>\n<figure><img src=\"/assets/editorial/2019/accessibility-error-contract-2019.svg\" alt=\"Схема контракта ошибки формы: submit запускает проверку, затем обновляются текст ошибки и состояние поля, после чего фокус переходит к месту исправления\"><figcaption>Ошибка становится полезной, когда сообщение, состояние input и решение о фокусе меняются согласованно.</figcaption></figure>\n<p>Схема показывает направление данных, а не результат реального прогона. В браузере нужно отдельно подтвердить, что сообщение видно, поле сохраняет фокус и его вычисленные имя, роль и описание соответствуют ожиданию.</p>\n<h2>Проверка клавиатурой и в дереве доступности</h2>\n<p>DOM-проверка находит атрибуты, но не отвечает на весь вопрос. Клавиатура показывает маршрут и видимый фокус. Accessibility tree показывает представление control, которое собрал браузер. Проверяйте оба слоя на собранной странице, а не только JSX или шаблон.</p>\n<ol><li>Откройте форму в чистом состоянии. Перейдите к полю клавишей Tab и проверьте видимый индикатор фокуса. Убедитесь, что подпись кликабельна и связана с input.</li><li>Оставьте почту пустой или введите учебное неверное значение. Нажмите Tab до кнопки и активируйте submit клавиатурой.</li><li>Проверьте текст ошибки. Он должен назвать проблему и следующий шаг: «Укажите почту в формате name@example.com» лучше, чем «Неверно».</li><li>Запишите активный элемент после submit. Для выбранного варианта это первое ошибочное поле. Если продукт использует summary, проверьте ссылку из summary к этому полю.</li><li>В DevTools откройте Accessibility tree для input. Сверьте role, accessible name, description и invalid state. Не подменяйте эти значения исходным HTML.</li><li>Исправьте значение и повторите submit. Проверьте, что текст ошибки скрыт, <code>aria-invalid</code> сброшен, ссылка на error удалена или обновлена, а фокус не прыгает без причины.</li><li>Повторите сценарий с серверной ошибкой и с медленным ответом. Зафиксируйте, кто владеет состоянием ожидания и в какой момент ошибка становится актуальной.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Этот пример покрывает одно текстовое поле и одну ошибку после submit. Он не решает групповые ошибки, радиокнопки, сложные маски, вложенные формы, ошибки сети и серверные сообщения, которые приходят после смены страницы. Для каждого такого случая нужен отдельный маршрут.</p>\n<p>Если сервер отвечает после того, как пользователь уже исправил значение, старый ответ нельзя безусловно применять. Сравните запрос с актуальным значением или отмените устаревший запрос. Иначе исправленное поле снова станет ошибочным. Во время ожидания покажите состояние загрузки и не меняйте фокус без действия, которое пользователь может объяснить.</p>\n<p>Не добавляйте положительный <code>tabindex</code>, чтобы скрыть неверный порядок DOM. Сначала расставьте элементы в логическом порядке. Не заменяйте нативный input кастомным div с role без причины: вместе с ролью придётся восстановить фокус, клавиатурные команды, значение и состояния.</p>\n<p>Нельзя объявлять всю страницу соответствующей WCAG после проверки одной формы. Здесь проверяются идентификация ошибки, видимый фокус, имя поля и маршрут исправления. Контраст, масштабирование, язык, автозаполнение и остальные экраны требуют своих проверок.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Контракт готов, если тестировщик без мыши повторяет один и тот же сценарий и получает четыре наблюдаемые результата: ошибочное поле названо текстом, сообщение связано с input, invalid state отражён в браузере, а фокус ведёт к выбранной точке исправления. После корректного значения все четыре состояния возвращаются в валидный вид. Эти условия можно закрепить DOM-тестом и ручным проходом в поддерживаемом браузере.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.w3.org/TR/WCAG21/#focus-visible\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Web Content Accessibility Guidelines (WCAG) 2.1, Focus Visible</a> — критерий 2.4.7 о видимом фокусе.</li><li><a href=\"https://www.w3.org/TR/WCAG21/#error-identification\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Web Content Accessibility Guidelines (WCAG) 2.1, Error Identification</a> — критерий 3.3.1 об идентификации ошибки.</li><li><a href=\"https://html.spec.whatwg.org/multipage/forms.html#the-label-element\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG HTML Standard: label element</a> — правила связи подписи с form control.</li><li><a href=\"https://www.w3.org/TR/wai-aria-1.1/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WAI-ARIA 1.1</a> — определения <code>aria-invalid</code>, <code>aria-describedby</code> и <code>aria-errormessage</code>; атрибуты нужно проверять в конкретном браузере и вспомогательной технологии.</li></ul>"
|
||
}
|