Horizontal Scrolling on React Component Using Vertical Mouse Wheel
Asked Answered
W

7

23

I have a component that resizes into a horizontal row of bootstrap cards when in a smaller desktop window. For users without a horizontal mouse wheel and not using a touchpad, I would like to allow users to scroll horizontally using their vertical mouse wheel movements when hovering over this particular component.

Here is the original StackOverflow issue I based my code off of: https://mcmap.net/q/385754/-horizontal-scrolling-with-mouse-wheel-in-a-div

Horizontal Scroll helper component:

function horizontalScroll (event) {
  const delta = Math.max(-1, Math.min(1, (event.nativeEvent.wheelDelta || -event.nativeEvent.detail)))
  event.currentTarget.scrollLeft -= (delta * 10)
  event.preventDefault
}

How I've implemented it on component requiring horizontal scrolling:

<Row className='announcements-home' onWheel={horizontalScroll} >

When I've placed this horizontalScroll helper function within the React onWheel event, it scrolls horizontally AND vertically. My desired outcome is just horizontal scrolling. Also, Firefox does not appear to respond at all to horizontal scrolling with these changes.

Windle answered 15/5, 2019 at 16:24 Comment(0)
M
21

Okay, so the issue seems to be that you only refer to the function event.preventDefault rather than invoking it. Adding some brackets at the end to invoke it should do the trick: event.preventDefault().

I however found this issue while looking for some simple code to use, so I will also leave the hook I made for this if others in the same situation:

import { useRef, useEffect } from "react";

export function useHorizontalScroll() {
  const elRef = useRef();
  useEffect(() => {
    const el = elRef.current;
    if (el) {
      const onWheel = e => {
        if (e.deltaY == 0) return;
        e.preventDefault();
        el.scrollTo({
          left: el.scrollLeft + e.deltaY,
          behavior: "smooth"
        });
      };
      el.addEventListener("wheel", onWheel);
      return () => el.removeEventListener("wheel", onWheel);
    }
  }, []);
  return elRef;
}

Usage:

import React from "react";
import { useSideScroll } from "./useSideScroll";

export const SideScrollTest = () => {
  const scrollRef = useHorizontalScroll();
  return (
    <div ref={scrollRef} style={{ width: 300, overflow: "auto" }}>
      <div style={{ whiteSpace: "nowrap" }}>
        I will definitely overflow due to the small width of my parent container
      </div>
    </div>
  );
};

Note: The scroll behavior "smooth" seems to be giving some trouble when trying to do continuous scrolling. This behavior can be omitted to have proper continuous scrolling, but it will look jerky.

As far as I know, there is no easy solution for this. I have however created a rather involved solution in my own project, so thought some people may appreciate that also: https://gist.github.com/TarVK/4cc89772e606e57f268d479605d7aded

Marthamarthe answered 1/8, 2020 at 9:48 Comment(4)
There may be an easier way to achieve this using the wheel event target as shown in the question, but for me this target in my testing was the content of the div rather than the container. So the element reference approach seemed more reliable to me.Marthamarthe
Thanks for the solution. It works. It however disables trackpad left and right scroll. Any suggestion on how to tweak?Lebeau
@Lebeau I had never tested it with touchpad, so wasn't aware of the issue, thanks for mentioning it! Now I can also address this issue in my own project :) I have updated the code of the post with a simple fix (simply don't prevent the event if it isn't a y axis scroll). I think it would be neat if we could detect whether the user has a way of horizontally scrolling (E.g. a touchpad) themselves, so we could prevent doing anything in that case, but this doesn't seem possible right now.Marthamarthe
The behavior: "smooth" part caused scroll to be super slow for me. Removing that fixed it.Asti
H
10
onWheel = (e) => {
    e.preventDefault()
    var container = document.getElementById('container')
    var containerScrollPosition = document.getElementById('container').scrollLeft
    container.scrollTo({
        top: 0,
        left: largeContainerScrollPosition + e.deltaY
        behaviour: 'smooth' //if you want smooth scrolling
    })
}
Haemocyte answered 10/10, 2019 at 3:16 Comment(1)
this works well, but e.preventDefault() throws warning as it's for a passive component. What do you think of this?Priestley
H
8

There is another small problem with TarVK's proposed hook. Once you scroll to the end and continue scrolling nothing happens, when we are used to containing elements starting to scroll as well. So I made a fix for that:

export function useHorizontalScroll () {
  const elRef = useRef();
  useEffect(() => {
    const el = elRef.current;
    if (el) {
      const onWheel = (e) => {
        if (e.deltaY === 0) return;
        if (
          !(el.scrollLeft === 0 && e.deltaY < 0) &&
          !(el.scrollWidth - el.clientWidth - Math.round(el.scrollLeft) === 0 && 
              e.deltaY > 0)
        ) {
          e.preventDefault();
        }
        el.scrollTo({
          left: el.scrollLeft + e.deltaY,
          behavior: 'smooth'
        });
      };
      el.addEventListener('wheel', onWheel);
      return () => el.removeEventListener('wheel', onWheel);
    }
  }, []);
  return elRef;
}

It's conditionally preventing default behavior only when there is space to scroll in that direction, so when there is no space to scroll, for example the whole page will start to scroll. The change is here:

        if (
          !(el.scrollLeft === 0 && e.deltaY < 0) &&
          !(el.scrollWidth - el.clientWidth - Math.round(el.scrollLeft) === 0 && 
              e.deltaY > 0)
        ) {
          e.preventDefault();
        }
Hyperphagia answered 18/3, 2021 at 9:3 Comment(1)
Great answer! You can propose an edit for TarVK's answer to keep it clean. anw, I'd rather separate this function from the component and declare it with the ref as parameter like so: const useHorizontalScroll = (ref) => {…} and remove this line const elRef = useRef();.Flattish
N
6

I cannot comment, as my reputation is not sufficient.

@arVK's answer works, but using scrollBy instead of scrollTo can achieve smoother scrolling.

import { useRef, useEffect } from "react";

export function useHorizontalScroll() {
  const elRef = useRef();
  useEffect(() => {
    const el = elRef.current;
    if (el) {
      const onWheel = e => {
        if (e.deltaY == 0) return;
        e.preventDefault();
        el.scrollBy(e.deltaY, 0);
      };
      el.addEventListener("wheel", onWheel);
      return () => el.removeEventListener("wheel", onWheel);
    }
  }, []);
  return elRef;
}
Nullify answered 18/7, 2022 at 5:29 Comment(0)
E
1

You can use onWheel event directly:

import React, { useRef } from "react";
export const Content = () => {
  const ref = useRef<HTMLDivElement>(null);

  const onWheel = (e: UIEvent) => {
    const elelemnt = ref.current;
    if (elelemnt) {
      if (e.deltaY == 0) return;
      elelemnt.scrollTo({
        left: elelemnt.scrollLeft + e.deltaY,
      });
    }
  };
  return (
    <div ref={ref} onWheel={onWheel}>
          TEST CONTENT
    </div>
  );
};
Erigena answered 27/1, 2023 at 3:8 Comment(0)
H
1

I have used the answer provided by @TarVK, works pretty good. There was just a problem: When the input comes from a trackpad, the scrolling gets very erratic. My solution to that looks like this:

import { useEffect, useRef } from 'react';

export function useHorizontalScroll() {
    const elRef = useRef();

    useEffect(() => {
        const el = elRef.current;
        if (el) {
            const onWheel = (e: WheelEvent) => {
                if (e.deltaY === 0) {
                    return;
                }
                if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
                    e.preventDefault();
                    el.scrollLeft += e.deltaX;
                }
                el.scrollTo({ left: el.scrollLeft + e.deltaY });
            };
            el.addEventListener('wheel', onWheel);
            return () => el.removeEventListener('wheel', onWheel);
        }
    }, []);
    return elRef;
}

This updated implementation checks if the horizontal scrolling distance (e.deltaX) is greater than the vertical scrolling distance (e.deltaY) and only prevents default behavior and scrolls horizontally when the former is greater.

Hartsell answered 8/3, 2023 at 15:24 Comment(0)
C
0

Ended up on this question while looking for a solution for my MUI Tabs, and none of the solution worked because Tabs component doesn't accept a ref property, and any external element like a Box taking the ref property, would not scroll the tabs horizontally anyway.

So if you find yourself in this very specific scenario too, this is what worked for me: overriding MUI scroller.

  useEffect(() => {
    // code to make vertical scroll be horizontal scroll on tabs, ref didn't work.
    const tabsScroller = document.querySelector('.MuiTabs-scroller')

    const handleWheel = (event: WheelEvent) => {
      if (event.deltaY !== 0) {
        event.preventDefault()
        if (tabsScroller) {
          tabsScroller.scrollLeft += event.deltaY
        }
      }
    }

    tabsScroller?.addEventListener('wheel', handleWheel)

    return () => {
      tabsScroller?.removeEventListener('wheel', handleWheel)
    }
  }, [])
Cheerio answered 15/6 at 13:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.