Files
diplom-frontend/app/components/visible-trigger.tsx
2025-05-21 08:52:33 +03:00

99 lines
3.8 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>
);
}