102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface UseDragToScrollOptions {
|
|
/**
|
|
* Whether drag-to-scroll is enabled
|
|
*/
|
|
enabled?: boolean;
|
|
/**
|
|
* The direction to scroll
|
|
*/
|
|
direction?: "horizontal" | "vertical" | "both";
|
|
}
|
|
|
|
/**
|
|
* Hook to enable drag-to-scroll functionality on a scrollable element
|
|
* @param options Configuration options
|
|
* @returns Ref to attach to the scrollable element and mouse event handlers
|
|
*/
|
|
export function useDragToScroll({
|
|
enabled = true,
|
|
direction = "horizontal",
|
|
}: UseDragToScrollOptions = {}) {
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const startPosRef = useRef({ x: 0, y: 0 });
|
|
const scrollStartRef = useRef({ x: 0, y: 0 });
|
|
|
|
useEffect(() => {
|
|
if (!enabled || !isDragging) return;
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (!scrollRef.current) return;
|
|
|
|
const deltaX = e.clientX - startPosRef.current.x;
|
|
const deltaY = e.clientY - startPosRef.current.y;
|
|
|
|
if (direction === "horizontal" || direction === "both") {
|
|
scrollRef.current.scrollLeft = scrollStartRef.current.x - deltaX;
|
|
}
|
|
if (direction === "vertical" || direction === "both") {
|
|
scrollRef.current.scrollTop = scrollStartRef.current.y - deltaY;
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(false);
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
document.body.style.cursor = "grabbing";
|
|
document.body.style.userSelect = "none";
|
|
|
|
return () => {
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
document.body.style.cursor = "";
|
|
document.body.style.userSelect = "";
|
|
};
|
|
}, [enabled, isDragging, direction]);
|
|
|
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!scrollRef.current) return;
|
|
|
|
const target = e.target as HTMLElement;
|
|
const container = scrollRef.current;
|
|
|
|
// Check if the click is on an interactive or draggable element
|
|
// We need to be careful not to interfere with react-beautiful-dnd dragging
|
|
const isInteractiveElement =
|
|
target.closest("a") ||
|
|
target.closest("button") ||
|
|
target.closest("input") ||
|
|
target.closest("textarea") ||
|
|
target.closest("[role='button']") ||
|
|
target.closest("[draggable='true']") ||
|
|
target.closest(".react-beautiful-dnd-drag-handle") ||
|
|
target.closest("[data-rbd-drag-handle-draggable-id]") ||
|
|
target.closest("[data-rbd-draggable-id]");
|
|
|
|
// Don't start dragging if clicking on interactive elements
|
|
if (isInteractiveElement) return;
|
|
|
|
// Enable drag-to-scroll for any non-interactive element within the container
|
|
if (container.contains(target)) {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
startPosRef.current = { x: e.clientX, y: e.clientY };
|
|
scrollStartRef.current = {
|
|
x: container.scrollLeft,
|
|
y: container.scrollTop,
|
|
};
|
|
}
|
|
};
|
|
|
|
return {
|
|
ref: scrollRef,
|
|
onMouseDown: handleMouseDown,
|
|
isDragging,
|
|
};
|
|
}
|