import { useRef, useCallback, useEffect, ReactNode, FC } from 'react'; import { useStore } from '../services/store'; type NodeRef = Element | null; // Time in millisecond before sending an event const VISIBILITY_TIME_FOR_EVENT = 100; // Module-level guard: prevents widgetViewed from firing more than once per // widget instance, even when React StrictMode double-mounts in development. const widgetViewedFired = new Set(); interface VisibilityProps { /** * children must be a function waiting for a ref callback * to give to the children element */ children(ref: (node: NodeRef) => void): ReactNode; } /* eslint-disable @typescript-eslint/unbound-method */ /* "children" is not a method, it’s a prop containing a function */ const Visibility: FC = ({ children }) => { const store = useStore(); const observerRef = useRef(); const timeoutRef = useRef(); const triggeredRef = useRef(false); const childrenRefValue = useRef(null); const handleIntersection = useCallback( (entries: IntersectionObserverEntry[]) => { const [entry] = entries; if (entry) { if (!entry.isIntersecting && timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = undefined; // Avoid leaking pending timers } if (entry.isIntersecting && !triggeredRef.current && !timeoutRef.current) { timeoutRef.current = setTimeout(() => { if (!widgetViewedFired.has(store.data.id)) { widgetViewedFired.add(store.data.id); triggeredRef.current = true; store.triggerEvent('widgetViewed', { layout: store.data.settings.type }, store.data.id); } timeoutRef.current = undefined; }, VISIBILITY_TIME_FOR_EVENT); } } }, [], ); const onChildrenRef = useCallback( (ref: NodeRef) => { if (!observerRef.current) { observerRef.current = new IntersectionObserver(handleIntersection, { root: null, threshold: [0.5], }); } if (childrenRefValue.current) { observerRef.current.unobserve(childrenRefValue.current); } if (ref) { observerRef.current.observe(ref); } childrenRefValue.current = ref; }, [handleIntersection], ); useEffect( () => () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = undefined; } if (observerRef.current && childrenRefValue.current) { observerRef.current.unobserve(childrenRefValue.current); observerRef.current.disconnect(); } }, [], ); return children(onChildrenRef); }; export default Visibility;