После небольшой правки control может выглядеть лучше и стать менее доступным. Например, новая панель красиво закрывается после save, но focused element удаляется; статус виден в toast, но код не оставляет проверяемого состояния; invalid choice подсвечивается, но уже перезаписала сохранённое значение. Симптомы похожи на «мелкие UI-детали», пока человек не пытается выполнить путь без мыши. Цена — не только один дефект: следующий рефакторинг получает неявное состояние, которое невозможно безопасно отменить.
Диагностику полезно начать не с перебора aria-атрибутов, а с вопроса, в какой момент расходятся два представления одного действия. Есть draft value, committed value, visual marker, declared semantic state, focus route и сообщение результата. Если все шесть меняются в разных местах, «поправить доступность» превращается в широкий рискованный diff. Эта статья сужает проверку до учебной панели. В ней нет production-метрик, ручного screen-reader результата или реального browser trace; всё, что похоже на фокус и объявление, помечено как модельное значение.
Пять причин похожего симптома
| Наблюдение | Вероятная причина | Минимальный факт | Обратимое действие |
|---|---|---|---|
| После закрытия нет понятной точки продолжения | return target не определён до unmount | named focus route не содержит trigger | сохранить trigger как return target и проверить его до close |
| Ошибка подсвечена, но сохранённый выбор изменился | draft и committed state склеены | invalid branch меняет saved value | разделить значения и остановить commit до valid branch |
| Видна галочка, но semantic state старый | CSS и declared state имеют разные owner | одна операция меняет только class | вывести оба значения из одного source of truth |
| Есть toast, но нет проверяемого статуса | текст создаётся вне contract | нет named result state | сформировать declared status до platform validation |
| Undo возвращает не то или работает бесконечно | prior state не сохранён либо не очищен | pending undo не связан с commit | сохранить один prior value и очистить его после undo |
Не начинайте с «проверим всё»
Большой accessibility audit нужен, но он плохо отвечает на локальный вопрос «почему после save исчез фокус». Сначала нужно построить временную последовательность одного действия. До save есть trigger, открытая панель и draft. Во время save выбирается valid branch, фиксируется prior saved value, формируется declared status, панель закрывается, route возвращается на trigger. В invalid branch порядок другой: commit не происходит, панель остаётся открытой, route идёт к полю исправления. Если код не может показать эту последовательность, проверять версию aria-атрибута ещё рано.
Такой порядок переносит привычную практику 2022 года из производительности и SSR: не считать два исхода одинаковыми только потому, что оба показывают новый экран. Здесь valid save и invalid input могут иметь одинаковую раскладку, но отличаются полномочием менять saved value. У valid save есть обратимый снимок. У invalid branch его быть не должно, потому что ничего не было сохранено. Это маленький контракт, который удобно защитить fixture до запуска ручных сценариев.
Fixture проверяет rollback, но не симулирует браузер
Модель открывает панель, выбирает sms, применяет выбор и вызывает undoLastSave. После undo оба значения снова email, а pendingUndo очищен. Повторный undo получает nothing-to-undo. Это важно для безопасной правки: тест не говорит, что в продукте есть кнопка «Отменить» или что она доступна с клавиатуры. Он фиксирует более узкое утверждение — сохранённое изменение не теряет предыдущее значение и не откатывается бесконечно.
const panel = createTeachingPanel();
openPanel(panel);
chooseChannel(panel, "sms");
applyChoice(panel);
undoLastSave(panel);
console.log(panel.state.savedChannel); // "email"
console.log(panel.state.pendingUndo); // null
// Undo restores the teaching value once; it is not a browser rollback.
Можно запустить fixture целиком командой из первой статьи. Она не импортирует React, не создаёт element, не шлёт key event и не записывает звук. Поля focus и declaredAnnouncement не надо называть результатом accessibility tree. В них записаны ожидаемые точки проекта. Это делает тест строгим в том, что он умеет проверить, и честным в том, чего в нём нет.
Проверка реального компонента: собрать факты в правильном порядке
- Симптом. Зафиксируйте один наблюдаемый сбой: фокус потерян после close, state не соответствует визуальному выбору или ошибка не даёт следующего шага. Не называйте причину заранее.
- Контракт. Выпишите name, role, selected/expanded state, draft value, committed value, trigger, invalid target и return target. Это минимальный список, а не полный audit страницы.
- Проверка кода. Найдите место, где каждая часть меняется. Особенно проверьте, что invalid branch выполняется до commit и что return target не удаляется до записи маршрута.
- Проверка модели. Запустите
node web/scripts/upgrade-2022-04.mjs --verify-fixture. Она должна сохранить invalid branch без изменения saved value и вернуть prior value одним undo. - Проверка платформы. В реальном DOM пройдите согласованный keyboard scenario, затем проверьте семантику и выбранные assistive-technology combinations. Запишите среду и результат отдельно; модель не может заменить эту работу.
- Действие и откат. Вносите одно изменение: native host, единый state owner, return target или status boundary. Перед rollout оставьте способ вернуть прежний state/markup и повторите тот же сценарий.
Откуда берётся ложная уверенность
Ложная уверенность появляется, когда один артефакт получает чужую работу. Snapshot DOM не доказывает клавиатурную последовательность. Скринкаст с мышью не доказывает name/role. Валидный aria-live не доказывает конкретную фразу, произнесённую конкретной технологией. И наоборот, успешный ручной проход одного сочетания не доказывает, что state ownership не сломается после следующего refactor. WCAG разделяет проверяемые критерии, а практики ARIA описывают поведение паттернов; проекту всё равно нужно выбрать доказательство для каждого риска.
Полезная запись review выглядит скромно: «в fixture invalid branch не меняет committed value; в таком-то браузере команда вручную проверила такой-то сценарий; остальные среды не проверялись». Она не так эффектна, как фраза «полная доступность», зато следующему разработчику ясно, что можно воспроизвести и где находится неизвестное. Для автора 2022 года это продолжение технической дисциплины: измерение или тест имеют ценность ровно в пределах своих входов.
Undo-safe изменение не равно глобальному rollback
Учебный undo хранит одну строку prior value. Он не учитывает серверные конфликты, несколько вкладок, историю браузера, права, offline и долгоживущие черновики. Если реальный save вызывает сеть, отмена должна иметь отдельный контракт: отменяем ли мы запрос, создаём ли compensating change, как показываем конфликт и кто владеет retry. Нельзя переносить маленькую fixture в эти условия без новых тестов. Её роль — не решить распределённую задачу, а не дать локальному control потерять собственный предыдущий state.
Точно так же return focus не всегда обязан идти на trigger. Если после save появляется новый результат, пользователь может ожидаемо перейти к нему; если trigger исчезает по изменённому route, нужен другой named target. Важно не конкретное слово preferences-trigger, а решение до unmount и проверка его причины. В модели trigger выбран потому, что панель закрывается без смены страницы. Другая структура должна назвать свою предпосылку, а не копировать route как ритуал.
Следующий шаг для команды
Начните с одного компонента, который часто открывает и закрывает состояние: menu button, dialog trigger, selection control или save panel. Составьте короткую матрицу из этой статьи и добавьте к PR два правила: invalid branch не коммитит state; успешный branch имеет named focus target и обратимое значение, если продукт обещает отмену. Затем выберите одну платформенную проверку, которую реально можно повторить. Не нужно ждать общей a11y-платформы, чтобы перестать выпускать control без контракта.
После того как один сценарий стабилен, расширяйте его осторожно. Следующая статья может связать этот contract с ошибками формы: там появятся асинхронный ответ, несколько полей и приоритет сообщений. Но сначала нужно, чтобы текущий элемент различал draft, commit и отмену. Без этой границы новая сложность лишь добавит больше уведомлений вокруг того же скрытого состояния.
Историческая граница апреля 2022
Ссылки ведут на датированные документы, доступные до апреля 2022 года: WCAG 2.1 Recommendation 2018 года, APG 1.2 Group Note ноября 2021 года и WAI-ARIA 1.2 Candidate Recommendation Draft от 8 декабря 2021 года. В тексте нет отчёта о проведённом исследовании, реальном дефекте или произнесённой фразе screen reader. Все numbers, labels и ветки fixture относятся только к учебному контракту этого пакета.
Проверяемые источники
- WCAG 2.1, W3C Recommendation от 5 июня 2018 года — неизменяемая нормативная версия. В ней есть проверяемые критерии Keyboard, Focus Order, Name, Role, Value и Status Messages; она не описывает поля учебной fixture.
- WAI-ARIA Authoring Practices 1.2, W3C Group Note от 29 ноября 2021 года — датированное официальное руководство с паттернами кнопок, клавиатурной навигацией и фокусом. Это guidance, а не запись о ручной проверке конкретного компонента.
- WAI-ARIA 1.2, W3C Candidate Recommendation Draft от 8 декабря 2021 года — неизменяемая версия периода. Она фиксирует статус Candidate Recommendation Draft; материал не приписывает апрелю 2022 будущую Recommendation 2023 года.