8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"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. Сообщение может быть видно, но не иметь программно определяемого статуса. Поэтому диагностика должна начинаться с последовательности переходов, а не с перебора атрибутов.</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>Toast виден, но результат непредсказуем</td><td>Текст создаётся отдельно от result state</td><td>Найти место формирования статуса и его семантическую границу</td><td>Сформировать статус в контракте, затем проверить платформенный output</td></tr></tbody></table></div>\n<h2>Один пример и отрицательный путь</h2>\n<p>Рассмотрим панель «Канал уведомлений». Trigger имеет имя «Настроить уведомления» и сообщает, открыта ли панель. Группа выбора получает имя «Канал уведомлений». В ней есть две radio options: Email и SMS. Кнопка «Сохранить» подтверждает draft. При успехе панель закрывается, focus возвращается на trigger, а статус сообщает о принятом действии. Это не единственный возможный маршрут. Если после сохранения появляется отдельный результат, продукт может направить фокус туда. Важно принять решение до удаления панели.</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/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C WAI, ARIA Authoring Practices Guide</a> — официальные паттерны ролей, состояний, клавиатурного поведения и фокуса.</li></ul>"
|
||
}
|