solana-multiagent/hooks/use-scroll-to-bottom.js

112 lines
2.9 KiB
JavaScript
Raw Normal View History

2025-02-17 15:21:20 +07:00
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
export function useScrollToBottom() {
const containerRef = useRef(null);
const [showScrollButton, setShowScrollButton] = useState(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const isUserScrolling = useRef(false);
const isGrowing = useRef(false);
const getViewport = useCallback((element) => {
return element?.closest("[data-radix-scroll-area-viewport]");
}, []);
const isAtBottom = useCallback((viewport) => {
const { scrollTop, scrollHeight, clientHeight } = viewport;
return Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
}, []);
const updateScrollState = useCallback((viewport) => {
const { scrollHeight, clientHeight } = viewport;
const hasScrollableContent = scrollHeight > clientHeight;
const atBottom = isAtBottom(viewport);
setShowScrollButton(hasScrollableContent && !atBottom);
if (!isUserScrolling.current) {
setShouldAutoScroll(atBottom);
}
}, [isAtBottom]);
useEffect(() => {
const container = containerRef.current;
const viewport = getViewport(container);
if (!container || !viewport) {
return;
}
updateScrollState(viewport);
const handleScroll = () => {
if (!isUserScrolling.current) {
updateScrollState(viewport);
}
};
const handleTouchStart = () => {
isUserScrolling.current = true;
};
const handleTouchEnd = () => {
isUserScrolling.current = false;
updateScrollState(viewport);
};
let growthTimeout;
const observer = new MutationObserver(() => {
isGrowing.current = true;
window.clearTimeout(growthTimeout);
if (shouldAutoScroll && !isUserScrolling.current) {
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: "instant",
});
}
updateScrollState(viewport);
growthTimeout = window.setTimeout(() => {
isGrowing.current = false;
}, 100);
});
viewport.addEventListener("scroll", handleScroll, { passive: true });
viewport.addEventListener("touchstart", handleTouchStart);
viewport.addEventListener("touchend", handleTouchEnd);
observer.observe(container, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});
return () => {
window.clearTimeout(growthTimeout);
observer.disconnect();
viewport.removeEventListener("scroll", handleScroll);
viewport.removeEventListener("touchstart", handleTouchStart);
viewport.removeEventListener("touchend", handleTouchEnd);
};
}, [getViewport, updateScrollState, shouldAutoScroll]);
const scrollToBottom = () => {
const viewport = getViewport(containerRef.current);
if (!viewport) {
return;
}
setShouldAutoScroll(true);
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: isGrowing.current ? "instant" : "smooth",
});
};
return [containerRef, showScrollButton, scrollToBottom];
}