import React, { useEffect, useRef, useState, useCallback } from 'react'; import styles from './AutoScroll.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; export interface AutoScrollProps { /** * Children to render inside the scrollable container */ children: React.ReactNode; /** * Optional className for the container */ className?: string; /** * Dependency array to watch for changes that should trigger auto-scroll * Typically the length of the items array or a unique identifier */ scrollDependency?: any; /** * Threshold in pixels from bottom to consider user "at bottom" * @default 100 */ threshold?: number; } /** * AutoScroll component that automatically scrolls to the bottom when new content is added, * unless the user has scrolled up. Shows a button when user has scrolled up. */ const AutoScroll: React.FC = ({ children, className = '', scrollDependency, threshold = 100 }) => { const { t } = useLanguage(); const containerRef = useRef(null); const [showNewMessageButton, setShowNewMessageButton] = useState(false); const [isUserScrolling, setIsUserScrolling] = useState(false); const scrollTimeoutRef = useRef(null); const lastScrollDependencyRef = useRef(scrollDependency); const isScrollingProgrammaticallyRef = useRef(false); // Check if user is near the bottom of the scroll container const isNearBottom = useCallback((): boolean => { const container = containerRef.current; if (!container) return true; const { scrollTop, scrollHeight, clientHeight } = container; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; return distanceFromBottom <= threshold; }, [threshold]); // Scroll to bottom const scrollToBottom = useCallback((smooth: boolean = true) => { const container = containerRef.current; if (!container) return; isScrollingProgrammaticallyRef.current = true; container.scrollTo({ top: container.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }); // Reset flag and update button visibility after scroll completes setTimeout(() => { isScrollingProgrammaticallyRef.current = false; setShowNewMessageButton(!isNearBottom()); }, smooth ? 500 : 100); }, [isNearBottom]); // Handle scroll events const handleScroll = useCallback(() => { // Ignore programmatic scrolling if (isScrollingProgrammaticallyRef.current) { return; } // Clear existing timeout if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } // Mark that user is scrolling setIsUserScrolling(true); // Check if user is near bottom const nearBottom = isNearBottom(); // Always show button when scrolled up setShowNewMessageButton(!nearBottom); // Reset user scrolling flag after a delay scrollTimeoutRef.current = setTimeout(() => { setIsUserScrolling(false); }, 150); }, [isNearBottom]); // Auto-scroll when content changes useEffect(() => { const container = containerRef.current; if (!container) return; const hasNewContent = scrollDependency !== lastScrollDependencyRef.current; lastScrollDependencyRef.current = scrollDependency; // Only auto-scroll if: // 1. There's new content // 2. User is not currently scrolling // 3. User is near the bottom (or was near bottom before new content) if (hasNewContent && !isUserScrolling) { if (isNearBottom()) { // User is at bottom, scroll to show new content scrollToBottom(true); setShowNewMessageButton(false); } else { // User has scrolled up, show button setShowNewMessageButton(true); } } else if (!isUserScrolling) { // Check scroll position even if no new content (to update button visibility) setShowNewMessageButton(!isNearBottom()); } }, [scrollDependency, isUserScrolling, isNearBottom, scrollToBottom]); // Handle new message button click const handleNewMessageClick = useCallback(() => { scrollToBottom(true); setShowNewMessageButton(false); }, [scrollToBottom]); // Initial scroll to bottom on mount and check scroll position useEffect(() => { if (containerRef.current) { scrollToBottom(false); // Check scroll position after a brief delay to ensure DOM is ready setTimeout(() => { setShowNewMessageButton(!isNearBottom()); }, 100); } }, [scrollToBottom, isNearBottom]); return (
{showNewMessageButton && ( )}
{children}
); }; export default AutoScroll;