use auto scroll with radix

This commit is contained in:
Nate Kelley 2025-04-10 09:46:27 -06:00
parent 9e2511d6ef
commit 5524f5fd4e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
2 changed files with 68 additions and 5 deletions

View File

@ -224,7 +224,7 @@ export const ScrollAreaComponentWithAutoScroll: Story = {
} }
setIsAutoAddEnabled(false); setIsAutoAddEnabled(false);
} else { } else {
intervalRef.current = setInterval(addCard, 2000); intervalRef.current = setInterval(addCard, 1000);
setIsAutoAddEnabled(true); setIsAutoAddEnabled(true);
enableAutoScroll(); // Enable auto-scroll when auto-adding cards enableAutoScroll(); // Enable auto-scroll when auto-adding cards
} }

View File

@ -1,6 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
/**
* Options for configuring the auto-scroll behavior
*/
interface UseAutoScrollOptions { interface UseAutoScrollOptions {
/** Debounce delay in milliseconds for scroll events */ /** Debounce delay in milliseconds for scroll events */
debounceDelay?: number; debounceDelay?: number;
@ -8,10 +11,13 @@ interface UseAutoScrollOptions {
scrollBehavior?: ScrollBehavior; scrollBehavior?: ScrollBehavior;
/** Whether the auto-scroll functionality is enabled */ /** Whether the auto-scroll functionality is enabled */
enabled?: boolean; enabled?: boolean;
/** Whether to observe deep changes */ /** Whether to observe deep changes in the DOM tree */
observeDeepChanges?: boolean; observeDeepChanges?: boolean;
} }
/**
* Return type for the useAutoScroll hook
*/
interface UseAutoScrollReturn { interface UseAutoScrollReturn {
/** Whether auto-scrolling is currently enabled */ /** Whether auto-scrolling is currently enabled */
isAutoScrollEnabled: boolean; isAutoScrollEnabled: boolean;
@ -21,18 +27,47 @@ interface UseAutoScrollReturn {
scrollToTop: (behavior?: ScrollBehavior) => void; scrollToTop: (behavior?: ScrollBehavior) => void;
/** Manually scroll to a specific node */ /** Manually scroll to a specific node */
scrollToNode: (node: HTMLElement, behavior?: ScrollBehavior) => void; scrollToNode: (node: HTMLElement, behavior?: ScrollBehavior) => void;
/** Enable auto-scrolling */ /** Enable auto-scrolling */
enableAutoScroll: () => void; enableAutoScroll: () => void;
/** Disable auto-scrolling */ /** Disable auto-scrolling */
disableAutoScroll: () => void; disableAutoScroll: () => void;
} }
/**
* Checks if the scrollable element is at the bottom within a given threshold
* @param element - The HTML element to check
* @param threshold - The pixel threshold to consider as "at bottom"
* @returns boolean indicating if the element is scrolled to the bottom
*/
const isAtBottom = (element: HTMLElement, threshold = 30) => { const isAtBottom = (element: HTMLElement, threshold = 30) => {
const { scrollHeight, scrollTop, clientHeight } = element; const { scrollHeight, scrollTop, clientHeight } = element;
return scrollHeight - (scrollTop + clientHeight) <= threshold; return scrollHeight - (scrollTop + clientHeight) <= threshold;
}; };
/**
* A React hook that provides auto-scrolling functionality for a container element.
* It automatically scrolls to the bottom when new content is added and provides
* manual scroll controls.
*
* @param containerRef - React ref object pointing to the scrollable container element
* @param options - Configuration options for the auto-scroll behavior
*
* @example
* ```tsx
* const containerRef = useRef<HTMLDivElement>(null);
* const {
* isAutoScrollEnabled,
* scrollToBottom,
* enableAutoScroll,
* disableAutoScroll
* } = useAutoScroll(containerRef, {
* debounceDelay: 150,
* scrollBehavior: 'smooth'
* });
* ```
*
* @returns An object containing the auto-scroll state and control functions
*/
export const useAutoScroll = ( export const useAutoScroll = (
containerRef: React.RefObject<HTMLElement>, containerRef: React.RefObject<HTMLElement>,
options: UseAutoScrollOptions = {} options: UseAutoScrollOptions = {}
@ -44,10 +79,15 @@ export const useAutoScroll = (
observeDeepChanges = true observeDeepChanges = true
} = options; } = options;
/** Current state of auto-scroll functionality */
const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(enabled); const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(enabled);
/** Tracks if the container was at the bottom during the last scroll event */
const wasAtBottom = useRef(true); const wasAtBottom = useRef(true);
/** Tracks if a scroll operation is currently in progress */
const isScrollingRef = useRef(false); const isScrollingRef = useRef(false);
/** Reference for the mutation observer's debounced callback */
const mutationDebounceRef = useRef<number>(); const mutationDebounceRef = useRef<number>();
/** Flag to indicate if a scroll is being forced programmatically */
const forceScrollRef = useRef(false); const forceScrollRef = useRef(false);
// Update isAutoScrollEnabled when enabled prop changes // Update isAutoScrollEnabled when enabled prop changes
@ -55,6 +95,10 @@ export const useAutoScroll = (
setIsAutoScrollEnabled(enabled); setIsAutoScrollEnabled(enabled);
}, [enabled]); }, [enabled]);
/**
* Scrolls the container to the bottom
* @param behavior - The scroll behavior to use ('auto', 'smooth', or 'instant')
*/
const scrollToBottom = useCallback( const scrollToBottom = useCallback(
(behavior: ScrollBehavior = scrollBehavior) => { (behavior: ScrollBehavior = scrollBehavior) => {
if (!containerRef.current) return; if (!containerRef.current) return;
@ -92,6 +136,10 @@ export const useAutoScroll = (
[containerRef, scrollBehavior, enabled] [containerRef, scrollBehavior, enabled]
); );
/**
* Scrolls the container to the top
* @param behavior - The scroll behavior to use ('auto', 'smooth', or 'instant')
*/
const scrollToTop = useCallback( const scrollToTop = useCallback(
(behavior: ScrollBehavior = scrollBehavior) => { (behavior: ScrollBehavior = scrollBehavior) => {
if (!containerRef.current) return; if (!containerRef.current) return;
@ -104,6 +152,11 @@ export const useAutoScroll = (
[containerRef, scrollBehavior] [containerRef, scrollBehavior]
); );
/**
* Scrolls the container to bring a specific node into view
* @param node - The HTML element to scroll into view
* @param behavior - The scroll behavior to use ('auto', 'smooth', or 'instant')
*/
const scrollToNode = useCallback( const scrollToNode = useCallback(
(node: HTMLElement, behavior: ScrollBehavior = scrollBehavior) => { (node: HTMLElement, behavior: ScrollBehavior = scrollBehavior) => {
if (!containerRef.current || !node) return; if (!containerRef.current || !node) return;
@ -152,7 +205,9 @@ export const useAutoScroll = (
[containerRef, scrollBehavior, enabled] [containerRef, scrollBehavior, enabled]
); );
// Debounced scroll handler /**
* Debounced scroll event handler that manages auto-scroll state based on scroll position
*/
const handleScrollThrottled = useCallback( const handleScrollThrottled = useCallback(
debounce(() => { debounce(() => {
if (!containerRef.current || forceScrollRef.current || !enabled) return; if (!containerRef.current || forceScrollRef.current || !enabled) return;
@ -173,7 +228,9 @@ export const useAutoScroll = (
[containerRef, enabled] [containerRef, enabled]
); );
// Immediate scroll handler that calls the debounced version /**
* Immediate scroll handler that triggers the debounced version
*/
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
if (forceScrollRef.current || !enabled) return; if (forceScrollRef.current || !enabled) return;
@ -234,12 +291,18 @@ export const useAutoScroll = (
}; };
}, [containerRef, handleScroll, handleScrollThrottled, enabled]); }, [containerRef, handleScroll, handleScrollThrottled, enabled]);
/**
* Enables auto-scroll functionality and immediately scrolls to bottom
*/
const enableAutoScroll = useCallback(() => { const enableAutoScroll = useCallback(() => {
if (!enabled) return; if (!enabled) return;
setIsAutoScrollEnabled(true); setIsAutoScrollEnabled(true);
scrollToBottom(); scrollToBottom();
}, [scrollToBottom, enabled]); }, [scrollToBottom, enabled]);
/**
* Disables auto-scroll functionality
*/
const disableAutoScroll = useCallback(() => { const disableAutoScroll = useCallback(() => {
setIsAutoScrollEnabled(false); setIsAutoScrollEnabled(false);
}, []); }, []);