103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
|
|
interface VisibleTriggerProps {
|
|
onVisible: () => void | Promise<void>;
|
|
options?: IntersectionObserverInit;
|
|
triggerOnce?: boolean;
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
}
|
|
|
|
/**
|
|
* A component that calls a function when it becomes visible in the viewport
|
|
* or a specified scrollable container.
|
|
*/
|
|
export default function VisibleTrigger({
|
|
onVisible, // Function to call when the element becomes visible
|
|
options = {}, // Optional: IntersectionObserver options (root, rootMargin, threshold)
|
|
triggerOnce = true, // Optional: If true, trigger only the first time it becomes visible
|
|
children,
|
|
style,
|
|
...props
|
|
}: VisibleTriggerProps & React.ComponentProps<"div">) {
|
|
const elementRef = useRef(null); // Ref to attach to the DOM element we want to observe
|
|
|
|
useEffect(() => {
|
|
const element = elementRef.current;
|
|
|
|
// Only proceed if we have the DOM element and the function to call
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
// Default IntersectionObserver options
|
|
const defaultOptions = {
|
|
root: null, // default is the viewport
|
|
rootMargin: "0px", // No margin by default
|
|
threshold: 0, // Trigger as soon as any part of the element is visible
|
|
};
|
|
|
|
// Merge provided options with defaults
|
|
const observerOptions = { ...defaultOptions, ...options };
|
|
|
|
// Create the Intersection Observer instance
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const entry = entries[0]; // Assuming only one target element
|
|
|
|
// If the element is intersecting (visible)...
|
|
if (entry.isIntersecting) {
|
|
// console.log('VisibleTrigger: Element is intersecting.', entry);
|
|
|
|
// Call the provided function
|
|
onVisible();
|
|
|
|
// If triggerOnce is true, stop observing this element immediately
|
|
if (triggerOnce) {
|
|
// console.log('VisibleTrigger: Triggered once, disconnecting observer.');
|
|
observer.disconnect(); // Disconnect stops all observations by this instance
|
|
}
|
|
} else {
|
|
// console.log('VisibleTrigger: Element is NOT intersecting.', entry);
|
|
}
|
|
},
|
|
observerOptions, // Pass the options to the observer
|
|
);
|
|
|
|
// Start observing the element
|
|
// console.log('VisibleTrigger: Starting observation.', element);
|
|
observer.observe(element);
|
|
|
|
// Cleanup function: Disconnect the observer when the component unmounts
|
|
// or when the effect dependencies change.
|
|
return () => {
|
|
// console.log('VisibleTrigger: Cleaning up observer.');
|
|
if (observer) {
|
|
// Calling disconnect multiple times is safe.
|
|
observer.disconnect();
|
|
}
|
|
};
|
|
|
|
// Effect dependencies:
|
|
// - elementRef: Need the DOM element reference.
|
|
// - onVisible: If the function prop changes, we need a new observer with the new function.
|
|
// - options: If observer options change, we need a new observer.
|
|
// - triggerOnce: If triggerOnce changes, the logic inside the observer callback changes,
|
|
// so we need a new observer instance.
|
|
}, [elementRef, onVisible, options, triggerOnce]);
|
|
|
|
// Render a div that we will attach the ref to.
|
|
// Ensure it has some minimal dimension if no children are provided,
|
|
// so the observer can detect its presence.
|
|
return (
|
|
<div
|
|
ref={elementRef}
|
|
style={{ minHeight: children ? "auto" : "1px", ...style }}
|
|
{...props}
|
|
>
|
|
{children} {/* Render any children passed to the component */}
|
|
</div>
|
|
);
|
|
}
|