react-scroll-to-top icon indicating copy to clipboard operation
react-scroll-to-top copied to clipboard

Feature request: ability to pass in an HTMLElement to use as reference, instead of window

Open dsebastien opened this issue 4 years ago • 3 comments

Currently, the component always uses window to listen & react to scroll events. In some applications (e.g., Next.js), what scrolls is actually a specific div (e.g., div id="__next" ...) in the case of Next.js.

It would be great to be able to pass in an HTML element for this component to listen/react to, and set scroll position of.

I've done the following for my project:

import React, { useState, useEffect } from "react";
import {FaArrowUp} from "react-icons/fa";

type ScrollToTopProps = React.HTMLAttributes<HTMLButtonElement> & {
  scrollingElement?: HTMLElement | undefined;
  top?: number;
  smooth?: boolean;
  icon?: React.ReactNode;
  ariaLabelText?: string;
};

function scrollToTop(smooth = false, scrollingElement: HTMLElement | undefined) {
  if(!scrollingElement) {
    if (smooth) {
      window.scrollTo({
        top: 0,
        behavior: "smooth",
      });
      return;
    }
    document.documentElement.scrollTop = 0;
  } else {
    if (smooth) {
      scrollingElement.scrollTo({
        top: 0,
        behavior: "smooth",
      });
      return;
    }
    scrollingElement.scrollTop = 0;
  }
}

const ScrollToTop = ({
                       scrollingElement,
                       top = 20,
                       className = "scroll-to-top",
                       icon,
                       ariaLabelText = "Scroll to top",
                       smooth = false,
                     }: ScrollToTopProps) => {
  const [visible, setVisible] = useState(false);
  const onScroll = () => {
    if(!scrollingElement) {
      setVisible(document.documentElement.scrollTop > top);
    } else {
      setVisible(scrollingElement.scrollTop > top);
    }
  };
  useEffect(() => {
    if(!scrollingElement) {
      document.addEventListener("scroll", onScroll);
    } else {
      //const scrollingElement = document.getElementById("__next")!;
      scrollingElement.addEventListener("scroll", onScroll);
    }

    // Remove listener on unmount
    return () => {
      if(!scrollingElement) {
        document.removeEventListener("scroll", onScroll);
      } else {
        scrollingElement.removeEventListener("scroll", onScroll);
      }
    }
  });

  return <>
      {visible && (
        <button
          className={className}
          onClick={() => scrollToTop(smooth, scrollingElement)}
          aria-label={ariaLabelText}
        >
          {icon || (
            <FaArrowUp />
          )}
        </button>
      )}
    </>;
};

export default ScrollToTop;

I then use it as follows:

<ScrollToTop smooth={true} scrollingElement={IS_BROWSER? document.getElementById("__next")!: undefined} className="scroll-to-top" icon={<FaArrowUp className="w-full h-full text-white" />} />

dsebastien avatar Mar 03 '21 13:03 dsebastien

Hi, thank you for the request + the star! 😄 I will look in to this. Is there something not working as expected when using Nextjs? AFAIK the button appears after scroll etc.

HermanNygaard avatar Mar 05 '21 05:03 HermanNygaard

Well, no idea if it's specific to Next.js, but for my site, document.documentElement.scrollTop always seemed to remain at 0. The element that did scroll was the __next wrapper div added by Next.js.

You can find the sources of my site here if you want to test this behavior: https://github.com/dsebastien/website-dsebastien

It's also possible that I created the issue with my bad CSS skills ;-)

dsebastien avatar Mar 05 '21 07:03 dsebastien

Sorry for the late reply, I see. Feel free to submit a PR if you'd like! 😄

HermanNygaard avatar Mar 12 '21 12:03 HermanNygaard