Lberteh
Lberteh

Reputation: 175

Continuous scroll animation while mouse down or button hovered in ReactJs

Some context:

I'm trying to achieve a similar scrolling effect as in Etsy's product image thumbnails carousel. When you hover over the top part of the div, it automatically scrolls until it reveals the last image and same occurs on the bottom part.

Here's a link to a random product where you can check the functionality.

I've been trying to achieve this with react so I decided to start with scroll on mouse down and stop scrolling on mouse up.

I found a great example of this here

Problem is that it's using jquery. I tried to convert it to vanilla js and use on my react app but had no success.

After some more research I ended up with a working solution but the animation is not smooth at all. I used a setInterval hook. Here's my code.

import { useRef, useState, useLayoutEffect, useEffect } from "react";
import styled from "styled-components";

const Wrapper = styled.div`
  margin: 5% 40%;
  .test {
    height: 300px;
    width: 100px;

    overflow: scroll;
    .inner {
      background-image: linear-gradient(red, blue);
      height: 800px;
      width: 100%;
    }
  }
`;

function useInterval(callback, delay) {
  const savedCallback = useRef(callback);

  useLayoutEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {

    if (!delay) {
      return;
    }

    const id = setInterval(() => savedCallback.current(), delay);

    return () => clearInterval(id);
  }, [delay]);
}

const Scroll = () => {
  const scrollRef = useRef();
  const [delay, setDelay] = useState(100);
  const [scrolling, setScrolling] = useState(false);

  useInterval(
    () => {
      console.log("Scrolling");
      scrollRef.current.scrollBy({ top: 10, behavior: "smooth" });
    },
    scrolling ? delay : null
  );

  return (
    <Wrapper>
      <div ref={scrollRef} className="test">
        <div className="inner"></div>
      </div>

      <button
        onMouseDown={() => setScrolling(true)}
        onMouseUp={() => setScrolling(false)}
      >
        HOLD DOWN TO SCROLL
      </button>
    </Wrapper>
  );
};

export default Scroll;

I would really appreciate some directions and if possible a quick example on how I can achieve a smooth continuous scrolling

Upvotes: 2

Views: 2301

Answers (2)

Antonio Della Fortuna
Antonio Della Fortuna

Reputation: 256

Here's a version with configurable speed, and different speed precision. Note that the more speed steps you will have, the more the scrolling will result not smooth (Because scrollTop cannot be increased by a decimal number).

    import { useRef, useState, useLayoutEffect, useEffect } from "react";
import styled from "styled-components";

const Wrapper = styled.div`
  margin: 5% 40%;
  .test {
    height: 300px;
    width: 100px;
    overflow: scroll;
    .inner {
      background-image: linear-gradient(red, blue);
      height: 800px;
      width: 100%;
    }
  }
`;
function useInterval(callback, active, speed, speedRange = 10) {
  const savedCallback = useRef(callback);
  const intervalIdRef = useRef(null);

  useLayoutEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    clearInterval(intervalIdRef.current);

    if (speedRange < 1) {
      console.error(`Speed range must be >= 1`);
      return;
    }

    if (!(speed >= 1 && speed <= speedRange) || speed < 1) {
      console.error(`Speed must be in range [1...${speedRange}]`);
      return;
    }

    if (!active) {
      return;
    }

    intervalIdRef.current = setInterval(
      () => savedCallback.current(),
      speedRange / speed
    );

    return () => clearInterval(intervalIdRef.current);
  }, [active, speed, speedRange]);
}

const Scroll = () => {
  const scrollRef = useRef();
  const [speed, setSpeed] = useState(1);

  //Default speedRange is 10 = 10 different speeds [1..10]
  // (The higher this number the less smooth are the low speeds,
  // but there will be more speeds to chose from)
  const [speedRange, setSpeedRange] = useState(10);
  const [scrolling, setScrolling] = useState(false);

  useInterval(
    () => {
      scrollRef.current.scrollTop += 1;
    },
    scrolling,
    speed,
    speedRange
  );

  return (
    <Wrapper>
      <div ref={scrollRef} className="test">
        <div className="inner"></div>
      </div>

      <button
        onMouseDown={() => setScrolling(true)}
        onMouseUp={() => setScrolling(false)}
      >
        HOLD DOWN TO SCROLL
      </button>
    </Wrapper>
  );
};

export default Scroll;

Upvotes: 2

aleks korovin
aleks korovin

Reputation: 744

It looks like useRef and requestAnimationFrame together with scrollTop could make it.

Please check examples (Stackblitz - https://stackblitz.com/edit/react-cuf5cl?file=src%2FApp.js):

import React from "react";
import "./style.css";

import { useRef } from "react";
import styled from "styled-components";

const Wrapper = styled.div`
  margin: 5% 40%;
  .test {
    height: 300px;
    width: 100px;

    overflow: scroll;
    .inner {
      background-image: linear-gradient(red, blue);
      height: 800px;
      width: 100%;
    }
  }
`;


const Scroll = () => {
  const step = 10;
  const scrollRef = useRef();
  const isScrollRef = useRef();

  const setMove = (state) => isScrollRef.current = state;

  const move = () => {
    if (isScrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollTop + step;
      requestAnimationFrame(move);
    }    
  };

  return (
    <Wrapper>
      <div ref={scrollRef} className="test">
        <div className="inner"></div>
      </div>

      <button
        onMouseDown={() => { setMove(true); move();}}
        onMouseUp={() => setMove(false)}
      >
        HOLD DOWN TO SCROLL
      </button>
    </Wrapper>
  );
};

export default Scroll;

export default function App() {
  return (
    <div>
      <Scroll />
    </div>
  );
}

Upvotes: 2

Related Questions