Files
progcode/editorial/agent-rewrites/299.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
18 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": 299,
"slug": "editorial-2019-09-mechanism-accessibility-basics",
"title": "Почему DOM не доказывает доступность: имя, роль и фокус",
"excerpt": "В разметке есть input и кнопка, но пользователь всё равно может потерять фокус или не понять ошибку. Разбираем, как браузер превращает HTML в доступное представление и что проверить на живой форме.",
"contentHtml": "<p>Симптом обычно появляется на готовом экране. Мышью форма работает: поле принимает текст, кнопка отправляет данные, красная рамка показывает ошибку. После перехода клавишей Tab фокус пропадает на фоне. У поля «Почта» нет понятного имени. После submit остаётся только цвет, а текст причины не связан с input. Цена ошибки — сорванная операция для пользователя и дорогое исправление общего компонента после того, как его скопировали на несколько страниц.</p>\n<p>Тезис простой: DOM — это вход для механизма доступности, а не его итог. Браузер читает порядок узлов, нативные элементы, подписи и ARIA-атрибуты. Затем строит представление с ролью, именем, описанием и состояниями. Пользователь и вспомогательная технология взаимодействуют с этим представлением через браузер. Поэтому наличие узла в инспекторе не доказывает, что control получил имя, доступен с клавиатуры или объявляет актуальную ошибку.</p>\n<h2>Как работает механизм</h2>\n<p>У механизма есть несколько последовательных слоёв. HTML создаёт элементы и связывает их отношениями. Браузер определяет, какие элементы интерактивны, в каком порядке они получают фокус и какую нативную роль имеют. Текущее состояние страницы меняет доступное представление: поле может стать недействительным, ошибка может появиться или исчезнуть, фокус может перейти к другому control. Затем браузер передаёт эти сведения платформенному accessibility API.</p>\n<p>Разрыв возникает, когда проверяют только первый слой. Разработчик видит <code>input</code> с <code>id</code> и текст рядом с ним. Пользователь видит поле без подписи, если рядом стоит обычный <code>span</code>, а не связанный <code>label</code>. Разработчик видит обработчик <code>click</code> на <code>div</code>. Пользователь клавиатуры не получает нативную активацию, предсказуемый фокус и обязательное поведение кнопки. Разработчик видит элемент с <code>aria-describedby</code>. Пользователь не получает описание, если ссылка указывает на несуществующий ID или текст остаётся скрытым.</p>\n<figure><img src='/assets/editorial/2019/accessibility-computed-semantics-2019.svg' alt='Схема преобразования HTML в доступное представление: связанный label и input дают проверяемые имя, роль, описание и состояние, а несвязанный текст оставляет имя пустым' loading='lazy' /><figcaption>DOM задаёт входные данные. Проверять нужно вычисленное представление выбранного control и его поведение в клавиатурном маршруте.</figcaption></figure>\n<h2>Имя начинается с HTML-связи</h2>\n<p>Видимая подпись и программное имя должны описывать один и тот же control. Для обычного поля начните с нативного HTML: у <code>label</code> должен быть атрибут <code>for</code>, равный <code>id</code> поля, либо control должен быть вложен в <code>label</code>. Соседний <code>span</code> не создаёт такую связь. Уникальный <code>id</code> тоже не создаёт имя сам по себе.</p>\n<pre><code>&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\n id='profile-email'\n name='email'\n type='email'\n autocomplete='email'\n required\n aria-describedby='profile-email-hint profile-email-error'\n aria-errormessage='profile-email-error'\n aria-invalid='false'\n /&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 message = email.validity.valid\n ? ''\n : 'Введите адрес в формате name@example.com';\n setEmailError(message);\n if (message) email.focus();\n});</code></pre>\n<p>Пример учебный. Он не выполняет серверную проверку, не имитирует сеть и не доказывает, что конкретная вспомогательная технология произнесёт сообщение. Его задача — показать единый переход состояния. При ошибке меняются текст, видимость и <code>aria-invalid</code>. Фокус возвращается в поле после неудачной отправки. В реальном продукте сервер остаётся источником окончательного решения, а клиентский текст не должен обещать проверку, которой он не выполняет.</p>\n<p><code>aria-describedby</code> связывает control с подсказкой и текстом ошибки. Все ID должны существовать, а текст должен быть понятен без цвета. <code>aria-errormessage</code> указывает на сообщение об ошибке и используется вместе с <code>aria-invalid</code>. Атрибуты не создают содержимое и не исправляют неверную логику фокуса. Если error-элемент остаётся скрытым после ошибки или содержит только «Неверно», формальная связь не даёт следующего действия.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<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 не видно активный элемент</td><td>outline отключён без равноценного индикатора</td><td>Пройти маршрут на настоящем фоне и записать activeElement</td><td>Вернуть заметный :focus-стиль и проверить контраст индикатора</td></tr><tr><td>У поля нет понятного имени</td><td>Рядом стоит span или for указывает не на этот id</td><td>Сверить label/for/id и имя поля в Accessibility tree</td><td>Исправить нативную связь; не добавлять случайный aria-label</td></tr><tr><td>Кнопка работает только мышью</td><td>Действие повесили на div или span</td><td>Активировать Tab, Enter и Space в поддерживаемом браузере</td><td>Использовать button или отдельно реализовать весь keyboard-контракт</td></tr><tr><td>Есть красная рамка, но нет объяснения</td><td>Ошибка выражена только CSS-классом</td><td>Отправить пустую форму и найти видимый текст рядом с полем</td><td>Вывести причину, связать её с control и обновить invalid state</td></tr><tr><td>Порядок прыгает между блоками</td><td>Положительный tabindex отделил фокус от DOM-порядка</td><td>Сравнить Tab и Shift+Tab с визуальным порядком страницы</td><td>Собрать логичный DOM и убрать положительные значения tabindex</td></tr></tbody></table>\n<p>Таблица помогает выбрать малую проверку, но не заменяет полный аудит. Она не измеряет контраст всего интерфейса, масштабирование текста, жесты, язык страницы или сложное модальное окно. Дерево доступности также не заменяет прогон с screen reader. Сначала фиксируйте наблюдение: браузер, версия, шаг, ожидаемое и фактическое состояние. Не записывайте «ошибка озвучивается», пока вы действительно не прошли этот сценарий с выбранной связкой браузера и вспомогательной технологии.</p>\n<h2>Фокус связывает смысл и действие</h2>\n<p>Порядок фокуса — это порядок выполнения операции. Если пользователь после поля попадает на декоративный узел, скрытую кнопку или другой блок, он теряет модель страницы. WCAG требует сохранять смысл и работоспособность последовательной навигации. Поэтому сначала проверяйте порядок документа и нативные элементы. Положительный <code>tabindex</code> создаёт отдельный порядок и быстро ломается после добавления нового control.</p>\n<p>Видимый фокус тоже функционален. CSS вроде <code>outline: none</code> допустим только вместе с равноценным индикатором. Цвет рамки должен отличаться от фона и соседних состояний. На длинной форме проверьте, что sticky-панель или прокрутка не закрывают сфокусированный элемент. Это отдельная проверка: наличие фокуса в DOM ещё не означает, что его можно увидеть.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выберите один реальный сценарий: поиск, регистрация или сохранение профиля. Назовите начальное действие, поля, кнопку и ожидаемый результат.</li><li>Прочитайте разметку только для первой гипотезы. Проверьте нативные <code>label</code>, <code>input</code>, <code>button</code>, существование ID и отсутствие положительных <code>tabindex</code>.</li><li>Откройте страницу в поддерживаемом браузере и пройдите Tab без мыши. Запишите порядок, видимость фокуса и возможность активировать действие с клавиатуры.</li><li>На каждом спорном control откройте панель Accessibility. Сверьте role, name, description и состояния с текстом и состоянием страницы.</li><li>Вызовите отрицательный путь: оставьте обязательное поле пустым или введите учебное неверное значение. Проверьте текст ошибки, связь по ID, <code>aria-invalid</code> и решение о следующем фокусе.</li><li>Сделайте одну минимальную правку и повторите маршрут с чистого состояния. В результат добавьте браузер, версию, шаги и фактическое наблюдение.</li></ol>\n<h2>Что проверка не обещает</h2>\n<p>Нативный HTML не делает сложный виджет доступным автоматически. Диалог, комбобокс, вкладки и drag-and-drop имеют дополнительные клавиатурные правила и состояния. Их нельзя закрыть копированием атрибутов из примера формы.</p>\n<p>ARIA не заменяет нативную семантику. Добавленный <code>role='button'</code> не превращает div в полноценную кнопку: автору всё равно нужно обеспечить фокус, активацию Enter и Space, состояние и доступное имя. Там, где подходит <code>button</code>, он уменьшает количество правил и поверхность ошибки.</p>\n<p>Проверка одного браузера не доказывает совместимость со всеми браузерами и технологиями. Панель Accessibility показывает представление конкретной среды. Screen reader может иметь собственные особенности. Проверяйте поддерживаемые комбинации из матрицы проекта и разделяйте факты от предположений.</p>\n<h2>Критерий готовности</h2>\n<p>Форма готова к этому уровню проверки, если один человек может пройти выбранный сценарий без мыши и без угадывания. На каждом шаге виден фокус. У каждого control есть ожидаемые role и name. Подсказка и ошибка имеют понятный текст и действующие ID-связи. После неудачного submit пользователь попадает в заранее выбранное место и понимает следующий шаг. Эти факты записаны для конкретного браузера. Отдельно отмечено, какие проверки со screen reader ещё не выполнены.</p>\n<p>Такой критерий не означает соответствие всей странице WCAG. Он закрывает один проверяемый контракт формы: HTML задаёт семантику, браузер строит доступное представление, клавиатура проверяет маршрут, а отрицательный путь проверяет состояние ошибки. Если любой из этих фактов не подтверждён, задача ещё не готова.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://www.w3.org/TR/WCAG22/' target='_blank' rel='noopener noreferrer'>W3C: Web Content Accessibility Guidelines (WCAG) 2.2</a> — критерии Focus Order, Focus Visible, Name, Role, Value и Error Identification.</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> — правила связи label с form control.</li><li><a href='https://www.w3.org/TR/wai-aria/' target='_blank' rel='noopener noreferrer'>W3C: WAI-ARIA 1.2</a> — свойства aria-describedby, aria-errormessage и состояния доступных компонентов.</li></ul>"
}