Add InfiniteScrollSentinel and ScrollArea viewport context

This commit is contained in:
Rūdolfs Ošiņš
2026-04-27 18:00:01 +02:00
parent c0b1e41aef
commit bb0ae84156
2 changed files with 86 additions and 10 deletions
@@ -0,0 +1,44 @@
<script lang="ts">
import { getScrollViewport } from "@app/components/ScrollArea.svelte";
interface Props {
onIntersect: () => void | Promise<void>;
disabled?: boolean;
rootMargin?: string;
}
const {
onIntersect,
disabled = false,
rootMargin = "500%",
}: Props = $props();
const getViewport = getScrollViewport();
let sentinel: HTMLElement | undefined = $state();
let firing = false;
$effect(() => {
if (disabled) return;
const viewport = getViewport();
if (!viewport || !sentinel) return;
if (!("IntersectionObserver" in window)) return;
const observer = new IntersectionObserver(
entries => {
for (const entry of entries) {
if (!entry.isIntersecting || firing) continue;
firing = true;
void Promise.resolve(onIntersect()).finally(() => {
firing = false;
});
}
},
{ root: viewport, rootMargin },
);
observer.observe(sentinel);
return () => observer.disconnect();
});
</script>
<div bind:this={sentinel} aria-hidden="true" style="height: 1px;"></div>
+42 -10
View File
@@ -1,7 +1,21 @@
<script lang="ts" module>
import { getContext } from "svelte";
const SCROLL_VIEWPORT_CONTEXT = Symbol("scroll-viewport");
export function getScrollViewport(): () => HTMLElement | undefined {
return (
getContext<() => HTMLElement | undefined>(SCROLL_VIEWPORT_CONTEXT) ??
(() => undefined)
);
}
</script>
<script lang="ts">
import type { Snippet } from "svelte";
import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
import { setContext } from "svelte";
interface Props {
children: Snippet;
@@ -14,6 +28,26 @@
style = "height: 100%;",
onScrollHalf = undefined,
}: Props = $props();
let viewport: HTMLElement | undefined = $state(undefined);
setContext(SCROLL_VIEWPORT_CONTEXT, () => viewport);
function shouldLoadMore(instance: {
elements(): { target: HTMLElement };
}): boolean {
const el = instance.elements().target;
const threshold = 200;
return el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
}
const scrollHalfHandler = onScrollHalf
? (instance: Parameters<typeof shouldLoadMore>[0]) => {
if (shouldLoadMore(instance)) {
onScrollHalf();
}
}
: undefined;
</script>
<OverlayScrollbarsComponent
@@ -22,16 +56,14 @@
options={{
scrollbars: { theme: "global-os-theme-radicle", autoHide: "scroll" },
}}
events={onScrollHalf
? {
scroll: instance => {
const el = instance.elements().target;
if (el.scrollTop + el.clientHeight >= el.scrollHeight / 2) {
onScrollHalf();
}
},
}
: undefined}
events={{
initialized: instance => {
viewport = instance.elements().viewport;
scrollHalfHandler?.(instance);
},
scroll: scrollHalfHandler,
updated: scrollHalfHandler,
}}
defer>
{@render children()}
</OverlayScrollbarsComponent>