// Taxed from https://github.com/justin-chu/react-fast-marquee import React, { Fragment, useEffect, useState, useRef, useCallback, useMemo, ReactNode, CSSProperties, forwardRef, Children, MutableRefObject, RefAttributes, HTMLAttributes, } from "react"; import classNames from "classnames"; import { bemHOF } from "../../utilities"; import { Box } from "../Box"; const cn = bemHOF("Marquee"); export type MarqueeProps = { /** * Inline style for the container div */ style?: CSSProperties; /** * Class name to style the container div */ className?: string; /** * Whether to automatically fill blank space in the marquee with copies of the children or not */ autoFill?: boolean; /** * Whether to play or pause the marquee */ play?: boolean; /** * Whether to pause the marquee when hovered */ pauseOnHover?: boolean; /** * Whether to pause the marquee when clicked */ pauseOnClick?: boolean; /** * The direction the marquee is sliding */ direction?: "left" | "right" | "up" | "down"; /** * Speed calculated as pixels/second */ speed?: number; /** * Duration to delay the animation after render, in seconds */ delay?: number; /** * The number of times the marquee should loop, 0 is equivalent to infinite */ loop?: number; /** * Whether to show the gradient or not */ gradient?: boolean; /** * The color of the gradient */ gradientColor?: string; /** * The width of the gradient on either side */ gradientWidth?: number | string; /** * A callback for when the marquee finishes scrolling and stops. Only calls if loop is non-zero. */ onFinish?: () => void; /** * A callback for when the marquee finishes a loop. Does not call if maximum loops are reached (use onFinish instead). */ onCycleComplete?: () => void; /** * A callback function that is invoked once the marquee has finished mounting. It can be utilized to recalculate the page size, if necessary. */ onMount?: () => void; /** * The children rendered inside the marquee */ children?: ReactNode; } & RefAttributes; const MarqueeChild = ({ children, style, }: { children: ReactNode; style: CSSProperties; }) => { return ( {children} ); }; interface MarqueeContentProps extends HTMLAttributes { children: ReactNode; } const MarqueeContent = ({ children, ...props }: MarqueeContentProps) => { return ( {children} ); }; const MarqueeGradient = ({ style }: { style: CSSProperties }) => { return ; }; /** * @deprecated */ export const Marquee = forwardRef( function Marquee( { style = {}, className = "", autoFill = false, play = true, pauseOnHover = false, pauseOnClick = false, direction = "left", speed = 50, delay = 0, loop = 0, gradient = false, gradientColor = "white", gradientWidth = 200, onFinish, onCycleComplete, onMount, children, }, ref, ) { const [containerWidth, setContainerWidth] = useState(0); const [marqueeWidth, setMarqueeWidth] = useState(0); const [multiplier, setMultiplier] = useState(1); const [isMounted, setIsMounted] = useState(false); const rootRef = useRef(null); const containerRef = (ref as MutableRefObject) || rootRef; const childrenRef = useRef(null); // Calculate width of container and marquee and set multiplier const calculateWidth = useCallback(() => { if (childrenRef.current && containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); const marqueeRect = childrenRef.current.getBoundingClientRect(); let containerRectWidth = containerRect.width; let marqueeRectWidth = marqueeRect.width; // Swap width and height if direction is up or down if (direction === "up" || direction === "down") { containerRectWidth = containerRect.height; marqueeRectWidth = marqueeRect.height; } if (autoFill && containerRectWidth && marqueeRectWidth) { setMultiplier( marqueeRectWidth < containerRectWidth ? Math.ceil(containerRectWidth / marqueeRectWidth) : 1, ); } else { setMultiplier(1); } setContainerWidth(containerRectWidth); setMarqueeWidth(marqueeRectWidth); } }, [autoFill, containerRef, direction]); // Calculate width and multiplier on mount and on window resize useEffect(() => { if (!isMounted) return; calculateWidth(); if (childrenRef.current && containerRef.current) { const resizeObserver = new ResizeObserver(() => calculateWidth()); resizeObserver.observe(containerRef.current); resizeObserver.observe(childrenRef.current); // eslint-disable-next-line consistent-return return () => { if (!resizeObserver) return; resizeObserver.disconnect(); }; } }, [calculateWidth, containerRef, isMounted]); // Recalculate width when children change useEffect(() => { calculateWidth(); }, [calculateWidth, children]); useEffect(() => { setIsMounted(true); }, []); // Runs the onMount callback, if it is a function, when Marquee is mounted. useEffect(() => { if (typeof onMount === "function") { onMount(); } }, [onMount]); // Animation duration const duration = useMemo(() => { if (autoFill) { return (marqueeWidth * multiplier) / speed; } return marqueeWidth < containerWidth ? containerWidth / speed : marqueeWidth / speed; }, [autoFill, containerWidth, marqueeWidth, multiplier, speed]); const rootStyle = useMemo( () => ({ ...style, ["--ads-marquee-pause-on-hover" as string]: !play || pauseOnHover ? "paused" : "running", ["--ads-marquee-pause-on-click" as string]: !play || (pauseOnHover && !pauseOnClick) || pauseOnClick ? "paused" : "running", ["--ads-marquee-width" as string]: direction === "up" || direction === "down" ? `100vh` : "100%", ["--ads-marquee-transform" as string]: // eslint-disable-next-line no-nested-ternary direction === "up" ? "rotate(-90deg)" : direction === "down" ? "rotate(90deg)" : "none", }), [style, play, pauseOnHover, pauseOnClick, direction], ); const gradientStyle = useMemo( () => ({ ["--ads-marquee-gradient-color" as string]: gradientColor, ["--ads-marquee-gradient-width" as string]: typeof gradientWidth === "number" ? `${gradientWidth}px` : gradientWidth, }), [gradientColor, gradientWidth], ); const contentStyle = useMemo( () => ({ ["--ads-marquee-play" as string]: play ? "running" : "paused", ["--ads-marquee-direction" as string]: direction === "left" ? "normal" : "reverse", ["--ads-marquee-duration" as string]: `${duration}s`, ["--ads-marquee-delay" as string]: `${delay}s`, ["--ads-marquee-iteration-count" as string]: loop !== 0 ? `${loop}` : "infinite", ["--ads-marquee-min-width" as string]: autoFill ? `auto` : "100%", }), [play, direction, duration, delay, loop, autoFill], ); const childStyle = useMemo( () => ({ ["--ads-marquee-transform" as string]: // eslint-disable-next-line no-nested-ternary direction === "up" ? "rotate(90deg)" : direction === "down" ? "rotate(-90deg)" : "none", }), [direction], ); // Render {multiplier} number of children const multiplyChildren = useCallback( (childrenMultiplier: number) => { return [ ...Array( Number.isFinite(childrenMultiplier) && childrenMultiplier >= 0 ? childrenMultiplier : 0, ), ].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key {Children.map(children, (child) => { return {child}; })} )); }, [childStyle, children], ); return !isMounted ? null : ( {gradient && } {Children.map(children, (child) => { return {child}; })} {multiplyChildren(multiplier - 1)} {multiplyChildren(multiplier)} ); }, );