{ "index": 192, "slug": "editorial-2022-09-practice-mobile-ux", "title": "Мобильное действие без скрытого шага: как убрать зависимость от hover", "excerpt": "Если главное действие исчезает на узком экране, ищите не «неправильный breakpoint», а потерянный маршрут. Разбираем явный mobile-путь, отрицательную ветку и проверку, которую можно повторить.", "contentHtml": "
На телефоне пользователь открывает экран заказа, видит сумму, но не находит переход к подтверждению. На десктопе переход появляется при наведении на строку таблицы. В другом варианте кнопка остаётся в последней колонке и уезжает за горизонтальный scroll. Симптом один: человек не может завершить действие, хотя данные уже введены.
\nЦена ошибки не сводится к плохому впечатлению. Незавершённая форма создаёт обращение в поддержку. Скрытая команда провоцирует повторный ввод. Если команда временно увеличивает кнопку, но теряет детали перед подтверждением, пользователь получает новый путь с другим риском. Релиз при этом трудно проверить: непонятно, какой элемент владел переходом и при каком состоянии он исчезал.
\nТезис статьи простой: мобильный сценарий нужно проверять как маршрут, а не как набор размеров. У основного действия должны быть видимый контекст, явный переход к деталям и отдельное подтверждение. Hover может улучшать обзор, но не должен оставаться единственным способом добраться до операции. Это правило не доказывает удобство интерфейса. Оно задаёт минимальный контракт, который можно проверить в коде и затем подтвердить в браузере.
\nВозьмём одно действие: проверить данные заказа и подтвердить его. Не переделываем весь экран. Сначала описываем три состояния. summary показывает предмет операции и краткий контекст. details открывает данные, которые нужно проверить до необратимого шага. confirm содержит явный control с понятной меткой.
Такое разбиение отделяет содержание от раскладки. В узком контейнере строки могут перейти в столбец, а детали — открываться во вкладке или disclosure. Но порядок решения сохраняется. Человек сначала понимает, что изменится, потом смотрит данные и только затем подтверждает. Если компонент пропускает первый или второй шаг, изменение CSS не исправит потерю смысла.
\n| Состояние | Что должно быть видно | Что проверяем | Чего контракт не обещает |
|---|---|---|---|
| summary | название операции и краткий контекст | состояние существует первым | что любой перевод поместится в одну строку |
| details | явный переход к данным | переход не требует hover | что выбранный accordion удобен всем |
| confirm | видимая метка основного действия | control достижим явным вводом | размер зоны нажатия и качество текста |
| rollback | сохранённый рабочий путь | непринятый маршрут не вытесняет текущий | автоматический откат серверного состояния |
Слово «видимый» здесь означает часть проверяемого договора. Оно не означает, что элемент уже имеет правильный размер, цвет, фокус или положение. Эти свойства требуют отдельной проверки разметки и рендера. Контракт не заменяет accessibility review и usability study.
\nHover описывает возможность среды указателя, а не намерение человека. Один пользователь может подключить мышь к узкому экрану. Другой может использовать клавиатуру, экранную лупу или другой способ ввода. Поэтому условие hover: none не равно утверждению «здесь есть только touch». Оно лишь сообщает характеристику pointing device, которую браузер определил для среды.
Проблема появляется, когда команда превращает эту характеристику в единственный путь. Иконка показывает tooltip только при наведении. Строка становится ссылкой только в :hover. Действие раскрывается по перемещению указателя, но не имеет кнопки и не получает фокус. На экране телефона этот путь может исчезнуть, а на клавиатуре — остаться недостижимым.
Безопаснее разделить основной control и подсказку. Основной control присутствует в DOM и имеет текстовую метку. Hover может показать дополнительные сведения или подсветить область. Если hover недоступен, сведения остаются достижимыми через явную кнопку или ссылку. Такой механизм работает и на широкой странице: desktop получает ускоренное обнаружение, но не теряет базовый маршрут.
\nНиже — маленькая модель маршрута. Она принимает три проектных значения: ширина контекста, характеристику основного указателя и доступность hover. Эти строки задаёт тест, а не браузер. Модель не читает matchMedia, не создаёт DOM, не отправляет pointer events и не измеряет людей. Поэтому её результат ограничен проверкой порядка шагов и отказа от hover-only.
function planMobilePath(profile, action) {\n const empty = {\n platform: 'not-observed',\n usability: 'not-measured',\n steps: [],\n };\n\n const validProfile = profile\n && ['narrow', 'wide'].includes(profile.viewport)\n && ['coarse', 'fine'].includes(profile.primaryPointer)\n && ['available', 'unavailable'].includes(profile.hover);\n\n if (!validProfile || !action?.id || !action?.label) {\n return { ...empty, accepted: false, reason: 'invalid-contract' };\n }\n\n const explicit = profile.viewport === 'narrow'\n || profile.primaryPointer === 'coarse'\n || profile.hover === 'unavailable';\n\n const steps = ['summary', 'details', 'confirm'];\n\n if (action.activation === 'hover-only' && explicit) {\n return {\n ...empty,\n accepted: false,\n reason: 'hover-only-blocked',\n steps,\n rollback: 'keep-current-path',\n };\n }\n\n return {\n ...empty,\n accepted: true,\n reason: 'explicit-route',\n steps,\n rollback: 'replace-route-only',\n };\n}\n\nconst route = planMobilePath(\n { viewport: 'narrow', primaryPointer: 'coarse', hover: 'unavailable' },\n { id: 'confirm-order', label: 'Подтвердить заказ', activation: 'explicit' },\n);\n\nconsole.log(route.accepted); // true\nconsole.log(route.steps); // summary, details, confirm\n\nconst rejected = planMobilePath(\n { viewport: 'narrow', primaryPointer: 'coarse', hover: 'unavailable' },\n { id: 'open-details', label: 'Открыть итог', activation: 'hover-only' },\n);\n\nconsole.log(rejected.reason); // hover-only-blocked\nПоложительная ветка подтверждает только три вещи: входной контракт корректен, шаги идут в заданном порядке, явное действие принято. Отрицательная ветка важнее для ревью. Она запрещает объявить маршрут принятым, если на выбранном профиле единственный переход зависит от hover. Значение keep-current-path не откатывает приложение само по себе. Оно говорит, что текущий путь пока нельзя заменять этим учебным маршрутом.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Команда видна только при наведении | hover владеет переходом | найти DOM-элемент, focus state и visibility rules | добавить явный button или link |
| Кнопка находится в последней колонке | таблица требует ширины, которой нет | проверить overflow и порядок данных на узком контейнере | вынести основное действие из таблицы, детали оставить отдельно |
| После раскрытия нет понятного подтверждения | details и commit смешаны в одном control | проверить состояния до и после раскрытия | разделить просмотр и необратимое действие |
| Новый путь вызывает сомнение | замена сделана без безопасной границы | описать старый route и условия возврата | сохранить current path до отдельной проверки |
| «Мобильный» тест зелёный, UI не проверен | учебную модель приняли за browser test | посмотреть, какие данные реально создаёт тест | добавить отдельную проверку браузера с фиксированными условиями |
Таблица нужна для смены уровня разговора. Запись «на телефоне неудобно» не объясняет, что наблюдалось. Запись «в состоянии A переход к details появляется только после hover» уже задаёт проверку. В реальном отчёте добавьте URL или экран, браузер, viewport, масштаб, локаль, способ ввода, начальное состояние и тестовые данные. Не заполняйте неизвестные поля догадками.
\nhover-only. Ожидайте отказ с причиной hover-only-blocked. Если тест принимает путь, контракт ослаблен.Контракт не выбирает breakpoint и не задаёт универсальный размер зоны нажатия. Он не описывает экранную клавиатуру, поворот экрана, длинные локализованные строки, screen reader, ошибку сети или конкурирующее обновление данных. Он также не доказывает, что человеку понятна метка «Продолжить». Эти вопросы относятся к реализации, доступности и исследованию.
\nНельзя объявлять production-эффектом число успешных assertion. В примере нет пользователей, реального устройства, телеметрии и замера времени. Нельзя называть строку narrow конкретным viewport в CSS pixels. Нельзя трактовать hover: none как запрет мыши. Нельзя использовать учебный отказ как автоматическое решение для серверной операции.
Сценарий готов к следующей проверке, если выполнены четыре условия. Для основного действия существует видимый control с предметной меткой. До confirm доступен отдельный summary и путь к details. На заданном учебном профиле hover-only отклоняется, а на реализацию не распространяется обещание usability. В браузере другой инженер может повторить описанное состояние по зафиксированным входам и увидеть тот же порядок: summary → details → confirm.
\npointer, hover, any-pointer и any-hover; это свойства среды, а не измерение удобства.