File size: 2,739 Bytes
63522a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
88
89
90
91
92
93
94
95
import {
  useCallback,
  useEffect,
  useRef,
  useState,
  type RefObject,
} from "react";

/** Movement required before a mouse gesture is treated as a scroll drag. */
const DRAG_COMMIT_PX = 2;

/**
 * Mouse drag-to-scroll for a horizontal `overflow-x-auto` container. Touch
 * input keeps the browser's native panning; only primary-button mouse drags
 * are handled. After a committed drag, the click that follows mouseup is
 * suppressed so child buttons are not activated by the drag release.
 */
export function useDragScroll(scrollRef: RefObject<HTMLElement | null>) {
  const [isDragging, setIsDragging] = useState(false);
  const startXRef = useRef(0);
  const startScrollLeftRef = useRef(0);
  const dragCommittedRef = useRef(false);

  const handleMouseDown = useCallback(
    (event: React.MouseEvent) => {
      const element = scrollRef.current;
      if (event.button !== 0 || !element) {
        return;
      }
      dragCommittedRef.current = false;
      startXRef.current = event.clientX;
      startScrollLeftRef.current = element.scrollLeft;
      setIsDragging(true);
    },
    [scrollRef],
  );

  const handleClickCapture = useCallback((event: React.MouseEvent) => {
    if (!dragCommittedRef.current) {
      return;
    }
    dragCommittedRef.current = false;
    event.preventDefault();
    event.stopPropagation();
  }, []);

  const handleDragStart = useCallback((event: React.DragEvent) => {
    // Child <img> elements would otherwise start a native image drag.
    event.preventDefault();
  }, []);

  useEffect(() => {
    if (!isDragging) {
      return undefined;
    }

    const handleMouseMove = (event: MouseEvent) => {
      const element = scrollRef.current;
      if (!element) {
        return;
      }

      // The mouseup can be missed when released outside the window; end the
      // drag once the primary button is no longer held.
      if ((event.buttons & 1) === 0) {
        setIsDragging(false);
        return;
      }

      if (!dragCommittedRef.current) {
        if (Math.abs(event.clientX - startXRef.current) < DRAG_COMMIT_PX) {
          return;
        }
        dragCommittedRef.current = true;
      }

      event.preventDefault();
      element.scrollLeft =
        startScrollLeftRef.current - (event.clientX - startXRef.current);
    };

    const handleMouseUp = () => setIsDragging(false);

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);

    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [isDragging, scrollRef]);

  return { handleMouseDown, handleClickCapture, handleDragStart };
}