Files
progcode/editorial/agent-rewrites/300.json
T

8 lines
21 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": 300,
"slug": "editorial-2019-09-practice-accessibility-basics",
"title": "Базовая доступность формы: имя, фокус и понятная ошибка",
"excerpt": "Форма может работать мышью и всё равно оставлять пользователя без имени поля, видимого фокуса и объяснения ошибки. Разбираем короткий проверяемый маршрут и правки в нативном HTML.",
"contentHtml": "<p>Форма выглядит готовой, пока её не проходят клавишей Tab. Фокус пропадает на светлом фоне. Поле с подписью «Почта» не получает понятного имени. После отправки появляется красная рамка, но причина ошибки остаётся неизвестной. Мышью такой экран ещё можно пройти. С клавиатурой пользователь теряет маршрут и не понимает, что исправить.</p>\n<p>Представим страницу профиля на учебном стенде. Разработчик сначала проверяет успешную отправку мышью и видит ожидаемый результат. Затем он проходит форму с клавиатурой: Tab перескакивает через поле, а при пустой почте фокус остаётся на кнопке. Это не один визуальный недочёт. Пользователь не видит текущий control, не получает его имя и не знает, какое значение исправить.</p>\n<p>Цена ошибки растёт после первого релиза. Дефект повторяется на каждой форме с тем же компонентом. Поддержка получает вопросы о «неработающей кнопке». Команда добавляет случайные <code>tabindex</code> и ARIA-атрибуты, не понимая, какое состояние они меняют. Базовую доступность выгоднее проверять до распространения компонента: по одному действию, видимому симптому и повторяемому результату.</p>\n<h2>Проверяем маршрут, а не набор атрибутов</h2>\n<p>Для формы нужны три наблюдаемых свойства. Пользователь должен видеть, где находится фокус. Браузер должен вычислять для control имя, связанное с его назначением. Ошибка должна появляться текстом и быть связана с ошибочным полем. DOM помогает найти причину, но сам по себе не доказывает результат. Проверка начинается с живого маршрута клавиатуры, затем продолжается в панели Accessibility.</p>\n<p>Нативный HTML задаёт большую часть механизма. <code>label</code> связывает подпись с полем через пару <code>for</code>/<code>id</code> или через вложение control. <code>button</code> уже умеет получать фокус и активироваться клавишей. Браузер использует эти отношения, чтобы построить доступное представление текущей страницы. ARIA уточняет имя, описание и состояние, но не заменяет сломанный порядок DOM и не превращает произвольный <code>div</code> в готовую кнопку.</p>\n<h2>Наблюдаемый пример: поле с подсказкой и ошибкой</h2>\n<p>Ниже самостоятельный учебный пример для разбора в связке инструментов 2019 года. Он не отправляет данные на сервер и не заявляет результат для конкретного продукта или вспомогательной технологии. В примере разделены разметка, заметный стиль фокуса и обработчик ошибки: так видно, какое состояние меняется после неудачной отправки.</p>\n<pre><code>&lt;style&gt;\n input:focus, button:focus {\n outline: 3px solid #005fcc;\n outline-offset: 2px;\n }\n&lt;/style&gt;\n\n&lt;form id=\"profile-form\" novalidate&gt;\n &lt;label for=\"profile-email\"&gt;Рабочая почта&lt;/label&gt;\n &lt;p id=\"profile-email-hint\"&gt;На этот адрес придёт ссылка.&lt;/p&gt;\n &lt;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\" /&gt;\n &lt;p id=\"profile-email-error\" hidden&gt;&lt;/p&gt;\n &lt;button type=\"submit\"&gt;Сохранить&lt;/button&gt;\n&lt;/form&gt;\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) =&gt; {\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});</code></pre>\n<p>В разметке есть две разные связи. <code>label</code> даёт полю имя «Рабочая почта». <code>aria-describedby</code> перечисляет идентификаторы подсказки и места ошибки. Когда значение неверно, обработчик ставит <code>aria-invalid=\"true\"</code>, показывает текст и возвращает фокус в поле. Пользователь получает причину и может сразу исправить значение.</p>\n<p>Порядок имеет значение. Ошибка не должна существовать только в цвете рамки: человек с нарушением цветового восприятия может её не заметить, а вспомогательная технология не получит объяснения. Текст должен назвать проблему и, если это уместно, ожидаемый формат. Если приложение рисует общий summary, он тоже должен вести к ошибочному полю. Не оставляйте пользователя в верхней части страницы без понятного следующего шага. Синий цвет в CSS ниже — пример; его контраст и видимость нужно проверить на реальном фоне.</p>\n<h2>Симптомы и точечные проверки</h2>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После Tab не видно активный control</td><td>Убран <code>outline</code> или контраст фокуса слабый</td><td>Пройти маршрут на реальном фоне и наблюдать активный элемент</td><td>Вернуть заметный <code>:focus</code>-стиль и проверить его на светлой и тёмной поверхностях</td></tr><tr><td>Подпись видна, но имя поля пустое</td><td>Рядом стоит <code>span</code>, либо <code>for</code> не совпадает с <code>id</code></td><td>Открыть поле в панели Accessibility и сравнить name с видимой подписью</td><td>Использовать связанный <code>label</code>; убрать дублирующие и расходящиеся имена</td></tr><tr><td>Кнопка доступна мышью, но не клавиатурой</td><td>Действие повесили на <code>div</code> без полной клавиатурной модели</td><td>Дойти до элемента Tab и активировать Enter или Space по правилам проекта</td><td>Заменить элемент на <code>button</code> или отдельно реализовать и проверить всю модель</td></tr><tr><td>После submit видна только красная рамка</td><td>Ошибка не выведена текстом или состояние не синхронизировано</td><td>Проверить текст, <code>aria-invalid</code>, описание и положение фокуса</td><td>Показать понятный текст, связать его с полем и выбрать один предсказуемый фокус</td></tr><tr><td>Tab прыгает в неожиданном порядке</td><td>Положительные значения <code>tabindex</code> отделили фокус от DOM</td><td>Записать порядок вперёд и назад, сравнить его с визуальным и смысловым порядком</td><td>Вернуть логичный DOM и убрать положительные номера, если для них нет строгой причины</td></tr></tbody></table></div>\n<p>Каждая строка отделяет факт от догадки. Если в панели нет имени, сначала исправляйте связь label и control. Если имя есть, но сообщение об ошибке не читается в выбранной вспомогательной технологии, одной правки HTML недостаточно: нужно проверить динамическое обновление и выбранный API. Не подменяйте один результат другим.</p>\n<h2>Почему DOM не равен доступному представлению</h2>\n<p>DOM отвечает на вопрос, какие узлы создал код и в каком порядке. Клавиатурный маршрут отвечает, к каким узлам пользователь может прийти. Панель Accessibility показывает, какое имя, роль, описание и состояние браузер вычислил для выбранного элемента. Эти слои связаны, но могут расходиться.</p>\n<p>Например, такой фрагмент виден на экране, но не создаёт формальную подпись:</p>\n<pre><code>&lt;span class=\"field-title\"&gt;Поиск&lt;/span&gt;\n&lt;input id=\"query\" type=\"search\" /&gt;</code></pre>\n<p>Исправленный вариант задаёт связь явно:</p>\n<pre><code>&lt;label for=\"query-good\"&gt;Поиск&lt;/label&gt;\n&lt;input id=\"query-good\" type=\"search\" /&gt;</code></pre>\n<p>Та же граница действует для интерактивных элементов. <code>&lt;div tabindex=\"0\"&gt;Сохранить&lt;/div&gt;</code> только добавляет узел в последовательность фокуса. Он не даёт автоматически роли кнопки, активации Space, корректного состояния или поведения формы. <code>&lt;button type=\"button\"&gt;Сохранить&lt;/button&gt;</code> передаёт браузеру больше нужной семантики. Чем меньше самодельных правил, тем короче маршрут проверки.</p>\n<figure><img src=\"/assets/editorial/2019/accessibility-keyboard-route-2019.svg\" alt=\"Схема проверки формы с клавиатуры: видимый фокус, поле с именем, кнопка и текстовая ошибка\" loading=\"lazy\" /><figcaption>Клавиатурный маршрут формы: фокус ведёт к названному полю, затем к действию; неудачная отправка показывает текстовую ошибку и возвращает фокус. Схема показывает порядок наблюдений, а не результат конкретного браузера.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li>Выберите одну форму и назовите операцию: например, ввести рабочую почту и сохранить профиль. Зафиксируйте ожидаемый результат без общих слов.</li><li>Откройте страницу в браузере из поддерживаемой матрицы. Начните с начала документа и не ставьте курсор мышью внутрь формы.</li><li>Нажимайте Tab. Запишите последовательность: ссылка пропуска блока, поле, следующая часть формы, кнопка. На каждом шаге проверьте видимый фокус.</li><li>Нажмите Shift+Tab из кнопки. Обратный маршрут должен возвращать к ожидаемому control, а не перескакивать через часть формы.</li><li>Оставьте обязательное поле пустым. Активируйте submit клавиатурой. Запишите, куда попал фокус и какой текст объясняет ошибку.</li><li>Откройте активное поле в панели Accessibility. Сверьте role, name, description и invalid state с тем, что видит пользователь.</li><li>Исправьте одну причину: нативный элемент, связь <code>label</code>, порядок DOM, стиль фокуса или состояние ошибки. Не добавляйте ARIA наугад.</li><li>Повторите тот же маршрут вперёд и назад. Если менялся динамический текст, отдельно проверьте его в выбранной связке браузера и вспомогательной технологии.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Этот маршрут закрывает узкий риск: базовое управление формой с клавиатуры, понятное имя control и текстовую ошибку. Он не измеряет контраст, масштабирование, жесты, язык страницы, ловушку фокуса в диалоге и все критерии WCAG. Для них нужны отдельные сценарии.</p>\n<p>Панель Accessibility зависит от браузера и версии. Её снимок показывает наблюдение конкретного user agent, а не универсальную гарантию для всех платформ. Наличие ARIA-атрибута тоже не доказывает полезный опыт. Вспомогательные технологии могут по-разному обрабатывать динамическую ошибку, поэтому «текст появился в DOM» и «сообщение было объявлено» — разные результаты.</p>\n<p>Если поле проходит DOM-проверку, но пользователь со screen reader не получает ожидаемое сообщение, отрицательный путь не закрыт. Повторите сценарий на согласованной связке браузера и вспомогательной технологии. Зафиксируйте, что именно прозвучало и куда переместился фокус. Если такого прогона нет, честный вывод ограничивается проверкой разметки и поведения клавиатуры.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Форму можно считать прошедшей этот базовый контроль, если один и тот же человек повторяет сценарий без мыши: видит каждый переход фокуса, проходит элементы в смысловом порядке, активирует отправку, получает текстовую причину ошибки, видит ошибочное поле и возвращается к нему предсказуемо. В панели Accessibility у поля есть ожидаемые role, name, description и invalid state. Браузер и версия записаны рядом с результатом.</p>\n<p>Это не сертификат доступности и не обещание одинакового поведения на всех устройствах. Это узкий критерий, который можно проверить снова после изменения компонента. Если хотя бы один пункт не выполнен, форма остаётся в отрицательном пути: сначала исправьте наблюдаемый симптом, затем повторите весь маршрут.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/WCAG21/#focus-order\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WCAG 2.1 — Focus Order</a> — при последовательной навигации фокусируемые компоненты сохраняют смысл и работоспособность.</li><li><a href=\"https://www.w3.org/TR/WCAG21/#focus-visible\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WCAG 2.1 — Focus Visible</a> — текущая позиция фокуса должна быть визуально различима.</li><li><a href=\"https://www.w3.org/TR/WCAG21/#error-identification\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WCAG 2.1 — Error Identification</a> — автоматически обнаруженная ошибка идентифицируется и описывается текстом.</li><li><a href=\"https://www.w3.org/TR/WCAG21/#name-role-value\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WCAG 2.1 — Name, Role, Value</a> — имя, роль и состояние компонентов должны быть программно определимы.</li><li><a href=\"https://html.spec.whatwg.org/multipage/forms.html#the-label-element\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG HTML Standard: The label element</a> — правила связи подписи с form control через <code>for</code>/<code>id</code> или вложение.</li><li><a href=\"https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG HTML Standard: The tabindex attribute</a> — значение <code>tabindex</code> участвует в правилах фокусировки и не заменяет логичный порядок документа.</li><li><a href=\"https://www.w3.org/TR/wai-aria-1.1/#aria-describedby\" target=\"_blank\" rel=\"noopener noreferrer\">WAI-ARIA 1.1: aria-describedby</a> — атрибут задаёт ссылки на элементы, составляющие описание объекта.</li><li><a href=\"https://www.w3.org/TR/wai-aria-1.1/#aria-errormessage\" target=\"_blank\" rel=\"noopener noreferrer\">WAI-ARIA 1.1: aria-errormessage</a> — сообщение об ошибке связывается с объектом вместе с <code>aria-invalid</code> и не должно оставаться скрытым, когда ошибка актуальна.</li></ul>"
}