Привет, я Андрей Беннер, фронтенд-разработчик в Mish. Сейчас расскажем, как мы решали проблему залипающего hover на мобильных устройствах.

Залипающий hover на мобильных устройствах – частый баг. Раньше мы решали это просто плагином PostCSS Hover Media Feature, он оборачивает все псевдоклассы :hover в медиазапрос @media (hover: hover) {}. Так мы проверяли поддержку hover и отключали его на мобилках. Но во время тестирования одного проекта оказалось, что на некоторых устройствах hover продолжает залипать.

Первая теория: плагин работает некорректно

Что мы проверили:

  • Плагин не оборачивает правила? Нет, всё ок. После сборки hover был в медиазапросах.

  • Не работает hover: hover? Проверили в эмуляции мобильных устройств — работает.

Значит, проблема не в плагине.

Вторая теория: баг на отдельных устройствах

Мы начали тестировать разн��е модели через Browserstack. На большинстве устройств всё нормально, но есть одно но.

Samsung Galaxy игнорирует всё.

Ему всё равно, что мы оборачиваем hover в @media (hover: hover). Он отвечает: «Да-да, я поддерживаю hover», а потом предательски залипает.

Дальнейшая проверка показала, что дело не только в Galaxy. Похожая история случается и на некоторых моделях Vivo.

В процессе поиска ответов мы наткнулись на интересную статью о том, как определять устройства с тачскрином на чистом CSS. Такой способ удобен и для SSR, поэтому мы его изучили. Были варианты и с хуками, но они работают только на клиенте.

Как проверить вашу мобилку

Если интересно, вот ссылки, чтобы прогнать ваши устройства:

Глубже в проблему: откуда берётся true?

Возникает логичный вопрос: как браузер определяет, что событие hover поддерживается?

Мы обратились к исходникам Chromium. Там есть файл event.cc, где перечислены все события. В нём видно: браузер получает события от ОС.

А в файле platform_event.h можно заметить, что тип события зависит от платформы (Windows, Mac, iOS, Linux).

То есть:

  • Аппаратная часть устройства сообщает ОС, что за событие произошло.

  • ОС передаёт его браузеру через драйверы и обработчики.

  • Браузер преобразует его в знакомые нам события, с которыми мы работаем.

Отсюда и ответ: если устройство сообщает, что hover есть, браузер возвращает true на window.matchMedia(’(hover: hover)’).

MDN и медиазапрос pointer

На MDN мы нашли медиазапрос pointer. Он определяет, какое устройство ввода используется:

  • fine — точное устройство (мышь, тачпад, стилус).

  • coarse — неточное (сенсорный экран).

Проверить это можно так:

window.matchMedia('(pointer: coarse)');
window.matchMedia(’(pointer: fine)’).

Метод возвращает объект с полем matches, где и видно, какое устройство ввода сейчас активно.

Почему pointer: fine недостаточно

На первый взгляд кажется: давайте использовать только pointer: fine и забудем про hover. Но не всё так просто.

  • У некоторых устройств pointer: fine возвращает true, но hover они не поддерживают.

  • Стилусы иногда определяются как fine, но тоже не имеют hover.

 

Поэтому мы должны проверять оба условия:

@media (hover: hover) and (pointer: fine) { ... }

А как же спецификация?

Если обратиться к спеке W3C, то там есть важное уточнение. Даже если устройство имеет pointer: coarse, это не значит, что точные клики невозможны. Это говорит о том, что мы можем совершать точные клики при помощи того же стилуса.

Иными словами: медиазапросы дают разработчику лишь подсказку о том, как лучше строить интерфейс, а не жёсткую гарантию.

Решение:

Мы пришли к следующему варианту: используем медиазапрос (hover: hover) and (pointer: fine), чтобы hover-стили применялись только на устройствах с мышью или тачпадом.

Варианты внедрения

  • Обернуть стили вручную. Подходит для небольших проектов.

  • Допилить оригинальный плагин.

  • Написать свой.

Так как в репозитории оригинального плагина давно висят нерешённые реквесты, мы решили не ждать и сделать свой.

Наш плагин

Когда стало ясно, что простого hover: hover недостаточно, мы решили создать свой плагин.

Чтобы сделать использование плагина удобнее и безопаснее, мы сделали версию на TypeScript.

Это дало:

  • Чёткую типизацию входных и выходных данных;

  • Упрощённое отлавливание ошибок на этапе разработки;

  • Более структурный и понятный код для будущих изменений.

Теперь у нас есть «боевая» версия плагина, которую можно смело подключать в проект и не переживать за hover на мобильных устройствах.

Итог

История с залипающим hover показала: полагаться только на hover: hover не стоит — иногда он «залипает» и слегка раздражает пользователя, хотя интерфейс при этом и не ломается.

С нашим решением hover остаётся только там, где он действительно нужен, а «притворщики» типа тачскринов больше не вмешиваются.

В результате мы получили полезный и интересный опыт в автоматическом оборачивании hover в медиазапросы с более полным условием. Залипающий hover больше не раздражает, а ручное прописывание условий стало не нашей головной болью — так мы (а теперь и вы) можем чуть расслабиться и довериться автоматике.