Last active
February 21, 2024 07:56
-
-
Save ahallora/441f7d7b37688433cf6077458f0240e0 to your computer and use it in GitHub Desktop.
useScrollableMask
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
HOW TO USE: | |
import React from "react"; | |
import useScrollableMask from "./useScrollableMask"; | |
const Example = () => { | |
const { scrollDiv, scrollDivStyles } = useScrollableMask(); | |
return ( | |
<div | |
style={{ | |
maxHeight: "50svh", | |
...scrollDivStyles, | |
}} | |
ref={scrollDiv} | |
> | |
<div | |
style={{ | |
height: 3000, | |
width: "100%", | |
background: "linear-gradient(0deg, red, blue)", | |
}} | |
/> | |
</div> | |
); | |
}; | |
export default Example; | |
*/ | |
import { useCallback, useEffect, useRef, useState } from "react"; | |
/** | |
* A helper function that adds masking gradients for scrollable elements | |
* to increase visual guidance on scrollability on both desktop and mobile. | |
* @param maxHeight max height before applying scrolling in pixels | |
* @param gradientSize the gradient height in pixels | |
* @returns - `scrollDiv`: the react reference to be used on the scrolling element | |
* - `scrollDivStyles`: the default styles to be applied to the scrolling element | |
*/ | |
const useScrollableMask = (gradientSize = 48) => { | |
const MASK = { | |
top: `linear-gradient(#0000001a, #000 ${gradientSize}px)`, | |
middle: `linear-gradient(#0000001a, #000 ${gradientSize}px, #000 calc(100% - ${gradientSize}px), #0000001a)`, | |
bottom: `linear-gradient(#000, #000 calc(100% - ${gradientSize}px), #0000001a)`, | |
}; | |
const [isHovered, setIsHovered] = useState(false); | |
const [canScroll, setCanScroll] = useState(false); | |
const scrollDivRef = useRef<HTMLDivElement | null>(null); | |
const initScrollDivRef = useCallback(node => { | |
if (!node) return; | |
scrollDivRef.current = node; | |
setCanScroll(node.scrollHeight > node.clientHeight); | |
}, []); | |
const scrollDivStyles = canScroll | |
? { | |
position: "relative", | |
maskImage: MASK.bottom, | |
} | |
: {}; | |
const getActiveMask = (target: HTMLElement) => { | |
if (!target) return MASK.bottom; | |
const { scrollHeight, scrollTop, clientHeight } = target; | |
const isAtBottom = scrollHeight - scrollTop === clientHeight; | |
const isAtTop = scrollTop === 0; | |
if (isAtBottom) return MASK.top; | |
if (isAtTop) return MASK.bottom; | |
return MASK.middle; | |
}; | |
useEffect(() => { | |
if (!scrollDivRef.current || !canScroll) return undefined; | |
const handleHover = (event: Event) => { | |
const target = event.target as HTMLElement; | |
const mouseover = event.type === "mouseenter"; | |
target.style.maskImage = mouseover ? "unset" : getActiveMask(target); | |
setIsHovered(mouseover); | |
}; | |
const handleScroll = (event: Event) => { | |
const target = event.target as HTMLElement; | |
if (isHovered) return; | |
target.style.maskImage = getActiveMask(target); | |
}; | |
scrollDivRef.current.addEventListener("mouseenter", handleHover); | |
scrollDivRef.current.addEventListener("mouseleave", handleHover); | |
scrollDivRef.current.addEventListener("scroll", handleScroll); | |
return () => { | |
scrollDivRef.current?.removeEventListener("mouseenter", handleHover); | |
scrollDivRef.current?.removeEventListener("mouseleave", handleHover); | |
scrollDivRef.current?.removeEventListener("scroll", handleScroll); | |
}; | |
}, [isHovered, setIsHovered, scrollDivRef, canScroll]); | |
return { scrollDiv: initScrollDivRef, scrollDivStyles }; | |
}; | |
export default useScrollableMask; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment