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 885x   5x 5x   5x 39x   5x         47x 47x   47x   7x       7x         7x 7x 7x         47x 32x 19x       13x   13x 606x   606x       606x       606x   606x 6x 6x     606x       600x 600x       13x 13x   13x 6x     13x 13x 1x         47x     5x  
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;