8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"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><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});</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><span class=\"field-title\">Поиск</span>\n<input id=\"query\" type=\"search\" /></code></pre>\n<p>Исправленный вариант задаёт связь явно:</p>\n<pre><code><label for=\"query-good\">Поиск</label>\n<input id=\"query-good\" type=\"search\" /></code></pre>\n<p>Та же граница действует для интерактивных элементов. <code><div tabindex=\"0\">Сохранить</div></code> только добавляет узел в последовательность фокуса. Он не даёт автоматически роли кнопки, активации Space, корректного состояния или поведения формы. <code><button type=\"button\">Сохранить</button></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>"
|
||
}
|