Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add InfiniteScrollSentinel and ScrollArea viewport context
Rūdolfs Ošiņš committed 2 days ago
commit bb0ae8415652724af873e51bf8fd89e4258c557a
parent c0b1e41
2 files changed +86 -10
added src/components/InfiniteScrollSentinel.svelte
@@ -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>
modified src/components/ScrollArea.svelte
@@ -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>