Событием колеса мыши редко занимаются специально. Обычный <div> со стандартным скроллом прекрасно работает без единой строчки JS. К wheel-событию обращаются, когда браузерного скролла недостаточно: кастомный скроллер, canvas-редактор с зумом колесом, галерея с горизонтальной прокруткой, панорама на карте. И вот тут выясняется, что за простым на вид событием стоит много ньюансов и боли. Здесь мы попробуем разобраться в том, как это все работает и почему именно так.
Первое, что стоит подсветить: wheel и scroll это разные события с разной природой.
wheel возникает от физического движения колеса или тачпада и не обязан приводить к прокрутке. Элемент может быть вообще нескроллящимся, а событие всё равно придёт.
scroll, наоборот, срабатывает при любом изменении позиции прокрутки: от клавиатуры, от перетаскивания скроллбара, от scrollTo() и независимо от того, было колесо задействовано или нет.
Спецификация UI Events прямо предупреждает: даже если wheel-событие вызвало прокрутку, значения дельты не обязательно совпадают с тем, насколько именно сдвинулся контент.
Ловушка deltaModeWheelEvent содержит три числа: deltaX, deltaY, deltaZ. Это горизонтальный, вертикальный сдвиг и «зум» по оси Z (актуален для трёхмерных сцен и жестов масштабирования). Знак определяется правосторонней системой координат: положительная ось Y направлена вниз, получается deltaY > 0 обозначает прокрутку содержимого вниз.
Остается вопрос. В каких единицах измерения приходят эти числа? И тут мы конечно ожидаем пиксели, как что-то стандартное и привычное. Но не стоит забывать про deltaMode, который так же приходит в WheelEvent. Он может быть разных типов:
DOM_DELTA_PIXEL- дельта в пикселях;DOM_DELTA_LINE- дельта в «строках» (высота строки считается как font-size);DOM_DELTA_PAGE- дельта в «страницах».
Какой тип придет зависит от движка браузера и системных настроек ОС, но про это чуть позже.
Поведение явно спорное и это показывает реальный случай: сайт privacy.google.com жёстко предполагал пиксели и в Firefox прокрутка у них просто ломалась. Подробнее можно почитать багтрекере Mozilla по ссылке. И кажется на момент написания статьи вопрос остается открытым в связанном баге.
Когда какой режимВот тут начинается расхождение, которое видимо стоило веб-разработчикам немало нервов. Chrome и Safari практически во всех конфигурациях ОС отдают DOM_DELTA_PIXEL причем независимо от того, что на самом деле сообщило оборудование. Firefox исторически поступал иначе: отдавал «lossless»-значение от нативного события, и для обычной мыши с дискретным колесом это означало DOM_DELTA_LINE, где deltaY: ±1 за один щелчок колеса.
Разработчик Gecko-реализации WheelEvent в одном из обсуждений в Bugzilla прямо признал: раз Chrome и Edge почти всегда используют DOM_DELTA_PIXEL, множество сайтов, не проверяющих deltaMode, ломаются именно в Firefox. Хотя Firefox единственный соблюдает букву спецификации. В итоге команда Firefox констатировала, что цена совместимости выше, чем ценность «честных» единиц измерения, и начала склоняться к пиксельному режиму по умолчанию.
Для простоты пока можно пользоваться утверждением: в Chrome почти всегда DOM_DELTA_PIXEL, Firefox почти всегда DOM_DELTA_LINE.
Пинч-зум(Pinch zoom) - это жест масштабирования содержимого на экране двумя пальцами.
Когда пользователь сводит или разводит два пальца на трекпаде, браузеры не создают отдельное «событие зума», а переиспользуют обычный wheel.
Chrome и Firefox делают так: присылают wheel-событие, но с флагом ctrlKey: true. По этому флагу разработчик должен догадаться, что это не прокрутка, это зум и взять deltaY не как сдвиг в пикселях, а как готовый коэффициент масштаба. Так исторически делал ещё Internet Explorer, и остальные подхватили это поведение.
Safari пошёл по-своему. У него нет ctrlKey-трюка, но вместо этого с версии 9.1 есть отдельное, собственное событие gesturestart / gesturechange / gestureend, которое сразу приходит с готовыми полями scale (насколько увеличили/уменьшили) и rotation (насколько повернули).
То, что физически ощущается пользователем как «один щелчок колеса», кодируется на уровне ОС по-разному.
Для Windows нативное сообщение WM_MOUSEWHEEL даёт дельту, кратную 120 на один щелчок. Это исторический стандарт ещё с Windows 95. Пользовательская настройка «сколько строк прокручивать за один щелчок» интерпретируется браузером самостоятельно, а не ОС. Иными словами, ОС лишь хранит настройку, а применяет её браузер.
В Linux похожая история: одно нативное wheel-событие даёт 120 или −120. Поведение здесь ближе к Windows.
В macOS все сложнее. Значение зависит от того, поддерживает ли устройство непрерывную прокрутку. Трекпад MacBook и «плавные» колёса мышей дают ускоренную, непрерывную дельту (дробные значения, которые меняются в зависимости от скорости жеста). Обычное дискретное колесо старого образца даёт фиксированные 120 на щелчок, без ускорения. Это значит, что на одном и том же Mac одно и то же приложение может получать структурно разные данные в зависимости от того, какое устройство ввода к нему подключено, а достоверно определить, какое именно устройство сгенерировало событие, у веб-страницы возможности нет.
Еще один нюанс. В операционной системе можно изменить настройку «Направление скрола», в зависимости от нее ожидаемо поменяется и знак дельты при движении трекпада.
Трекпад против мышиОтдельно стоит разница между внешней мышью с механическим или оптическим колесом и трекпадом. Колесо мыши физически дискретно то есть оно устроено так, что регистрирует шаги, и даже «свободно вращающиеся» колёса без выраженных щелчков всё равно дают грубые, ступенчатые события. Трекпад, наоборот, способен генерировать по-настоящему плавный, непрерывный поток дельт.
Таким образом, не имея признака устройства в событии wheel, отличить мышь от трекпада все же можно, оценив приходящие дельты. Для трекпада характерны дробные числа и разная скорость скрола. Для колесика мыши события приходят реже, с фиксированным шагом и с большим значением дельты за раз.
Практических выводов из всего вышеперечисленного немного:
- Всегда проверяйте
deltaMode, прежде чем использоватьdeltaX/deltaYкак пиксели. Если он неDOM_DELTA_PIXEL, домножьте на примерную высоту строки (16–20pxкак грубое приближение) или на высоту вьюпорта для режима страниц. - Не полагайтесь на конкретную величину дельты как на признак устройства или скорости. Она регулируется пользовательскими настройками ОС и может меняться даже на одной машине.
- Не хардкодьте знак направления прокрутки потому-что он может зависеть настройки операционной системы.
- Для пинч-зума разводите Safari и Chrome/Firefox отдельно:
gesturestart/gesturechangeпротивwheelсctrlKey. - Проверяйте, что
addEventListenerимеет{ passive: true }, если не вызываетеpreventDefault(). Такой подход рекомендует спецификация ради производительности скролла, и без неё браузер вынужден ждать выполнения обработчика перед тем, как начать прокрутку.