8 lines
21 KiB
JSON
8 lines
21 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 и устройства вывода, а не классификация человека. В гибридном устройстве primary pointer и любой доступный pointer могут иметь разные значения. Поэтому условие <code>hover: none</code> нельзя читать как «это точно touch-only» и нельзя использовать как доказательство удобства.</p>\n<p>Компонент отвечает за маршрут и состояние. В нём должны существовать label действия, summary перед необратимым шагом, details до подтверждения, ошибка и отмена. Продуктовый контракт отвечает за отдельное правило: главное действие нельзя оставлять только в hover-ветке, если выбранный путь должен работать без наведения. Это правило не следует автоматически из CSS-спецификации; его принимают для конкретной операции.</p>\n<p>Pointer Events задаёт модель событий и тип указателя. Наличие события ещё не подтверждает, что control виден, назван и позволяет завершить операцию. WCAG 2.1 помогает проверить клавиатурный путь и reflow, но соответствие относится к конкретной разметке и условиям. Ни одна из этих спецификаций не заменяет проверку фактического маршрута в браузере.</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>Учебный контракт с воспроизводимым результатом</h2>\n<p>Ниже — самостоятельный пример на JavaScript. Он не вызывает <code>matchMedia</code>, не рендерит DOM, не создаёт pointer events и не сообщает о поведении пользователей. Профиль и действие заданы вручную, поэтому функция проверяет только локальный порядок решения: hover-only блокируется на ограниченном пути, а явный control сохраняет маршрут <code>summary → details → confirm</code>.</p>\n<pre><code>function chooseMobileRoute(profile, action) {\n if (!profile || !action?.label) {\n return { status: 'invalid-contract' };\n }\n\n const constrainedPath = profile.width === 'narrow' || profile.hover === 'none';\n const hoverOnly = action.activation === 'hover-only';\n\n if (constrainedPath && hoverOnly) {\n return {\n status: 'blocked',\n reason: 'explicit-control-required',\n rollback: 'keep-current-path',\n };\n }\n\n if (action.activation !== 'explicit') {\n return { status: 'blocked', reason: 'unknown-activation' };\n }\n\n return { status: 'allowed', route: ['summary', 'details', 'confirm'] };\n}\n\nconst cases = [\n {\n profile: { width: 'narrow', hover: 'none' },\n action: { label: 'Открыть подтверждение', activation: 'hover-only' },\n },\n {\n profile: { width: 'narrow', hover: 'none' },\n action: { label: 'Открыть подтверждение', activation: 'explicit' },\n },\n];\n\nconsole.log(cases.map(({ profile, action }) => chooseMobileRoute(profile, action)));</code></pre>\n<p>Ожидаемый результат — сначала объект со статусом <code>blocked</code>, причиной <code>explicit-control-required</code> и rollback-маркером <code>keep-current-path</code>; затем объект со статусом <code>allowed</code> и маршрутом <code>summary</code>, <code>details</code>, <code>confirm</code>. Этот вывод можно получить обычным запуском файла в Node.js. Он не означает, что браузер уже проверен: <code>keep-current-path</code> запрещает принять новую ветку, но не выполняет автоматический откат приложения.</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 не должен прятаться за overflow. Для клавиатуры отдельно пройдите Tab, активацию и возврат фокуса. Для reflow проверьте узкую область просмотра без потери содержимого и действия.</p>\n<p>Проверьте не только условные 320 или 375 CSS-пикселей. Длинная локаль, крупный системный текст, пустые данные, длинное имя и landscape могут сломать маршрут иначе. Фиксируйте фактический текст, browser, zoom, viewport, способ ввода и состояние данных. «Одна колонка» не является доказательством 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<p>Для материала о сентябре 2022 года используются версии, доступные к этой дате: Media Queries Level 4 Candidate Recommendation Draft от 25 декабря 2021 года, Pointer Events Level 2 Recommendation от 4 апреля 2019 года и WCAG 2.1 Recommendation от 5 июня 2018 года. Плавающие текущие страницы не нужны для доказательства исторического утверждения; важны датированные документы и их ограниченный статус.</p>\n<ul><li><a href=\"https://www.w3.org/TR/2021/CRD-mediaqueries-4-20211225/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Media Queries Level 4, CRD 25.12.2021</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/2019/REC-pointerevents2-20190404/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Pointer Events Level 2, Recommendation 04.04.2019</a> — описывает модель pointer events и тип указателя; событие само по себе не подтверждает достижимость действия.</li><li><a href=\"https://www.w3.org/TR/2018/REC-WCAG21-20180605/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WCAG 2.1, Recommendation 05.06.2018</a> — задаёт критерии доступности, включая клавиатурное взаимодействие и reflow; соответствие требует проверки конкретной реализации.</li></ul>"
|
||
}
|