feat(frontend): bidirectional lazy load for anchored grid returns
Returning to the grid at a deep position (deep link / hard reload to a file, then back → /files?anchor=<id>) used to load only a tiny forward window at the anchor. Now the grid fills the viewport around the anchor and pages in both directions as the user scrolls. - loadAroundAnchor fetches a window centred on the anchor and pre-fills a few pages each way sequentially, then centres on the anchor once. Doing the initial fill explicitly (rather than via the sentinels) keeps the pages contiguous and leaves the sentinels out of range, so there's no mount-time load storm. - loading starts true when the URL carries an ?anchor, so the child InfiniteScroll sentinels (whose effects run before this page's reset effect on mount) can't fire a stray page-1 loadMore that interleaves with loadAroundAnchor. - loadPrev pages backward (direction=backward) and prepends, then shifts the scroller down by the added height via flushSync (no paint between prepend and correction) so the viewport stays visually fixed. - InfiniteScroll gains an `edge` prop; a top instance (shown only when hasPrev) drives upward loading. Both loaders share the `loading` guard. - Mock: honour direction=backward and emit prev_cursor; the Go backend already supports backward keyset pagination. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,26 +3,32 @@
|
||||
loading?: boolean;
|
||||
hasMore?: boolean;
|
||||
onLoadMore: () => void;
|
||||
/** Which edge to watch: 'bottom' loads on scroll down, 'top' on scroll up. */
|
||||
edge?: 'top' | 'bottom';
|
||||
}
|
||||
|
||||
let { loading = false, hasMore = true, onLoadMore }: Props = $props();
|
||||
let { loading = false, hasMore = true, onLoadMore, edge = 'bottom' }: Props = $props();
|
||||
|
||||
// Lookahead distance below the viewport at which we start loading.
|
||||
// Lookahead distance past the viewport edge at which we start loading.
|
||||
const MARGIN = 300;
|
||||
|
||||
let sentinel = $state<HTMLDivElement | undefined>();
|
||||
|
||||
// Fire onLoadMore while the sentinel is within MARGIN px of the viewport
|
||||
// bottom. Measuring the sentinel's viewport rect (rather than a scroll
|
||||
// container's scrollHeight/clientHeight) makes this correct whether the page
|
||||
// scrolls on <main> or on the window — and it loads exactly enough pages to
|
||||
// reach past the viewport, instead of eagerly loading everything.
|
||||
// True while the sentinel is within MARGIN px of the watched viewport edge.
|
||||
// Measuring the sentinel's viewport rect (rather than a scroll container's
|
||||
// scrollHeight/clientHeight) makes this correct whether the page scrolls on
|
||||
// <main> or on the window, and loads only enough to reach past the viewport.
|
||||
function nearViewport(): boolean {
|
||||
if (!sentinel) return false;
|
||||
const rect = sentinel.getBoundingClientRect();
|
||||
return edge === 'bottom'
|
||||
? rect.top <= window.innerHeight + MARGIN
|
||||
: rect.bottom >= -MARGIN;
|
||||
}
|
||||
|
||||
function maybeLoad() {
|
||||
if (loading || !hasMore || !sentinel) return;
|
||||
const rect = sentinel.getBoundingClientRect();
|
||||
if (rect.top <= window.innerHeight + MARGIN) {
|
||||
onLoadMore();
|
||||
}
|
||||
if (nearViewport()) onLoadMore();
|
||||
}
|
||||
|
||||
// Load on scroll: the observer notifies us when the sentinel nears the viewport.
|
||||
@@ -39,9 +45,8 @@
|
||||
});
|
||||
|
||||
// After each load settles (loading → false), re-check synchronously: if the
|
||||
// freshly appended content still didn't push the sentinel past the viewport,
|
||||
// load again. This fills short pages without the throttled observer lagging
|
||||
// and over-fetching.
|
||||
// freshly added content still didn't push the sentinel past the viewport, load
|
||||
// again. This fills short pages without the throttled observer lagging.
|
||||
$effect(() => {
|
||||
if (!loading) maybeLoad();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user