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

8 lines
17 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": 205,
"slug": "editorial-2022-04-field-accessible-interface",
"title": "Доступный control после правки: как не потерять фокус, состояние и обратный путь",
"excerpt": "Визуально исправный control может потерять смысл для клавиатуры и screen reader. Разбираем расхождение состояний на примере панели настроек и задаём проверяемый путь исправления.",
"contentHtml": "<p>Кнопка открывает панель настроек. Пользователь выбирает SMS, нажимает «Сохранить», панель закрывается. Глаз видит галочку и новый текст. Но фокус исчезает, имя группы не читается, а повторное открытие показывает старое значение. Другой вариант хуже: ошибка подсвечивается, но уже изменила сохранённую настройку. Такой дефект легко пропустить при проверке мышью. Цена ошибки — потерянный путь навигации, неверное действие и повторная работа пользователя. Для критичного интерфейса это может остановить операцию целиком.</p>\n<p>Тезис простой: доступность control — это не набор атрибутов рядом с CSS. Это согласованный контракт состояния, имени, роли, фокуса и результата. Один источник состояния должен объяснять, что сейчас выбрано. Каждая ветка должна иметь понятный следующий фокус. Успех и ошибка должны менять разные данные. Если эти условия не записаны, визуальный патч легко создаёт недоступный интерфейс.</p>\n<h2>Сначала разделите черновик и сохранённое значение</h2>\n<p>Панель выбора имеет как минимум два значения. <code>draftChannel</code> показывает выбор внутри открытой панели. <code>savedChannel</code> отражает настройку, которая уже принята продуктом. Пока пользователь не подтвердил выбор, эти значения могут различаться. Ошибка валидации не должна менять <code>savedChannel</code>. Успешное сохранение сначала запоминает старое значение, затем коммитит новое и только после этого закрывает панель.</p>\n<p>У control есть и другие части контракта: имя и роль группы, состояние выбранной опции, trigger, target для ошибки, target для возврата после закрытия и сообщение о результате. Их не нужно хранить в шести независимых переменных. Они должны выводиться из одной модели переходов. Тогда проверка отвечает на конкретный вопрос: какое действие изменило состояние и куда попал фокус после этого действия.</p>\n<pre><code>const state = {\n open: true,\n draftChannel: 'sms',\n savedChannel: 'email',\n previousChannel: null,\n focusTarget: 'channel-sms',\n status: ''\n};\n\nfunction save(state) {\n if (!['email', 'sms'].includes(state.draftChannel)) {\n return { ...state, focusTarget: 'channel-error', status: 'Выберите канал' };\n }\n\n return {\n ...state,\n open: false,\n previousChannel: state.savedChannel,\n savedChannel: state.draftChannel,\n focusTarget: 'preferences-trigger',\n status: 'Канал сохранён'\n };\n}</code></pre>\n<p>Это учебный пример переходов. Он не создаёт DOM, не отправляет события клавиатуры и не доказывает, что конкретная технология озвучит сообщение. В нём важна граница: невалидная ветка не коммитит значение, а успешная ветка сохраняет предыдущее значение до закрытия панели. В реальном компоненте эти переходы нужно связать с семантической разметкой и проверить в выбранных браузерах и assistive technology.</p>\n<h2>Почему одинаковый симптом скрывает разные причины</h2>\n<p>После клика пользователь может увидеть новую галочку и всё равно получить старое состояние в accessibility tree. Так происходит, когда CSS меняет класс, а <code>aria-checked</code> получает значение из другой переменной. Фокус может исчезнуть не из-за screen reader, а потому, что компонент удалили до того, как выбрали return target. Сообщение может быть видно, но не быть доступным для assistive technology без перевода фокуса. Поэтому диагностика должна начинаться с последовательности переходов, а не с перебора атрибутов.</p>\n<div class=\"table-scroll\"><table><caption>Диагностика разрыва контракта control</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После закрытия фокус неясен</td><td>Панель удаляется до выбора return target</td><td>Записать активный элемент до и после close</td><td>Выбрать существующий target до unmount и проверить его клавиатурой</td></tr><tr><td>Галочка видна, но состояние старое</td><td>CSS и семантика используют разные источники</td><td>Сравнить visual value и declared value после одного выбора</td><td>Вывести оба значения из одного state owner</td></tr><tr><td>Ошибка есть только для мыши</td><td>Сообщение не связано с полем и фокусом</td><td>Проверить имя, описание, target ошибки и порядок перехода</td><td>Связать ошибку с control и направить фокус на исправление</td></tr><tr><td>Старое значение изменилось при ошибке</td><td>Draft и committed state склеены</td><td>Выполнить invalid branch и сравнить saved value</td><td>Остановить commit до успешной проверки</td></tr><tr><td>Статус виден, но результат непредсказуем</td><td>Текст создаётся отдельно от result state</td><td>Проверить семантику status message и порядок обновления</td><td>Сформировать статус в контракте, затем проверить platform output</td></tr></tbody></table></div>\n<h2>Один пример и отрицательный путь</h2>\n<p>Рассмотрим панель «Канал уведомлений». Trigger имеет имя «Настроить уведомления» и сообщает, открыта ли панель. Группа выбора получает имя «Канал уведомлений». В ней есть две radio options: Email и SMS. Кнопка «Сохранить» подтверждает draft. При успехе панель закрывается, focus возвращается на trigger, а статус сообщает о принятом действии. Если панель модальная, при её открытии фокус должен оказаться внутри, а после закрытия обычно вернуться на элемент, который её вызвал; для другого return target нужна причина из сценария. Если результат не меняет контекст, status message должен быть программно определимым, но его конкретную озвучку всё равно проверяют в выбранной связке браузера и assistive technology.</p>\n<p>Отрицательная ветка начинается с невозможного или неподдерживаемого значения. Панель остаётся открытой. <code>savedChannel</code> не меняется. Сообщение об ошибке связано с местом исправления. Фокус получает понятный target. Пользователь может изменить draft и повторить действие. Если обработчик сначала записывает значение, а потом проверяет его, последующее сообщение уже не исправит неверное состояние.</p>\n<figure><img src=\"/assets/editorial/2022/accessible-interface-diagnosis-rollback-2022.svg\" alt=\"Схема двух веток панели настроек: ошибка оставляет сохранённое значение без изменений, успех сохраняет предыдущее значение, закрывает панель и возвращает фокус на trigger.\" loading=\"lazy\" /><figcaption>Учебная схема разделяет invalid branch и valid save. Она показывает границы состояния, фокуса и отмены; фактический DOM и речь screen reader нужно проверять отдельно.</figcaption></figure>\n<h2>Порядок проверки</h2>\n<ol><li><strong>Опишите наблюдаемый сбой.</strong> Запишите один симптом: фокус потерян, состояние не совпало с визуальным выбором или ошибка не дала следующего шага. Не называйте причину до проверки.</li><li><strong>Назовите контракт.</strong> Зафиксируйте имя, роль, выбранное состояние, draft value, committed value, trigger, target ошибки и return target.</li><li><strong>Проследите переход.</strong> Найдите в коде места, где меняются CSS, семантическое состояние, сохранённое значение, фокус и статус. Проверьте порядок операций.</li><li><strong>Проверьте отрицательную ветку.</strong> Передайте невалидный draft. Убедитесь, что committed value не изменился, панель не закрылась, а фокус получил исправляемый target.</li><li><strong>Проверьте успешную ветку.</strong> Сохраните новый выбор. Убедитесь, что старое значение сохранено для предусмотренной отмены, панель закрылась, а return target существует.</li><li><strong>Проверьте реальный интерфейс.</strong> Пройдите сценарий только клавиатурой. Затем проверьте DOM, доступное имя, роль, состояние, порядок фокуса и статус в согласованной комбинации браузера и assistive technology.</li><li><strong>Оставьте обратимый diff.</strong> Вносите одно изменение за раз. После каждого изменения повторяйте обе ветки. Если результат ухудшился, верните прежнюю разметку или state transition и сохраните причину отката.</li></ol>\n<h2>Что этот подход не доказывает</h2>\n<p>Учебная модель не симулирует браузер. Поле <code>focusTarget</code> не равно вызову <code>HTMLElement.focus()</code>. Строка <code>status</code> не доказывает конкретную фразу, которую услышит пользователь. Unit-тест переходов не проверяет tab order соседних компонентов, видимый focus indicator, порталы, конфликт сочетаний клавиш или поведение нескольких вкладок.</p>\n<p>Undo для такой панели тоже ограничен. Он может хранить одно предыдущее значение, но не решает конфликт серверных изменений, offline, retry и прав доступа. Если сохранение идёт по сети, нужно отдельно решить, отменяется ли запрос или создаётся compensating change. Нельзя называть локальную отмену глобальным rollback.</p>\n<p>Native control часто даёт готовую семантику и клавиатурное поведение. Custom widget нужен только при ясной причине. Если его оставляют, keyboard contract, focus route и state mapping должны быть частью того же изменения. Атрибут <code>role</code> без соответствующего поведения не делает элемент доступным.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово, когда одна и та же проверяемая последовательность проходит для двух веток. При ошибке сохранённое значение остаётся прежним, панель остаётся доступной для исправления, сообщение связано с control, а фокус имеет существующий target. При успехе выбранное значение становится сохранённым, старое значение доступно для предусмотренной отмены, панель закрывается предсказуемо, а фокус возвращается на заранее выбранный target или на явно обоснованный результат. В согласованной среде проверки DOM и keyboard path подтверждают этот контракт. Если проверена только модель или только визуальный экран, готовность ещё не доказана.</p>\n<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> — нормативные проверяемые критерии для клавиатуры, фокуса, имени, роли, значения и status messages.</li><li><a href=\"https://www.w3.org/WAI/ARIA/apg/patterns/radio/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C WAI, ARIA Authoring Practices Guide: Radio Group Pattern</a> — правила checked-state и клавиатурного поведения radio group.</li><li><a href=\"https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C WAI, ARIA Authoring Practices Guide: Dialog (Modal) Pattern</a> — границы модального диалога, начальный фокус и возврат фокуса после закрытия.</li><li><a href=\"https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html\" target=\"_blank\" rel=\"noopener noreferrer\">W3C WAI, Understanding Success Criterion 4.1.3: Status Messages</a> — программно определимые status messages, которые assistive technology может представить без получения фокуса.</li></ul>"
}