diff --git a/editorial/agent-rewrites/192.json b/editorial/agent-rewrites/192.json index 91fce62..bcc85ab 100644 --- a/editorial/agent-rewrites/192.json +++ b/editorial/agent-rewrites/192.json @@ -1,7 +1,7 @@ { "index": 192, "slug": "editorial-2022-09-practice-mobile-ux", - "title": "Мобильное действие без скрытого шага: как убрать зависимость от hover", - "excerpt": "Если главное действие исчезает на узком экране, ищите не «неправильный breakpoint», а потерянный маршрут. Разбираем явный mobile-путь, отрицательную ветку и проверку, которую можно повторить.", - "contentHtml": "

На телефоне пользователь открывает экран заказа, видит сумму, но не находит переход к подтверждению. На десктопе переход появляется при наведении на строку таблицы. В другом варианте кнопка остаётся в последней колонке и уезжает за горизонтальный scroll. Симптом один: человек не может завершить действие, хотя данные уже введены.

\n

Цена ошибки не сводится к плохому впечатлению. Незавершённая форма создаёт обращение в поддержку. Скрытая команда провоцирует повторный ввод. Если команда временно увеличивает кнопку, но теряет детали перед подтверждением, пользователь получает новый путь с другим риском. Релиз при этом трудно проверить: непонятно, какой элемент владел переходом и при каком состоянии он исчезал.

\n

Тезис статьи простой: мобильный сценарий нужно проверять как маршрут, а не как набор размеров. У основного действия должны быть видимый контекст, явный переход к деталям и отдельное подтверждение. Hover может улучшать обзор, но не должен оставаться единственным способом добраться до операции. Это правило не доказывает удобство интерфейса. Оно задаёт минимальный контракт, который можно проверить в коде и затем подтвердить в браузере.

\n

Сначала восстановите маршрут

\n

Возьмём одно действие: проверить данные заказа и подтвердить его. Не переделываем весь экран. Сначала описываем три состояния. summary показывает предмет операции и краткий контекст. details открывает данные, которые нужно проверить до необратимого шага. confirm содержит явный control с понятной меткой.

\n

Такое разбиение отделяет содержание от раскладки. В узком контейнере строки могут перейти в столбец, а детали — открываться во вкладке или disclosure. Но порядок решения сохраняется. Человек сначала понимает, что изменится, потом смотрит данные и только затем подтверждает. Если компонент пропускает первый или второй шаг, изменение CSS не исправит потерю смысла.

\n
Минимальный контракт мобильного действия
СостояниеЧто должно быть видноЧто проверяемЧего контракт не обещает
summaryназвание операции и краткий контекстсостояние существует первымчто любой перевод поместится в одну строку
detailsявный переход к даннымпереход не требует hoverчто выбранный accordion удобен всем
confirmвидимая метка основного действияcontrol достижим явным вводомразмер зоны нажатия и качество текста
rollbackсохранённый рабочий путьнепринятый маршрут не вытесняет текущийавтоматический откат серверного состояния
\n

Слово «видимый» здесь означает часть проверяемого договора. Оно не означает, что элемент уже имеет правильный размер, цвет, фокус или положение. Эти свойства требуют отдельной проверки разметки и рендера. Контракт не заменяет accessibility review и usability study.

\n

Механизм: почему hover ломает действие

\n

Hover описывает возможность среды указателя, а не намерение человека. Один пользователь может подключить мышь к узкому экрану. Другой может использовать клавиатуру, экранную лупу или другой способ ввода. Поэтому условие hover: none не равно утверждению «здесь есть только touch». Оно лишь сообщает характеристику pointing device, которую браузер определил для среды.

\n

Проблема появляется, когда команда превращает эту характеристику в единственный путь. Иконка показывает tooltip только при наведении. Строка становится ссылкой только в :hover. Действие раскрывается по перемещению указателя, но не имеет кнопки и не получает фокус. На экране телефона этот путь может исчезнуть, а на клавиатуре — остаться недостижимым.

\n

Безопаснее разделить основной control и подсказку. Основной control присутствует в DOM и имеет текстовую метку. Hover может показать дополнительные сведения или подсветить область. Если hover недоступен, сведения остаются достижимыми через явную кнопку или ссылку. Такой механизм работает и на широкой странице: desktop получает ускоренное обнаружение, но не теряет базовый маршрут.

\n

Учебный пример с отрицательной веткой

\n

Ниже — маленькая модель маршрута. Она принимает три проектных значения: ширина контекста, характеристику основного указателя и доступность hover. Эти строки задаёт тест, а не браузер. Модель не читает matchMedia, не создаёт DOM, не отправляет pointer events и не измеряет людей. Поэтому её результат ограничен проверкой порядка шагов и отказа от hover-only.

\n
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 не откатывает приложение само по себе. Оно говорит, что текущий путь пока нельзя заменять этим учебным маршрутом.

\n
\"Схема
Иллюстрация показывает границу контракта. Это схема переходов, а не снимок браузера и не результат теста пользователей.
\n

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

\n
Рабочая таблица диагностики
СимптомПричинаПроверкаДействие
Команда видна только при наведенииhover владеет переходомнайти DOM-элемент, focus state и visibility rulesдобавить явный button или link
Кнопка находится в последней колонкетаблица требует ширины, которой нетпроверить overflow и порядок данных на узком контейнеревынести основное действие из таблицы, детали оставить отдельно
После раскрытия нет понятного подтвержденияdetails и commit смешаны в одном controlпроверить состояния до и после раскрытияразделить просмотр и необратимое действие
Новый путь вызывает сомнениезамена сделана без безопасной границыописать старый route и условия возвратасохранить current path до отдельной проверки
«Мобильный» тест зелёный, UI не проверенучебную модель приняли за browser testпосмотреть, какие данные реально создаёт тестдобавить отдельную проверку браузера с фиксированными условиями
\n

Таблица нужна для смены уровня разговора. Запись «на телефоне неудобно» не объясняет, что наблюдалось. Запись «в состоянии A переход к details появляется только после hover» уже задаёт проверку. В реальном отчёте добавьте URL или экран, браузер, viewport, масштаб, локаль, способ ввода, начальное состояние и тестовые данные. Не заполняйте неизвестные поля догадками.

\n

Порядок исправления

\n
  1. Зафиксируйте симптом. Назовите одно состояние, в котором основное действие не видно или достижимо только через hover. Отделите наблюдение от предположения о пользователях.
  2. Найдите владельца перехода. Проверьте DOM, состояние компонента, правила видимости, overflow и обработчики. Не меняйте breakpoint, пока не ясно, какой элемент должен вести дальше.
  3. Опишите контракт. Запишите summary, details и confirm. Для каждого шага укажите видимую метку, ожидаемый результат и способ отмены.
  4. Проверьте отрицательный путь. Передайте модели narrow/coarse/unavailable и действие с hover-only. Ожидайте отказ с причиной hover-only-blocked. Если тест принимает путь, контракт ослаблен.
  5. Добавьте явный control. Оставьте контекст перед подтверждением. Не прячьте ошибку, отмену или важные данные в другом hover-меню.
  6. Проверьте реализацию в браузере. Зафиксируйте viewport, zoom, шрифты, локаль, данные, клавиатуру и способ указателя. Проверьте render, focus, keyboard path, раскрытие details и состояние ошибки.
  7. Определите откат. До выпуска сохраните способ вернуть текущий путь: флаг, отдельная ветка, revert или ограниченный rollout. Учебная модель не меняет сервер и не выполняет откат за команду.
\n

Ограничения и критерий готовности

\n

Контракт не выбирает breakpoint и не задаёт универсальный размер зоны нажатия. Он не описывает экранную клавиатуру, поворот экрана, длинные локализованные строки, screen reader, ошибку сети или конкурирующее обновление данных. Он также не доказывает, что человеку понятна метка «Продолжить». Эти вопросы относятся к реализации, доступности и исследованию.

\n

Нельзя объявлять production-эффектом число успешных assertion. В примере нет пользователей, реального устройства, телеметрии и замера времени. Нельзя называть строку narrow конкретным viewport в CSS pixels. Нельзя трактовать hover: none как запрет мыши. Нельзя использовать учебный отказ как автоматическое решение для серверной операции.

\n

Сценарий готов к следующей проверке, если выполнены четыре условия. Для основного действия существует видимый control с предметной меткой. До confirm доступен отдельный summary и путь к details. На заданном учебном профиле hover-only отклоняется, а на реализацию не распространяется обещание usability. В браузере другой инженер может повторить описанное состояние по зафиксированным входам и увидеть тот же порядок: summary → details → confirm.

\n

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

\n" + "title": "Мобильное действие без скрытого шага: маршрут, который не зависит от hover", + "excerpt": "Если главное действие исчезает на узком экране, ищите не только breakpoint, а потерянный маршрут. Разбираем явный mobile-путь, fail-closed проверку и границу между учебной моделью и браузером.", + "contentHtml": "

На телефоне пользователь открывает экран заказа, видит сумму, но не находит переход к подтверждению. На десктопе переход появляется при наведении на строку таблицы. В другом варианте кнопка остаётся в последней колонке и уезжает за горизонтальную прокрутку. Симптом один: человек не может завершить действие, хотя данные уже введены.

Цена ошибки — не только плохое впечатление. Незавершённая форма создаёт обращение в поддержку и провоцирует повторный ввод. Если команда временно увеличивает кнопку, но теряет детали перед подтверждением, новый путь приносит другой риск. Релиз тоже трудно проверить: непонятно, какой элемент владел переходом и при каком состоянии он исчезал.

Мобильный сценарий нужно проверять как маршрут, а не как набор размеров. У основного действия должны быть видимый контекст, явный переход к деталям и отдельное подтверждение. Hover может ускорять обзор, но не должен оставаться единственным способом добраться до операции. Это правило не доказывает удобство интерфейса. Оно задаёт минимальный контракт, который можно проверить в коде, а затем подтвердить в браузере.

Сначала восстановите маршрут

Возьмём одно действие: проверить данные заказа и подтвердить его. Не переделываем весь экран. Сначала описываем три состояния. summary показывает предмет операции и краткий контекст. details открывает данные, которые нужно проверить до необратимого шага. confirm содержит явный элемент управления с понятной меткой.

Такое разбиение отделяет содержание от раскладки. В узком контейнере строки могут перейти в столбец, а детали — открываться во вкладке или disclosure. Но порядок решения сохраняется: человек сначала понимает, что изменится, потом смотрит данные и только затем подтверждает. Если компонент пропускает первый или второй шаг, изменение CSS не исправит потерю смысла.

Минимальный контракт мобильного действия
СостояниеЧто должно быть видноЧто проверяемЧего контракт не обещает
summaryназвание операции и краткий контекстсостояние существует первымчто любой текст поместится в одну строку
detailsявный переход к даннымпереход не требует hoverчто выбранный accordion удобен всем
confirmвидимая метка основного действияэлемент достижим явным вводомразмер зоны нажатия и качество текста
rollbackсохранённый рабочий путьнепринятый маршрут не вытесняет текущийавтоматический откат серверного состояния

Слово «видимый» здесь означает часть проверяемого договора. Оно не означает, что элемент уже имеет правильный размер, цвет, фокус или положение. Эти свойства требуют отдельной проверки разметки и рендера. Контракт не заменяет проверку доступности и исследование удобства.

Механизм: почему hover ломает действие

Media Queries Level 4 определяет hover как способность наводить указатель на элементы с помощью основного pointing device. При нескольких устройствах значение выбирает user agent. Поэтому hover: none не означает «в системе никогда не будет hover» и не доказывает, что доступен только touch. Дополнительная мышь может изменить фактическое поведение псевдокласса :hover, хотя основной профиль останется без hover.

Это media feature не сообщает о клавиатуре или screen reader. pointer: coarse описывает ограниченную точность основного pointing device, но не навык человека и не запрет точного клика. Для всех доступных pointing devices существуют any-pointer и any-hover; они также не учитывают непоинтерные способы ввода. Эти значения помогают выбрать presentation, но не дают разрешения сделать обязательную операцию hover-only.

Сбой появляется, когда команда превращает характеристику среды в единственный маршрут. Иконка показывает tooltip только при наведении. Строка становится ссылкой только в :hover. Действие раскрывается движением указателя, но не имеет кнопки и не получает фокус. На телефоне такой путь может исчезнуть, а на клавиатуре — остаться недостижимым.

Безопаснее разделить основной элемент управления и подсказку. Основной control присутствует в DOM и имеет текстовую метку. Hover может показать дополнительные сведения или подсветить область. Если hover недоступен, сведения остаются достижимыми через явную кнопку или ссылку. На широкой странице desktop получает ускоренное обнаружение, но не теряет базовый маршрут.

Учебный пример с отрицательной веткой

Ниже — маленькая модель маршрута. Она принимает проектные значения: условную ширину контекста, характеристику основного указателя и capability 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    && ['none', 'coarse', 'fine'].includes(profile.primaryPointer)\n    && ['none', 'hover'].includes(profile.hover);\n\n  const validActivation =\n    action?.activation === 'explicit'\n    || action?.activation === 'hover-only';\n\n  if (!validProfile || !action?.id || !action?.label || !validActivation) {\n    return { ...empty, accepted: false, reason: 'invalid-contract' };\n  }\n\n  const requiresExplicit = profile.viewport === 'narrow'\n    || profile.primaryPointer !== 'fine'\n    || profile.hover === 'none';\n  const steps = ['summary', 'details', 'confirm'];\n\n  if (action.activation === 'hover-only' && requiresExplicit) {\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 mobile = {\n  viewport: 'narrow',\n  primaryPointer: 'coarse',\n  hover: 'none',\n};\n\nconst route = planMobilePath(mobile, {\n  id: 'confirm-order',\n  label: 'Подтвердить заказ',\n  activation: 'explicit',\n});\nconsole.log(route.accepted); // true\nconsole.log(route.steps); // summary, details, confirm\n\nconst rejected = planMobilePath(mobile, {\n  id: 'open-details',\n  label: 'Открыть итог',\n  activation: 'hover-only',\n});\nconsole.log(rejected.reason); // hover-only-blocked\n\nconst malformed = planMobilePath(mobile, {\n  id: 'confirm-order',\n  label: 'Подтвердить заказ',\n});\nconsole.log(malformed.reason); // invalid-contract

Положительная ветка подтверждает только три вещи: профиль и действие соответствуют входному контракту, шаги идут в заданном порядке, явное действие принято. Отрицательная ветка важнее зелёной: для narrow/coarse/none она запрещает объявить hover-only маршрутом. Неизвестная активация также отклоняется, поэтому случайно добавленный тип не становится разрешённым по умолчанию.

В профиле wide/fine/hover модель не блокирует hover-only, потому что это учебная проверка именно мобильной развилки. Это не разрешение сделать обязательную операцию зависимой от hover в продукте: для неё всё равно нужен явный control и отдельная проверка клавиатурного пути. Значение keep-current-path не откатывает приложение. Оно запрещает заменять текущий маршрут этим учебным вариантом.

Схема мобильного маршрута: summary ведёт к details, затем к явному confirm; hover-only на профиле narrow, coarse, none блокируется и сохраняет текущий путь.
Иллюстрация показывает границу контракта. Это схема переходов для учебного mobile-профиля, а не снимок браузера и не результат теста пользователей.

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

Рабочая таблица диагностики
СимптомПричинаПроверкаДействие
Команда видна только при наведенииhover владеет переходомнайти DOM-элемент, focus state и visibility rulesдобавить явный button или link
Кнопка находится в последней колонкетаблица требует ширины, которой нетпроверить overflow и порядок данных в узком контейнеревынести основное действие из таблицы, детали оставить отдельно
После раскрытия нет понятного подтвержденияdetails и commit смешаны в одном controlпроверить состояния до и после раскрытияразделить просмотр и необратимое действие
Новый путь вызывает сомнениезамена сделана без безопасной границыописать старый route и условия возвратасохранить current path до отдельной проверки
«Мобильный» тест зелёный, UI не проверенучебную модель приняли за browser testпосмотреть, какие данные реально создаёт тестдобавить отдельную проверку браузера с фиксированными условиями

Таблица меняет уровень разговора. Запись «на телефоне неудобно» не объясняет, что наблюдалось. Запись «в состоянии A переход к details появляется только после hover» уже задаёт проверку. В реальном отчёте добавьте URL или экран, браузер, viewport, масштаб, локаль, способ ввода, начальное состояние и тестовые данные. Не заполняйте неизвестные поля догадками.

Порядок исправления

  1. Зафиксируйте симптом. Назовите одно состояние, в котором основное действие не видно или достижимо только через hover. Отделите наблюдение от предположения о пользователях.
  2. Зафиксируйте условия. Укажите экран, данные, локаль, viewport, масштаб, способ ввода и состояние компонента. Не подставляйте значения, которых никто не наблюдал.
  3. Найдите владельца перехода. Проверьте DOM, состояние компонента, правила видимости, overflow и обработчики. Не меняйте breakpoint, пока не ясно, какой элемент должен вести дальше.
  4. Опишите контракт. Запишите summary, details и confirm. Для каждого шага укажите видимую метку, ожидаемый результат и способ отмены.
  5. Проверьте отрицательную ветку. Передайте модели narrow/coarse/none и действие с hover-only. Ожидайте отказ с причиной hover-only-blocked. Отдельно передайте действие без activation и ожидайте invalid-contract.
  6. Добавьте явный control. Оставьте контекст перед подтверждением. Не прячьте ошибку, отмену или важные данные в другом hover-меню.
  7. Проверьте реализацию в браузере. Зафиксируйте viewport, zoom, шрифты, локаль, данные, клавиатуру и способ указателя. Проверьте render, focus, keyboard path, раскрытие details и состояние ошибки.
  8. Определите откат. До выпуска сохраните способ вернуть текущий путь: флаг, отдельная ветка, revert или ограниченный rollout. Учебная модель не меняет сервер и не выполняет откат за команду.

Ограничения и критерий готовности

Контракт не выбирает breakpoint и не задаёт универсальный размер зоны нажатия. Он не описывает экранную клавиатуру, поворот экрана, длинные локализованные строки, screen reader, ошибку сети или конкурирующее обновление данных. Он также не доказывает, что человеку понятна метка «Продолжить». Эти вопросы относятся к реализации, доступности и исследованию.

Нельзя объявлять production-эффектом число успешных assertion. В примере нет пользователей, реального устройства, телеметрии и замера времени. Нельзя называть строку narrow конкретным viewport в CSS pixels. Нельзя трактовать hover: none как запрет мыши. Нельзя использовать учебный отказ как автоматическое решение для серверной операции.

Сценарий готов к следующей проверке, если выполнены четыре условия. Для основного действия существует видимый control с предметной меткой. До confirm доступен отдельный summary и путь к details. На заданном учебном профиле hover-only отклоняется, а неизвестная активация не проходит fail-closed проверку. В браузере другой инженер может повторить описанное состояние по зафиксированным входам и увидеть тот же порядок: summary → details → confirm.

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

" }