@razorpay/blade
Version:
The Design System that powers Razorpay
231 lines (222 loc) • 9.07 kB
JavaScript
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