From c9c8b9abfc07e4ef14dc985fe0a624703ee94548 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 2 Feb 2026 12:11:48 +0300 Subject: [PATCH] feat(Section): add logic that triggers a callback when sections title moves out of the viewport --- src/shared/ui/Section/Section.svelte | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/shared/ui/Section/Section.svelte b/src/shared/ui/Section/Section.svelte index b9ececc..e5b0c5f 100644 --- a/src/shared/ui/Section/Section.svelte +++ b/src/shared/ui/Section/Section.svelte @@ -29,15 +29,53 @@ interface Props extends Omit, 'title'> { * Index of the section */ index?: number; + /** + * Callback function to notify when the title visibility status changes + * + * @param index - Index of the section + * @param isPast - Whether the section is past the current scroll position + * @param title - Snippet for a title itself + * @returns Cleanup callback + */ + onTitleStatusChange?: (index: number, isPast: boolean, title?: Snippet<[{ className?: string }]>) => () => void; /** * Snippet for the section content */ children?: Snippet; } -const { class: className, title, icon, index, children }: Props = $props(); +const { class: className, title, icon, index = 0, onTitleStatusChange, children }: Props = $props(); +let titleContainer = $state(); const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }; + +// Track if the user has actually scrolled away from view +let isScrolledPast = $state(false); + +$effect(() => { + if (!titleContainer) { + return; + } + let cleanup: ((index: number) => void) | undefined; + const observer = new IntersectionObserver(entries => { + const entry = entries[0]; + const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0; + + if (isPast !== isScrolledPast) { + isScrolledPast = isPast; + cleanup = onTitleStatusChange?.(index, isPast, title); + } + }, { + // Set threshold to 0 to trigger exactly when the last pixel leaves + threshold: 0, + }); + + observer.observe(titleContainer); + return () => { + observer.disconnect(); + cleanup?.(index); + }; +});
-
+
{#if icon} {@render icon({ className: 'size-4 stroke-gray-900 stroke-1' })}