This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-present react-component
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+149
View File
@@ -0,0 +1,149 @@
# @rc-component/tour
React 18 supported Tour Component.
[![NPM version][npm-image]][npm-url] [![dumi](https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square)](https://github.com/umijs/dumi) [![build status][github-actions-image]][github-actions-url] [![Codecov][codecov-image]][codecov-url] [![npm download][download-image]][download-url]
[npm-image]: http://img.shields.io/npm/v/@rc-component/tour.svg?style=flat-square
[npm-url]: http://npmjs.org/package/@rc-component/tour
[github-actions-image]: https://github.com/react-component/tour/workflows/CI/badge.svg
[github-actions-url]: https://github.com/react-component/tour/actions
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/tour/master.svg?style=flat-square
[codecov-url]: https://app.codecov.io/gh/react-component/tour
[download-image]: https://img.shields.io/npm/dm/@rc-component/tour.svg?style=flat-square
[download-url]: https://npmjs.org/package/@rc-component/tour
## Development
```bash
npm install
npm start
open http://localhost:8000
```
## Feature
- React life cycle support tour component
## Install
[![@rc-component/tour](https://nodei.co/npm/@rc-component/tour.png)](https://www.npmjs.com/package/@rc-component/tour)
## Usage
<!-- prettier-ignore -->
```js | pure
import { useRef } from 'react';
import Tour from '@rc-component/tour';
const Demo = () => {
const createBtnRef = useRef<HTMLButtonElement>(null);
const updateBtnRef = useRef<HTMLButtonElement>(null);
const deleteBtnRef = useRef<HTMLButtonElement>(null);
return (
<div style={{ margin: 20 }}>
<div>
<button
className="ant-target"
ref={createBtnRef}
style={{ marginLeft: 100 }}
>
Create
</button>
<div style={{ height: 200 }} />
<button className="ant-target" ref={updateBtnRef}>
Update
</button>
<button className="ant-target" ref={deleteBtnRef}>
Delete
</button>
</div>
<div style={{ height: 200 }} />
<Tour
defaultCurrent={0}
steps={[
{
title: '创建',
description: '创建一条数据',
target: () => createBtnRef.current,
mask: true,
},
{
title: '更新',
description: (
<div>
<span>更新一条数据</span>
<button>帮助文档</button>
</div>
),
target: () => updateBtnRef.current,
},
{
title: '删除',
description: (
<div>
<span>危险操作:删除一条数据</span>
<button>帮助文档</button>
</div>
),
target: () => deleteBtnRef.current,
mask: true,
style: { color: 'red' },
},
]}
/>
</div>
);
};
export default Demo;
```
## 🔥 API
We use typescript to create the Type definition. You can view directly in IDE. But you can still check the type definition [here](https://github.com/react-component/tour/blob/master/src/interface.ts).
### Tour
<!-- prettier-ignore -->
| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| closeIcon | `React.ReactNode` | - | 自定义关闭按钮 |
| steps | `TourStepProps[]` | - | 引导步骤 |
| open | `boolean` | `true` | 受控打开引导(与 `current` 受控分开) |
| current | `number` | 0 | 受控当前处于哪一步 |
| defaultCurrent | `number` | 0 | 默认处于哪一步 |
| gap | `{ offset?: number \| [number, number]; radius?: number }` | - | 控制引导显示间距 |
| onChange | `(current: number) => void` | - | 步骤改变时的回调,`current`为改变前的步骤,`next`为改变后的步骤 |
| onClose | `(current: number) => void` | - | 关闭引导时的回调 |
| onFinish | `() => void` | - | 完成引导时的回调 |
| mask | `boolean \| { style?: React.CSSProperties; color?: string; }` | `true` | 整体是否启用蒙层,也可以传入自定义样式修改蒙层样式 |
| animated | `boolean \| { placeholder: boolean }` | `false` | 是否启用目标遮罩动画 |
| arrow | `boolean \| { pointAtCenter: boolean}` | `true` | 整体是否显示箭头,包含是否指向元素中心的配置 |
| scrollIntoViewOptions | `boolean \| ScrollIntoViewOptions` | `true` | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数 |
| onPopupAlign | `function(popupDomNode, align)` | - | 当弹出框对齐后回调 |
| zIndex | `number` | 1001 | 弹层的层级 |
### TourStep
<!-- prettier-ignore -->
| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| closeIcon | `React.ReactNode` | - | 自定义关闭按钮 |
| target | `() => HTMLElement` \| `HTMLElement` | - | 获取引导卡片指向的元素 |
| arrow | `boolean` \| `{ pointAtCenter: boolean}` | `true` | 是否显示箭头,包含是否指向元素中心的配置 |
| placement | `left` \| `leftTop` \| `leftBottom` \| `right` \| `rightTop` \| `rightBottom` \| `top` \| `topLeft` \| `topRight` \| `bottom` \| `bottomLeft` \| `bottomRight` | `bottom` | 引导卡片相对于目标元素的位置 |
| onClose | `() => void` | - | 关闭引导时的回调函数 |
| mask | `boolean \| { style?: React.CSSProperties; color?: string; }` | `true` | 是否启用蒙层,也可以传入自定义样式修改蒙层样式,默认跟随 Tour 的 `mask` 属性 |
| renderPanel | `(props: TourStepProps, current: number) => ReactNode;` | | 渲染 popoup 弹窗方法 |
| className | `string` | - | - |
| style | `React.CSSProperties` | - | - |
| scrollIntoViewOptions | `boolean \| ScrollIntoViewOptions` | `true` | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数,默认跟随 Tour 的 `scrollIntoViewOptions` 属性 |
## 🤝 Contributing
<a href="https://openomy.app/github/react-component/tour" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=react-component/tour&chart=bubble&latestMonth=24" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import type { PosInfo } from './hooks/useTarget';
import type { SemanticName, TourProps } from './interface';
export interface MaskProps {
prefixCls?: string;
pos: PosInfo;
rootClassName?: string;
showMask?: boolean;
style?: React.CSSProperties;
fill?: string;
open?: boolean;
animated?: boolean | {
placeholder: boolean;
};
zIndex?: number;
disabledInteraction?: boolean;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
getPopupContainer?: TourProps['getPopupContainer'];
onEsc?: (info: {
top: boolean;
event: KeyboardEvent;
}) => void;
}
declare const Mask: React.FC<MaskProps>;
export default Mask;
+106
View File
@@ -0,0 +1,106 @@
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 React from 'react';
import { clsx } from 'clsx';
import Portal from '@rc-component/portal';
import useId from "@rc-component/util/es/hooks/useId";
const COVER_PROPS = {
fill: 'transparent',
pointerEvents: 'auto'
};
const Mask = props => {
const {
prefixCls,
rootClassName,
pos,
showMask,
style = {},
fill = 'rgba(0,0,0,0.5)',
open,
animated,
zIndex,
disabledInteraction,
styles,
classNames: tourClassNames,
getPopupContainer,
onEsc
} = props;
const id = useId();
const maskId = `${prefixCls}-mask-${id}`;
const mergedAnimated = typeof animated === 'object' ? animated?.placeholder : animated;
const isSafari = typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const maskRectSize = isSafari ? {
width: '100%',
height: '100%'
} : {
width: '100vw',
height: '100vh'
};
const inlineMode = getPopupContainer === false;
return /*#__PURE__*/React.createElement(Portal, {
open: open,
autoLock: !inlineMode,
getContainer: getPopupContainer,
onEsc: onEsc
}, /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-mask`, rootClassName, tourClassNames?.mask),
style: {
position: inlineMode ? 'absolute' : 'fixed',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex,
pointerEvents: pos && !disabledInteraction ? 'none' : 'auto',
...style,
...styles?.mask
}
}, showMask ? /*#__PURE__*/React.createElement("svg", {
style: {
width: '100%',
height: '100%'
}
}, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("mask", {
id: maskId
}, /*#__PURE__*/React.createElement("rect", _extends({
x: "0",
y: "0"
}, maskRectSize, {
fill: "white"
})), pos && /*#__PURE__*/React.createElement("rect", {
x: pos.left,
y: pos.top,
rx: pos.radius,
width: pos.width,
height: pos.height,
fill: "black",
className: mergedAnimated ? `${prefixCls}-placeholder-animated` : ''
}))), /*#__PURE__*/React.createElement("rect", {
x: "0",
y: "0",
width: "100%",
height: "100%",
fill: fill,
mask: `url(#${maskId})`
}), pos && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", _extends({}, COVER_PROPS, {
x: "0",
y: "0",
width: "100%",
height: Math.max(pos.top, 0)
})), /*#__PURE__*/React.createElement("rect", _extends({}, COVER_PROPS, {
x: "0",
y: "0",
width: Math.max(pos.left, 0),
height: "100%"
})), /*#__PURE__*/React.createElement("rect", _extends({}, COVER_PROPS, {
x: "0",
y: pos.top + pos.height,
width: "100%",
height: `calc(100% - ${pos.top + pos.height}px)`
})), /*#__PURE__*/React.createElement("rect", _extends({}, COVER_PROPS, {
x: pos.left + pos.width,
y: "0",
width: `calc(100% - ${pos.left + pos.width}px)`,
height: "100%"
})))) : null));
};
export default Mask;
+10
View File
@@ -0,0 +1,10 @@
import { type PortalProps } from '@rc-component/portal';
import * as React from 'react';
export interface PlaceholderProps extends Pick<PortalProps, 'open' | 'autoLock' | 'getContainer'> {
domRef: React.RefObject<HTMLDivElement>;
className: string;
style: React.CSSProperties;
fallbackDOM: () => HTMLElement | null;
}
declare const Placeholder: React.ForwardRefExoticComponent<PlaceholderProps & React.RefAttributes<HTMLElement>>;
export default Placeholder;
+27
View File
@@ -0,0 +1,27 @@
import Portal from '@rc-component/portal';
import * as React from 'react';
const Placeholder = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
open,
autoLock,
getContainer,
domRef,
className,
style,
fallbackDOM
} = props;
React.useImperativeHandle(ref, () => domRef.current || fallbackDOM());
return /*#__PURE__*/React.createElement(Portal, {
open: open,
autoLock: autoLock,
getContainer: getContainer
}, /*#__PURE__*/React.createElement("div", {
ref: domRef,
className: className,
style: style
}));
});
if (process.env.NODE_ENV !== 'production') {
Placeholder.displayName = 'Placeholder';
}
export default Placeholder;
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import type { TourProps } from './interface';
export type { TourProps };
declare const Tour: React.FC<TourProps>;
export default Tour;
+243
View File
@@ -0,0 +1,243 @@
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 Trigger from '@rc-component/trigger';
import { clsx } from 'clsx';
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import useEvent from "@rc-component/util/es/hooks/useEvent";
import KeyCode from "@rc-component/util/es/KeyCode";
import useControlledState from "@rc-component/util/es/hooks/useControlledState";
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";
import Placeholder from "./Placeholder";
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,
keyboard = true,
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] = useControlledState(defaultCurrent || 0, current);
const [internalOpen, setMergedOpen] = useControlledState(defaultOpen, open);
const mergedOpen = mergedCurrent < 0 || mergedCurrent >= steps.length ? false : internalOpen ?? 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, setMergedCurrent]);
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]);
const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent);
};
// ========================= Esc Close =========================
// Use Portal's onEsc to handle Escape key with proper stacking logic
const handleEscClose = useEvent(({
event
}) => {
if (keyboard && mergedClosable !== null) {
event.preventDefault();
handleClose();
}
});
// ========================= Keyboard =========================
// Support ArrowLeft/ArrowRight to navigate steps.
const keyboardHandler = useEvent(e => {
// Ignore keyboard events from input-like elements to avoid interfering when typing
if (KeyCode.isEditableTarget(e)) {
return;
}
if (keyboard && e.key === 'ArrowLeft') {
if (mergedCurrent > 0) {
e.preventDefault();
onInternalChange(mergedCurrent - 1);
}
return;
}
if (keyboard && e.key === 'ArrowRight') {
if (mergedCurrent < steps.length - 1) {
e.preventDefault();
onInternalChange(mergedCurrent + 1);
}
return;
}
});
useLayoutEffect(() => {
if (!mergedOpen) return;
window.addEventListener('keydown', keyboardHandler);
return () => {
window.removeEventListener('keydown', keyboardHandler);
};
}, [mergedOpen, keyboardHandler]);
// ========================= Render =========================
// Skip if not init yet
if (targetElement === undefined || !hasOpened) {
return null;
}
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 fallbackDOM = () => {
return 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,
onEsc: handleEscClose
}), /*#__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: clsx(rootClassName, stepClassName),
prefixCls: prefixCls,
popup: getPopupElement,
forceRender: false,
autoDestroy: true,
zIndex: zIndex,
arrow: !!mergedArrow
}), /*#__PURE__*/React.createElement(Placeholder, {
open: mergedOpen,
autoLock: !inlineMode,
getContainer: getPopupContainer,
domRef: placeholderRef,
fallbackDOM: fallbackDOM,
className: clsx(className, rootClassName, `${prefixCls}-target-placeholder`),
style: {
...(posInfo || CENTER_PLACEHOLDER),
position: inlineMode ? 'absolute' : 'fixed',
pointerEvents: 'none',
...style
}
})));
};
export default Tour;
@@ -0,0 +1,6 @@
import * as React from 'react';
import type { TourStepProps } from '../interface';
export type DefaultPanelProps = Exclude<TourStepProps, 'closable'> & {
closable: Exclude<TourStepProps['closable'], boolean>;
};
export default function DefaultPanel(props: DefaultPanelProps): React.JSX.Element;
@@ -0,0 +1,71 @@
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 { clsx } from 'clsx';
import pickAttrs from "@rc-component/util/es/pickAttrs";
export default function DefaultPanel(props) {
const {
prefixCls,
current,
total,
title,
description,
onClose,
onPrev,
onNext,
onFinish,
className,
closable,
classNames: tourClassNames,
styles
} = props;
const ariaProps = pickAttrs(closable || {}, true);
const closeIcon = closable?.closeIcon ?? /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-close-x`
}, "\xD7");
const mergedClosable = !!closable;
return /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-panel`, className)
}, /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-section`, tourClassNames?.section),
style: styles?.section
}, mergedClosable && /*#__PURE__*/React.createElement("button", _extends({
type: "button",
onClick: onClose,
"aria-label": "Close"
}, ariaProps, {
className: `${prefixCls}-close`
}), closeIcon), /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-header`, tourClassNames?.header),
style: styles?.header
}, /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-title`, tourClassNames?.title),
style: styles?.title
}, title)), /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-description`, tourClassNames?.description),
style: styles?.description
}, description), /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-footer`, tourClassNames?.footer),
style: styles?.footer
}, /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-sliders`
}, total > 1 ? [...Array.from({
length: total
}).keys()].map((item, index) => {
return /*#__PURE__*/React.createElement("span", {
key: item,
className: index === current ? 'active' : ''
});
}) : null), /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-actions`, tourClassNames?.actions),
style: styles?.actions
}, current !== 0 ? /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-prev-btn`,
onClick: onPrev
}, "Prev") : null, current === total - 1 ? /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-finish-btn`,
onClick: onFinish
}, "Finish") : /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-next-btn`,
onClick: onNext
}, "Next")))));
}
@@ -0,0 +1,6 @@
import * as React from 'react';
import { type DefaultPanelProps } from './DefaultPanel';
import type { TourStepProps, TourStepInfo } from '../interface';
export type { TourStepProps, TourStepInfo, };
declare const TourStep: (props: DefaultPanelProps) => React.JSX.Element;
export default TourStep;
+10
View File
@@ -0,0 +1,10 @@
import * as React from 'react';
import DefaultPanel from "./DefaultPanel";
const TourStep = props => {
const {
current,
renderPanel
} = props;
return /*#__PURE__*/React.createElement(React.Fragment, null, typeof renderPanel === 'function' ? renderPanel(props, current) : /*#__PURE__*/React.createElement(DefaultPanel, props));
};
export default TourStep;
@@ -0,0 +1,8 @@
import * as React from 'react';
import type { TourProps } from '../interface';
import type { TourStepInfo } from '../TourStep';
export declare function useClosable(stepClosable: TourStepInfo['closable'], stepCloseIcon: TourStepInfo['closeIcon'], closable: TourProps['closable'], closeIcon: TourProps['closeIcon']): {
closeIcon?: React.ReactNode;
} & React.AriaAttributes & {
[key: `data-${string}`]: unknown;
} & Pick<React.HTMLAttributes<HTMLDivElement>, "role">;
@@ -0,0 +1,35 @@
import * as React from 'react';
function isConfigObj(closable) {
return closable !== null && typeof closable === 'object';
}
/**
* Convert `closable` to ClosableConfig.
* When `preset` is true, will auto fill ClosableConfig with default value.
*/
function getClosableConfig(closable, closeIcon, preset) {
if (closable === false || closeIcon === false && (!isConfigObj(closable) || !closable.closeIcon)) {
return null;
}
const mergedCloseIcon = typeof closeIcon !== 'boolean' ? closeIcon : undefined;
if (isConfigObj(closable)) {
return {
...closable,
closeIcon: closable.closeIcon ?? mergedCloseIcon
};
}
// When StepClosable no need auto fill, but RootClosable need this.
return preset || closable || closeIcon ? {
closeIcon: mergedCloseIcon
} : 'empty';
}
export function useClosable(stepClosable, stepCloseIcon, closable, closeIcon) {
return React.useMemo(() => {
const stepClosableConfig = getClosableConfig(stepClosable, stepCloseIcon, false);
const rootClosableConfig = getClosableConfig(closable, closeIcon, true);
if (stepClosableConfig !== 'empty') {
return stepClosableConfig;
}
return rootClosableConfig;
}, [closable, closeIcon, stepClosable, stepCloseIcon]);
}
@@ -0,0 +1,14 @@
/// <reference types="react" />
import type { TourStepInfo } from '..';
export interface Gap {
offset?: number | [number, number];
radius?: number;
}
export interface PosInfo {
left: number;
top: number;
height: number;
width: number;
radius: number;
}
export default function useTarget(target: TourStepInfo['target'], open: boolean, gap?: Gap, scrollIntoViewOptions?: boolean | ScrollIntoViewOptions, inlineMode?: boolean, placeholderRef?: React.RefObject<HTMLDivElement>): [PosInfo, HTMLElement];
+90
View File
@@ -0,0 +1,90 @@
import useEvent from "@rc-component/util/es/hooks/useEvent";
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import { useMemo, useState } from 'react';
import { isInViewPort } from "../util";
function isValidNumber(val) {
return typeof val === 'number' && !Number.isNaN(val);
}
export default function useTarget(target, open, gap, scrollIntoViewOptions, inlineMode, placeholderRef) {
// ========================= Target =========================
// We trade `undefined` as not get target by function yet.
// `null` as empty target.
const [targetElement, setTargetElement] = useState(undefined);
useLayoutEffect(() => {
const nextElement = typeof target === 'function' ? target() : target;
setTargetElement(nextElement || null);
});
// ========================= Align ==========================
const [posInfo, setPosInfo] = useState(null);
const updatePos = useEvent(() => {
if (targetElement) {
// Exist target element. We should scroll and get target position
if (!inlineMode && !isInViewPort(targetElement) && open) {
targetElement.scrollIntoView(scrollIntoViewOptions);
}
const {
left,
top,
width,
height
} = targetElement.getBoundingClientRect();
const nextPosInfo = {
left,
top,
width,
height,
radius: 0
};
// If `inlineMode` we need cut off parent offset
if (inlineMode) {
const parentRect = placeholderRef.current?.parentElement?.getBoundingClientRect();
if (parentRect) {
nextPosInfo.left -= parentRect.left;
nextPosInfo.top -= parentRect.top;
}
}
setPosInfo(origin => {
if (JSON.stringify(origin) !== JSON.stringify(nextPosInfo)) {
return nextPosInfo;
}
return origin;
});
} else {
// Not exist target which means we just show in center
setPosInfo(null);
}
});
const getGapOffset = index => (Array.isArray(gap?.offset) ? gap?.offset[index] : gap?.offset) ?? 6;
useLayoutEffect(() => {
updatePos();
// update when window resize
window.addEventListener('resize', updatePos);
// update when `document.body.style.overflow` is set to visible
// by default, it will be set to hidden
window.addEventListener('scroll', updatePos);
return () => {
window.removeEventListener('resize', updatePos);
window.removeEventListener('scroll', updatePos);
};
}, [targetElement, open, updatePos]);
// ======================== PosInfo =========================
const mergedPosInfo = useMemo(() => {
if (!posInfo) {
return posInfo;
}
const gapOffsetX = getGapOffset(0);
const gapOffsetY = getGapOffset(1);
const gapRadius = isValidNumber(gap?.radius) ? gap?.radius : 2;
return {
left: posInfo.left - gapOffsetX,
top: posInfo.top - gapOffsetY,
width: posInfo.width + gapOffsetX * 2,
height: posInfo.height + gapOffsetY * 2,
radius: gapRadius
};
}, [posInfo, gap]);
return [mergedPosInfo, targetElement];
}
+3
View File
@@ -0,0 +1,3 @@
import Tour from './Tour';
export type { TourProps, TourStepInfo, TourStepProps } from './interface';
export default Tour;
+2
View File
@@ -0,0 +1,2 @@
import Tour from "./Tour";
export default Tour;
+80
View File
@@ -0,0 +1,80 @@
import type { TriggerProps } from '@rc-component/trigger';
import type { PlacementType } from './placements';
import type { CSSProperties, ReactNode } from 'react';
import type { Gap } from './hooks/useTarget';
import { type DefaultPanelProps } from './TourStep/DefaultPanel';
export type SemanticName = 'section' | 'footer' | 'actions' | 'header' | 'title' | 'description' | 'mask';
export type HTMLAriaDataAttributes = React.AriaAttributes & {
[key: `data-${string}`]: unknown;
} & Pick<React.HTMLAttributes<HTMLDivElement>, 'role'>;
export interface TourStepInfo {
arrow?: boolean | {
pointAtCenter: boolean;
};
target?: HTMLElement | (() => HTMLElement) | null | (() => null);
title: ReactNode;
description?: ReactNode;
placement?: PlacementType;
mask?: boolean | {
style?: React.CSSProperties;
color?: string;
};
className?: string;
style?: CSSProperties;
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
closeIcon?: ReactNode;
closable?: boolean | ({
closeIcon?: ReactNode;
} & HTMLAriaDataAttributes);
}
export interface TourStepProps extends TourStepInfo {
prefixCls?: string;
total?: number;
current?: number;
onClose?: () => void;
onFinish?: () => void;
renderPanel?: (step: TourStepProps, current: number) => ReactNode;
onPrev?: () => void;
onNext?: () => void;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
}
export interface TourProps extends Pick<TriggerProps, 'onPopupAlign'> {
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
className?: string;
style?: React.CSSProperties;
steps?: TourStepInfo[];
open?: boolean;
keyboard?: boolean;
defaultOpen?: boolean;
defaultCurrent?: number;
current?: number;
onChange?: (current: number) => void;
onClose?: (current: number) => void;
onFinish?: () => void;
closeIcon?: TourStepProps['closeIcon'];
closable?: TourStepProps['closable'];
mask?: boolean | {
style?: React.CSSProperties;
color?: string;
};
arrow?: boolean | {
pointAtCenter: boolean;
};
rootClassName?: string;
placement?: PlacementType;
prefixCls?: string;
renderPanel?: (props: DefaultPanelProps, current: number) => ReactNode;
gap?: Gap;
animated?: boolean | {
placeholder: boolean;
};
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
zIndex?: number;
getPopupContainer?: TriggerProps['getPopupContainer'] | false;
builtinPlacements?: TriggerProps['builtinPlacements'] | ((config?: {
arrowPointAtCenter?: boolean;
}) => TriggerProps['builtinPlacements']);
disabledInteraction?: boolean;
}
+1
View File
@@ -0,0 +1 @@
export {};
+4
View File
@@ -0,0 +1,4 @@
import type { BuildInPlacements } from '@rc-component/trigger';
export type PlacementType = 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom' | 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'center';
export declare function getPlacements(arrowPointAtCenter?: boolean): BuildInPlacements;
export declare const placements: BuildInPlacements;
+63
View File
@@ -0,0 +1,63 @@
const targetOffset = [0, 0];
const basePlacements = {
left: {
points: ['cr', 'cl'],
offset: [-8, 0]
},
right: {
points: ['cl', 'cr'],
offset: [8, 0]
},
top: {
points: ['bc', 'tc'],
offset: [0, -8]
},
bottom: {
points: ['tc', 'bc'],
offset: [0, 8]
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -8]
},
leftTop: {
points: ['tr', 'tl'],
offset: [-8, 0]
},
topRight: {
points: ['br', 'tr'],
offset: [0, -8]
},
rightTop: {
points: ['tl', 'tr'],
offset: [8, 0]
},
bottomRight: {
points: ['tr', 'br'],
offset: [0, 8]
},
rightBottom: {
points: ['bl', 'br'],
offset: [8, 0]
},
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 8]
},
leftBottom: {
points: ['br', 'bl'],
offset: [-8, 0]
}
};
export function getPlacements(arrowPointAtCenter = false) {
const placements = {};
Object.keys(basePlacements).forEach(key => {
placements[key] = {
...basePlacements[key],
autoArrow: arrowPointAtCenter,
targetOffset
};
});
return placements;
}
export const placements = getPlacements();
+3
View File
@@ -0,0 +1,3 @@
import type { PlacementType } from './placements';
export declare function isInViewPort(element: HTMLElement): boolean;
export declare function getPlacement(targetElement?: HTMLElement | null, placement?: PlacementType, stepPlacement?: PlacementType): PlacementType;
+14
View File
@@ -0,0 +1,14 @@
export function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left
} = element.getBoundingClientRect();
return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
}
export function getPlacement(targetElement, placement, stepPlacement) {
return stepPlacement ?? placement ?? (targetElement === null ? 'center' : 'bottom');
}
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import type { PosInfo } from './hooks/useTarget';
import type { SemanticName, TourProps } from './interface';
export interface MaskProps {
prefixCls?: string;
pos: PosInfo;
rootClassName?: string;
showMask?: boolean;
style?: React.CSSProperties;
fill?: string;
open?: boolean;
animated?: boolean | {
placeholder: boolean;
};
zIndex?: number;
disabledInteraction?: boolean;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
getPopupContainer?: TourProps['getPopupContainer'];
onEsc?: (info: {
top: boolean;
event: KeyboardEvent;
}) => void;
}
declare const Mask: React.FC<MaskProps>;
export default Mask;
+113
View File
@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireDefault(require("react"));
var _clsx = require("clsx");
var _portal = _interopRequireDefault(require("@rc-component/portal"));
var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); }
const COVER_PROPS = {
fill: 'transparent',
pointerEvents: 'auto'
};
const Mask = props => {
const {
prefixCls,
rootClassName,
pos,
showMask,
style = {},
fill = 'rgba(0,0,0,0.5)',
open,
animated,
zIndex,
disabledInteraction,
styles,
classNames: tourClassNames,
getPopupContainer,
onEsc
} = props;
const id = (0, _useId.default)();
const maskId = `${prefixCls}-mask-${id}`;
const mergedAnimated = typeof animated === 'object' ? animated?.placeholder : animated;
const isSafari = typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const maskRectSize = isSafari ? {
width: '100%',
height: '100%'
} : {
width: '100vw',
height: '100vh'
};
const inlineMode = getPopupContainer === false;
return /*#__PURE__*/_react.default.createElement(_portal.default, {
open: open,
autoLock: !inlineMode,
getContainer: getPopupContainer,
onEsc: onEsc
}, /*#__PURE__*/_react.default.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-mask`, rootClassName, tourClassNames?.mask),
style: {
position: inlineMode ? 'absolute' : 'fixed',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex,
pointerEvents: pos && !disabledInteraction ? 'none' : 'auto',
...style,
...styles?.mask
}
}, showMask ? /*#__PURE__*/_react.default.createElement("svg", {
style: {
width: '100%',
height: '100%'
}
}, /*#__PURE__*/_react.default.createElement("defs", null, /*#__PURE__*/_react.default.createElement("mask", {
id: maskId
}, /*#__PURE__*/_react.default.createElement("rect", _extends({
x: "0",
y: "0"
}, maskRectSize, {
fill: "white"
})), pos && /*#__PURE__*/_react.default.createElement("rect", {
x: pos.left,
y: pos.top,
rx: pos.radius,
width: pos.width,
height: pos.height,
fill: "black",
className: mergedAnimated ? `${prefixCls}-placeholder-animated` : ''
}))), /*#__PURE__*/_react.default.createElement("rect", {
x: "0",
y: "0",
width: "100%",
height: "100%",
fill: fill,
mask: `url(#${maskId})`
}), pos && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("rect", _extends({}, COVER_PROPS, {
x: "0",
y: "0",
width: "100%",
height: Math.max(pos.top, 0)
})), /*#__PURE__*/_react.default.createElement("rect", _extends({}, COVER_PROPS, {
x: "0",
y: "0",
width: Math.max(pos.left, 0),
height: "100%"
})), /*#__PURE__*/_react.default.createElement("rect", _extends({}, COVER_PROPS, {
x: "0",
y: pos.top + pos.height,
width: "100%",
height: `calc(100% - ${pos.top + pos.height}px)`
})), /*#__PURE__*/_react.default.createElement("rect", _extends({}, COVER_PROPS, {
x: pos.left + pos.width,
y: "0",
width: `calc(100% - ${pos.left + pos.width}px)`,
height: "100%"
})))) : null));
};
var _default = exports.default = Mask;
+10
View File
@@ -0,0 +1,10 @@
import { type PortalProps } from '@rc-component/portal';
import * as React from 'react';
export interface PlaceholderProps extends Pick<PortalProps, 'open' | 'autoLock' | 'getContainer'> {
domRef: React.RefObject<HTMLDivElement>;
className: string;
style: React.CSSProperties;
fallbackDOM: () => HTMLElement | null;
}
declare const Placeholder: React.ForwardRefExoticComponent<PlaceholderProps & React.RefAttributes<HTMLElement>>;
export default Placeholder;
+36
View File
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _portal = _interopRequireDefault(require("@rc-component/portal"));
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const Placeholder = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
open,
autoLock,
getContainer,
domRef,
className,
style,
fallbackDOM
} = props;
React.useImperativeHandle(ref, () => domRef.current || fallbackDOM());
return /*#__PURE__*/React.createElement(_portal.default, {
open: open,
autoLock: autoLock,
getContainer: getContainer
}, /*#__PURE__*/React.createElement("div", {
ref: domRef,
className: className,
style: style
}));
});
if (process.env.NODE_ENV !== 'production') {
Placeholder.displayName = 'Placeholder';
}
var _default = exports.default = Placeholder;
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import type { TourProps } from './interface';
export type { TourProps };
declare const Tour: React.FC<TourProps>;
export default Tour;
+252
View File
@@ -0,0 +1,252 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _trigger = _interopRequireDefault(require("@rc-component/trigger"));
var _clsx = require("clsx");
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _useEvent = _interopRequireDefault(require("@rc-component/util/lib/hooks/useEvent"));
var _KeyCode = _interopRequireDefault(require("@rc-component/util/lib/KeyCode"));
var _useControlledState = _interopRequireDefault(require("@rc-component/util/lib/hooks/useControlledState"));
var _useClosable = require("./hooks/useClosable");
var _useTarget = _interopRequireDefault(require("./hooks/useTarget"));
var _Mask = _interopRequireDefault(require("./Mask"));
var _placements = require("./placements");
var _TourStep = _interopRequireDefault(require("./TourStep"));
var _util = require("./util");
var _Placeholder = _interopRequireDefault(require("./Placeholder"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
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); }
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,
keyboard = true,
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] = (0, _useControlledState.default)(defaultCurrent || 0, current);
const [internalOpen, setMergedOpen] = (0, _useControlledState.default)(defaultOpen, open);
const mergedOpen = mergedCurrent < 0 || mergedCurrent >= steps.length ? false : internalOpen ?? true;
// Record if already rended in the DOM to avoid `findDOMNode` issue
const [hasOpened, setHasOpened] = React.useState(mergedOpen);
const openRef = React.useRef(mergedOpen);
(0, _useLayoutEffect.default)(() => {
if (mergedOpen) {
if (!openRef.current) {
setMergedCurrent(0);
}
setHasOpened(true);
}
openRef.current = mergedOpen;
}, [mergedOpen, setMergedCurrent]);
const {
target,
placement: stepPlacement,
style: stepStyle,
arrow: stepArrow,
className: stepClassName,
mask: stepMask,
scrollIntoViewOptions: stepScrollIntoViewOptions = defaultScrollIntoViewOptions,
closeIcon: stepCloseIcon,
closable: stepClosable
} = steps[mergedCurrent] || {};
const mergedClosable = (0, _useClosable.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] = (0, _useTarget.default)(target, open, gap, mergedScrollIntoViewOptions, inlineMode, placeholderRef);
const mergedPlacement = (0, _util.getPlacement)(targetElement, placement, stepPlacement);
// ========================= arrow =========================
const mergedArrow = targetElement ? typeof stepArrow === 'undefined' ? arrow : stepArrow : false;
const arrowPointAtCenter = typeof mergedArrow === 'object' ? mergedArrow.pointAtCenter : false;
(0, _useLayoutEffect.default)(() => {
triggerRef.current?.forceAlign();
}, [arrowPointAtCenter, mergedCurrent]);
// ========================= Change =========================
const onInternalChange = nextCurrent => {
setMergedCurrent(nextCurrent);
onChange?.(nextCurrent);
};
const mergedBuiltinPlacements = (0, _react.useMemo)(() => {
if (builtinPlacements) {
return typeof builtinPlacements === 'function' ? builtinPlacements({
arrowPointAtCenter
}) : builtinPlacements;
}
return (0, _placements.getPlacements)(arrowPointAtCenter);
}, [builtinPlacements, arrowPointAtCenter]);
const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent);
};
// ========================= Esc Close =========================
// Use Portal's onEsc to handle Escape key with proper stacking logic
const handleEscClose = (0, _useEvent.default)(({
event
}) => {
if (keyboard && mergedClosable !== null) {
event.preventDefault();
handleClose();
}
});
// ========================= Keyboard =========================
// Support ArrowLeft/ArrowRight to navigate steps.
const keyboardHandler = (0, _useEvent.default)(e => {
// Ignore keyboard events from input-like elements to avoid interfering when typing
if (_KeyCode.default.isEditableTarget(e)) {
return;
}
if (keyboard && e.key === 'ArrowLeft') {
if (mergedCurrent > 0) {
e.preventDefault();
onInternalChange(mergedCurrent - 1);
}
return;
}
if (keyboard && e.key === 'ArrowRight') {
if (mergedCurrent < steps.length - 1) {
e.preventDefault();
onInternalChange(mergedCurrent + 1);
}
return;
}
});
(0, _useLayoutEffect.default)(() => {
if (!mergedOpen) return;
window.addEventListener('keydown', keyboardHandler);
return () => {
window.removeEventListener('keydown', keyboardHandler);
};
}, [mergedOpen, keyboardHandler]);
// ========================= Render =========================
// Skip if not init yet
if (targetElement === undefined || !hasOpened) {
return null;
}
const getPopupElement = () => /*#__PURE__*/React.createElement(_TourStep.default, _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 fallbackDOM = () => {
return targetElement || document.body;
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(_Mask.default, {
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,
onEsc: handleEscClose
}), /*#__PURE__*/React.createElement(_trigger.default, _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: (0, _clsx.clsx)(rootClassName, stepClassName),
prefixCls: prefixCls,
popup: getPopupElement,
forceRender: false,
autoDestroy: true,
zIndex: zIndex,
arrow: !!mergedArrow
}), /*#__PURE__*/React.createElement(_Placeholder.default, {
open: mergedOpen,
autoLock: !inlineMode,
getContainer: getPopupContainer,
domRef: placeholderRef,
fallbackDOM: fallbackDOM,
className: (0, _clsx.clsx)(className, rootClassName, `${prefixCls}-target-placeholder`),
style: {
...(posInfo || CENTER_PLACEHOLDER),
position: inlineMode ? 'absolute' : 'fixed',
pointerEvents: 'none',
...style
}
})));
};
var _default = exports.default = Tour;
@@ -0,0 +1,6 @@
import * as React from 'react';
import type { TourStepProps } from '../interface';
export type DefaultPanelProps = Exclude<TourStepProps, 'closable'> & {
closable: Exclude<TourStepProps['closable'], boolean>;
};
export default function DefaultPanel(props: DefaultPanelProps): React.JSX.Element;
@@ -0,0 +1,80 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = DefaultPanel;
var React = _interopRequireWildcard(require("react"));
var _clsx = require("clsx");
var _pickAttrs = _interopRequireDefault(require("@rc-component/util/lib/pickAttrs"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
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); }
function DefaultPanel(props) {
const {
prefixCls,
current,
total,
title,
description,
onClose,
onPrev,
onNext,
onFinish,
className,
closable,
classNames: tourClassNames,
styles
} = props;
const ariaProps = (0, _pickAttrs.default)(closable || {}, true);
const closeIcon = closable?.closeIcon ?? /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-close-x`
}, "\xD7");
const mergedClosable = !!closable;
return /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-panel`, className)
}, /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-section`, tourClassNames?.section),
style: styles?.section
}, mergedClosable && /*#__PURE__*/React.createElement("button", _extends({
type: "button",
onClick: onClose,
"aria-label": "Close"
}, ariaProps, {
className: `${prefixCls}-close`
}), closeIcon), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-header`, tourClassNames?.header),
style: styles?.header
}, /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-title`, tourClassNames?.title),
style: styles?.title
}, title)), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-description`, tourClassNames?.description),
style: styles?.description
}, description), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-footer`, tourClassNames?.footer),
style: styles?.footer
}, /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-sliders`
}, total > 1 ? [...Array.from({
length: total
}).keys()].map((item, index) => {
return /*#__PURE__*/React.createElement("span", {
key: item,
className: index === current ? 'active' : ''
});
}) : null), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-actions`, tourClassNames?.actions),
style: styles?.actions
}, current !== 0 ? /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-prev-btn`,
onClick: onPrev
}, "Prev") : null, current === total - 1 ? /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-finish-btn`,
onClick: onFinish
}, "Finish") : /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-next-btn`,
onClick: onNext
}, "Next")))));
}
@@ -0,0 +1,6 @@
import * as React from 'react';
import { type DefaultPanelProps } from './DefaultPanel';
import type { TourStepProps, TourStepInfo } from '../interface';
export type { TourStepProps, TourStepInfo, };
declare const TourStep: (props: DefaultPanelProps) => React.JSX.Element;
export default TourStep;
+19
View File
@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _DefaultPanel = _interopRequireDefault(require("./DefaultPanel"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const TourStep = props => {
const {
current,
renderPanel
} = props;
return /*#__PURE__*/React.createElement(React.Fragment, null, typeof renderPanel === 'function' ? renderPanel(props, current) : /*#__PURE__*/React.createElement(_DefaultPanel.default, props));
};
var _default = exports.default = TourStep;
@@ -0,0 +1,8 @@
import * as React from 'react';
import type { TourProps } from '../interface';
import type { TourStepInfo } from '../TourStep';
export declare function useClosable(stepClosable: TourStepInfo['closable'], stepCloseIcon: TourStepInfo['closeIcon'], closable: TourProps['closable'], closeIcon: TourProps['closeIcon']): {
closeIcon?: React.ReactNode;
} & React.AriaAttributes & {
[key: `data-${string}`]: unknown;
} & Pick<React.HTMLAttributes<HTMLDivElement>, "role">;
@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useClosable = useClosable;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function isConfigObj(closable) {
return closable !== null && typeof closable === 'object';
}
/**
* Convert `closable` to ClosableConfig.
* When `preset` is true, will auto fill ClosableConfig with default value.
*/
function getClosableConfig(closable, closeIcon, preset) {
if (closable === false || closeIcon === false && (!isConfigObj(closable) || !closable.closeIcon)) {
return null;
}
const mergedCloseIcon = typeof closeIcon !== 'boolean' ? closeIcon : undefined;
if (isConfigObj(closable)) {
return {
...closable,
closeIcon: closable.closeIcon ?? mergedCloseIcon
};
}
// When StepClosable no need auto fill, but RootClosable need this.
return preset || closable || closeIcon ? {
closeIcon: mergedCloseIcon
} : 'empty';
}
function useClosable(stepClosable, stepCloseIcon, closable, closeIcon) {
return React.useMemo(() => {
const stepClosableConfig = getClosableConfig(stepClosable, stepCloseIcon, false);
const rootClosableConfig = getClosableConfig(closable, closeIcon, true);
if (stepClosableConfig !== 'empty') {
return stepClosableConfig;
}
return rootClosableConfig;
}, [closable, closeIcon, stepClosable, stepCloseIcon]);
}
@@ -0,0 +1,14 @@
/// <reference types="react" />
import type { TourStepInfo } from '..';
export interface Gap {
offset?: number | [number, number];
radius?: number;
}
export interface PosInfo {
left: number;
top: number;
height: number;
width: number;
radius: number;
}
export default function useTarget(target: TourStepInfo['target'], open: boolean, gap?: Gap, scrollIntoViewOptions?: boolean | ScrollIntoViewOptions, inlineMode?: boolean, placeholderRef?: React.RefObject<HTMLDivElement>): [PosInfo, HTMLElement];
+97
View File
@@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useTarget;
var _useEvent = _interopRequireDefault(require("@rc-component/util/lib/hooks/useEvent"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _react = require("react");
var _util = require("../util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function isValidNumber(val) {
return typeof val === 'number' && !Number.isNaN(val);
}
function useTarget(target, open, gap, scrollIntoViewOptions, inlineMode, placeholderRef) {
// ========================= Target =========================
// We trade `undefined` as not get target by function yet.
// `null` as empty target.
const [targetElement, setTargetElement] = (0, _react.useState)(undefined);
(0, _useLayoutEffect.default)(() => {
const nextElement = typeof target === 'function' ? target() : target;
setTargetElement(nextElement || null);
});
// ========================= Align ==========================
const [posInfo, setPosInfo] = (0, _react.useState)(null);
const updatePos = (0, _useEvent.default)(() => {
if (targetElement) {
// Exist target element. We should scroll and get target position
if (!inlineMode && !(0, _util.isInViewPort)(targetElement) && open) {
targetElement.scrollIntoView(scrollIntoViewOptions);
}
const {
left,
top,
width,
height
} = targetElement.getBoundingClientRect();
const nextPosInfo = {
left,
top,
width,
height,
radius: 0
};
// If `inlineMode` we need cut off parent offset
if (inlineMode) {
const parentRect = placeholderRef.current?.parentElement?.getBoundingClientRect();
if (parentRect) {
nextPosInfo.left -= parentRect.left;
nextPosInfo.top -= parentRect.top;
}
}
setPosInfo(origin => {
if (JSON.stringify(origin) !== JSON.stringify(nextPosInfo)) {
return nextPosInfo;
}
return origin;
});
} else {
// Not exist target which means we just show in center
setPosInfo(null);
}
});
const getGapOffset = index => (Array.isArray(gap?.offset) ? gap?.offset[index] : gap?.offset) ?? 6;
(0, _useLayoutEffect.default)(() => {
updatePos();
// update when window resize
window.addEventListener('resize', updatePos);
// update when `document.body.style.overflow` is set to visible
// by default, it will be set to hidden
window.addEventListener('scroll', updatePos);
return () => {
window.removeEventListener('resize', updatePos);
window.removeEventListener('scroll', updatePos);
};
}, [targetElement, open, updatePos]);
// ======================== PosInfo =========================
const mergedPosInfo = (0, _react.useMemo)(() => {
if (!posInfo) {
return posInfo;
}
const gapOffsetX = getGapOffset(0);
const gapOffsetY = getGapOffset(1);
const gapRadius = isValidNumber(gap?.radius) ? gap?.radius : 2;
return {
left: posInfo.left - gapOffsetX,
top: posInfo.top - gapOffsetY,
width: posInfo.width + gapOffsetX * 2,
height: posInfo.height + gapOffsetY * 2,
radius: gapRadius
};
}, [posInfo, gap]);
return [mergedPosInfo, targetElement];
}
+3
View File
@@ -0,0 +1,3 @@
import Tour from './Tour';
export type { TourProps, TourStepInfo, TourStepProps } from './interface';
export default Tour;
+9
View File
@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _Tour = _interopRequireDefault(require("./Tour"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _default = exports.default = _Tour.default;
+80
View File
@@ -0,0 +1,80 @@
import type { TriggerProps } from '@rc-component/trigger';
import type { PlacementType } from './placements';
import type { CSSProperties, ReactNode } from 'react';
import type { Gap } from './hooks/useTarget';
import { type DefaultPanelProps } from './TourStep/DefaultPanel';
export type SemanticName = 'section' | 'footer' | 'actions' | 'header' | 'title' | 'description' | 'mask';
export type HTMLAriaDataAttributes = React.AriaAttributes & {
[key: `data-${string}`]: unknown;
} & Pick<React.HTMLAttributes<HTMLDivElement>, 'role'>;
export interface TourStepInfo {
arrow?: boolean | {
pointAtCenter: boolean;
};
target?: HTMLElement | (() => HTMLElement) | null | (() => null);
title: ReactNode;
description?: ReactNode;
placement?: PlacementType;
mask?: boolean | {
style?: React.CSSProperties;
color?: string;
};
className?: string;
style?: CSSProperties;
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
closeIcon?: ReactNode;
closable?: boolean | ({
closeIcon?: ReactNode;
} & HTMLAriaDataAttributes);
}
export interface TourStepProps extends TourStepInfo {
prefixCls?: string;
total?: number;
current?: number;
onClose?: () => void;
onFinish?: () => void;
renderPanel?: (step: TourStepProps, current: number) => ReactNode;
onPrev?: () => void;
onNext?: () => void;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
}
export interface TourProps extends Pick<TriggerProps, 'onPopupAlign'> {
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
className?: string;
style?: React.CSSProperties;
steps?: TourStepInfo[];
open?: boolean;
keyboard?: boolean;
defaultOpen?: boolean;
defaultCurrent?: number;
current?: number;
onChange?: (current: number) => void;
onClose?: (current: number) => void;
onFinish?: () => void;
closeIcon?: TourStepProps['closeIcon'];
closable?: TourStepProps['closable'];
mask?: boolean | {
style?: React.CSSProperties;
color?: string;
};
arrow?: boolean | {
pointAtCenter: boolean;
};
rootClassName?: string;
placement?: PlacementType;
prefixCls?: string;
renderPanel?: (props: DefaultPanelProps, current: number) => ReactNode;
gap?: Gap;
animated?: boolean | {
placeholder: boolean;
};
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
zIndex?: number;
getPopupContainer?: TriggerProps['getPopupContainer'] | false;
builtinPlacements?: TriggerProps['builtinPlacements'] | ((config?: {
arrowPointAtCenter?: boolean;
}) => TriggerProps['builtinPlacements']);
disabledInteraction?: boolean;
}
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
+4
View File
@@ -0,0 +1,4 @@
import type { BuildInPlacements } from '@rc-component/trigger';
export type PlacementType = 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom' | 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'center';
export declare function getPlacements(arrowPointAtCenter?: boolean): BuildInPlacements;
export declare const placements: BuildInPlacements;
+70
View File
@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getPlacements = getPlacements;
exports.placements = void 0;
const targetOffset = [0, 0];
const basePlacements = {
left: {
points: ['cr', 'cl'],
offset: [-8, 0]
},
right: {
points: ['cl', 'cr'],
offset: [8, 0]
},
top: {
points: ['bc', 'tc'],
offset: [0, -8]
},
bottom: {
points: ['tc', 'bc'],
offset: [0, 8]
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -8]
},
leftTop: {
points: ['tr', 'tl'],
offset: [-8, 0]
},
topRight: {
points: ['br', 'tr'],
offset: [0, -8]
},
rightTop: {
points: ['tl', 'tr'],
offset: [8, 0]
},
bottomRight: {
points: ['tr', 'br'],
offset: [0, 8]
},
rightBottom: {
points: ['bl', 'br'],
offset: [8, 0]
},
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 8]
},
leftBottom: {
points: ['br', 'bl'],
offset: [-8, 0]
}
};
function getPlacements(arrowPointAtCenter = false) {
const placements = {};
Object.keys(basePlacements).forEach(key => {
placements[key] = {
...basePlacements[key],
autoArrow: arrowPointAtCenter,
targetOffset
};
});
return placements;
}
const placements = exports.placements = getPlacements();
+3
View File
@@ -0,0 +1,3 @@
import type { PlacementType } from './placements';
export declare function isInViewPort(element: HTMLElement): boolean;
export declare function getPlacement(targetElement?: HTMLElement | null, placement?: PlacementType, stepPlacement?: PlacementType): PlacementType;
+21
View File
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getPlacement = getPlacement;
exports.isInViewPort = isInViewPort;
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left
} = element.getBoundingClientRect();
return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
}
function getPlacement(targetElement, placement, stepPlacement) {
return stepPlacement ?? placement ?? (targetElement === null ? 'center' : 'bottom');
}
+77
View File
@@ -0,0 +1,77 @@
{
"name": "@rc-component/tour",
"version": "2.3.0",
"description": "React tour Component",
"keywords": [
"react",
"react-component",
"react-tour"
],
"homepage": "https://github.com/react-component/tour",
"bugs": {
"url": "https://github.com/react-component/tour/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/react-component/tour.git"
},
"license": "MIT",
"author": "wangning4567@163.com",
"main": "./lib/index",
"module": "./es/index",
"typings": "es/index.d.ts",
"files": [
"lib",
"es"
],
"scripts": {
"compile": "father build",
"deploy": "npm run docs:build && npm run docs:deploy",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"lint": "eslint src/ --ext .tsx,.ts",
"lint:tsc": "tsc -p tsconfig.json --noEmit",
"now-build": "npm run docs:build",
"prepare": "dumi setup",
"prepublishOnly": "npm run compile && rc-np",
"prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
"start": "dumi dev",
"test": "rc-test",
"watch": "father dev"
},
"dependencies": {
"@rc-component/portal": "^2.2.0",
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.7.0",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.3",
"@rc-component/np": "^1.0.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^15.0.0",
"@types/jest": "^29.0.0",
"@types/minimatch": "^5.1.2",
"@types/node": "^24.5.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@umijs/fabric": "^4.0.0",
"cheerio": "1.0.0-rc.12",
"dumi": "^2.1.2",
"eslint": "^8.0.0",
"father": "^4.0.0",
"gh-pages": "^3.1.0",
"prettier": "^3.0.0",
"rc-test": "^7.1.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.0.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"engines": {
"node": ">=8.x"
}
}