import { useCallback, useEffect, useRef } from "react" import { isMobileDevice } from "../../utils/device" // Scroll position thresholds const SCROLL_THRESHOLD = 60; export const SECOND_LEVEL_POSITION = 205; export const FULLSCREEN_POSITION = 340; const FULLSCREEN_THRESHOLD = SECOND_LEVEL_POSITION + SCROLL_THRESHOLD; const CLOSE_THRESHOLD = SECOND_LEVEL_POSITION - SCROLL_THRESHOLD; // Animation delays const SCROLL_END_DETECTION_DELAY = 66 const STATE_TRANSITION_DELAY = 100 // Panel states enum PanelState { CLOSED = "CLOSED", SECOND_LEVEL = "SECOND_LEVEL", FULLSCREEN = "FULLSCREEN", } interface ScrollState { fullScreen: boolean; canFullScreen?: boolean; } interface ScrollStateActions { setFullScreen: (value: boolean) => void onClose: () => void } export const useScrollState = ( ref: React.RefObject, state: ScrollState, actions: ScrollStateActions ) => { const { fullScreen, canFullScreen = true } = state; const { setFullScreen, onClose } = actions; const scrollTimer = useRef(undefined); const isMobile = isMobileDevice(); const scrollToSecondLevel = useCallback(() => { ref.current?.scrollTo({ top: SECOND_LEVEL_POSITION, behavior: "smooth", }); }, [ref]); const handleStateTransition = useCallback( (targetState: PanelState) => { switch (targetState) { case PanelState.FULLSCREEN: ref.current?.scrollTo({ top: FULLSCREEN_POSITION, behavior: "smooth", }); setFullScreen(true); break; case PanelState.CLOSED: onClose() break case PanelState.SECOND_LEVEL: scrollToSecondLevel(); break; } }, [ref, setFullScreen, onClose, scrollToSecondLevel], ); const handleScrollEnd = useCallback(() => { if (!ref.current) return const scrollTop = ref.current.scrollTop const getCurrentPanelState = (st: number): PanelState => { if (st >= FULLSCREEN_THRESHOLD && canFullScreen) return PanelState.FULLSCREEN; if (st < CLOSE_THRESHOLD) return PanelState.CLOSED; return PanelState.SECOND_LEVEL; }; // full screen if (fullScreen && scrollTop < FULLSCREEN_POSITION - 1) { ref.current.scrollTo({ top: 0, behavior: "smooth", }) setTimeout(() => { onClose() }, STATE_TRANSITION_DELAY) return } // not full screen const targetState = getCurrentPanelState(scrollTop); handleStateTransition(targetState); }, [canFullScreen, fullScreen, handleStateTransition, onClose, ref]); useEffect(() => { const element = ref.current if (!element) return const handleScroll = (e: Event) => { const scrollTop = (e.target as HTMLDivElement).scrollTop if (!scrollTop) return clearTimeout(scrollTimer.current); // toggling full screen should be handled early if (!fullScreen && scrollTop >= FULLSCREEN_POSITION) { return handleScrollEnd(); } else if (fullScreen && scrollTop < FULLSCREEN_THRESHOLD) { return handleScrollEnd(); } clearTimeout(scrollTimer.current); // @ts-ignore scrollTimer.current = setTimeout(() => { handleScrollEnd() }, SCROLL_END_DETECTION_DELAY) } element.addEventListener("scroll", handleScroll); return () => { element.removeEventListener("scroll", handleScroll); }; }, [ handleScrollEnd, isMobile, ref, fullScreen, setFullScreen, canFullScreen, ]); const scrollToFullScreen = useCallback(() => { ref.current?.scrollTo({ top: FULLSCREEN_POSITION, behavior: "smooth", }) }, [ref]) return { scrollToFullScreen, scrollToSecondLevel, } }