|
34 | 34 | /** Handler for when scroll has reached with a margin of the bottom. */ |
35 | 35 | onloadmore?: () => Promise<void>; |
36 | 36 | grow?: boolean; |
37 | | - /** Whether to initialize scroll position at top or bottom. */ |
38 | | - initialPosition?: 'top' | 'bottom'; |
| 37 | + /** Whether to initialize scroll position at top or bottom (tail). */ |
| 38 | + tail?: boolean; |
39 | 39 | /** Auto-scroll to bottom when new items are added (useful for chat). */ |
40 | 40 | stickToBottom?: boolean; |
41 | 41 | visibility: ScrollbarVisilitySettings; |
|
59 | 59 | padding, |
60 | 60 | visibility, |
61 | 61 | defaultHeight, |
62 | | - initialPosition = 'top', |
| 62 | + tail, |
63 | 63 | stickToBottom = false |
64 | 64 | }: Props = $props(); |
65 | 65 |
|
|
79 | 79 |
|
80 | 80 | // Virtual scrolling state |
81 | 81 | let visibleRange = $state({ |
82 | | - start: initialPosition === 'bottom' ? Infinity : 0, |
83 | | - end: initialPosition === 'bottom' ? Infinity : 0 |
| 82 | + start: tail ? Infinity : 0, |
| 83 | + end: tail ? Infinity : 0 |
84 | 84 | }); |
85 | 85 |
|
86 | 86 | // An array mapping items to element heights |
|
223 | 223 |
|
224 | 224 | let isRecalculating = false; |
225 | 225 |
|
226 | | - async function recalculateVisibleRange(isScroll?: boolean) { |
| 226 | + async function recalculateVisibleRange() { |
227 | 227 | if (!viewport || !visibleRowElements) return; |
228 | 228 | if (isRecalculating) return; // One at a time. |
229 | 229 |
|
230 | 230 | isRecalculating = true; |
231 | 231 | heightMap.length = itemChunks.length; |
232 | 232 |
|
233 | 233 | // Handle bottom initialization |
234 | | - if (!hasInitialized && initialPosition === 'bottom') { |
| 234 | + if (!hasInitialized && tail) { |
235 | 235 | // Start from the last chunk and work backwards |
236 | 236 | visibleRange.end = itemChunks.length; |
237 | 237 | offset.bottom = 0; |
|
249 | 249 | }, 20); |
250 | 250 | } else { |
251 | 251 | await tick(); |
252 | | - const previousDistanceFromBottom = lastDistanceFromBottom; |
253 | 252 | const previousStartIndex = visibleRange.start; |
254 | 253 |
|
255 | 254 | visibleRange = { |
256 | 255 | start: await calculateVisibleStartIndex(), |
257 | 256 | end: await calculateVisibleEndIndex() |
258 | 257 | }; |
259 | | - offset.bottom = calculateHeightSum(visibleRange.end, heightMap.length); |
260 | | - offset.top = calculateHeightSum(0, visibleRange.start); |
| 258 | + offset = { |
| 259 | + bottom: calculateHeightSum(visibleRange.end, heightMap.length), |
| 260 | + top: calculateHeightSum(0, visibleRange.start) |
| 261 | + }; |
261 | 262 |
|
262 | 263 | if (visibleRange.start < previousStartIndex) { |
263 | 264 | await tick(); |
|
269 | 270 | } |
270 | 271 | } |
271 | 272 | await tick(); |
272 | | -
|
273 | | - if ( |
274 | | - !isScroll && |
275 | | - stickToBottom && |
276 | | - previousDistanceFromBottom < STICKY_DISTANCE && |
277 | | - getDistanceFromBottom() > previousDistanceFromBottom |
278 | | - ) { |
279 | | - setTimeout(() => { |
280 | | - if (!viewport) return; |
281 | | - viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }); |
282 | | - }, 0); |
283 | | - await tick(); |
284 | | - } |
285 | | -
|
286 | 273 | totalHeight = calculateHeightSum(0, heightMap.length); |
287 | 274 | } |
288 | 275 |
|
|
302 | 289 | $effect(() => { |
303 | 290 | if (viewport) { |
304 | 291 | visibleRowElements = viewport.getElementsByClassName('list-row'); |
305 | | - resizeObserver = new ResizeObserver(() => untrack(() => recalculateVisibleRange())); |
| 292 | + resizeObserver = new ResizeObserver(() => |
| 293 | + untrack(() => { |
| 294 | + // recalculateVisibleRange(); |
| 295 | + const hasGrown = getDistanceFromBottom() > lastDistanceFromBottom; |
| 296 | + if (hasGrown && stickToBottom && lastDistanceFromBottom < STICKY_DISTANCE) { |
| 297 | + if (viewport) { |
| 298 | + viewport.scrollTo({ |
| 299 | + top: viewport.scrollHeight, |
| 300 | + behavior: hasInitialized ? 'smooth' : 'instant' |
| 301 | + }); |
| 302 | + } |
| 303 | + } |
| 304 | + }) |
| 305 | + ); |
306 | 306 | return () => { |
307 | 307 | resizeObserver?.disconnect(); |
308 | 308 | }; |
|
333 | 333 | if (!viewport) return; |
334 | 334 | hasNewUnreadItems = false; |
335 | 335 | visibleRange = { end: itemChunks.length, start: itemChunks.length - 1 }; |
336 | | - offset.bottom = 0; |
337 | | - offset.top = calculateHeightSum(0, visibleRange.start); |
| 336 | + offset = { bottom: 0, top: calculateHeightSum(0, visibleRange.start) }; |
338 | 337 | lastDistanceFromBottom = 0; |
339 | 338 | await tick(); |
340 | 339 | viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'instant' }); |
|
352 | 351 | // bottom of the chat bubble. |
353 | 352 | setTimeout(() => { |
354 | 353 | if (!viewport) return; |
355 | | - viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }); |
356 | | - }, 1000); |
| 354 | + viewport.scrollTo({ |
| 355 | + top: viewport.scrollHeight, |
| 356 | + behavior: hasInitialized ? 'smooth' : 'instant' |
| 357 | + }); |
| 358 | + }, 0); |
357 | 359 | }); |
358 | 360 | } else if (items) { |
359 | 361 | untrack(() => { |
360 | 362 | const hadNewItems = items.length > previousItemsLength && items.length > visibleRange.end; |
361 | 363 | recalculateVisibleRange(); |
362 | | - if (initialPosition === 'bottom' && hadNewItems) { |
| 364 | + if (tail && hadNewItems) { |
363 | 365 | hasNewUnreadItems = true; |
364 | 366 | } |
365 | 367 | }); |
|
371 | 373 | <ScrollableContainer |
372 | 374 | bind:viewportHeight |
373 | 375 | bind:viewport |
374 | | - onscroll={() => recalculateVisibleRange(true)} |
| 376 | + onscroll={() => recalculateVisibleRange()} |
375 | 377 | wide={grow} |
376 | 378 | whenToShow={visibility} |
377 | 379 | {padding} |
|
0 commit comments