Skip to content

Commit 496ebd5

Browse files
committed
feat(scroll): enhance auto-scroll behavior with user scroll tracking
1 parent b076c15 commit 496ebd5

File tree

1 file changed

+50
-13
lines changed

1 file changed

+50
-13
lines changed

playground/src/App.vue

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ const showSettings = ref(false)
123123
const messagesContainer = ref<HTMLElement | null>(null)
124124
const autoScrollEnabled = ref(true) // Track if auto-scroll is enabled
125125
const lastScrollTop = ref(0) // Track last scroll position to detect scroll direction
126+
// Track the last user-driven scroll direction: 'none' (no user scroll yet), 'up', or 'down'
127+
const lastUserScrollDirection = ref<'none' | 'up' | 'down'>('none')
128+
// Timestamp of last user scroll event (ms)
129+
const lastUserScrollTime = ref(0)
130+
// Flag to ignore scroll events caused by our own programmatic scrolling
131+
const isProgrammaticScroll = ref(false)
126132
127133
// Check if user is at the bottom of scroll area
128134
function isAtBottom(element: HTMLElement, threshold = 50): boolean {
@@ -134,19 +140,28 @@ function handleContainerScroll() {
134140
if (!messagesContainer.value)
135141
return
136142
143+
// Ignore scroll events initiated by our programmatic scrollTo calls
144+
if (isProgrammaticScroll.value)
145+
return
146+
137147
const currentScrollTop = messagesContainer.value.scrollTop
138148
139-
// Detect scroll direction: if user scrolls up (scrollTop decreased), disable auto-scroll immediately
149+
// Update timestamp and determine direction
150+
lastUserScrollTime.value = Date.now()
140151
if (currentScrollTop < lastScrollTop.value) {
141-
// User is scrolling up - disable auto-scroll
152+
// User scrolled up
153+
lastUserScrollDirection.value = 'up'
142154
autoScrollEnabled.value = false
143155
}
144-
else if (isAtBottom(messagesContainer.value)) {
145-
// User is scrolling down and near bottom - re-enable auto-scroll
146-
autoScrollEnabled.value = true
156+
else if (currentScrollTop > lastScrollTop.value) {
157+
// User scrolled down
158+
lastUserScrollDirection.value = 'down'
159+
// If near bottom, re-enable auto-scroll
160+
if (isAtBottom(messagesContainer.value))
161+
autoScrollEnabled.value = true
147162
}
148163
149-
// Update last scroll position
164+
// Update last scroll position for future comparisons
150165
lastScrollTop.value = currentScrollTop
151166
}
152167
@@ -160,12 +175,16 @@ function handleWheel(e: WheelEvent) {
160175
if (!messagesContainer.value)
161176
return
162177
163-
// User scrolled up (want older content)
178+
// Treat wheel as a user-driven scroll; record time and direction
179+
lastUserScrollTime.value = Date.now()
164180
if (e.deltaY < 0) {
181+
// Scrolling up
182+
lastUserScrollDirection.value = 'up'
165183
autoScrollEnabled.value = false
166184
}
167-
else {
168-
// Scrolling down: if near bottom, re-enable
185+
else if (e.deltaY > 0) {
186+
// Scrolling down
187+
lastUserScrollDirection.value = 'down'
169188
if (isAtBottom(messagesContainer.value))
170189
autoScrollEnabled.value = true
171190
}
@@ -189,10 +208,13 @@ function handleTouchMove(e: TouchEvent) {
189208
const currentY = e.touches[0].clientY
190209
const delta = currentY - touchStartY.value
191210
// Positive delta means finger moved down -> content scrolls up (towards top) -> user viewing earlier content
211+
lastUserScrollTime.value = Date.now()
192212
if (delta > 0) {
213+
lastUserScrollDirection.value = 'up'
193214
autoScrollEnabled.value = false
194215
}
195-
else {
216+
else if (delta < 0) {
217+
lastUserScrollDirection.value = 'down'
196218
if (isAtBottom(messagesContainer.value))
197219
autoScrollEnabled.value = true
198220
}
@@ -206,10 +228,13 @@ function handlePointerDown(e: PointerEvent) {
206228
if (pointerStartY.value == null)
207229
return
208230
const delta = ev.clientY - pointerStartY.value
231+
lastUserScrollTime.value = Date.now()
209232
if (delta > 0) {
233+
lastUserScrollDirection.value = 'up'
210234
autoScrollEnabled.value = false
211235
}
212-
else {
236+
else if (delta < 0) {
237+
lastUserScrollDirection.value = 'down'
213238
if (messagesContainer.value && isAtBottom(messagesContainer.value))
214239
autoScrollEnabled.value = true
215240
}
@@ -285,8 +310,20 @@ watch(content, () => {
285310
286311
const el = messagesContainer.value
287312
const prevScrollHeight = el.scrollHeight
288-
// Force immediate jump to bottom
289-
el.scrollTo({ top: el.scrollHeight, behavior: 'auto' })
313+
// Force immediate jump to bottom. Mark as programmatic so our scroll handlers ignore it.
314+
try {
315+
isProgrammaticScroll.value = true
316+
el.scrollTo({ top: el.scrollHeight, behavior: 'auto' })
317+
}
318+
finally {
319+
// Allow handlers to run again after a short tick so lastScrollTop can be updated correctly
320+
// We clear the flag after next frame below (so handlers triggered this frame are ignored).
321+
}
322+
323+
// Yield a frame to ensure the scroll event (if emitted) happens while isProgrammaticScroll is true
324+
await new Promise(resolve => requestAnimationFrame(() => resolve(undefined)))
325+
// Clear programmatic flag now so future user scrolls are handled
326+
isProgrammaticScroll.value = false
290327
291328
// If height didn't change much or we're at bottom, stop retrying
292329
if (Math.abs(el.scrollHeight - prevScrollHeight) < 2 || isAtBottom(el, 2))

0 commit comments

Comments
 (0)