All files / src/components/ui/hooks use-cached-scroll.ts

90.69% Statements 39/43
87.5% Branches 28/32
100% Functions 6/6
90.47% Lines 38/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 884x   4x 4x   4x 39x   4x         45x 45x   45x   7x       7x         7x 7x 7x         45x 30x 18x       12x   12x 606x   606x       606x       606x   606x 6x 6x     606x       600x 600x       12x 12x   12x 6x     12x 12x 1x         45x     4x  
import React, { useCallback, useEffect, useRef } from "react";
 
const scrollCache: Record<string, number> = {};
const MAX_SCROLL_RESTORE_FRAMES = 600;
 
export const getCachedScrollPosition = (cacheKey: string | undefined) =>
  cacheKey ? scrollCache[cacheKey] : undefined;
 
const useCachedScroll = (
  cacheKey: string | undefined,
  scrollElement: HTMLDivElement | null,
  restoreScroll = true,
) => {
  const programmaticScrollPosition = useRef<number | null>(null);
  const isUserScrolling = useRef(false);
 
  const onScroll = useCallback<React.UIEventHandler<HTMLDivElement>>(
    (e) => {
      Iif (!cacheKey) {
        return;
      }
 
      Iif (e.currentTarget.scrollTop === programmaticScrollPosition.current) {
        programmaticScrollPosition.current = null;
        return;
      }
 
      programmaticScrollPosition.current = null;
      scrollCache[cacheKey] = e.currentTarget.scrollTop;
      isUserScrolling.current = true;
    },
    [cacheKey],
  );
 
  useEffect(() => {
    if (!cacheKey || !scrollElement || !restoreScroll) {
      return;
    }
 
    let animationFrameId: number | undefined;
    let restoreFrames = 0;
 
    const checkScroll = () => {
      const savedPosition = scrollCache[cacheKey] ?? 0;
 
      Iif (isUserScrolling.current) {
        return;
      }
 
      const maxScrollTop = Math.max(
        0,
        scrollElement.scrollHeight - scrollElement.clientHeight,
      );
      const targetScrollTop = Math.min(savedPosition, maxScrollTop);
 
      if (scrollElement.scrollTop !== targetScrollTop) {
        scrollElement.scrollTop = targetScrollTop;
        programmaticScrollPosition.current = scrollElement.scrollTop;
      }
 
      if (
        scrollElement.scrollTop < savedPosition &&
        restoreFrames < MAX_SCROLL_RESTORE_FRAMES
      ) {
        restoreFrames += 1;
        animationFrameId = requestAnimationFrame(checkScroll);
      }
    };
 
    const savedPosition = scrollCache[cacheKey] ?? 0;
    isUserScrolling.current = false;
 
    if (savedPosition > 0) {
      checkScroll();
    }
 
    return () => {
      if (animationFrameId !== undefined) {
        cancelAnimationFrame(animationFrameId);
      }
    };
  }, [cacheKey, restoreScroll, scrollElement]);
 
  return { onScroll } as const;
};
 
export default useCachedScroll;