From a51da7342697d6e567821eb156e901adba0876f7 Mon Sep 17 00:00:00 2001 From: Arush Srivastava Date: Mon, 6 Apr 2026 23:56:20 -0400 Subject: [PATCH] fixed scrolling in calendar --- .../selectCalendarComponents/CalBlock.tsx | 250 ++++++++++++++---- .../selectCalendarComponents/CalendarApp.tsx | 5 +- 2 files changed, 204 insertions(+), 51 deletions(-) diff --git a/src/components/selectCalendarComponents/CalBlock.tsx b/src/components/selectCalendarComponents/CalBlock.tsx index 6047d4b..1b9fc01 100644 --- a/src/components/selectCalendarComponents/CalBlock.tsx +++ b/src/components/selectCalendarComponents/CalBlock.tsx @@ -47,6 +47,9 @@ interface BoundingBox { maxBlock: number; } +const AUTO_SCROLL_EDGE_PX = 56; +const AUTO_SCROLL_MAX_STEP_PX = 18; + export default function CalBlock({ blockID, columnID, @@ -84,6 +87,11 @@ export default function CalBlock({ const dragStartPointRef = useRef(null); const selectionModeRef = useRef(false); const hasDraggedRef = useRef(false); + const pointerClientRef = useRef<{ x: number; y: number } | null>(null); + const dragOriginClientRef = useRef<{ x: number; y: number } | null>(null); + const pointerHasMovedRef = useRef(false); + const autoScrollFrameRef = useRef(null); + const scrollContainerRef = useRef(null); // Initialize chartedUsers useEffect(() => { @@ -387,6 +395,29 @@ export default function CalBlock({ [] ); + const findScrollContainer = useCallback((startNode: Element | null) => { + const taggedContainer = startNode?.closest( + '[data-calendar-scroll-container="true"]' + ) as HTMLElement | null; + if (taggedContainer) { + return taggedContainer; + } + + let current: HTMLElement | null = + (startNode as HTMLElement | null) ?? dragRef.current; + + while (current) { + const { overflowY } = window.getComputedStyle(current); + const canScrollY = /(auto|scroll|overlay)/.test(overflowY); + if (canScrollY && current.scrollHeight > current.clientHeight) { + return current; + } + current = current.parentElement; + } + + return (document.scrollingElement as HTMLElement | null) || null; + }, []); + const updateCalendarForBoundingBoxes = useCallback( ( currentBox: BoundingBox, @@ -429,19 +460,23 @@ export default function CalBlock({ [user, setCalendarState] ); - const handleGlobalMouseMove = useCallback( - (event: MouseEvent) => { + const updateDragSelectionAtPoint = useCallback( + (clientX: number, clientY: number) => { if ( !isDraggingRef.current || !lastDragPoint.current || !previousBoundingBox.current || !dragStartPointRef.current - ) + ) { return; + } - const element = document.elementFromPoint(event.clientX, event.clientY); + const element = document.elementFromPoint(clientX, clientY); if (!element?.id?.includes('-')) return; + const [newCol, newBlock] = element.id.split('-').map(Number); + if (Number.isNaN(newCol) || Number.isNaN(newBlock)) return; + const [lastCol, lastBlock] = lastDragPoint.current; if (newCol === lastCol && newBlock === lastBlock) return; hasDraggedRef.current = true; @@ -477,10 +512,116 @@ export default function CalBlock({ [isAdmin, debouncedSetDragState, updateCalendarForBoundingBoxes] ); + const stopAutoScroll = useCallback(() => { + if (autoScrollFrameRef.current !== null) { + window.cancelAnimationFrame(autoScrollFrameRef.current); + autoScrollFrameRef.current = null; + } + }, []); + + const startAutoScroll = useCallback(() => { + stopAutoScroll(); + + const step = () => { + if (!isDraggingRef.current) { + autoScrollFrameRef.current = null; + return; + } + + const pointer = pointerClientRef.current; + if (!pointer) { + autoScrollFrameRef.current = window.requestAnimationFrame(step); + return; + } + + if (!pointerHasMovedRef.current) { + autoScrollFrameRef.current = window.requestAnimationFrame(step); + return; + } + + let scrollContainer = scrollContainerRef.current; + if (!scrollContainer || !document.contains(scrollContainer)) { + const hoveredElement = document.elementFromPoint(pointer.x, pointer.y); + scrollContainer = findScrollContainer(hoveredElement); + scrollContainerRef.current = scrollContainer; + } + + if (scrollContainer) { + const isDocumentScrollContainer = + scrollContainer === document.body || + scrollContainer === document.documentElement || + scrollContainer === document.scrollingElement; + + const containerTop = isDocumentScrollContainer + ? 0 + : scrollContainer.getBoundingClientRect().top; + const containerBottom = isDocumentScrollContainer + ? window.innerHeight + : scrollContainer.getBoundingClientRect().bottom; + + let scrollDelta = 0; + + if (pointer.y > containerBottom - AUTO_SCROLL_EDGE_PX) { + const normalizedDistance = Math.min( + 1, + (pointer.y - (containerBottom - AUTO_SCROLL_EDGE_PX)) / + AUTO_SCROLL_EDGE_PX + ); + scrollDelta = Math.ceil(normalizedDistance * AUTO_SCROLL_MAX_STEP_PX); + } else if (pointer.y < containerTop + AUTO_SCROLL_EDGE_PX) { + const normalizedDistance = Math.min( + 1, + (containerTop + AUTO_SCROLL_EDGE_PX - pointer.y) / + AUTO_SCROLL_EDGE_PX + ); + scrollDelta = -Math.ceil(normalizedDistance * AUTO_SCROLL_MAX_STEP_PX); + } + + if (scrollDelta !== 0) { + const scrollTarget = isDocumentScrollContainer + ? document.scrollingElement + : scrollContainer; + const previousTop = scrollTarget?.scrollTop ?? 0; + + if (scrollTarget) { + scrollTarget.scrollTop += scrollDelta; + } else { + window.scrollBy(0, scrollDelta); + } + + if ((scrollTarget?.scrollTop ?? previousTop) !== previousTop) { + updateDragSelectionAtPoint(pointer.x, pointer.y); + } + } + } + + autoScrollFrameRef.current = window.requestAnimationFrame(step); + }; + + autoScrollFrameRef.current = window.requestAnimationFrame(step); + }, [findScrollContainer, stopAutoScroll, updateDragSelectionAtPoint]); + + const handleGlobalMouseMove = useCallback( + (event: MouseEvent) => { + const dragOrigin = dragOriginClientRef.current; + if ( + dragOrigin && + (Math.abs(event.clientX - dragOrigin.x) > 3 || + Math.abs(event.clientY - dragOrigin.y) > 3) + ) { + pointerHasMovedRef.current = true; + } + pointerClientRef.current = { x: event.clientX, y: event.clientY }; + updateDragSelectionAtPoint(event.clientX, event.clientY); + }, + [updateDragSelectionAtPoint] + ); + const handleDragEnd = useCallback( () => { document.removeEventListener('mousemove', handleGlobalMouseMove); isDraggingRef.current = false; + stopAutoScroll(); if (isAdmin && dragStartPointRef.current) { setDragState((prev) => { @@ -504,9 +645,19 @@ export default function CalBlock({ previousBoundingBox.current = null; dragStartTime.current = null; dragStartPointRef.current = null; + pointerClientRef.current = null; + dragOriginClientRef.current = null; + pointerHasMovedRef.current = false; + scrollContainerRef.current = null; debouncedSetDragState.cancel(); }, - [isAdmin, setDragState, debouncedSetDragState, handleGlobalMouseMove] + [ + isAdmin, + setDragState, + debouncedSetDragState, + handleGlobalMouseMove, + stopAutoScroll, + ] ); const handleSelectionStart = useCallback( @@ -517,6 +668,18 @@ export default function CalBlock({ event.preventDefault(); dragStartTime.current = Date.now(); + const pointer = + 'touches' in event && event.touches.length > 0 + ? { x: event.touches[0].clientX, y: event.touches[0].clientY } + : 'clientX' in event + ? { x: event.clientX, y: event.clientY } + : null; + pointerClientRef.current = pointer; + dragOriginClientRef.current = pointer; + pointerHasMovedRef.current = false; + scrollContainerRef.current = findScrollContainer( + event.currentTarget as Element + ); const newSelectionMode = !isInSelection(); @@ -544,6 +707,7 @@ export default function CalBlock({ lastDragPoint.current = [columnID, blockID]; document.addEventListener('mousemove', handleGlobalMouseMove); document.addEventListener('mouseup', () => handleDragEnd(), { once: true }); + startAutoScroll(); }, [ draggable, @@ -557,65 +721,40 @@ export default function CalBlock({ dragState.completedSelections, handleDragEnd, handleGlobalMouseMove, + findScrollContainer, + startAutoScroll, ] ); const handleSelectionMove = useCallback( (event: React.TouchEvent) => { - if ( - !isDraggingRef.current || - !lastDragPoint.current || - !previousBoundingBox.current || - !dragStartPointRef.current - ) - return; - const touch = event.touches[0]; - const element = document.elementFromPoint(touch.clientX, touch.clientY); - - if (!element?.id?.includes('-')) return; - - const [newCol, newBlock] = element.id.split('-').map(Number); - const [lastCol, lastBlock] = lastDragPoint.current; - - if (newCol === lastCol && newBlock === lastBlock) return; - - const startPoint = dragStartPointRef.current; - const mode = selectionModeRef.current; - - const intermediateBox = getBoundingBox( - [lastCol, lastBlock], - [newCol, newBlock] - ); - - const currentBox = getBoundingBox(startPoint, [newCol, newBlock]); - - if (!isAdmin) { - updateCalendarForBoundingBoxes(intermediateBox, null, mode); - updateCalendarForBoundingBoxes( - currentBox, - previousBoundingBox.current, - mode - ); + const dragOrigin = dragOriginClientRef.current; + if ( + dragOrigin && + (Math.abs(touch.clientX - dragOrigin.x) > 3 || + Math.abs(touch.clientY - dragOrigin.y) > 3) + ) { + pointerHasMovedRef.current = true; } - - previousBoundingBox.current = currentBox; - - debouncedSetDragState((prev: dragProperties) => ({ - ...prev, - endPoint: [newCol, newBlock], - lastPosition: [newCol, newBlock], - })); - - lastDragPoint.current = [newCol, newBlock]; + pointerClientRef.current = { x: touch.clientX, y: touch.clientY }; + updateDragSelectionAtPoint(touch.clientX, touch.clientY); }, - [isAdmin, debouncedSetDragState, updateCalendarForBoundingBoxes] + [updateDragSelectionAtPoint] ); + useEffect(() => { + return () => { + document.removeEventListener('mousemove', handleGlobalMouseMove); + stopAutoScroll(); + }; + }, [handleGlobalMouseMove, stopAutoScroll]); + // ${isInSelection() && is30Minute ? 'border-t-white' : ''} return (
{(isEventStart || isOnGcal || isEventEnd) && ( diff --git a/src/components/selectCalendarComponents/CalendarApp.tsx b/src/components/selectCalendarComponents/CalendarApp.tsx index 306d5f8..78e2148 100644 --- a/src/components/selectCalendarComponents/CalendarApp.tsx +++ b/src/components/selectCalendarComponents/CalendarApp.tsx @@ -201,7 +201,10 @@ export default function Calendar({ className="absolute mt-0 ml-0 top-0 left-0 bg-white dark:bg-secondary_background-dark rounded-tl-none rounded-tr-none z-20" >
-
+
{/* handles aligning it with the cal */}