Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/components/Scroll/story/ai-chat/AIChatDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@ import { convertToItems } from './utils';
export const AIChatDemo: React.FC = () => {
const { messages, isGenerating, generateResponse } = useChatMessages();

const { scrollContainerRef, shouldAutoScroll, scrollToBottom, forceAutoScrollOn } = useAutoScroll({
hasContent: messages.length > 0,
});
const { scrollContainerRef, shouldAutoScroll, scrollToBottom } = useAutoScroll();

// Handle generate button click - force scroll to bottom
// Handle generate button click - scroll to bottom
const handleGenerate = useCallback(() => {
forceAutoScrollOn();
generateResponse();
// Scroll to bottom after a small delay to let the message render
requestAnimationFrame(() => {
scrollToBottom();
});
}, [forceAutoScrollOn, generateResponse, scrollToBottom]);
}, [generateResponse, scrollToBottom]);

// Auto-scroll when messages change (only if user is near bottom and not actively scrolling)
useEffect(() => {
Expand Down
78 changes: 4 additions & 74 deletions src/components/Scroll/story/ai-chat/hooks/use-auto-scroll.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useRef } from 'react';

interface UseAutoScrollOptions {
threshold?: number;
scrollIdleDelay?: number;
hasContent?: boolean;
}

interface UseAutoScrollReturn {
scrollContainerRef: React.RefObject<HTMLDivElement>;
shouldAutoScroll: () => boolean;
scrollToBottom: () => void;
forceAutoScrollOn: () => void;
}

export const useAutoScroll = ({
threshold = 100,
scrollIdleDelay = 150,
hasContent = false,
}: UseAutoScrollOptions = {}): UseAutoScrollReturn => {
export const useAutoScroll = ({ threshold = 100 }: UseAutoScrollOptions = {}): UseAutoScrollReturn => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true);
const isUserScrollingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isProgrammaticScrollRef = useRef(false);
const lastScrollListRef = useRef<HTMLElement | null>(null);

const getScrollList = useCallback((): HTMLElement | null => {
const container = scrollContainerRef.current;
Expand All @@ -43,77 +31,19 @@ export const useAutoScroll = ({
const scrollList = getScrollList();
if (!scrollList) return;

// Mark this as a programmatic scroll so we don't treat it as user scrolling
isProgrammaticScrollRef.current = true;

scrollList.scrollTo({
top: scrollList.scrollHeight,
behavior: 'smooth',
});

// Reset the programmatic flag after the scroll animation completes
// but don't change isAutoScrollEnabled - only user scrolling should change that
setTimeout(() => {
isProgrammaticScrollRef.current = false;
}, 300);
}, [getScrollList]);

const shouldAutoScroll = useCallback((): boolean => {
return isAutoScrollEnabled && !isUserScrollingRef.current;
}, [isAutoScrollEnabled]);

const forceAutoScrollOn = useCallback(() => {
setIsAutoScrollEnabled(true);
isUserScrollingRef.current = false;
}, []);

// Attach scroll listener - re-run when hasContent changes to ensure we catch the scroll-list
useEffect(() => {
const scrollList = getScrollList();
if (!scrollList) return;

// Avoid re-attaching to the same element
if (lastScrollListRef.current === scrollList) return;
lastScrollListRef.current = scrollList;

const handleScroll = () => {
// Ignore programmatic scrolls
if (isProgrammaticScrollRef.current) {
return;
}

// User is actively scrolling
isUserScrollingRef.current = true;

// Clear any existing timeout
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}

// Set a timeout to mark scrolling as stopped
scrollTimeoutRef.current = setTimeout(() => {
isUserScrollingRef.current = false;
// Update auto-scroll status based on position when scrolling stops
const nearBottom = checkIfNearBottom();
setIsAutoScrollEnabled(nearBottom);
}, scrollIdleDelay);
};

scrollList.addEventListener('scroll', handleScroll, { passive: true });

return () => {
scrollList.removeEventListener('scroll', handleScroll);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
lastScrollListRef.current = null;
};
}, [getScrollList, checkIfNearBottom, scrollIdleDelay, hasContent]);
return checkIfNearBottom();
}, [checkIfNearBottom]);

return {
scrollContainerRef,
shouldAutoScroll,
scrollToBottom,
forceAutoScrollOn,
};
};