From b83186166270c1edf1a09b10665df0a558493866 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 20 Apr 2026 22:14:44 +0300 Subject: [PATCH] feat(VirtualList): add onJump callback for scroll-beyond-loaded detection --- src/shared/ui/VirtualList/VirtualList.svelte | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/shared/ui/VirtualList/VirtualList.svelte b/src/shared/ui/VirtualList/VirtualList.svelte index 56f1ffb..1accfa1 100644 --- a/src/shared/ui/VirtualList/VirtualList.svelte +++ b/src/shared/ui/VirtualList/VirtualList.svelte @@ -62,6 +62,10 @@ interface Props extends * Near bottom callback */ onNearBottom?: (lastVisibleIndex: number) => void; + /** + * Fires when scroll position exceeds loaded content — user jumped beyond data + */ + onJump?: (targetIndex: number) => void; /** * Item render snippet */ @@ -95,6 +99,7 @@ let { class: className, onVisibleItemsChange, onNearBottom, + onJump, children, useWindowScroll = false, isLoading = false, @@ -170,6 +175,10 @@ const throttledNearBottom = throttle((lastVisibleIndex: number) => { onNearBottom?.(lastVisibleIndex); }, 200); // 200ms throttle +const throttledOnJump = throttle((targetIndex: number) => { + onJump?.(targetIndex); +}, 200); + // Calculate top/bottom padding for spacer elements // In CSS Grid, gap creates space BETWEEN elements. // The top spacer should place the first row at its virtual offset. @@ -227,6 +236,26 @@ $effect(() => { } } }); + +$effect(() => { + // Fire onJump when scroll is beyond the loaded content boundary. + // Target index estimates which item the user scrolled to. + if (!onJump || !virtualizer.containerHeight || virtualizer.scrollOffset <= 0) { + return; + } + + const isAhead = virtualizer.scrollOffset > virtualizer.totalSize; + if (!isAhead) { + return; + } + + const estimatedItemHeight = typeof itemHeight === 'number' ? itemHeight : 80; + // Include visible rows + overscan so the bottom of the viewport is fully covered + const topItemIndex = Math.floor(virtualizer.scrollOffset / estimatedItemHeight) * columns; + const visibleRows = Math.ceil(virtualizer.containerHeight / estimatedItemHeight); + const targetIndex = topItemIndex + (visibleRows + overscan) * columns; + throttledOnJump(targetIndex); +}); {#snippet content()}