//To know when scroll event was triggered by scrollIntoView, it needs to polyfill scrollIntoView method import '../../../extensions/scrollIntoView' import React, { useState, useEffect, useRef } from 'react' import { throttle } from './utils' /** * BackToTopButton component * @param {string} startSelector - user need to scroll over this element to make the button visible * @param {Record} attributes - attributes to be added to the button * @param {Window | HTMLElement} scrollableElement - element that will be listened to scroll events * @returns {JSX.Element} * @description Component that renders a button that scrolls users to the top of the page when clicked * and is only visible when the user has scrolled down at least to the startSelector element and then scrolls up */ type BackToTopProps = { startSelector?: string attributes?: Record scrollableElement?: Window | HTMLElement } const BackToTop: React.FC = ({ startSelector, attributes = {}, scrollableElement = window, }) => { const [visible, setVisible] = useState(false) const [scrolledDownFirstTime, setScrolledDownFirstTime] = useState(false) const lastScroll = useRef(0) const element = useRef(null) // receives e as parameter that is a scroll event const scrollMargin = 10 const handleScroll = () => { setScrolledDownFirstTime((scrolledDownFirstTime) => { if (!scrolledDownFirstTime && getScrollY() > lastScroll.current) { return true } return scrolledDownFirstTime }) setVisible((visible) => { let result = visible if ( // Scrolling direction is up getScrollY() < lastScroll.current && // Button is not visible !visible && // The user scrolled down at least once scrolledDownFirstTime ) { const startElement = startSelector ? document.querySelector(startSelector) : null // has passed the element that selector selects or the top of the parent container const hasPassedTriggerPosition = startElement ? startElement?.getBoundingClientRect()?.bottom <= 0 : (element?.current?.getBoundingClientRect()?.top ?? 0) <= 0 // Add a fallback value of 0 if element is undefined // if the user has scrolled past the trigger element and the scroll is not programatic const window = ((scrollableElement as HTMLElement)?.ownerDocument ?.defaultView as Window & { programmaticScroll?: boolean }) || (scrollableElement as Window & { programmaticScroll?: boolean }) if (hasPassedTriggerPosition && !window?.programmaticScroll) { result = true } } //if scroll to top and component is visible, it will be hidden else if (getScrollY() < scrollMargin && visible) { result = false } // if scroll down and component is visible, it will be hidden else if (getScrollY() > lastScroll.current && visible) { result = false } // update last scroll position lastScroll.current = getScrollY() return result }) } useEffect(() => { const handler = throttle(handleScroll, 250) if (element.current && scrollableElement) { lastScroll.current = getScrollY() // Add a throttle to the scroll event to prevent performance issues scrollableElement.addEventListener('scroll', handler) } return () => scrollableElement && scrollableElement.removeEventListener('scroll', handler) }, [element, scrollableElement, scrolledDownFirstTime]) const handleClick = () => { scrollableElement?.scrollTo({ top: 0, left: 0, behavior: 'smooth', }) setVisible(false) } const getScrollY = () => { if (scrollableElement && scrollableElement instanceof Window) { return scrollableElement?.scrollY } else if (scrollableElement && scrollableElement instanceof HTMLElement) { return scrollableElement?.scrollTop } else { //fallback in case scrollableElement is null or undefined return window.scrollY } } return (
) } export default BackToTop