From 9e2511d6efddf8a2a2d01f53f3700882337722fe Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 10 Apr 2025 09:41:40 -0600 Subject: [PATCH] use auto scroll with radix --- .../components/ui/scroll-area/ScrollArea.tsx | 7 +- web/src/hooks/useAutoScroll.stories.tsx | 114 ++++++++++++++++++ web/src/hooks/useAutoScroll.ts | 20 ++- .../metrics/files/MyYamlEditor.stories.tsx | 2 +- 4 files changed, 136 insertions(+), 7 deletions(-) diff --git a/web/src/components/ui/scroll-area/ScrollArea.tsx b/web/src/components/ui/scroll-area/ScrollArea.tsx index cbdc7c285..e2e23636a 100644 --- a/web/src/components/ui/scroll-area/ScrollArea.tsx +++ b/web/src/components/ui/scroll-area/ScrollArea.tsx @@ -7,13 +7,16 @@ import { cn } from '@/lib/utils'; const ScrollArea = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + viewportRef?: React.RefObject; + } +>(({ className, children, viewportRef, ...props }, ref) => ( {children} diff --git a/web/src/hooks/useAutoScroll.stories.tsx b/web/src/hooks/useAutoScroll.stories.tsx index d9f44b7d6..ce14d35c5 100644 --- a/web/src/hooks/useAutoScroll.stories.tsx +++ b/web/src/hooks/useAutoScroll.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { useRef, useState, useCallback, useEffect } from 'react'; import { useAutoScroll } from './useAutoScroll'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { faker } from '@faker-js/faker'; interface Message { id: number; @@ -186,3 +188,115 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const ScrollAreaComponentWithAutoScroll: Story = { + render: () => { + const generateCard = (index: number) => ({ + id: index, + title: faker.company.name() + ' ' + index, + color: faker.color.rgb(), + sentences: faker.lorem.sentences(2) + }); + + const containerRef = useRef(null); + const [cards, setCards] = useState(() => + Array.from({ length: 9 }, (_, i) => generateCard(i + 1)) + ); + const [isAutoAddEnabled, setIsAutoAddEnabled] = useState(false); + const intervalRef = useRef(); + const { + isAutoScrollEnabled, + scrollToBottom, + scrollToTop, + enableAutoScroll, + disableAutoScroll + } = useAutoScroll(containerRef, { observeDeepChanges: true }); + + const addCard = useCallback(() => { + setCards((prev) => [...prev, generateCard(prev.length + 1)]); + }, []); + + const toggleAutoAdd = useCallback(() => { + if (isAutoAddEnabled) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = undefined; + } + setIsAutoAddEnabled(false); + } else { + intervalRef.current = setInterval(addCard, 2000); + setIsAutoAddEnabled(true); + enableAutoScroll(); // Enable auto-scroll when auto-adding cards + } + }, [isAutoAddEnabled, addCard, enableAutoScroll]); + + // Cleanup interval on unmount + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + return ( +
+
+

Scrollable Grid Layout

+
+ + + + + +
+
+ +
+ {cards.map((card) => ( +
+

{card.title}

+
+

{card.sentences}

+
+
+ ))} +
+
+
+ ); + } +}; diff --git a/web/src/hooks/useAutoScroll.ts b/web/src/hooks/useAutoScroll.ts index acde94267..9532b5f71 100644 --- a/web/src/hooks/useAutoScroll.ts +++ b/web/src/hooks/useAutoScroll.ts @@ -8,6 +8,8 @@ interface UseAutoScrollOptions { scrollBehavior?: ScrollBehavior; /** Whether the auto-scroll functionality is enabled */ enabled?: boolean; + /** Whether to observe deep changes */ + observeDeepChanges?: boolean; } interface UseAutoScrollReturn { @@ -35,7 +37,12 @@ export const useAutoScroll = ( containerRef: React.RefObject, options: UseAutoScrollOptions = {} ): UseAutoScrollReturn => { - const { debounceDelay = 150, scrollBehavior = 'smooth', enabled = true } = options; + const { + debounceDelay = 150, + scrollBehavior = 'smooth', + enabled = true, + observeDeepChanges = true + } = options; const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(enabled); const wasAtBottom = useRef(true); @@ -179,10 +186,14 @@ export const useAutoScroll = ( // Handle content changes useEffect(() => { const container = containerRef.current; - if (!container || !enabled) return; + + if (!container || !enabled || !isAutoScrollEnabled) return; + + let numberOfMutations = 0; // Debounced mutation handler to prevent rapid scroll updates const handleMutation = () => { + numberOfMutations++; if (mutationDebounceRef.current) { window.cancelAnimationFrame(mutationDebounceRef.current); } @@ -198,8 +209,9 @@ export const useAutoScroll = ( observer.observe(container, { childList: true, // Only observe direct children changes - subtree: false, // Don't observe deep changes - characterData: false // Don't observe text changes + subtree: observeDeepChanges, // Don't observe deep changes + characterData: false, // Don't observe text changes, + attributes: false }); return () => { diff --git a/web/src/lib/metrics/files/MyYamlEditor.stories.tsx b/web/src/lib/metrics/files/MyYamlEditor.stories.tsx index 07dc07fd8..71a65e734 100644 --- a/web/src/lib/metrics/files/MyYamlEditor.stories.tsx +++ b/web/src/lib/metrics/files/MyYamlEditor.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { MyYamlEditor } from './validateMetricYaml'; const meta: Meta = { - title: 'Metrics/Files/MyYamlEditor', + title: 'Lib/Files/MyYamlEditorWithValidation', component: MyYamlEditor, tags: ['autodocs'], parameters: {