UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

231 lines (222 loc) 9.07 kB
import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import { useState, useMemo, useCallback } from 'react'; import { FloatingPortal } from '@floating-ui/react'; import { TourContext } from './TourContext.js'; import { TourPopover } from './TourPopover.web.js'; import { useDelayedState, useIsTransitioningBetweenSteps, useIntersectionObserver, smoothScroll, useLockBodyScroll } from './utils.js'; import { SpotlightPopoverTourMask } from './TourMask.web.js'; import { transitionDelay } from './tourTokens.js'; import '../BladeProvider/index.js'; import { useIsomorphicLayoutEffect } from '../../utils/useIsomorphicLayoutEffect.js'; import { jsxs, jsx } from 'react/jsx-runtime'; import useTheme from '../BladeProvider/useTheme.js'; var SpotlightPopoverTour = function SpotlightPopoverTour(_ref) { var _steps$activeStep; var steps = _ref.steps, activeStep = _ref.activeStep, isOpen = _ref.isOpen, onFinish = _ref.onFinish, onOpenChange = _ref.onOpenChange, onStepChange = _ref.onStepChange, children = _ref.children; var _useTheme = useTheme(), theme = _useTheme.theme; var _useState = useState(new Map()), _useState2 = _slicedToArray(_useState, 2), refIdMap = _useState2[0], setRefIdMap = _useState2[1]; var _useState3 = useState({ x: 0, y: 0, height: 0, width: 0 }), _useState4 = _slicedToArray(_useState3, 2), size = _useState4[0], setSize = _useState4[1]; // delayed state is used to let the transition finish before reacting to the state changes var _useDelayedState = useDelayedState(activeStep, transitionDelay), _useDelayedState2 = _slicedToArray(_useDelayedState, 1), delayedActiveStep = _useDelayedState2[0]; var _useDelayedState3 = useDelayedState(size, transitionDelay), _useDelayedState4 = _slicedToArray(_useDelayedState3, 2), delayedSize = _useDelayedState4[0], setDelayedSize = _useDelayedState4[1]; // keep track of when we are transitioning between steps var isTransitioning = useIsTransitioningBetweenSteps(activeStep, transitionDelay); var _useState5 = useState(false), _useState6 = _slicedToArray(_useState5, 2), isScrolling = _useState6[0], setIsScrolling = _useState6[1]; var currentStepRef = refIdMap.get((_steps$activeStep = steps[activeStep]) === null || _steps$activeStep === void 0 ? void 0 : _steps$activeStep.name); var intersection = useIntersectionObserver(currentStepRef, { threshold: 0.5 }); // main step logic var totalSteps = steps.length; var currentStepData = useMemo(function () { return steps[activeStep]; }, [activeStep, steps]); var goToStep = useCallback(function (step) { if (step < 0 || step >= steps.length) return; onStepChange === null || onStepChange === void 0 ? void 0 : onStepChange(step); }, [onStepChange, steps.length]); var goToNext = useCallback(function () { var nextState = activeStep + 1; if (nextState >= steps.length) { nextState = steps.length - 1; } onStepChange === null || onStepChange === void 0 ? void 0 : onStepChange(nextState); }, [activeStep, onStepChange, steps.length]); var goToPrevious = useCallback(function () { var nextState = activeStep - 1; if (nextState < 0) { nextState = 0; } onStepChange === null || onStepChange === void 0 ? void 0 : onStepChange(nextState); }, [activeStep, onStepChange]); var stopTour = useCallback(function () { onFinish === null || onFinish === void 0 ? void 0 : onFinish(); }, [onFinish]); var attachStep = useCallback(function (id, ref) { if (!ref) return; setRefIdMap(function (prev) { return new Map(prev).set(id, ref); }); }, []); var removeStep = useCallback(function (id) { setRefIdMap(function (prev) { var newMap = new Map(prev); newMap["delete"](id); return newMap; }); }, []); var updateMaskSize = useCallback(function () { var _steps$activeStep2; var shouldSkipDelay = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var ref = refIdMap.get((_steps$activeStep2 = steps[activeStep]) === null || _steps$activeStep2 === void 0 ? void 0 : _steps$activeStep2.name); if (!(ref !== null && ref !== void 0 && ref.current)) return; var rect = ref.current.getBoundingClientRect(); setSize({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }); if (shouldSkipDelay) { setDelayedSize({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }); } }, [activeStep, refIdMap, setDelayedSize, steps]); var scrollToStep = useCallback(function () { var _steps$delayedActiveS; var ref = refIdMap.get((_steps$delayedActiveS = steps[delayedActiveStep]) === null || _steps$delayedActiveS === void 0 ? void 0 : _steps$delayedActiveS.name); if (!(ref !== null && ref !== void 0 && ref.current)) return; // If the element is already in view, don't scroll if (intersection !== null && intersection !== void 0 && intersection.isIntersecting) return; setIsScrolling(true); smoothScroll(ref.current, { behavior: 'smooth', block: 'center', inline: 'center' }).then(function () { // wait for the scroll to finish before updating the mask size // We also don't want to delay the size update since its already delayed by the scroll updateMaskSize(true); })["finally"](function () { setIsScrolling(false); }); }, [delayedActiveStep, refIdMap, steps, updateMaskSize, intersection === null || intersection === void 0 ? void 0 : intersection.isIntersecting]); // Update the size of the mask when the active step changes useIsomorphicLayoutEffect(function () { updateMaskSize(); }, [activeStep, updateMaskSize]); // Scroll into view when the active step changes useIsomorphicLayoutEffect(function () { // We need to wait for the transition to finish before scrolling // Otherwise the browser sometimes interrupts the scroll var scrollDelay = 100; setTimeout(function () { if (!isOpen) return; if (isTransitioning) return; scrollToStep(); }, scrollDelay); }, [isOpen, scrollToStep, isTransitioning]); // reset the mask size when the tour is closed useIsomorphicLayoutEffect(function () { if (isOpen) { // on initial mount, we don't want to delay the size update updateMaskSize(true); onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange({ isOpen: isOpen }); } if (!isOpen) { setSize({ x: 0, y: 0, width: 0, height: 0 }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); useLockBodyScroll(isOpen); var contextValue = useMemo(function () { return { attachStep: attachStep, removeStep: removeStep }; }, [attachStep, removeStep]); return /*#__PURE__*/jsxs(TourContext.Provider, { value: contextValue, children: [/*#__PURE__*/jsx(FloatingPortal, { children: isOpen ? /*#__PURE__*/jsx(SpotlightPopoverTourMask, { isTransitioning: isTransitioning || isScrolling, padding: theme.spacing[4], size: delayedSize }) : null }), steps.map(function (step) { var _step$content, _step$footer; var isStepActive = currentStepData.name === step.name; var attachTo = isStepActive ? currentStepRef : undefined; // 1. only show popover if the tour is opened // 2. only show the popover if the step is active // 3. do not show the popover if we are transitioning between steps // this ensures popover suddenly doesn't jump to the next step, // instead it waits for the transition to finish var isPopoverVisible = isOpen && isStepActive && !isTransitioning; return /*#__PURE__*/jsx(TourPopover, { isTransitioning: delayedActiveStep !== activeStep, placement: step.placement, isOpen: isPopoverVisible, onOpenChange: onOpenChange, title: step.title, titleLeading: step.titleLeading, content: step === null || step === void 0 ? void 0 : (_step$content = step.content) === null || _step$content === void 0 ? void 0 : _step$content.call(step, { activeStep: delayedActiveStep, goToPrevious: goToPrevious, goToNext: goToNext, goToStep: goToStep, totalSteps: totalSteps, stopTour: stopTour }), footer: step === null || step === void 0 ? void 0 : (_step$footer = step.footer) === null || _step$footer === void 0 ? void 0 : _step$footer.call(step, { activeStep: delayedActiveStep, goToPrevious: goToPrevious, goToNext: goToNext, goToStep: goToStep, totalSteps: totalSteps, stopTour: stopTour }), attachTo: attachTo }, step.name); }), children] }); }; export { SpotlightPopoverTour }; //# sourceMappingURL=Tour.web.js.map