DarkRiDDeR13 мин

Мобильный сценарий: контракт ввода, состояния и границы

UXFrontend

Когда десктопный интерфейс требует 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 содержит критерии для взаимодействия, но проверка соответствия требует конкретного контента и реализации. Поэтому этот материал использует источники для точности терминов, а не превращает их в обещание доступности готового продукта.

Диаграмма разделяет input environment, component route, product decision и research; narrow/coarse/no-hover передаётся в учебную модель как проектный профиль, а не считывается из браузера.
Граница важнее стрелок: fixture проверяет только contract route, а проверка платформы и исследования остаются отдельными работами.

Детерминированная модель и её ответ

Функция 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 до удаления прежней ветки

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Опишите, что именно невозможно завершить без мыши, широкого контейнера или второй руки. Не используйте общий ярлык «плохой UX».
  2. Причина. Разложите зависимость: это условие среды, CSS visibility, component state или решение спрятать действие?
  3. Проверка контракта. Прогоните fixture и убедитесь, что selected profile не принимает hover-only для главного действия. Это проверка локальной функции, не браузера.
  4. Проверка среды. Если implementation читает media features, отдельно документируйте браузер и реальные query results. Не подменяйте их литералами fixture.
  5. Действие. Добавьте explicit control, сохраните порядок context → details → action и назначьте owner для состояния после клика.
  6. Откат. Перед выпуском договоритесь, как вернуть старый 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 они уже были доступны.

Проверяемые источники