Files
progcode/editorial/agent-rewrites/298.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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-errormessage</code> требует того же состояния и не отменяет видимый текст.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика ошибки одного поля после submit</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</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>\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 hint = document.querySelector('#email-hint');\nconst error = document.querySelector('#email-error');\n\nfunction setEmailError(message) {\n const invalid = Boolean(message);\n\n email.setAttribute('aria-invalid', String(invalid));\n email.setAttribute(\n 'aria-describedby',\n invalid ? 'email-hint email-error' : 'email-hint',\n );\n error.hidden = !invalid;\n error.textContent = message || '';\n}\n\nform.addEventListener('submit', (event) =&gt; {\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>&lt;form data-signup&gt;\n &lt;label for=\"email\"&gt;Почта&lt;/label&gt;\n &lt;input id=\"email\" name=\"email\" type=\"email\"\n aria-describedby=\"email-hint\"&gt;\n &lt;p id=\"email-hint\"&gt;Например, name@example.com&lt;/p&gt;\n &lt;p id=\"email-error\" role=\"alert\" hidden&gt;&lt;/p&gt;\n &lt;button type=\"submit\"&gt;Продолжить&lt;/button&gt;\n&lt;/form&gt;</code></pre>\n<p>Валидация здесь намеренно грубая. Проверка символа <code>@</code> не является полной проверкой адреса, а сервер всё равно остаётся источником окончательного решения. В рабочем коде нельзя считать запрос успешным только потому, что клиентская ветка прошла. Серверная ошибка должна попасть в тот же контракт: текст, состояние поля и предсказуемый фокус.</p>\n<p>Для сообщения я использовал <code>role=\"alert\"</code>, потому что оно появляется после действия пользователя. Это не универсальная команда озвучить любой текст. Если сообщение уже находится рядом с control и пользователь получает фокус на поле, достаточно проверить связку <code>aria-describedby</code>. Живой регион добавляют только там, где он действительно нужен, иначе одна ошибка может прозвучать дважды.</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/WCAG22/\" target=\"_blank\" rel=\"noopener\">W3C: Web Content Accessibility Guidelines (WCAG) 2.2</a> — критерии 2.4.7 Focus Visible и 3.3.1 Error Identification.</li><li><a href=\"https://html.spec.whatwg.org/multipage/forms.html#the-label-element\" target=\"_blank\" rel=\"noopener\">WHATWG HTML Standard: label element</a> — правила связи подписи с form control.</li><li><a href=\"https://www.w3.org/TR/wai-aria/\" target=\"_blank\" rel=\"noopener\">W3C: WAI-ARIA 1.2</a> — состояния <code>aria-invalid</code>, <code>aria-describedby</code> и <code>aria-errormessage</code>.</li></ul>"
}