8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 207,
|
||
"slug": "editorial-2022-04-practice-accessible-interface",
|
||
"title": "Доступный интерфейс начинается с маршрута фокуса",
|
||
"excerpt": "Как связать имя, роль, состояние и фокус в одном интерактивном сценарии и проверить ошибку до того, как пользователь потеряет управление формой.",
|
||
"contentHtml": "<p>Панель настроек открывается по клику, но после нажатия Tab фокус уходит в неизвестное место. Клавиатурный пользователь не понимает, что панель появилась, а после сохранения фокус исчезает вместе с удалённым узлом. Другая частая ошибка выглядит тише: выбранный канал отмечен цветом и галочкой, но его имя и состояние не попадают в семантический слой. Цена ошибки — потерянное действие, повторный ввод и невозможность понять, сохранились ли данные.</p><p>Доступный интерфейс нужно проверять как маршрут состояния. Для каждого шага задайте имя элемента, его роль, текущее значение, точку фокуса и результат действия. Если одно из этих свойств живёт отдельно, компонент может выглядеть исправным и всё равно ломаться без мыши.</p><h2>Тезис: у действия есть контракт</h2><p>Интерактивный компонент не сводится к разметке и стилям. Его контракт описывает, что человек видит и что получает в ответ. Кнопка открытия имеет понятное имя и принимает фокус. Панель получает доступное имя. Группа вариантов сообщает выбранное значение. Ошибка оставляет прежнее сохранённое значение и даёт понятную точку исправления. После успешного закрытия фокус возвращается на кнопку, которая открыла панель, если сценарий не требует другой точки.</p><p>Этот порядок связывает четыре слоя: семантику, состояние, клавиатурное поведение и визуальный сигнал. Атрибут <code>role</code> не добавляет обработчик клавиши, не рисует focus ring и не возвращает фокус после удаления элемента. Если проект заменяет нативный <code>button</code> на <code>div</code>, он берёт на себя весь недостающий контракт. Поэтому первый выбор — сохранить нативный элемент. Custom control нужен только тогда, когда его поведение описано и проверено целиком.</p><h2>Сценарий панели уведомлений</h2><p>Рассмотрим небольшую панель «Настроить уведомления». На странице есть кнопка открытия. В панели пользователь выбирает один канал: Email или SMS. Кнопка «Сохранить» подтверждает выбор. Внутри нужно различать черновое значение и сохранённое значение. Черновик меняется при выборе. Сохранённое значение меняется только после подтверждения.</p><p>При открытии фокус переходит на заголовок или первый пригодный для действия элемент — выбор зависит от паттерна и размера панели. Для этой панели выберем первый вариант Email. При недопустимом выборе, например <code>carrier-pigeon</code>, система не меняет сохранённый канал. Ошибка связывается с полем и оставляет фокус на месте исправления. После сохранения панель закрывается, фокус возвращается на <code>notifications-trigger</code>, а статус сообщает о результате в доступной форме.</p><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 продолжает идти по странице</td><td>Панель не получила точку входа, а фокус остался на trigger или ушёл в фон</td><td>Открыть панель клавиатурой и назвать первый ожидаемый focus target</td><td>Задать маршрут входа и удерживать фокус в модальной области, если она действительно modal</td></tr><tr><td>Вариант отмечен цветом, но выбор не читается</td><td>Visual state и semantic state вычисляются из разных источников</td><td>Сравнить значение в state с <code>aria-checked</code> или нативным checked</td><td>Вывести оба сигнала из одного значения</td></tr><tr><td>Ошибка видна рядом с полем, но её не находят без мыши</td><td>Сообщение передаётся только цветом или не связано с control</td><td>Проверить label, связь ошибки и маршрут к invalid value</td><td>Сохранить старое значение, назвать ошибку и вернуть фокус к исправлению</td></tr><tr><td>После сохранения клавиатура теряет позицию</td><td>Focused node удалили вместе с панелью</td><td>Зафиксировать activeElement до открытия и после закрытия</td><td>Вернуть фокус на trigger либо на логичную следующую точку</td></tr><tr><td>Статус есть в DOM, но результат неясен</td><td>Текст статуса не связан с изменением состояния или заявлен без проверки</td><td>Проверить момент обновления и содержимое status region</td><td>Обновлять короткое сообщение после результата и отдельно проверить поддерживаемую связку браузера с assistive technology</td></tr></tbody></table></div><h2>Минимальная семантическая разметка</h2><p>Нативная форма уже даёт много нужного поведения. Подпись связана с полем через <code>for</code> и <code>id</code>. Кнопка имеет явный тип. Группа вариантов имеет видимый заголовок. Не добавляйте ARIA ради похожего HTML: сначала проверьте, что нативный элемент выражает требуемый смысл.</p><pre><code><fieldset>\n <legend>Канал уведомлений</legend>\n <label>\n <input type="radio" name="channel" value="email" checked>\n Email\n </label>\n <label>\n <input type="radio" name="channel" value="sms">\n SMS\n </label>\n <p id="channel-error" role="alert"></p>\n</fieldset>\n<button type="submit">Сохранить</button></code></pre><p>Это учебный фрагмент. Он показывает связь подписи, группы и ошибки, но не доказывает поведение конкретного приложения. В рабочем коде нужно проверить порядок фокуса, стили состояния, локализацию, отправку формы и поведение при асинхронном сохранении. Если панель является модальным диалогом, добавьте корректные границы диалога, название и обработку закрытия; не называйте обычный раскрывающийся блок modal только ради атрибута.</p><h2>Граница между состоянием и сообщением</h2><p>Компоненту полезно хранить состояния, которые нельзя смешивать. <code>channel</code> — текущий выбор. <code>savedChannel</code> — подтверждённое значение. <code>focusTarget</code> — ожидаемая точка маршрута. <code>statusText</code> — объявленный результат. Если обработчик сразу записывает выбор в <code>savedChannel</code>, отрицательная ветка становится опасной: ошибка или отмена уже меняет данные.</p><p>Ниже учебная модель. Она не создаёт DOM, не отправляет события клавиатуры и не утверждает, что screen reader произнёс строку. Её задача — показать инвариант: недопустимое значение не меняет сохранённое, а успешное закрытие имеет явный return target.</p><pre><code>const state = {\n channel: 'email',\n savedChannel: 'email',\n focusTarget: 'notifications-trigger',\n statusText: ''\n};\n\nfunction chooseChannel(value) {\n if (!['email', 'sms'].includes(value)) {\n state.statusText = 'Выберите Email или SMS';\n state.focusTarget = 'channel-email';\n return { ok: false, savedChannel: state.savedChannel };\n }\n state.channel = value;\n state.focusTarget = 'save-notifications';\n return { ok: true, savedChannel: state.savedChannel };\n}\n\nfunction save() {\n state.savedChannel = state.channel;\n state.focusTarget = 'notifications-trigger';\n state.statusText = 'Канал уведомлений сохранён';\n return { ok: true, returnFocus: state.focusTarget };\n}</code></pre><p>Модель ограничена намеренно. Она проверяет переходы данных, но не browser accessibility tree. Реальная проверка должна подтвердить, что DOM отражает эти переходы: selected state меняется на том же шаге, ошибка связана с нужным control, а activeElement получает ожидаемую точку после открытия и закрытия.</p><figure><img src='/assets/editorial/2022/accessible-interface-focus-route-2022.svg' alt='Маршрут фокуса панели уведомлений: кнопка открытия ведёт к Email, ошибка возвращает к выбору, сохранение возвращает на кнопку.' loading='lazy' /><figcaption>Маршрут показывает две ветки: исправление ошибочного выбора и возврат после успешного закрытия. Это схема ожидаемого поведения, а не запись работы screen reader.</figcaption></figure><h2>Порядок проверки</h2><ol><li>Запишите наблюдаемый симптом и цену ошибки. Например: после открытия панели клавиатура продолжает обходить фон, а пользователь не может понять, где находится.</li><li>Назовите каждый интерактивный элемент. Для него укажите доступное имя, роль, значение и действие. Если имя нельзя сформулировать одним предложением, остановите проверку разметки.</li><li>Разделите черновое и сохранённое состояние. Проверьте, что invalid input и отмена не меняют подтверждённое значение.</li><li>Опишите маршрут фокуса для входа, выбора, ошибки, сохранения и закрытия. Заранее назовите activeElement после каждого перехода.</li><li>Пройдите сценарий только клавиатурой: Tab, Shift+Tab, Enter и Space там, где они предусмотрены выбранным паттерном. Убедитесь, что видимый focus indicator не закрывает контент и не исчезает.</li><li>Проверьте DOM и accessibility tree в поддерживаемом браузере. Сверьте name, role, state, label, error и status с записанным контрактом.</li><li>Проверьте отрицательный путь: неизвестное значение, пустой ввод, ошибка сохранения и закрытие без подтверждения. Зафиксируйте, что остаётся неизменным и куда возвращается фокус.</li><li>Проверьте одну-две целевые связки браузера и ассистивной технологии. Запишите версии и фактический результат; объявленный текст в state не заменяет это наблюдение.</li><li>После исправления повторите тот же маршрут и добавьте автоматическую проверку для стабильных переходов состояния. Автотест не должен выдавать проверку DOM за исследование пользовательского опыта.</li></ol><h2>Ограничения</h2><p>Контракт одного компонента не делает доступным весь продукт. Он не проверяет контраст, масштабирование, порядок заголовков, локализацию, touch target, виртуальный курсор, тайм-ауты и работу при нестабильной сети. Он также не заменяет тестирование с людьми, которые используют разные способы ввода и разные assistive technology.</p><p>Нативный элемент снижает объём собственной логики, но не отменяет проверку. Можно скрыть focus ring стилями, дать кнопке пустое имя, обновить текст без изменения состояния или закрыть панель раньше завершения сохранения. ARIA помогает передать семантику, но не сообщает, что UX удобен и что реальная программа чтения экрана произнесёт ожидаемую фразу.</p><p>Учебный код выше не является production-результатом. Он не измеряет долю ошибок, скорость исправления и совместимость со всеми браузерами. Не подставляйте такие примеры в отчёт как доказательство качества. Production-критерий должен опираться на фактический DOM, клавиатурный проход и зафиксированную комбинацию браузера с assistive technology.</p><h2>Критерий готовности</h2><p>Сценарий готов к выпуску, когда команда может показать один и тот же проверяемый маршрут: открыть панель, увидеть фокус, назвать control, выбрать значение, получить понятную ошибку без изменения сохранённых данных, успешно сохранить и вернуть фокус на логичную точку. Для каждого перехода есть наблюдаемое свойство DOM или state. Для объявлений есть отдельная проверка поддерживаемой связки браузера и assistive technology. Если хотя бы один переход описан словами «должно работать», а не наблюдаемым результатом, компонент ещё не готов.</p><h2>Проверяемые источники</h2><ul><li><a href='https://www.w3.org/TR/WCAG22/' target='_blank' rel='noopener noreferrer'>W3C, Web Content Accessibility Guidelines (WCAG) 2.2</a> — критерии для видимого фокуса, имени, роли, значения и сообщений о статусе.</li><li><a href='https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/' target='_blank' rel='noopener noreferrer'>W3C WAI-ARIA Authoring Practices, Dialog (Modal) Pattern</a> — рекомендации для имени диалога, входа и возврата фокуса.</li><li><a href='https://www.w3.org/WAI/ARIA/apg/patterns/radio/' target='_blank' rel='noopener noreferrer'>W3C WAI-ARIA Authoring Practices, Radio Group Pattern</a> — клавиатурное поведение и семантика группы вариантов.</li></ul>"
|
||
}
|