8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 190,
|
||
"slug": "editorial-2022-09-field-mobile-ux",
|
||
"title": "Мобильный интерфейс: как вернуть скрытое действие и не сломать откат",
|
||
"excerpt": "Если на узком экране основной шаг виден только при наведении или теряется за таблицей, сначала восстановите наблюдаемое действие, затем проверьте причину и только после этого меняйте layout.",
|
||
"contentHtml": "<p>Пользователь открывает карточку на телефоне и видит данные, но не может перейти к подтверждению. Кнопка появляется только при наведении, уезжает за горизонтальный scroll или растворяется после смены ориентации. Иногда экран выглядит аккуратно: шрифт не наезжает, карточки складываются в одну колонку. Но сценарий всё равно обрывается. Цена ошибки — не только плохой отзыв. Человек может повторить операцию, обратиться в поддержку или подтвердить не то действие. Команда потратит релиз на косметический breakpoint и сохранит исходную зависимость.</p>\n<p>Тезис простой: мобильный интерфейс готов, когда основной путь остаётся видимым, понятным и обратимым при зафиксированных условиях ввода и ширины. Media query помогает выбрать представление. Она не доказывает, что действие достижимо. Для проверки нужно разделить четыре вещи: наблюдаемый симптом, техническую причину, проверку реализации и действие с понятным откатом.</p>\n<h2>Начните с конкретного разрыва</h2>\n<p>Фраза «мобильная версия неудобна» не задаёт следующую проверку. Запишите один разрыв: «в состоянии narrow карточка показывает summary, но переход к details доступен только через hover» или «confirm находится справа от таблицы, а контейнер не сообщает, что его можно прокрутить». Добавьте экран, состояние данных, viewport, масштаб, язык, способ ввода и ожидаемый результат, если эти факты известны. Не дополняйте отчёт модельным телефоном или выдуманным пользовательским наблюдением.</p>\n<p>Затем отделите симптом от причины. Скрытая кнопка может быть следствием <code>display</code>, переполнения, неверного component state, условного рендера или решения считать hover достаточным маршрутом. Один и тот же симптом требует разных действий. Если сразу менять ширину breakpoint, вы проверите только одну гипотезу и можете замаскировать ошибку состояния.</p>\n<h2>Механизм: среда, компонент и продуктовый контракт</h2>\n<p>Среда сообщает ограниченные свойства. Media Queries Level 4 описывает media features, включая <code>pointer</code>, <code>hover</code>, <code>any-pointer</code> и <code>any-hover</code>. Эти признаки относятся к возможностям указателей и user agent. Они не описывают намерение человека, его зрение, размер пальца или то, заметил ли он control. В гибридном устройстве primary pointer и любой доступный pointer могут вести к разным значениям.</p>\n<p>Компонент отвечает за маршрут и состояние. В нём должны существовать label действия, summary перед необратимым шагом, details до подтверждения, ошибки и отмена. Продуктовый контракт отвечает за правило: главное действие нельзя оставлять только в hover-ветке, если выбранный путь должен работать без наведения. Это правило не следует автоматически из CSS-спецификации. Его нужно явно принять для конкретной операции.</p>\n<p>Такое разделение убирает ложный вывод «<code>hover: none</code> означает touch-only». Запрос описывает capability среды, а не тип человека. Он может включить дополнительную панель или изменить плотность layout. Он не должен удалять единственный путь к действию. Pointer Events задаёт модель событий, но наличие события не подтверждает, что пользователь увидел control или смог завершить операцию.</p>\n<table><caption>Разделение границ перед исправлением</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Что можно наблюдать</th><th scope=\"col\">Что проверять</th><th scope=\"col\">Чего нельзя заключать</th></tr></thead><tbody><tr><td>Среда</td><td>media feature или тип pointer</td><td>какое представление применилось</td><td>что человеку удобно</td></tr><tr><td>Компонент</td><td>DOM, CSS и state</td><td>видимы ли label, details и confirm</td><td>что сценарий прошёл на всех устройствах</td></tr><tr><td>Продукт</td><td>главный и необратимый шаг</td><td>есть ли явный маршрут и отмена</td><td>что изменится конверсия</td></tr><tr><td>Исследование</td><td>наблюдение по описанному методу</td><td>условия, выборка и сигнал</td><td>что локальная fixture заменила людей</td></tr></tbody></table>\n<h2>Учебный пример: запретить hover-only до браузерной проверки</h2>\n<p>Ниже — ограниченный пример. Он принимает заранее заданный профиль и контракт действия. Он не вызывает <code>matchMedia</code>, не рендерит DOM, не создаёт pointer events и не сообщает о поведении пользователей. Его задача — проверить порядок решения: если основной шаг зависит только от hover в профиле без надёжного наведения, модель отклоняет маршрут и сохраняет текущий путь.</p>\n<pre><code>function chooseMobileRoute(profile, action) {\n if (!profile || !action?.label) {\n return { status: 'invalid-contract' };\n }\n\n const narrowInput = profile.width === 'narrow' || profile.hover === 'none';\n const hoverOnly = action.activation === 'hover-only';\n\n if (narrowInput && hoverOnly) {\n return { status: 'blocked', reason: 'explicit-control-required', rollback: 'keep-current-path' };\n }\n\n return { status: 'allowed', route: ['summary', 'details', 'confirm'] };\n}</code></pre>\n<p>В этом примере <code>blocked</code> не означает, что браузер сломан. Он означает, что выбранный контракт не принимает единственный hover-маршрут при заданном учебном профиле. Значение <code>keep-current-path</code> тоже не запускает автоматический rollback. Оно запрещает принять новую ветку без решения о безопасном возвращении. В рабочем приложении откат может быть feature flag, старым route, revert commit или другим механизмом. Его выбирает владелец состояния операции.</p>\n<figure><img src=\"/assets/editorial/2022/mobile-ux-2022-diagnosis-rollback.svg\" alt=\"Диагностический маршрут: симптом скрытого действия ведёт к проверке DOM и контракта, hover-only блокируется, явный control ведёт к проверке браузером, а при неопределённости сохраняется текущий путь\" loading=\"lazy\" /><figcaption>Схема показывает границу между локальным контрактом и проверкой реальной реализации. Она не изображает снимок браузера и не содержит production-результатов.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностическая карта мобильного маршрута</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Кнопка видна только при hover</td><td>Главный путь привязали к hover-ветке</td><td>Проверить DOM, CSS visibility и keyboard path</td><td>Добавить видимый labelled control</td></tr><tr><td>Confirm уехал за таблицу</td><td>Контейнер скрывает overflow или порядок выбран неверно</td><td>Проверить размеры, scroll boundary и порядок шагов</td><td>Вынести confirm после summary/details или дать явный scroll</td></tr><tr><td>Иконка есть, смысл неясен</td><td>Label заменили декоративным знаком</td><td>Проверить accessible name и текст до клика</td><td>Вернуть короткую метку и предмет действия</td></tr><tr><td>После раскрытия теряется focus</td><td>State меняет DOM без управляемого focus</td><td>Пройти клавиатурой и проверить focus order</td><td>Сохранить фокус и объявить новое состояние</td></tr><tr><td>После исправления непонятно, что откатывать</td><td>Новый route смешан с серверным состоянием</td><td>Назвать владельца состояния и необратимый шаг</td><td>Сохранить прежний route до проверки и описать rollback</td></tr></tbody></table>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите одно недоступное действие и условия, в которых оно исчезает. Не заменяйте описание ярлыком «плохой mobile UX».</li><li><strong>Найдите владельца перехода.</strong> Посмотрите DOM, component state, CSS visibility, overflow и route contract. Установите, построен ли control и только скрыт или он вообще не рендерится.</li><li><strong>Отделите capability от предположения.</strong> Если код читает media features, запишите, что именно они сообщают. Не называйте их классификатором пользователя и не делайте из них доказательство удобства.</li><li><strong>Проверьте контракт.</strong> Убедитесь, что путь сохраняет summary → details → confirm, имеет label, ошибку, отмену и явный способ активации. Учебный код проверяет только этот порядок.</li><li><strong>Исправьте маршрут.</strong> Добавьте видимый control, сохраните контекст перед подтверждением и не прячьте отмену или ошибку за hover. Не начинайте с декоративных отступов.</li><li><strong>Проверьте реализацию в браузере.</strong> Зафиксируйте viewport, zoom, шрифты, локаль, данные, клавиатуру, pointer и orientation. Пройдите normal, loading, error, empty и long-content состояния.</li><li><strong>Проверьте отрицательный путь.</strong> Остановитесь, если неизвестен владелец state, отсутствует label, данные меняются конкурентно, focus теряется или новый путь может повторить необратимое действие.</li><li><strong>Оформите откат.</strong> До выпуска назовите старый route, условие остановки и способ возврата. Не удаляйте прежнюю ветку только потому, что локальная модель вернула <code>allowed</code>.</li></ol>\n<h2>Что проверка должна показать</h2>\n<p>В browser-check основной action должен быть виден без hover. Перед ним должна быть понятна предметная операция. Details должны открываться явным управляемым способом. После открытия focus не должен исчезать, а ошибка должна оставаться читаемой в узком контейнере. Если таблица требует прокрутки, интерфейс должен сообщать о границе и не прятать единственный confirm за ней. Эти утверждения относятся к конкретной реализации и набору условий проверки.</p>\n<p>Проверьте не только узкий viewport. Медленный шрифт, длинная локаль, крупный системный текст, пустые данные, длинное имя и landscape могут сломать маршрут иначе, чем условные 320 или 375 CSS-пикселей. Проверяйте фактический текст. «Одна колонка» не является доказательством reflow, а большой touch target не объясняет, что именно он подтверждает.</p>\n<p>Нужен usability-test — проведите отдельный метод с задачей, условиями набора и способом записи наблюдений. Нужен accessibility review — зафиксируйте критерии и scope. Учебная функция и unit assertion не заменяют ни то ни другое. Нельзя писать «девять пользователей успешно прошли сценарий», если есть только девять проходов функции.</p>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Media query не решает проблему данных, авторизации, сетевой задержки, серверного конфликта или повторного подтверждения. Pointer event не проверяет зрительную заметность, фокус, semantics и размер текста. Browser test не доказывает потребности всех аудиторий. Accessibility-критерий не обещает коммерческий эффект. Каждый результат нужно связывать с тем артефактом и условиями, которые его получили.</p>\n<p>Остановитесь, если причина остаётся неразличимой. Например, если кнопка не видна, но неизвестно, отсутствует ли она в DOM или скрыта стилем, сначала получите этот факт. Остановитесь также, если новый маршрут меняет смысл операции и откат не возвращает прежнее состояние. Нельзя называть отсутствие production-данных доказательством отсутствия проблемы. В этом случае записывают неизвестное и не усиливают формулировку.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Работа готова к следующему этапу, если другой инженер без устного пояснения может ответить на пять вопросов: какое действие было недоступно; при каких условиях; где находится причина; каким тестом проверяется исправление; что произойдёт при отказе или откате. В browser-check есть видимый label, путь без hover, сохранённый focus, обработанные loading/error состояния и проверенная граница overflow. В задаче указаны владелец состояния и точка остановки.</p>\n<p>Если остаётся только фраза «на телефоне стало лучше», готовности нет. Если модель прошла, но браузерный путь не проверен, готов локальный контракт, а не интерфейс. Если браузерный путь прошёл, но неизвестно, как вернуть старый route, выпуск не готов. Такой критерий не обещает успех продукта. Он показывает, что команда проверила именно тот разрыв, который заявила, и умеет остановиться на отрицательной ветке.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/mediaqueries-4/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Media Queries Level 4</a> — определяет media queries и interaction features <code>pointer</code>, <code>hover</code>, <code>any-pointer</code> и <code>any-hover</code>; спецификация не описывает удобство конкретного человека.</li><li><a href=\"https://www.w3.org/TR/WCAG22/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Web Content Accessibility Guidelines (WCAG) 2.2</a> — задаёт проверяемые критерии доступности веб-контента; соответствие требует оценки конкретной реализации и контента.</li><li><a href=\"https://www.w3.org/TR/pointerevents2/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Pointer Events Level 2</a> — описывает модель pointer events и тип указателя; наличие события само по себе не подтверждает достижимость действия.</li></ul>"
|
||
}
|