mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Add InfiniteScrollSentinel and ScrollArea viewport context
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user