import { useEffect, useRef } from "react"; interface VisibleTriggerProps { onVisible: () => void | Promise; 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 (
{children} {/* Render any children passed to the component */}
); }