{ "index": 191, "slug": "editorial-2022-09-mechanism-mobile-ux", "title": "Мобильный интерфейс без скрытого действия: как разделить среду и маршрут", "excerpt": "На узком экране действие часто исчезает не из-за одного breakpoint. Разбираем границу между возможностями ввода, состоянием компонента и обязательным маршрутом, а затем проверяем отрицательный путь.", "contentHtml": "
На десктопе карточка может показывать «Открыть итог» после наведения. В touch-only сценарии указателя нет, а текстовой кнопки нет тоже. Человек видит данные, но не видит следующего шага. Похожий сбой возникает у таблицы: действие остаётся в последней колонке за пределами экрана. Ещё один вариант — подтверждение появляется только после hover, пока экранная клавиатура закрывает часть интерфейса. Цена ошибки — незавершённая операция, повторное обращение в поддержку или неверное подтверждение без нужного контекста.
\nОдин media query не исправляет этот класс проблем. Он меняет раскладку, но не отвечает, какое действие должно остаться доступным. Рабочий тезис проще: возможности среды подсказывают адаптацию, а маршрут компонента должен явно описывать обязательные шаги. Нельзя строить единственный путь на hover, широкой таблице или предположении о двух руках.
\nСначала есть среда. Media Queries описывают характеристики user agent и устройства: ширину, а также возможности основного указателя — pointer и hover. Это свойства среды, а не заключение о том, что конкретному человеку удобно. На одном устройстве могут быть доступны разные способы ввода: для совокупности указателей служат any-pointer и any-hover. Поэтому hover: none не означает «в системе никогда не будет hover», а pointer: coarse не измеряет размер пальца и не заменяет проверку конкретного действия.
Дальше работает компонент. Он строит summary, details, confirm и состояния loading, error, disabled. Его задача — сохранить порядок и смысл действия в конкретной разметке. Затем продукт решает, какой переход является главным. Если подтверждение обязательно, его нельзя оставлять единственным эффектом наведения. Наконец, отдельная проверка реализации показывает, как этот контракт ведёт себя в браузере и с реальным содержимым.
\n| Слой | Что известно | Что можно решить | Чего нельзя заключать |
|---|---|---|---|
| Среда | Доступны значения media features или события pointer | Подобрать раскладку и дополнительное улучшение | Что конкретному человеку удобно действовать |
| Компонент | Определены шаги и состояния маршрута | Показать явный control и сохранить контекст | Что разметка уже доступна во всех браузерах |
| Продукт | Названо обязательное действие | Запретить hover-only как единственный переход | Что изменится конверсия или скорость |
| Проверка | Есть условия, сценарий и ожидаемый результат | Провести browser, keyboard или accessibility check | Что локальная функция заменила исследование |
Такое разделение локализует ошибку. Если кнопка исчезла, можно проверить CSS visibility, состояние компонента, overflow и сам продуктовый маршрут. Без границы всё называют «мобильным UX», а исправление сводится к случайному breakpoint. После него экран может выглядеть аккуратно и всё равно оставаться незавершённым.
\nНиже приведена учебная функция. Она не создаёт DOM, не запускает браузер, не читает matchMedia, не получает pointer events и не измеряет удобство. Профиль — входные данные примера. Функция принимает только два значения активации: explicit-control и hover-only. Она проверяет одно правило: на узком маршруте главное действие должно иметь явный control. Этот код не является production-адаптером и не заменяет browser test.
function planMobilePath(profile, action) {\n const empty = {\n accepted: false,\n platform: 'not-observed',\n usability: 'not-measured',\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 const validActivation = ['explicit-control', 'hover-only'].includes(\n action?.activation,\n );\n\n if (!validProfile || !action?.id || !action?.label || !validActivation) {\n return { ...empty, reason: 'invalid-contract' };\n }\n\n const requiresExplicitControl =\n 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' && requiresExplicitControl) {\n return {\n ...empty,\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-accepted',\n steps,\n };\n}\n\nconst profile = {\n viewport: 'narrow',\n primaryPointer: 'coarse',\n hover: 'unavailable',\n};\n\nplanMobilePath(profile, {\n id: 'confirm-order',\n label: 'Подтвердить заказ',\n activation: 'explicit-control',\n});\n// accepted: true, steps: summary -> details -> confirm\n\nplanMobilePath(profile, {\n id: 'open-summary',\n label: 'Открыть итог',\n activation: 'hover-only',\n});\n// accepted: false, reason: hover-only-blocked\n\nplanMobilePath(profile, {\n id: 'unknown-action',\n label: 'Неизвестный способ',\n activation: 'gesture-only',\n});\n// accepted: false, reason: invalid-contract\nВ первой ветке функция принимает маршрут, потому что действие имеет метку и явный способ активации. Во второй она возвращает отказ. Третья ветка показывает, что неподдержанный способ активации тоже не считается явным control. Это полезная локальная проверка: код не может случайно объявить hover-only или неизвестное значение допустимым для выбранного профиля. Но результат ничего не говорит о настоящем viewport, фокусе, размерах touch target, screen reader, локали или сетевой задержке.
\nОтрицательный путь здесь важнее зелёной ветки. Неполный профиль возвращает invalid-contract. Пустая метка и неизвестная активация также останавливают переход. Для hover-only сохраняется keep-current-path. Это не автоматический rollback приложения. Это запрет считать новый маршрут принятым. Реальный откат выбирают отдельно: feature flag, возврат коммита, старый route или ограниченный rollout.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Меню видно только при наведении | Главный переход привязали к hover | Есть ли видимая кнопка или ссылка в маршруте | Добавить explicit control и оставить контекст |
| Кнопка находится справа за экраном | Действие закрепили за широкой таблицей | Проверить overflow и порядок summary/details/confirm | Вынести confirm в доступный поток |
| На hybrid input состояния расходятся | Primary capability приняли за все доступные способы ввода | Сравнить primary и any capability, не делая вывод о человеке | Оставить hover как улучшение, но не как единственный путь |
| После адаптации пропал контекст | Сразу сжали layout и спрятали обязательные данные | Проверить, что summary виден до действия | Вернуть краткий итог и вынести детали в отдельный шаг |
| Изменение нельзя безопасно отменить | Удалили старую ветку до проверки нового маршрута | Есть ли сохранённый current path и владелец отката | Сначала сохранить обратимый переход |
| Локальный тест зелёный, экран сломан | Контракт приняли за проверку реализации | Повторить сценарий в браузере с фактическими данными | Добавить browser, keyboard или accessibility check |
rollback готовым откатом сервера.Профиль narrow не равен конкретным 320, 375 или 412 CSS-pixels. Реальный breakpoint зависит от содержимого, шрифта, локали, масштаба и layout. Его нельзя вывести из этой функции. Значения coarse и unavailable также не описывают навык человека и не заменяют проверку assistive technology.
Media Queries помогают выбрать presentation, но не определяют бизнес-логику завершения операции. Pointer Events описывают модель событий, но не обещают, что control заметен или физически достижим. WCAG задаёт проверяемые критерии для конкретного контента и реализации, а соответствие нельзя вывести из одной unit-проверки. В учебном примере нет production-трафика, пользовательских наблюдений, конверсии и доказательства доступности.
\nНельзя считать любой explicit control достаточным. Он может быть слишком мал, терять фокус, иметь неясную метку, открывать устаревшие details или отправлять действие повторно. Нельзя исправлять скрытый маршрут только увеличением кнопки. Сначала восстановите смысл и порядок, затем проверяйте визуальные и технические свойства.
\nМаршрут готов к следующей проверке, если другой инженер без устного объяснения может показать summary, открыть details, выполнить confirm, увидеть loading и error, отменить действие и назвать путь отката. Главный переход доступен без hover. Условия браузерной проверки должны быть записаны. Длинный текст и узкий контейнер не скрывают control. Локальная модель отклоняет неполный профиль, неизвестную активацию и hover-only.
\nЭто ещё не заявление о доступности всего продукта и не доказательство роста метрики. Для такого вывода нужны отдельные browser, accessibility и usability-проверки с их методами и ограничениями. Проверяемый результат этой статьи уже: обязательное действие не зависит от одной capability, а граница между моделью и реальной средой остаётся явной.
\n