frontend_nyla/src/components/UiComponents/AutoScroll/AutoScroll.tsx
2026-04-09 00:11:35 +02:00

170 lines
5.1 KiB
TypeScript

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<AutoScrollProps> = ({ children,
className = '',
scrollDependency,
threshold = 100
}) => {
const { t } = useLanguage();
const containerRef = useRef<HTMLDivElement>(null);
const [showNewMessageButton, setShowNewMessageButton] = useState(false);
const [isUserScrolling, setIsUserScrolling] = useState(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastScrollDependencyRef = useRef<any>(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 (
<div className={`${styles.autoScrollContainer} ${className}`}>
{showNewMessageButton && (
<button
className={styles.scrollToBottomButton}
onClick={handleNewMessageClick}
aria-label={t('autoScroll.scrollToBottom')}
>
<span className={styles.scrollArrow}></span>
</button>
)}
<div
ref={containerRef}
className={styles.scrollableContent}
onScroll={handleScroll}
>
{children}
</div>
</div>
);
};
export default AutoScroll;