{ "index": 191, "slug": "editorial-2022-09-mechanism-mobile-ux", "title": "Мобильный интерфейс без скрытого действия: как разделить среду и маршрут", "excerpt": "На узком экране действие часто исчезает не из-за одного breakpoint. Разбираем границу между возможностями ввода, состоянием компонента и обязательным маршрутом, а затем проверяем отрицательный путь.", "contentHtml": "

На десктопе карточка может показывать «Открыть итог» после наведения. В touch-only сценарии указателя нет, а текстовой кнопки нет тоже. Человек видит данные, но не видит следующего шага. Похожий сбой возникает у таблицы: действие остаётся в последней колонке за пределами экрана. Ещё один вариант — подтверждение появляется только после hover, пока экранная клавиатура закрывает часть интерфейса. Цена ошибки — незавершённая операция, повторное обращение в поддержку или неверное подтверждение без нужного контекста.

\n

Один media query не исправляет этот класс проблем. Он меняет раскладку, но не отвечает, какое действие должно остаться доступным. Рабочий тезис проще: возможности среды подсказывают адаптацию, а маршрут компонента должен явно описывать обязательные шаги. Нельзя строить единственный путь на hover, широкой таблице или предположении о двух руках.

\n

Механизм: четыре границы одного экрана

\n

Сначала есть среда. Media Queries описывают характеристики user agent и устройства: ширину, а также возможности основного указателя — pointer и hover. Это свойства среды, а не заключение о том, что конкретному человеку удобно. На одном устройстве могут быть доступны разные способы ввода: для совокупности указателей служат any-pointer и any-hover. Поэтому hover: none не означает «в системе никогда не будет hover», а pointer: coarse не измеряет размер пальца и не заменяет проверку конкретного действия.

\n

Дальше работает компонент. Он строит summary, details, confirm и состояния loading, error, disabled. Его задача — сохранить порядок и смысл действия в конкретной разметке. Затем продукт решает, какой переход является главным. Если подтверждение обязательно, его нельзя оставлять единственным эффектом наведения. Наконец, отдельная проверка реализации показывает, как этот контракт ведёт себя в браузере и с реальным содержимым.

\n
Граница ответственности в мобильном маршруте
СлойЧто известноЧто можно решитьЧего нельзя заключать
СредаДоступны значения media features или события pointerПодобрать раскладку и дополнительное улучшениеЧто конкретному человеку удобно действовать
КомпонентОпределены шаги и состояния маршрутаПоказать явный control и сохранить контекстЧто разметка уже доступна во всех браузерах
ПродуктНазвано обязательное действиеЗапретить hover-only как единственный переходЧто изменится конверсия или скорость
ПроверкаЕсть условия, сценарий и ожидаемый результатПровести browser, keyboard или accessibility checkЧто локальная функция заменила исследование
\n

Такое разделение локализует ошибку. Если кнопка исчезла, можно проверить CSS visibility, состояние компонента, overflow и сам продуктовый маршрут. Без границы всё называют «мобильным UX», а исправление сводится к случайному breakpoint. После него экран может выглядеть аккуратно и всё равно оставаться незавершённым.

\n
Четыре слоя мобильного маршрута: среда ввода, маршрут компонента, продуктовое решение и отдельная проверка
Схема отделяет capability среды от маршрута и проверки. Профиль narrow/coarse/no-hover в примере задаётся явно и не считывается из браузера.
\n

Пример: контракт не принимает скрытый переход

\n

Ниже приведена учебная функция. Она не создаёт DOM, не запускает браузер, не читает matchMedia, не получает pointer events и не измеряет удобство. Профиль — входные данные примера. Функция принимает только два значения активации: explicit-control и hover-only. Она проверяет одно правило: на узком маршруте главное действие должно иметь явный control. Этот код не является production-адаптером и не заменяет browser test.

\n
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.

\n

Симптом → причина → проверка → действие

\n
Диагностическая карта скрытого действия
СимптомПричинаПроверкаДействие
Меню видно только при наведенииГлавный переход привязали к hoverЕсть ли видимая кнопка или ссылка в маршрутеДобавить explicit control и оставить контекст
Кнопка находится справа за экраномДействие закрепили за широкой таблицейПроверить overflow и порядок summary/details/confirmВынести confirm в доступный поток
На hybrid input состояния расходятсяPrimary capability приняли за все доступные способы вводаСравнить primary и any capability, не делая вывод о человекеОставить hover как улучшение, но не как единственный путь
После адаптации пропал контекстСразу сжали layout и спрятали обязательные данныеПроверить, что summary виден до действияВернуть краткий итог и вынести детали в отдельный шаг
Изменение нельзя безопасно отменитьУдалили старую ветку до проверки нового маршрутаЕсть ли сохранённый current path и владелец откатаСначала сохранить обратимый переход
Локальный тест зелёный, экран сломанКонтракт приняли за проверку реализацииПовторить сценарий в браузере с фактическими даннымиДобавить browser, keyboard или accessibility check
\n

Порядок действий

\n
  1. Опишите симптом. Назовите конкретное состояние: что не видно, какой переход требует hover, где появляется горизонтальная прокрутка и какое действие нельзя завершить.
  2. Зафиксируйте условия. Укажите экран, данные, локаль, viewport, масштаб, способ ввода и состояние компонента. Не подставляйте значения, которых никто не наблюдал.
  3. Разделите причину. Проверьте media rule, DOM, visibility, overflow, component state и продуктовое решение. Не объявляйте любую проблему проблемой ширины.
  4. Опишите маршрут. Запишите summary, details, confirm, отмену, loading и error. Для каждого шага назовите видимый control и ожидаемый результат.
  5. Проверьте отрицательную ветку. Передайте неполный профиль, пустую метку, неизвестную активацию и hover-only. Ожидайте отказа, а не молчаливого построения нового пути.
  6. Проверьте среду отдельно. Если реализация использует media features или pointer events, получите фактические значения в браузере и сохраните условия запуска.
  7. Проверьте реализацию. Пройдите маршрут клавиатурой, на узком и широком контейнере, с длинным текстом и ошибкой. Убедитесь, что focus и контекст не исчезают.
  8. Согласуйте откат. Определите, как вернуть current path, кто принимает решение и какие данные нельзя потерять. Не называйте поле rollback готовым откатом сервера.
\n

Ограничения

\n

Профиль narrow не равен конкретным 320, 375 или 412 CSS-pixels. Реальный breakpoint зависит от содержимого, шрифта, локали, масштаба и layout. Его нельзя вывести из этой функции. Значения coarse и unavailable также не описывают навык человека и не заменяют проверку assistive technology.

\n

Media Queries помогают выбрать presentation, но не определяют бизнес-логику завершения операции. Pointer Events описывают модель событий, но не обещают, что control заметен или физически достижим. WCAG задаёт проверяемые критерии для конкретного контента и реализации, а соответствие нельзя вывести из одной unit-проверки. В учебном примере нет production-трафика, пользовательских наблюдений, конверсии и доказательства доступности.

\n

Нельзя считать любой explicit control достаточным. Он может быть слишком мал, терять фокус, иметь неясную метку, открывать устаревшие details или отправлять действие повторно. Нельзя исправлять скрытый маршрут только увеличением кнопки. Сначала восстановите смысл и порядок, затем проверяйте визуальные и технические свойства.

\n

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

\n

Маршрут готов к следующей проверке, если другой инженер без устного объяснения может показать summary, открыть details, выполнить confirm, увидеть loading и error, отменить действие и назвать путь отката. Главный переход доступен без hover. Условия браузерной проверки должны быть записаны. Длинный текст и узкий контейнер не скрывают control. Локальная модель отклоняет неполный профиль, неизвестную активацию и hover-only.

\n

Это ещё не заявление о доступности всего продукта и не доказательство роста метрики. Для такого вывода нужны отдельные browser, accessibility и usability-проверки с их методами и ограничениями. Проверяемый результат этой статьи уже: обязательное действие не зависит от одной capability, а граница между моделью и реальной средой остаётся явной.

\n

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

\n" }