@rc-component/tour
Version:
React tour Component
204 lines (198 loc) • 6.91 kB
JavaScript
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import * as React from 'react';
import Portal from '@rc-component/portal';
import Trigger from '@rc-component/trigger';
import classNames from 'classnames';
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import useMergedState from "@rc-component/util/es/hooks/useMergedState";
import { useMemo } from 'react';
import { useClosable } from "./hooks/useClosable";
import useTarget from "./hooks/useTarget";
import Mask from "./Mask";
import { getPlacements } from "./placements";
import TourStep from "./TourStep";
import { getPlacement } from "./util";
const CENTER_PLACEHOLDER = {
left: '50%',
top: '50%',
width: 1,
height: 1
};
const defaultScrollIntoViewOptions = {
block: 'center',
inline: 'center'
};
const Tour = props => {
const {
prefixCls = 'rc-tour',
steps = [],
defaultCurrent,
current,
onChange,
onClose,
onFinish,
open,
defaultOpen,
mask = true,
arrow = true,
rootClassName,
placement,
renderPanel,
gap,
animated,
scrollIntoViewOptions = defaultScrollIntoViewOptions,
zIndex = 1001,
closeIcon,
closable,
builtinPlacements,
disabledInteraction,
styles,
classNames: tourClassNames,
className,
style,
getPopupContainer,
...restProps
} = props;
const triggerRef = React.useRef();
const [mergedCurrent, setMergedCurrent] = useMergedState(0, {
value: current,
defaultValue: defaultCurrent
});
const [mergedOpen, setMergedOpen] = useMergedState(defaultOpen, {
value: open,
postState: origin => mergedCurrent < 0 || mergedCurrent >= steps.length ? false : origin ?? true
});
// Record if already rended in the DOM to avoid `findDOMNode` issue
const [hasOpened, setHasOpened] = React.useState(mergedOpen);
const openRef = React.useRef(mergedOpen);
useLayoutEffect(() => {
if (mergedOpen) {
if (!openRef.current) {
setMergedCurrent(0);
}
setHasOpened(true);
}
openRef.current = mergedOpen;
}, [mergedOpen]);
const {
target,
placement: stepPlacement,
style: stepStyle,
arrow: stepArrow,
className: stepClassName,
mask: stepMask,
scrollIntoViewOptions: stepScrollIntoViewOptions = defaultScrollIntoViewOptions,
closeIcon: stepCloseIcon,
closable: stepClosable
} = steps[mergedCurrent] || {};
const mergedClosable = useClosable(stepClosable, stepCloseIcon, closable, closeIcon);
const mergedMask = mergedOpen && (stepMask ?? mask);
const mergedScrollIntoViewOptions = stepScrollIntoViewOptions ?? scrollIntoViewOptions;
// ====================== Align Target ======================
const placeholderRef = React.useRef(null);
const inlineMode = getPopupContainer === false;
const [posInfo, targetElement] = useTarget(target, open, gap, mergedScrollIntoViewOptions, inlineMode, placeholderRef);
const mergedPlacement = getPlacement(targetElement, placement, stepPlacement);
// ========================= arrow =========================
const mergedArrow = targetElement ? typeof stepArrow === 'undefined' ? arrow : stepArrow : false;
const arrowPointAtCenter = typeof mergedArrow === 'object' ? mergedArrow.pointAtCenter : false;
useLayoutEffect(() => {
triggerRef.current?.forceAlign();
}, [arrowPointAtCenter, mergedCurrent]);
// ========================= Change =========================
const onInternalChange = nextCurrent => {
setMergedCurrent(nextCurrent);
onChange?.(nextCurrent);
};
const mergedBuiltinPlacements = useMemo(() => {
if (builtinPlacements) {
return typeof builtinPlacements === 'function' ? builtinPlacements({
arrowPointAtCenter
}) : builtinPlacements;
}
return getPlacements(arrowPointAtCenter);
}, [builtinPlacements, arrowPointAtCenter]);
// ========================= Render =========================
// Skip if not init yet
if (targetElement === undefined || !hasOpened) {
return null;
}
const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent);
};
const getPopupElement = () => /*#__PURE__*/React.createElement(TourStep, _extends({
styles: styles,
classNames: tourClassNames,
arrow: mergedArrow,
key: "content",
prefixCls: prefixCls,
total: steps.length,
renderPanel: renderPanel,
onPrev: () => {
onInternalChange(mergedCurrent - 1);
},
onNext: () => {
onInternalChange(mergedCurrent + 1);
},
onClose: handleClose,
current: mergedCurrent,
onFinish: () => {
handleClose();
onFinish?.();
}
}, steps[mergedCurrent], {
closable: mergedClosable
}));
const mergedShowMask = typeof mergedMask === 'boolean' ? mergedMask : !!mergedMask;
const mergedMaskStyle = typeof mergedMask === 'boolean' ? undefined : mergedMask;
// when targetElement is not exist, use body as triggerDOMNode
const getTriggerDOMNode = node => {
return node || targetElement || document.body;
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Mask, {
getPopupContainer: getPopupContainer,
styles: styles,
classNames: tourClassNames,
zIndex: zIndex,
prefixCls: prefixCls,
pos: posInfo,
showMask: mergedShowMask,
style: mergedMaskStyle?.style,
fill: mergedMaskStyle?.color,
open: mergedOpen,
animated: animated,
rootClassName: rootClassName,
disabledInteraction: disabledInteraction
}), /*#__PURE__*/React.createElement(Trigger, _extends({}, restProps, {
// `rc-portal` def bug not support `false` but does support and in used.
getPopupContainer: getPopupContainer,
builtinPlacements: mergedBuiltinPlacements,
ref: triggerRef,
popupStyle: stepStyle,
popupPlacement: mergedPlacement,
popupVisible: mergedOpen,
popupClassName: classNames(rootClassName, stepClassName),
prefixCls: prefixCls,
popup: getPopupElement,
forceRender: false,
autoDestroy: true,
zIndex: zIndex,
getTriggerDOMNode: getTriggerDOMNode,
arrow: !!mergedArrow
}), /*#__PURE__*/React.createElement(Portal, {
open: mergedOpen,
autoLock: !inlineMode,
getContainer: getPopupContainer
}, /*#__PURE__*/React.createElement("div", {
ref: placeholderRef,
className: classNames(className, rootClassName, `${prefixCls}-target-placeholder`),
style: {
...(posInfo || CENTER_PLACEHOLDER),
position: inlineMode ? 'absolute' : 'fixed',
pointerEvents: 'none',
...style
}
}))));
};
export default Tour;