From e02f588a90a0ac52230df7a17864760e95cb998d Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Wed, 26 Nov 2025 00:00:50 +0400 Subject: [PATCH] style: add drag-to-scroll --- src/components/group-badges.tsx | 86 ++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/components/group-badges.tsx b/src/components/group-badges.tsx index 47efdd9..15e78ce 100644 --- a/src/components/group-badges.tsx +++ b/src/components/group-badges.tsx @@ -21,6 +21,10 @@ export function GroupBadges({ const scrollContainerRef = useRef(null); const [showLeftFade, setShowLeftFade] = useState(false); const [showRightFade, setShowRightFade] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const dragStartRef = useRef<{ x: number; scrollLeft: number } | null>(null); + const hasMovedRef = useRef(false); + const clickBlockedRef = useRef(false); const checkScrollPosition = useCallback(() => { const container = scrollContainerRef.current; @@ -31,6 +35,70 @@ export function GroupBadges({ setShowRightFade(scrollLeft < scrollWidth - clientWidth - 1); }, []); + const handleMouseDown = useCallback((e: React.MouseEvent) => { + const container = scrollContainerRef.current; + if (!container) return; + + dragStartRef.current = { + x: e.clientX, + scrollLeft: container.scrollLeft, + }; + hasMovedRef.current = false; + setIsDragging(true); + container.style.cursor = "grabbing"; + container.style.userSelect = "none"; + }, []); + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (!isDragging || !dragStartRef.current) return; + + const container = scrollContainerRef.current; + if (!container) return; + + const deltaX = e.clientX - dragStartRef.current.x; + const distance = Math.abs(deltaX); + + if (distance > 5) { + hasMovedRef.current = true; + } + + container.scrollLeft = dragStartRef.current.scrollLeft - deltaX; + checkScrollPosition(); + }, + [isDragging, checkScrollPosition], + ); + + const handleMouseUp = useCallback(() => { + if (!isDragging) return; + + const container = scrollContainerRef.current; + if (container) { + container.style.cursor = ""; + container.style.userSelect = ""; + } + + clickBlockedRef.current = hasMovedRef.current; + setIsDragging(false); + dragStartRef.current = null; + + setTimeout(() => { + hasMovedRef.current = false; + clickBlockedRef.current = false; + }, 100); + }, [isDragging]); + + useEffect(() => { + if (isDragging) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + } + }, [isDragging, handleMouseMove, handleMouseUp]); + useEffect(() => { const container = scrollContainerRef.current; if (!container) return; @@ -66,19 +134,33 @@ export function GroupBadges({ )}
{groups.map((group) => ( { + onClick={(e) => { + if (hasMovedRef.current || clickBlockedRef.current) { + e.preventDefault(); + e.stopPropagation(); + return; + } onGroupSelect( selectedGroupId === group.id ? "default" : group.id, ); }} + onMouseDown={(e) => { + if (isDragging) { + e.preventDefault(); + e.stopPropagation(); + } + }} > {group.name}