import * as React from "react"; import { UIManager, findNodeHandle } from "react-native"; import { useViewportEventsContext } from "../ViewportEvents"; import * as Utils from "../utils"; type Props = { innerRef?: (ref: React.Ref) => void; preTriggerRatio?: number; onViewportEnter?: () => void; onViewportLeave?: () => void; children: React.ReactElement; name?: string; testDimensions?: { componentOffsetX: number; componentOffsetY: number; componentWidth: number; componentHeight: number; inViewport: boolean; }; }; const initialDimensions = { componentOffsetX: null, componentOffsetY: null, componentWidth: null, componentHeight: null, inViewport: false, }; function getTestDimensions(testDimensions) { return (process.env.NODE_ENV === "test" && testDimensions) || null; } function ViewportAwareComponent(props: Props, ref) { const viewportEvents = useViewportEventsContext(); const { preTriggerRatio, onViewportEnter, onViewportLeave, children, testDimensions, } = props; const state = React.useRef( getTestDimensions(testDimensions) || initialDimensions ); const [viewportChangeEvent, setViewportChangeEvent] = React.useState(null); function assignRef(_ref) { if (_ref && !ref) { ref = _ref; } } const checkInViewport = (viewportChangeEvent, layoutEvent) => { const inVerticalViewport = Utils.isInViewport( viewportChangeEvent.offsetY, viewportChangeEvent.height, layoutEvent.offsetY, layoutEvent.height, preTriggerRatio ); const inHorizontalViewport = Utils.isInViewport( viewportChangeEvent.offsetX, viewportChangeEvent.width, layoutEvent.offsetX, layoutEvent.width, preTriggerRatio ); return inVerticalViewport && inHorizontalViewport; }; const checkViewportEnterOrLeave = (newInViewportState) => { const { inViewport } = state.current; if (!inViewport && newInViewportState) { onViewportEnter && onViewportEnter(); return true; } else if (inViewport && !newInViewportState) { onViewportLeave && onViewportLeave(); return true; } return false; }; const onViewportChange = (viewportChangeEvent) => { setViewportChangeEvent(viewportChangeEvent); const nodeHandle = findNodeHandle(ref); if (!nodeHandle) { return; } const { componentOffsetX, componentOffsetY, componentWidth, componentHeight, } = state.current; if ( viewportChangeEvent.shouldMeasureLayout || componentOffsetX == null || componentOffsetY == null || componentWidth == null || componentHeight == null ) { UIManager.measureLayout( nodeHandle, viewportChangeEvent.parentHandle, () => {}, (offsetX, offsetY, width, height) => { const inViewport = checkInViewport(viewportChangeEvent, { offsetX, offsetY, width, height, }); checkViewportEnterOrLeave(inViewport); state.current = { componentOffsetX: offsetX, componentOffsetY: offsetY, componentWidth: width, componentHeight: height, inViewport, }; } ); } else { const inViewport = checkInViewport(viewportChangeEvent, { offsetX: componentOffsetX, offsetY: componentOffsetY, width: componentWidth, height: componentHeight, }); checkViewportEnterOrLeave(inViewport); state.current = { ...state.current, inViewport, }; } }; React.useEffect(() => { if (viewportEvents) { viewportEvents?.addListener?.(onViewportChange); viewportChangeEvent && setTimeout(() => onViewportChange(viewportChangeEvent), 50); return () => { viewportEvents?.removeListener?.(onViewportChange); }; } }, [viewportEvents]); return React.cloneElement(React.Children.only(children), { ref: assignRef, }); } export const ViewportAware = React.forwardRef(ViewportAwareComponent);