Привет, я Андрей Беннер, фронтенд-разработчик в 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 больше не раздражает, а ручное прописывание условий стало не нашей головной болью — так мы (а теперь и вы) можем чуть расслабиться и довериться автоматике.
