Files
kan/apps/web/src/hooks/useDragToScroll.ts

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,
};
}