hooks.ts73 lines · main
1import { useCallback, useEffect, useRef, useState } from 'react'
2
3interface UseAutoScrollProps {
4 enabled?: boolean
5}
6
7export function useAutoScroll({ enabled = true }: UseAutoScrollProps = {}) {
8 const [container, setContainer] = useState<HTMLDivElement | null>(null)
9 const [isSticky, setIsSticky] = useState(true)
10 const isStickyRef = useRef(true)
11 const lastScrollHeightRef = useRef<number>(null)
12
13 const ref = useCallback((element: HTMLDivElement | null) => {
14 if (element) {
15 setContainer(element)
16 }
17 }, [])
18
19 const scrollToEnd = useCallback(() => {
20 if (container) {
21 isStickyRef.current = true
22 setIsSticky(true)
23 container.scrollTo({
24 top: container.scrollHeight,
25 behavior: 'smooth',
26 })
27 }
28 }, [container])
29
30 useEffect(() => {
31 if (!container || !enabled) return
32
33 let timeoutId: NodeJS.Timeout
34
35 const resizeObserver = new ResizeObserver(() => {
36 clearTimeout(timeoutId)
37 timeoutId = setTimeout(() => {
38 if (
39 lastScrollHeightRef.current != null &&
40 container.scrollHeight !== lastScrollHeightRef.current
41 ) {
42 lastScrollHeightRef.current = container.scrollHeight
43 if (isStickyRef.current) {
44 scrollToEnd()
45 }
46 }
47 }, 100)
48 })
49
50 const handleScroll = () => {
51 const isAtBottom =
52 Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) < 10
53
54 isStickyRef.current = isAtBottom
55 setIsSticky(isAtBottom)
56 }
57
58 // Observe all children of the container
59 Array.from(container.children).forEach((child) => {
60 resizeObserver.observe(child)
61 })
62
63 container.addEventListener('scroll', handleScroll)
64
65 return () => {
66 clearTimeout(timeoutId)
67 resizeObserver.disconnect()
68 container.removeEventListener('scroll', handleScroll)
69 }
70 }, [container, enabled, scrollToEnd])
71
72 return { ref, isSticky, scrollToEnd, setIsSticky }
73}