Когда десктопный интерфейс требует hover, точного указателя или двух рук, команда часто лечит симптом одним media query. Это может добавить отступ, но не отвечает на главный вопрос: какой путь остаётся доступным, если привычный способ активации неудобен или отсутствует. Цена ошибки — скрытое действие и спор в ревью, где один человек видит меню мышью, а другой получает неподвижный экран без следующего шага.
Полезнее отделить три слоя. Платформа сообщает некоторые характеристики input environment. Компонент рисует controls и управляет состоянием. Продукт принимает решение, какое действие нельзя прятать. Учебная fixture работает только на третьем слое: получает заранее заданный profile и возвращает route. Она не вызывает matchMedia, не ловит pointer event и не говорит, что любой реальный браузер вернёт те же значения.
Три слоя, которые нельзя склеивать
| Слой | Наблюдаемый факт | Допустимое решение | Чего нельзя заключать |
|---|---|---|---|
| Среда | media feature описывает capability primary или any pointer | добавить адаптацию layout | каким способом действует конкретный человек |
| Компонент | route содержит summary, details, confirm | показать отдельный control и state | что control физически доступен без browser-check |
| Продукт | действие признано главным | запретить hover-only как единственный путь | что изменится конверсия или скорость выполнения |
| Исследование | есть метод и протокол наблюдения | собрать отдельные результаты | что fixture уже заменила исследование |
Эта таблица нужна не для бюрократии. Она делает ошибку локальной. Если на narrow profile кнопка пропала, сначала видно, является ли это CSS condition, ошибкой route state или продуктовым решением. Без такого разделения слова «touch», «mobile» и «responsive» начинают означать всё сразу. Тогда невозможно назначить владельца исправления и нельзя объяснить, почему один откат безопасен, а другой может вернуть потерю контекста.
Как читаются pointer и hover
В Media Queries Level 4 pointer и hover относятся к primary pointing device, а any-pointer и any-hover — к совокупности доступных указателей. В датированном CRD отдельно сказано, что даже при hover: none автору не следует предполагать, что :hover никогда не совпадёт. Следствие для интерфейса простое: capability можно использовать как подсказку для presentation, но основной маршрут нельзя определять только видимостью hover-menu.
Pointer Events Level 2 описывает унифицированную модель событий и значение pointerType. Оно не говорит, достаточно ли места для таблицы, как человек держит устройство и замечает ли маленький control. WCAG 2.1 содержит критерии для взаимодействия, но проверка соответствия требует конкретного контента и реализации. Поэтому этот материал использует источники для точности терминов, а не превращает их в обещание доступности готового продукта.
Детерминированная модель и её ответ
Функция planTeachingMobilePath принимает profile и action. Если профиль неполный, она возвращает invalid-teaching-profile. Если у действия нет идентификатора или метки, возвращается invalid-action-contract. При корректном input модель всегда строит три шага. Для narrow, coarse или unavailable она помечает confirm как требующий явного control. А hover-only в этой ветке даёт hover-only-blocked-by-contract и rollback keep-current-path.
Этот ответ намеренно скромный. В нём нет selector, координат, CSS box, фокуса, screen reader, event loop, реального указателя или network. Это не недостаток unit-fixture, если назвать её правильно: она фиксирует наш выбор о том, что основной переход не должен быть только hover-only. Когда нужна проверка другого слоя, создаётся другой артефакт — browser test, accessibility audit или исследовательский протокол — с собственными входами и критериями.
const profile = {
viewport: 'narrow',
primaryPointer: 'coarse',
hover: 'unavailable',
};
const route = planTeachingMobilePath(profile, {
id: 'confirm-order',
label: 'Подтвердить заказ',
activation: 'explicit-control',
});
console.log(route.accepted); // true
console.log(route.steps.map(({ id }) => id));
// ['summary', 'details', 'confirm']
const rejected = planTeachingMobilePath(profile, {
id: 'open-summary', label: 'Открыть итог', activation: 'hover-only',
});
console.log(rejected.reason); // hover-only-blocked-by-contract
Где модель ловит полезную ошибку
Представим, что карточка на desktop показывает «Открыть итог» при hover. Разработчик переносит это условие в мобильный компонент, но текстовой кнопки не добавляет. В учебном profile путь будет отклонён ещё до запуска UI. Это не доказывает, что реальная карточка сломана именно так. Зато code review получает ясный вопрос: какое явное действие заменяет hover? Если ответа нет, нельзя считать migration завершённой только по красивому breakpoint.
| Симптом | Вероятная граница | Проверка | Действие |
|---|---|---|---|
| Меню видно лишь при наведении | product route | есть ли explicit control в контракте | добавить кнопку с меткой |
| Таблица скрывает действие справа | component layout | какой step содержит confirm | вынести confirm после summary или details |
| На hybrid input разные состояния | environment assumption | не перепутан ли primary с any capability | оставить hover enhancement, но не единственный маршрут |
| Изменение трудно откатить | release boundary | сохраняется ли старый route | включить rollback до удаления прежней ветки |
Маршрут: симптом → причина → проверка → действие
- Симптом. Опишите, что именно невозможно завершить без мыши, широкого контейнера или второй руки. Не используйте общий ярлык «плохой UX».
- Причина. Разложите зависимость: это условие среды, CSS visibility, component state или решение спрятать действие?
- Проверка контракта. Прогоните fixture и убедитесь, что selected profile не принимает hover-only для главного действия. Это проверка локальной функции, не браузера.
- Проверка среды. Если implementation читает media features, отдельно документируйте браузер и реальные query results. Не подменяйте их литералами fixture.
- Действие. Добавьте explicit control, сохраните порядок context → details → action и назначьте owner для состояния после клика.
- Откат. Перед выпуском договоритесь, как вернуть старый route без удаления данных и без повторного необратимого действия.
Почему «адаптивно» не является критерием готовности
Слово responsive описывает способ строить layout, но не подтверждает маршрут. Страница может красиво перестроиться в одну колонку и всё равно скрыть единственную команду за hover или переполнением. Обратная ошибка тоже встречается: команда делает огромный control, но не объясняет, что он подтверждает. Поэтому в контракте первым остаётся summary, а затем details. До нажатия человек должен видеть предмет действия, а не только форму кнопки.
Точно так же нельзя выбрать фиксированный viewport и объявить его «всеми мобильными». Наше narrow не равно 320, 375 или 412 CSS-pixels. Оно означает, что для выбранного route мы не полагаемся на широкое одновременное представление. Настоящие breakpoints определяет конкретный layout и контент. Их можно проверить только рендером со шрифтами, локалью, масштабом и данными, которые относятся к продукту.
Граница и следующий шаг
Fixture не является polyfill, design system или аналитикой. Она не покрывает фокус-порядок, ошибку валидации, экранную клавиатуру, orientation change, внешнюю мышь, assistive technology и конкурентные обновления. Она не говорит, что user agent действительно считает pointer coarse. Изменять значения profile ради зелёного результата нельзя: тогда исчезнет проверяемая связь между заявленным ограничением и маршрутом.
Следующий шаг — оформить component contract рядом с исходным кодом. Для каждого main action перечислите label, состояние enabled/disabled, способ получения details, результат, отмену и rollback. Затем в отдельном окружении проведите visual и keyboard checks. Если один из них обнаружит новый случай, добавьте его как новый input или отдельный тест, а не присваивайте его задним числом этой in-memory модели.
Историческая граница сентября 2022
Здесь не используются будущие WCAG 2.2 или текущие страницы документации. Источники — неизменяемые W3C Media Queries Level 4 CRD 25.12.2021, Pointer Events Level 2 Recommendation 04.04.2019 и WCAG 2.1 Recommendation 05.06.2018. Их статус и дата указаны в ссылках; на сентябрь 2022 они уже были доступны.
Проверяемые источники
- W3C Media Queries Level 4, Candidate Recommendation Draft от 25 декабря 2021 года — датированный CRD, доступный в сентябре 2022 года. Он определяет media features pointer, hover, any-pointer и any-hover; это характеристики среды, а не тест удобства конкретного человека.
- W3C Pointer Events Level 2, Recommendation от 4 апреля 2019 года — неизменяемая Recommendation. Она задаёт модель pointer events и pointerType; статья не утверждает, что fixture создаёт browser events.
- W3C Web Content Accessibility Guidelines 2.1, Recommendation от 5 июня 2018 года — неизменяемая Recommendation. В ней есть критерии, относящиеся к pointer gestures, ориентации и reflow; соответствие продукту требует отдельной оценки, которой здесь нет.