import { type RefObject, useCallback, useEffect, useRef, useState } from 'react'; import { usePortalElement } from '~/hooks/usePortalElement'; import type { MergedProps } from '~/hooks/useTourEngine'; import { ACTIONS, LIFECYCLE, ORIGIN, STATUS } from '~/literals'; import createStore from '~/modules/store'; import type { StoreState } from '~/modules/store'; import Loader from '~/components/Loader'; import Overlay from '~/components/Overlay'; import Portal from '~/components/Portal'; import Step from '~/components/Step'; import type { Controls, StepMerged } from '~/types'; interface TourRendererProps { controls: Controls; mergedProps: MergedProps; state: StoreState; step: StepMerged | null; store: RefObject>; } export default function TourRenderer({ controls, mergedProps, state, step, store, }: TourRendererProps) { const { continuous, debug, nonce, portalElement, scrollToFirstStep } = mergedProps; const element = usePortalElement(portalElement); const { index, lifecycle, status } = state; const isRunning = status === STATUS.RUNNING; const [showLoader, setShowLoader] = useState(false); const loaderTimerRef = useRef | null>(null); const loaderDelay = step?.loaderDelay ?? 0; useEffect(() => { if (state.waiting) { if (loaderDelay === 0) { setShowLoader(true); } else { loaderTimerRef.current = setTimeout(() => { setShowLoader(true); }, loaderDelay); } } else { setShowLoader(false); } return () => { if (loaderTimerRef.current) { clearTimeout(loaderTimerRef.current); loaderTimerRef.current = null; } }; }, [loaderDelay, state.waiting]); useEffect(() => { if (!isRunning) { return undefined; } const handleKeyboard = (event: KeyboardEvent) => { if (!step || lifecycle !== LIFECYCLE.TOOLTIP) { return; } if (event.key === 'Escape' && step.dismissKeyAction) { if (step.dismissKeyAction === 'next') { controls.next(ORIGIN.KEYBOARD); } else if (step.dismissKeyAction === 'replay') { controls.replay(ORIGIN.KEYBOARD); } else { controls.close(ORIGIN.KEYBOARD); } } }; document.body.addEventListener('keydown', handleKeyboard, { passive: true }); return () => { document.body.removeEventListener('keydown', handleKeyboard); }; }, [controls, isRunning, lifecycle, step]); const handleClickOverlay = useCallback(() => { switch (step?.overlayClickAction) { case 'close': { controls.close(ORIGIN.OVERLAY); break; } case 'next': { controls.next(ORIGIN.OVERLAY); break; } case 'replay': { controls.replay(ORIGIN.OVERLAY); break; } // No default } }, [controls, step?.overlayClickAction]); if (!step || !isRunning) { return null; } /* Hide the overlay when the tour starts, and a beacon will be shown. Prevent the overlay from flashing before the beacon is rendered. */ const hideOverlay = state.action === ACTIONS.START && !step.skipBeacon && step.placement !== 'center'; return ( <> {lifecycle !== LIFECYCLE.INIT && ( )} <> {showLoader && } {!hideOverlay && ( )} ); }