|
1 |
| -import { RefObject, useEffect, useState } from "react"; |
| 1 | +import { RefObject, useEffect, useState, useCallback } from "react"; |
2 | 2 |
|
3 | 3 | export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
|
4 |
| - // for auto-scroll |
| 4 | + // Track whether we should auto-scroll to the bottom when content changes |
| 5 | + const [shouldScrollToBottom, setShouldScrollToBottom] = useState(true); |
5 | 6 |
|
6 |
| - const [autoScroll, setAutoScroll] = useState(true); |
| 7 | + // Track whether the user is currently at the bottom of the scroll area |
7 | 8 | const [hitBottom, setHitBottom] = useState(true);
|
8 | 9 |
|
9 |
| - const onChatBodyScroll = (e: HTMLElement) => { |
10 |
| - const bottomHeight = e.scrollTop + e.clientHeight; |
| 10 | + // Check if the scroll position is at the bottom |
| 11 | + const isAtBottom = useCallback((element: HTMLElement): boolean => { |
| 12 | + const bottomThreshold = 10; // Pixels from bottom to consider "at bottom" |
| 13 | + const bottomPosition = element.scrollTop + element.clientHeight; |
| 14 | + return bottomPosition >= element.scrollHeight - bottomThreshold; |
| 15 | + }, []); |
11 | 16 |
|
12 |
| - const isHitBottom = bottomHeight >= e.scrollHeight - 10; |
| 17 | + // Handle scroll events |
| 18 | + const onChatBodyScroll = useCallback( |
| 19 | + (e: HTMLElement) => { |
| 20 | + const isCurrentlyAtBottom = isAtBottom(e); |
| 21 | + setHitBottom(isCurrentlyAtBottom); |
13 | 22 |
|
14 |
| - setHitBottom(isHitBottom); |
15 |
| - setAutoScroll(isHitBottom); |
16 |
| - }; |
| 23 | + // Only update shouldScrollToBottom when user manually scrolls |
| 24 | + // This prevents content changes from affecting our scroll behavior decision |
| 25 | + setShouldScrollToBottom(isCurrentlyAtBottom); |
| 26 | + }, |
| 27 | + [isAtBottom], |
| 28 | + ); |
17 | 29 |
|
18 |
| - function scrollDomToBottom() { |
| 30 | + // Scroll to bottom function with animation |
| 31 | + const scrollDomToBottom = useCallback(() => { |
19 | 32 | const dom = scrollRef.current;
|
20 | 33 | if (dom) {
|
21 | 34 | requestAnimationFrame(() => {
|
22 |
| - setAutoScroll(true); |
23 |
| - dom.scrollTo({ top: dom.scrollHeight, behavior: "auto" }); |
| 35 | + // Set shouldScrollToBottom to true when manually scrolling to bottom |
| 36 | + setShouldScrollToBottom(true); |
| 37 | + setHitBottom(true); |
| 38 | + |
| 39 | + // Use smooth scrolling but with a fast duration |
| 40 | + dom.scrollTo({ |
| 41 | + top: dom.scrollHeight, |
| 42 | + behavior: "smooth", |
| 43 | + }); |
24 | 44 | });
|
25 | 45 | }
|
26 |
| - } |
| 46 | + }, [scrollRef]); |
27 | 47 |
|
28 |
| - // auto scroll |
| 48 | + // Auto-scroll effect that runs when content changes |
29 | 49 | useEffect(() => {
|
30 |
| - if (autoScroll) { |
31 |
| - scrollDomToBottom(); |
| 50 | + // Only auto-scroll if the user was already at the bottom |
| 51 | + if (shouldScrollToBottom) { |
| 52 | + const dom = scrollRef.current; |
| 53 | + if (dom) { |
| 54 | + requestAnimationFrame(() => { |
| 55 | + dom.scrollTo({ |
| 56 | + top: dom.scrollHeight, |
| 57 | + behavior: "smooth", |
| 58 | + }); |
| 59 | + }); |
| 60 | + } |
32 | 61 | }
|
33 | 62 | });
|
34 | 63 |
|
35 | 64 | return {
|
36 | 65 | scrollRef,
|
37 |
| - autoScroll, |
38 |
| - setAutoScroll, |
| 66 | + autoScroll: shouldScrollToBottom, |
| 67 | + setAutoScroll: setShouldScrollToBottom, |
39 | 68 | scrollDomToBottom,
|
40 | 69 | hitBottom,
|
41 | 70 | setHitBottom,
|
|
0 commit comments