Adam 1 месяц назад
Родитель
Сommit
c69e3bbde7
1 измененных файлов с 46 добавлено и 1 удалено
  1. 46 1
      packages/ui/src/hooks/create-auto-scroll.tsx

+ 46 - 1
packages/ui/src/hooks/create-auto-scroll.tsx

@@ -13,8 +13,10 @@ export function createAutoScroll(options: AutoScrollOptions) {
   let scroll: HTMLElement | undefined
   let settling = false
   let settleTimer: ReturnType<typeof setTimeout> | undefined
+  let autoTimer: ReturnType<typeof setTimeout> | undefined
   let cleanup: (() => void) | undefined
   let resizeFrame: number | undefined
+  let auto: { top: number; time: number } | undefined
 
   const threshold = () => options.bottomThreshold ?? 10
 
@@ -29,10 +31,46 @@ export function createAutoScroll(options: AutoScrollOptions) {
     return el.scrollHeight - el.clientHeight - el.scrollTop
   }
 
+  // Browsers can dispatch scroll events asynchronously. If new content arrives
+  // between us calling `scrollTo()` and the subsequent `scroll` event firing,
+  // the handler can see a non-zero `distanceFromBottom` and incorrectly assume
+  // the user scrolled.
+  const markAuto = (el: HTMLElement) => {
+    auto = {
+      top: Math.max(0, el.scrollHeight - el.clientHeight),
+      time: Date.now(),
+    }
+
+    if (autoTimer) clearTimeout(autoTimer)
+    autoTimer = setTimeout(() => {
+      auto = undefined
+      autoTimer = undefined
+    }, 250)
+  }
+
+  const isAuto = (el: HTMLElement) => {
+    const a = auto
+    if (!a) return false
+
+    if (Date.now() - a.time > 250) {
+      auto = undefined
+      return false
+    }
+
+    return Math.abs(el.scrollTop - a.top) < 2
+  }
+
   const scrollToBottomNow = (behavior: ScrollBehavior) => {
     const el = scroll
     if (!el) return
-    el.scrollTo({ top: el.scrollHeight, behavior })
+    markAuto(el)
+    if (behavior === "smooth") {
+      el.scrollTo({ top: el.scrollHeight, behavior })
+      return
+    }
+
+    // `scrollTop` assignment bypasses any CSS `scroll-behavior: smooth`.
+    el.scrollTop = el.scrollHeight
   }
 
   const scrollToBottom = (force: boolean) => {
@@ -79,6 +117,12 @@ export function createAutoScroll(options: AutoScrollOptions) {
       return
     }
 
+    // Ignore scroll events triggered by our own scrollToBottom calls.
+    if (!store.userScrolled && isAuto(el)) {
+      scrollToBottom(false)
+      return
+    }
+
     stop()
   }
 
@@ -145,6 +189,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
 
   onCleanup(() => {
     if (settleTimer) clearTimeout(settleTimer)
+    if (autoTimer) clearTimeout(autoTimer)
     if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame)
     if (cleanup) cleanup()
   })