feat(Section): add logic that triggers a callback when sections title moves out of the viewport
This commit is contained in:
@@ -29,15 +29,53 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|||||||
* Index of the section
|
* Index of the section
|
||||||
*/
|
*/
|
||||||
index?: number;
|
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
|
* Snippet for the section content
|
||||||
*/
|
*/
|
||||||
children?: Snippet;
|
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<HTMLElement>();
|
||||||
const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 };
|
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);
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -48,7 +86,7 @@ const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, op
|
|||||||
in:fly={flyParams}
|
in:fly={flyParams}
|
||||||
out:fly={flyParams}
|
out:fly={flyParams}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2" bind:this={titleContainer}>
|
||||||
<div class="flex items-center gap-3 opacity-60">
|
<div class="flex items-center gap-3 opacity-60">
|
||||||
{#if icon}
|
{#if icon}
|
||||||
{@render icon({ className: 'size-4 stroke-gray-900 stroke-1' })}
|
{@render icon({ className: 'size-4 stroke-gray-900 stroke-1' })}
|
||||||
|
|||||||
Reference in New Issue
Block a user