fix(app): auto-scroll ux

This commit is contained in:
Adam
2026-01-21 12:52:52 -06:00
parent 2a370f8038
commit c69e3bbde7

View File

@@ -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()
})