type ScrollThreshold = { top: number left: number right: number bottom: number } interface ScrollConfig { scrollContainer: HTMLElement scrollThreshold: ScrollThreshold scrollSpeed: number initialX: number } interface ScrollState { isScrolling: boolean animationFrameId: number | null scrollDirection: { x: number; y: number } } interface ScrollControls { updateScroll: (pointerX: number, pointerY: number) => void stop: () => void } export function createAutoScroller(config: ScrollConfig): ScrollControls { const state: ScrollState = { isScrolling: false, animationFrameId: null, scrollDirection: { x: 0, y: 0 }, } /* This functionality is to prevent the grid from scrolling when the user is solely dragging within one of the sticky regions */ const initialRect = config.scrollContainer.getBoundingClientRect() let userExitedLeftThreshold = config.initialX > initialRect.left + config.scrollThreshold.left let userExitedRightThreshold = config.initialX < initialRect.right - config.scrollThreshold.right function calculateScrollSpeed(distance: number, threshold: number): number { const speed = (distance / threshold) * config.scrollSpeed return Math.min(Math.abs(speed), config.scrollSpeed) * Math.sign(speed) } function calculateScrollDirection( pointerPos: number, minBound: number, maxBound: number, lowerThreshold: number, upperThreshold: number ): number { if (pointerPos < minBound + lowerThreshold && userExitedLeftThreshold) { // When pointer is near the left/top edge, we want to scroll in negative direction (left/up) // The closer to the edge, the faster the scroll speed return calculateScrollSpeed( pointerPos - (minBound + lowerThreshold), lowerThreshold ) } else if ( pointerPos > maxBound - upperThreshold && userExitedRightThreshold ) { // When pointer is near the right/bottom edge, we want to scroll in positive direction (right/down) // The closer to the edge, the faster the scroll speed return calculateScrollSpeed( pointerPos - (maxBound - upperThreshold), upperThreshold ) } return 0 } function scroll(): void { if (!state.isScrolling) return const container = config.scrollContainer if (state.scrollDirection.x !== 0) { container.scrollLeft += state.scrollDirection.x } if (state.scrollDirection.y !== 0) { container.scrollTop += state.scrollDirection.y } state.animationFrameId = requestAnimationFrame(scroll) } function startScrolling(): void { if (!state.isScrolling) { state.isScrolling = true scroll() } } function stopScrolling(): void { state.isScrolling = false if (state.animationFrameId !== null) { cancelAnimationFrame(state.animationFrameId) state.animationFrameId = null } } function updateScroll(pointerX: number, pointerY: number): void { const container = config.scrollContainer const rect = container.getBoundingClientRect() const threshold = config.scrollThreshold userExitedLeftThreshold ||= pointerX > rect.left + threshold.left userExitedRightThreshold ||= pointerX < rect.right - threshold.right state.scrollDirection = { x: calculateScrollDirection( pointerX, rect.left, rect.right, threshold.left, threshold.right ), y: calculateScrollDirection( pointerY, rect.top, rect.bottom, threshold.top, threshold.bottom ), } if (state.scrollDirection.x !== 0 || state.scrollDirection.y !== 0) { startScrolling() } else { stopScrolling() } } function stop(): void { stopScrolling() } return { updateScroll, stop, } }