diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index e40f14b5..afeaed3f 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -272,5 +272,10 @@ export default { name: 'empty', component: () => import('tdesign-mobile-react/empty/_example/index.tsx'), }, + { + title: 'Guide 引导', + name: 'guide', + component: () => import('tdesign-mobile-react/guide/_example/index.tsx'), + }, ], }; diff --git a/site/web/site.config.js b/site/web/site.config.js index ecac7822..1552bb60 100644 --- a/site/web/site.config.js +++ b/site/web/site.config.js @@ -341,6 +341,12 @@ export default { path: '/mobile-react/components/drawer', component: () => import('tdesign-mobile-react/drawer/drawer.md'), }, + { + title: 'Guide 引导', + name: 'guide', + path: '/mobile-react/components/guide', + component: () => import('tdesign-mobile-react/guide/guide.md'), + }, { title: 'Loading 加载', name: 'loading', diff --git a/src/guide/Guide.tsx b/src/guide/Guide.tsx new file mode 100644 index 00000000..4445bf60 --- /dev/null +++ b/src/guide/Guide.tsx @@ -0,0 +1,432 @@ +import React, { FC, useCallback, useMemo, useRef, useState, useEffect } from 'react'; +import classNames from 'classnames'; +import TPopover, { PopoverProps } from '../popover'; +import TPopup, { PopupProps } from '../popup'; +import TButton, { ButtonProps } from '../button'; + +import { guideDefaultProps } from './defaultProps'; +import { TdGuideProps, GuideCrossProps } from './type'; + +import Portal from '../common/Portal'; +import { SizeEnum, StyledProps } from '../common'; +import setStyle from '../_common/js/utils/set-style'; + +import { usePrefixClass } from '../hooks/useClass'; +import useDefault from '../_util/useDefault'; +import parseTNode from '../_util/parseTNode'; +import useDefaultProps from '../hooks/useDefaultProps'; + +import { isFixed, getRelativePosition, getTargetElm, scrollToParentVisibleArea, scrollToElm } from './utils/index'; +import { addClass, getWindowScroll, removeClass } from './utils/shared'; + +export interface GuideProps extends TdGuideProps, StyledProps {} + +const DEFAULT_BUTTON_MAP = { + SKIP: '跳过', + NEXT: '下一步', + BACK: '返回', + FINISH: '完成', +}; + +const Guide: FC = (originProps) => { + const props = useDefaultProps(originProps, guideDefaultProps); + const { + className, + style, + zIndex, + onChange, + steps, + current, + defaultCurrent, + hideSkip, + hideCounter, + finishButtonProps, + } = props; + + const guideClass = usePrefixClass('guide'); + const LOCK_CLASS = `${guideClass}--lock`; + const overlayLayerRef = useRef(null); + const highlightLayerRef = useRef(null); + const popoverWrapperRef = useRef(null); + const referenceLayerRef = useRef(null); + const currentHighlightLayerElm = useRef(null); + + const [innerCurrent, setInnerCurrent] = useDefault(current, defaultCurrent, onChange); + const [popoverVisible, setPopoverVisible] = useState(false); + + const currentStepInfo = useMemo(() => steps?.[innerCurrent], [steps, innerCurrent]); + const getCurrentCrossProps = (propsName: Key) => + currentStepInfo?.[propsName] ?? props[propsName]; + + const isPopover = getCurrentCrossProps('mode') === 'popover'; + + const [actived, setActived] = useState(false); + + const stepsTotal = useMemo(() => steps?.length || 0, [steps]); + const isLast = useMemo(() => innerCurrent === stepsTotal - 1, [innerCurrent, stepsTotal]); + const buttonSize = useMemo(() => (isPopover ? 'extra-small' : 'medium') as SizeEnum, [isPopover]); + + const isPopoverCenter = useMemo( + () => isPopover && currentStepInfo?.placement === 'center', + [isPopover, currentStepInfo], + ); + + const stepProps = useMemo(() => { + if (isPopover) { + return { + visible: popoverVisible, + placement: isPopoverCenter ? 'bottom' : currentStepInfo?.placement, + theme: 'light', + showArrow: false, + ...currentStepInfo?.popoverProps, + } as PopoverProps; + } + return { + visible: popoverVisible, + zIndex, + placement: 'center', + class: `${guideClass}__dialog`, + overlayProps: { + zIndex: zIndex - 1, + }, + } as PopupProps; + }, [isPopover, popoverVisible, isPopoverCenter, currentStepInfo, zIndex, guideClass]); + + const currentElmIsFixed = isFixed(currentHighlightLayerElm.current || document.body); + + // highlight layer 相关 + // 获取当前步骤的用户设定的高亮内容 + const currentCustomHighlightContent = useCallback(() => { + if (!currentStepInfo) return null; + const { highlightContent } = currentStepInfo; + + return parseTNode(highlightContent); + }, [currentStepInfo]); + + // 是否展示高亮区域 + const showCustomHighlightContent = useMemo( + () => Boolean(currentCustomHighlightContent() && isPopover), + [currentCustomHighlightContent, isPopover], + ); + const showOverlay = getCurrentCrossProps('showOverlay'); + + // 设置高亮层的位置 + const setHighlightLayerPosition = (highlightLayer: HTMLElement, isReference = false) => { + let { top, left } = getRelativePosition(currentHighlightLayerElm.current); + let { width, height } = currentHighlightLayerElm.current.getBoundingClientRect(); + const highlightPadding = getCurrentCrossProps('highlightPadding'); + + if (isPopover) { + width += highlightPadding * 2; + height += highlightPadding * 2; + top -= highlightPadding; + left -= highlightPadding; + } else { + const { scrollTop, scrollLeft } = getWindowScroll(); + top += scrollTop; + left += scrollLeft; + } + + const style = { + top: `${top}px`, + left: `${left}px`, + }; + + // 展示自定义高亮 + if (showCustomHighlightContent) { + // 高亮框本身不设定宽高,引用用框的宽高设定为用户自定义的宽高 + if (isReference) { + const { width, height } = highlightLayerRef.current.getBoundingClientRect(); + Object.assign(style, { + width: `${width}px`, + height: `${height}px`, + }); + } else { + Object.assign(style, { + width: 'auto', + height: 'auto', + }); + } + } else { + Object.assign(style, { + width: `${width}px`, + height: `${height}px`, + }); + } + + setStyle(highlightLayer, style); + }; + + const setReferenceFullW = (referenceElements: HTMLElement[]): void => { + const style = { + left: 0, + width: '100vw', + }; + + referenceElements.forEach((elem) => setStyle(elem, style)); + }; + + const showPopoverGuide = () => { + setTimeout(() => { + currentHighlightLayerElm.current = getTargetElm(currentStepInfo?.element); + if (!currentHighlightLayerElm.current) return; + scrollToParentVisibleArea(currentHighlightLayerElm.current); + setHighlightLayerPosition(highlightLayerRef.current); + setHighlightLayerPosition(popoverWrapperRef.current, true); + setHighlightLayerPosition(referenceLayerRef.current, true); + scrollToElm(currentHighlightLayerElm.current); + isPopoverCenter && setReferenceFullW([referenceLayerRef.current, popoverWrapperRef.current]); + }); + }; + + const showDialogGuide = () => { + setTimeout(() => { + currentHighlightLayerElm.current = getTargetElm(currentStepInfo?.element); + scrollToParentVisibleArea(currentHighlightLayerElm.current); + setHighlightLayerPosition(highlightLayerRef.current); + scrollToElm(currentHighlightLayerElm.current); + }); + }; + + const showGuide = () => { + if (isPopover) { + showPopoverGuide(); + } else { + showDialogGuide(); + } + setTimeout(() => { + setPopoverVisible(true); + }); + }; + + const destroyGuide = () => { + highlightLayerRef.current?.parentNode.removeChild(highlightLayerRef.current); + overlayLayerRef.current?.parentNode.removeChild(overlayLayerRef.current); + removeClass(document.body, LOCK_CLASS); + }; + + const renderButtonContent = (buttonProps: ButtonProps, defaultContent: string) => { + const { content } = buttonProps || {}; + return parseTNode(content) || defaultContent; + }; + + const handleSkip = (e) => { + const total = stepsTotal; + setActived(false); + setInnerCurrent(-1, { e, total }); + props.onSkip?.({ e, current: innerCurrent, total }); + }; + + const handleNext = (e) => { + const total = stepsTotal; + setInnerCurrent(innerCurrent + 1, { e, total }); + props.onNextStepClick?.({ + e, + next: innerCurrent + 1, + current: innerCurrent, + total, + }); + }; + + const handleFinish = (e) => { + const total = stepsTotal; + setActived(false); + setInnerCurrent(-1, { e, total }); + props.onFinish?.({ e, current: innerCurrent, total }); + }; + + const handleBack = (e) => { + const total = stepsTotal; + setInnerCurrent(0, { e, total }); + props.onBack?.({ e, current: innerCurrent, total }); + }; + + const initGuide = () => { + if (steps?.length && innerCurrent >= 0 && innerCurrent < steps.length) { + if (!actived) { + setActived(true); + addClass(document.body, LOCK_CLASS); + } + showGuide(); + } + }; + + useEffect(() => { + if (innerCurrent >= 0 && innerCurrent < stepsTotal) { + isPopover && setPopoverVisible(false); + initGuide(); + } else { + setActived(false); + destroyGuide(); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [innerCurrent, isPopover, stepsTotal]); + + const renderStepContent = () => { + const renderTitleNode = () => { + if (!currentStepInfo) return null; + const { title } = currentStepInfo; + return parseTNode(title); + }; + + const renderBodyNode = () => { + if (!currentStepInfo) return null; + const { body } = currentStepInfo; + return parseTNode(body); + }; + + const renderCounterNode = () => { + const params = { + total: stepsTotal, + current: innerCurrent, + }; + const { counter } = props; + const renderCounter = parseTNode(counter, params); + + return renderCounter || ` (${innerCurrent + 1}/${stepsTotal})`; + }; + return ( + <> +
+
+
{renderTitleNode()}
+
{renderBodyNode()}
+
+
+ {!hideSkip && !isLast && ( + + )} + {!isLast && ( + + {renderButtonContent(getCurrentCrossProps('nextButtonProps'), DEFAULT_BUTTON_MAP.NEXT)} + {!hideCounter && renderCounterNode()} + + } + > + )} + {isLast && ( + + )} + {isLast && ( + + {renderButtonContent(finishButtonProps, DEFAULT_BUTTON_MAP.FINISH)} + {!hideCounter && renderCounterNode()} + + } + > + )} +
+
+ + ); + }; + + const renderContentNode = () => { + if (!currentStepInfo) return null; + const { content } = currentStepInfo; + const contentProps = { + handleSkip, + handleNext, + handleFinish, + handleBack, + current: innerCurrent, + total: stepsTotal, + }; + + return parseTNode(content, contentProps); + }; + + const renderPopover = () => ( + } + content={renderContentNode() || renderStepContent()} + > + ); + const renderPopup = () => ( + {renderContentNode() || renderStepContent()} + ); + + return ( + actived && ( + +
+
+ {showCustomHighlightContent && currentCustomHighlightContent()} +
+
+ {isPopover ? renderPopover() : renderPopup()} +
+
+ ) + ); +}; + +Guide.displayName = 'Guide'; + +export default Guide; diff --git a/src/guide/_example/base.tsx b/src/guide/_example/base.tsx new file mode 100644 index 00000000..8cefa494 --- /dev/null +++ b/src/guide/_example/base.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { Guide, Button, Popup, Input, TdGuideProps } from 'tdesign-mobile-react'; +import './style/index.less'; + +export default function Demo() { + const [visible, setVisible] = useState(false); + const [current, setCurrent] = useState(-1); + + const steps: TdGuideProps['steps'] = [ + { + element: () => document.querySelector('.base-guide .main-title'), + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'center', + }, + { + element: '.base-guide .label-field', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom', + highlightPadding: 0, + }, + { + element: '.base-guide .action', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom-right', + }, + ]; + + const handleClick = () => { + setVisible(true); + setTimeout(() => { + setCurrent(0); + }, 800); + }; + + const handleChange: TdGuideProps['onChange'] = (current: number, { e, total }) => { + console.log(current, e, total); + setCurrent(current); + }; + + const handleNextStepClick: TdGuideProps['onNextStepClick'] = ({ e, next, current, total }) => { + console.log(e, next, current, total); + }; + + const handleFinish: TdGuideProps['onFinish'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + + const handleSkip: TdGuideProps['onSkip'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + const handleBack: TdGuideProps['onBack'] = ({ e, current, total }) => { + console.log(e, current, total); + }; + + return ( +
+ + +
+ + + + + + + ); +} diff --git a/src/guide/_example/custom-popover.tsx b/src/guide/_example/custom-popover.tsx new file mode 100644 index 00000000..96117953 --- /dev/null +++ b/src/guide/_example/custom-popover.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { Guide, Button, Popup, Input, TdGuideProps } from 'tdesign-mobile-react'; +import './style/index.less'; +import MyPopover from './my-popover'; + +export default function Demo() { + const [visible, setVisible] = useState(false); + const [current, setCurrent] = useState(-1); + + const steps: TdGuideProps['steps'] = [ + { + element: '.custom-popover .main-title', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'center', + content: MyPopover, + }, + { + element: '.custom-popover .label-field', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom', + highlightPadding: 0, + content: MyPopover, + }, + { + element: '.custom-popover .action', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom-right', + content: MyPopover, + }, + ]; + + const handleClick = () => { + setVisible(true); + setTimeout(() => { + setCurrent(0); + }, 800); + }; + + const handleChange: TdGuideProps['onChange'] = (current: number, { e, total }) => { + console.log(current, e, total); + setCurrent(current); + }; + + const handleNextStepClick: TdGuideProps['onNextStepClick'] = ({ e, next, current, total }) => { + console.log(e, next, current, total); + }; + + const handleFinish: TdGuideProps['onFinish'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + + const handleSkip: TdGuideProps['onSkip'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + const handleBack: TdGuideProps['onBack'] = ({ e, current, total }) => { + console.log(e, current, total); + }; + + return ( +
+ + +
+ + + + + + + ); +} diff --git a/src/guide/_example/dialog-body.tsx b/src/guide/_example/dialog-body.tsx new file mode 100644 index 00000000..2f0df717 --- /dev/null +++ b/src/guide/_example/dialog-body.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import './style/dialog-body.less'; + +export default function DialogBody() { + return ( +
+

用户引导的说明文案

+
+ demo +
+
+ ); +} diff --git a/src/guide/_example/dialog.tsx b/src/guide/_example/dialog.tsx new file mode 100644 index 00000000..76324759 --- /dev/null +++ b/src/guide/_example/dialog.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { Guide, Button, Popup, Input, TdGuideProps } from 'tdesign-mobile-react'; +import DialogBody from './dialog-body'; +import './style/index.less'; + +export default function Demo() { + const [visible, setVisible] = useState(false); + const [current, setCurrent] = useState(-1); + + const steps: TdGuideProps['steps'] = [ + { + element: '.dialog-guide .main-title', + title: '用户引导标题', + body: DialogBody, + mode: 'dialog', + }, + { + element: '.dialog-guide .label-field', + title: '用户引导标题', + body: DialogBody, + mode: 'dialog', + }, + { + element: '.dialog-guide .action', + title: '用户引导标题', + body: DialogBody, + mode: 'dialog', + }, + ]; + + const handleClick = () => { + setVisible(true); + setTimeout(() => { + setCurrent(0); + }, 800); + }; + + const handleChange: TdGuideProps['onChange'] = (current: number, { e, total }) => { + console.log(current, e, total); + setCurrent(current); + }; + + const handleNextStepClick: TdGuideProps['onNextStepClick'] = ({ e, next, current, total }) => { + console.log(e, next, current, total); + }; + + const handleFinish: TdGuideProps['onFinish'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + + const handleSkip: TdGuideProps['onSkip'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + const handleBack: TdGuideProps['onBack'] = ({ e, current, total }) => { + console.log(e, current, total); + }; + + return ( +
+ + +
+ + + + + + + ); +} diff --git a/src/guide/_example/index.tsx b/src/guide/_example/index.tsx new file mode 100644 index 00000000..49cc7a5b --- /dev/null +++ b/src/guide/_example/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import TDemoHeader from '../../../site/mobile/components/DemoHeader'; +import TDemoBlock from '../../../site/mobile/components/DemoBlock'; +import BaseDemo from './base'; +import NoMask from './no-mask'; +import DialogDemo from './dialog'; +import PopoverDialogDemo from './popover-dialog'; +import CustomPopover from './custom-popover'; + +export default function GuideDemo() { + return ( +
+ + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/guide/_example/my-popover.tsx b/src/guide/_example/my-popover.tsx new file mode 100644 index 00000000..b0200834 --- /dev/null +++ b/src/guide/_example/my-popover.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Button } from 'tdesign-mobile-react'; +import { ArrowUpIcon } from 'tdesign-icons-react'; + +import './style/my-popover.less'; + +export default function MyPopover({ current, total, handleSkip, handleBack, handleNext, handleFinish }) { + return ( +
+ +

自定义的图形或说明文案,用来解释或指导该功能使用。

+
+ {current + 1 !== total && ( + + )} + {current + 1 === total && ( + + )} + {current + 1 < total && ( + + )} + {current + 1 === total && ( + + )} +
+
+ ); +} diff --git a/src/guide/_example/no-mask.tsx b/src/guide/_example/no-mask.tsx new file mode 100644 index 00000000..8a650ca8 --- /dev/null +++ b/src/guide/_example/no-mask.tsx @@ -0,0 +1,98 @@ +import React, { useState } from 'react'; +import { Guide, Button, Popup, Input, TdGuideProps } from 'tdesign-mobile-react'; +import './style/index.less'; + +export default function Demo() { + const [visible, setVisible] = useState(false); + const [current, setCurrent] = useState(-1); + + const steps: TdGuideProps['steps'] = [ + { + element: '.no-mask .main-title', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'center', + }, + { + element: '.no-mask .label-field', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom', + highlightPadding: 0, + }, + { + element: '.no-mask .action', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom-right', + }, + ]; + + const handleClick = () => { + setVisible(true); + setTimeout(() => { + setCurrent(0); + }, 800); + }; + + const handleChange: TdGuideProps['onChange'] = (current: number, { e, total }) => { + console.log(current, e, total); + setCurrent(current); + }; + + const handleNextStepClick: TdGuideProps['onNextStepClick'] = ({ e, next, current, total }) => { + console.log(e, next, current, total); + }; + + const handleFinish: TdGuideProps['onFinish'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + + const handleSkip: TdGuideProps['onSkip'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + const handleBack: TdGuideProps['onBack'] = ({ e, current, total }) => { + console.log(e, current, total); + }; + + return ( +
+ + +
+ + + + + + ); +} diff --git a/src/guide/_example/popover-dialog.tsx b/src/guide/_example/popover-dialog.tsx new file mode 100644 index 00000000..c76e2613 --- /dev/null +++ b/src/guide/_example/popover-dialog.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { Guide, Button, Popup, Input, TdGuideProps } from 'tdesign-mobile-react'; +import DialogBody from './dialog-body'; +import './style/index.less'; + +export default function Demo() { + const [visible, setVisible] = useState(false); + const [current, setCurrent] = useState(-1); + + const steps: TdGuideProps['steps'] = [ + { + element: '.popover-dialog-guide .main-title', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'center', + }, + { + element: '.popover-dialog-guide .label-field', + title: '用户引导标题', + body: DialogBody, + mode: 'dialog', + }, + { + element: '.popover-dialog-guide .action', + title: '用户引导标题', + body: '用户引导的说明文案', + placement: 'bottom-right', + }, + ]; + + const handleClick = () => { + setVisible(true); + setTimeout(() => { + setCurrent(0); + }, 800); + }; + + const handleChange: TdGuideProps['onChange'] = (current: number, { e, total }) => { + console.log(current, e, total); + setCurrent(current); + }; + + const handleNextStepClick: TdGuideProps['onNextStepClick'] = ({ e, next, current, total }) => { + console.log(e, next, current, total); + }; + + const handleFinish: TdGuideProps['onFinish'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + + const handleSkip: TdGuideProps['onSkip'] = ({ e, current, total }) => { + setVisible(false); + console.log(e, current, total); + }; + const handleBack: TdGuideProps['onBack'] = ({ e, current, total }) => { + console.log(e, current, total); + }; + + return ( +
+ + +
+ + + + + + + ); +} diff --git a/src/guide/_example/style/dialog-body.less b/src/guide/_example/style/dialog-body.less new file mode 100644 index 00000000..62dcb8ba --- /dev/null +++ b/src/guide/_example/style/dialog-body.less @@ -0,0 +1,14 @@ +.dialog-body { + .img-wrapper { + border-radius: var(--td-radius-default); + overflow: hidden; + img { + vertical-align: bottom; + width: 100%; + } + } + + p { + margin-bottom: 24px; + } +} \ No newline at end of file diff --git a/src/guide/_example/style/index.less b/src/guide/_example/style/index.less new file mode 100644 index 00000000..8f5c2f2c --- /dev/null +++ b/src/guide/_example/style/index.less @@ -0,0 +1,32 @@ +.guide-demo { + display: flex; + justify-content: center; +} +.guide-container { + height: 100%; + width: 100%; + position: relative; +} +.main-title { + margin: 16px; + display: inline-block; +} +.title-major { + font-size: 24px; + font-weight: 600; + line-height: 36px; +} +.title-sub { + font-size: 16px; + font-weight: 400; + line-height: 24px; + margin-top: 4px; + color: rgba(0, 0, 0, 0.6); +} +.action { + margin: 16px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + diff --git a/src/guide/_example/style/my-popover.less b/src/guide/_example/style/my-popover.less new file mode 100644 index 00000000..220a4e14 --- /dev/null +++ b/src/guide/_example/style/my-popover.less @@ -0,0 +1,27 @@ +.my-popover { + width: 240px; +} + +.pop-icon { + color: white; + font-size: 32px; + font-weight: bold; +} + +.popover-desc { + margin-top: 16px; + color: #ffffff; + font-size: 16px; + font-weight: 600; + text-align: left; + line-height: 24px; +} + +.popover-action { + margin-top: 16px; + text-align: right; +} + +.popover-action button { + margin-left: 12px; +} \ No newline at end of file diff --git a/src/guide/defaultProps.ts b/src/guide/defaultProps.ts new file mode 100644 index 00000000..6def7a5d --- /dev/null +++ b/src/guide/defaultProps.ts @@ -0,0 +1,14 @@ +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdGuideProps } from './type'; + +export const guideDefaultProps: TdGuideProps = { + hideCounter: false, + hideSkip: false, + highlightPadding: 8, + mode: 'popover', + showOverlay: true, + zIndex: 999999, +}; diff --git a/src/guide/guide.en-US.md b/src/guide/guide.en-US.md new file mode 100644 index 00000000..f9cceaa6 --- /dev/null +++ b/src/guide/guide.en-US.md @@ -0,0 +1,49 @@ +:: BASE_DOC :: + +## API + + +### Guide Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | className of component | N +style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N +backButtonProps | Object | - | Typescript:`ButtonProps` | N +counter | TElement | - | Typescript:`TNode<{ current: number; total: number }>`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +current | Number | - | \- | N +defaultCurrent | Number | - | uncontrolled property | N +finishButtonProps | Object | - | Typescript:`ButtonProps` | N +hideCounter | Boolean | false | \- | N +hideSkip | Boolean | false | \- | N +highlightPadding | Number | 8 | \- | N +mode | String | popover | options: popover/dialog | N +nextButtonProps | Object | - | Typescript:`ButtonProps`,[Button API Documents](./button?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | \- | N +skipButtonProps | Object | - | Typescript:`ButtonProps` | N +steps | Array | - | Typescript:`Array` | N +zIndex | Number | 999999 | \- | N +onBack | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N +onChange | Function | | Typescript:`(current: number, context?: { e: MouseEvent, total: number }) => void`
| N +onFinish | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N +onNextStepClick | Function | | Typescript:`(context: { e: MouseEvent, next: number, current: number, total: number }) => void`
| N +onSkip | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N + +### GuideStep + +name | type | default | description | required +-- | -- | -- | -- | -- +backButtonProps | Object | - | Typescript:`ButtonProps` | N +body | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +content | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +element | String / Function | - | required。Typescript:`AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | Y +highlightContent | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +highlightPadding | Number | - | \- | N +mode | String | - | options: popover/dialog | N +nextButtonProps | Object | - | Typescript:`ButtonProps` | N +offset | Array | - | this api is in discussing. do not use it.。Typescript:`Array` | N +placement | String | 'top' | Typescript:`StepPopoverPlacement ` `type StepPopoverPlacement = 'top'\|'left'\|'right'\|'bottom'\|'top-left'\|'top-right'\|'bottom-left'\|'bottom-right'\|'left-top'\|'left-bottom'\|'right-top'\|'right-bottom'\|'center'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/guide/type.ts) | N +popoverProps | Object | - | Popover component props if `mode = popover`。Typescript:`PopoverProps`,[Popover API Documents](./popover?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | \- | N +skipButtonProps | Object | - | Typescript:`ButtonProps` | N +title | String / Slot / Function | - | title of current step。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N diff --git a/src/guide/guide.md b/src/guide/guide.md new file mode 100644 index 00000000..5e68e6d0 --- /dev/null +++ b/src/guide/guide.md @@ -0,0 +1,48 @@ +:: BASE_DOC :: + +## API + +### Guide Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +backButtonProps | Object | - | 透传 返回 的全部属性,示例:`{ content: '返回', theme: 'default' }`。TS 类型:`ButtonProps` | N +counter | TElement | - | 用于自定义渲染计数部分。TS 类型:`TNode<{ current: number; total: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +current | Number | - | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 | N +defaultCurrent | Number | - | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景。非受控属性 | N +finishButtonProps | Object | - | 透传 完成 的全部属性,示例:`{ content: '完成', theme: 'primary' }`。TS 类型:`ButtonProps` | N +hideCounter | Boolean | false | 是否隐藏计数 | N +hideSkip | Boolean | false | 是否隐藏跳过按钮 | N +highlightPadding | Number | 8 | 高亮框的内边距 | N +mode | String | popover | 引导框的类型。可选项:popover/dialog | N +nextButtonProps | Object | - | 透传 下一步按钮 的全部属性,示例:`{ content: '下一步', theme: 'primary' }`。TS 类型:`ButtonProps`,[Button API Documents](./button?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | 是否出现遮罩层 | N +skipButtonProps | Object | - | 透传 跳过按钮 的全部属性,{ content: '跳过', theme: 'default' }。TS 类型:`ButtonProps` | N +steps | Array | - | 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。。TS 类型:`Array` | N +zIndex | Number | 999999 | 提示框的层级 | N +onBack | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击返回按钮时触发 | N +onChange | Function | | TS 类型:`(current: number, context?: { e: MouseEvent, total: number }) => void`
当前步骤发生变化时触发 | N +onFinish | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击完成按钮时触发 | N +onNextStepClick | Function | | TS 类型:`(context: { e: MouseEvent, next: number, current: number, total: number }) => void`
点击下一步时触发 | N +onSkip | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击跳过按钮时触发 | N + +### GuideStep + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +backButtonProps | Object | - | 用于自定义当前引导框的返回按钮的内容。TS 类型:`ButtonProps` | N +body | String / Slot / Function | - | 当前步骤提示框的内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +content | Slot / Function | - | 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效)。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +element | String / Function | - | 必需。高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign')。TS 类型:`AttachNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | Y +highlightContent | Slot / Function | - | 用户自定义的高亮框 (仅当 `mode` 为 `popover` 时生效)。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +highlightPadding | Number | - | 高亮框的内边距 | N +mode | String | - | 引导框的类型。可选项:popover/dialog | N +nextButtonProps | Object | - | 用于自定义当前引导框的下一步按钮的内容。TS 类型:`ButtonProps` | N +offset | Array | - | 【讨论确认中】相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px']。TS 类型:`Array` | N +placement | String | 'top' | 引导框相对于高亮元素出现的位置,(仅当 `mode` 为 `popover` 时生效)。TS 类型:`StepPopoverPlacement ` `type StepPopoverPlacement = 'top'\|'left'\|'right'\|'bottom'\|'top-left'\|'top-right'\|'bottom-left'\|'bottom-right'\|'left-top'\|'left-bottom'\|'right-top'\|'right-bottom'\|'center'`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/guide/type.ts) | N +popoverProps | Object | - | 透传全部属性到 Popover 组件。`mode=popover` 时有效。TS 类型:`PopoverProps`,[Popover API Documents](./popover?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | 是否出现遮罩层 | N +skipButtonProps | Object | - | 用于自定义当前步骤引导框的跳过按钮的内容。TS 类型:`ButtonProps` | N +title | String / Slot / Function | - | 当前步骤的标题内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N diff --git a/src/guide/index.ts b/src/guide/index.ts new file mode 100644 index 00000000..b7be5c0f --- /dev/null +++ b/src/guide/index.ts @@ -0,0 +1,9 @@ +import _Guide from './Guide'; +import './style'; + +export type { GuideProps } from './Guide'; +export * from './type'; + +export const Guide = _Guide; + +export default Guide; diff --git a/src/guide/style/css.js b/src/guide/style/css.js new file mode 100644 index 00000000..6a9a4b13 --- /dev/null +++ b/src/guide/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/guide/style/index.js b/src/guide/style/index.js new file mode 100644 index 00000000..e6425e1f --- /dev/null +++ b/src/guide/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/mobile/components/guide/_index.less'; diff --git a/src/guide/type.ts b/src/guide/type.ts new file mode 100644 index 00000000..574194c0 --- /dev/null +++ b/src/guide/type.ts @@ -0,0 +1,176 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { ButtonProps } from '../button'; +import { PopoverProps } from '../popover'; +import { TNode, AttachNode } from '../common'; +import { MouseEvent } from 'react'; + +export interface TdGuideProps { + /** + * 透传 返回 的全部属性,示例:`{ content: '返回', theme: 'default' }` + */ + backButtonProps?: ButtonProps; + /** + * 用于自定义渲染计数部分 + */ + counter?: TNode<{ current: number; total: number }>; + /** + * 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 + */ + current?: number; + /** + * 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景,非受控属性 + */ + defaultCurrent?: number; + /** + * 透传 完成 的全部属性,示例:`{ content: '完成', theme: 'primary' }` + */ + finishButtonProps?: ButtonProps; + /** + * 是否隐藏计数 + * @default false + */ + hideCounter?: boolean; + /** + * 是否隐藏跳过按钮 + * @default false + */ + hideSkip?: boolean; + /** + * 高亮框的内边距 + * @default 8 + */ + highlightPadding?: number; + /** + * 引导框的类型 + * @default popover + */ + mode?: 'popover' | 'dialog'; + /** + * 透传 下一步按钮 的全部属性,示例:{ content: '下一步', theme: 'primary' } + */ + nextButtonProps?: ButtonProps; + /** + * 是否出现遮罩层 + * @default true + */ + showOverlay?: boolean; + /** + * 透传 跳过按钮 的全部属性,{ content: '跳过', theme: 'default' } + */ + skipButtonProps?: ButtonProps; + /** + * 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。 + */ + steps?: Array; + /** + * 提示框的层级 + * @default 999999 + */ + zIndex?: number; + /** + * 点击返回按钮时触发 + */ + onBack?: (context: { e: MouseEvent; current: number; total: number }) => void; + /** + * 当前步骤发生变化时触发 + */ + onChange?: (current: number, context?: { e: MouseEvent; total: number }) => void; + /** + * 点击完成按钮时触发 + */ + onFinish?: (context: { e: MouseEvent; current: number; total: number }) => void; + /** + * 点击下一步时触发 + */ + onNextStepClick?: (context: { e: MouseEvent; next: number; current: number; total: number }) => void; + /** + * 点击跳过按钮时触发 + */ + onSkip?: (context: { e: MouseEvent; current: number; total: number }) => void; +} + +export interface GuideStep { + /** + * 用于自定义当前引导框的返回按钮的内容 + */ + backButtonProps?: ButtonProps; + /** + * 当前步骤提示框的内容 + */ + body?: string | TNode; + /** + * 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效) + */ + content?: TNode; + /** + * 高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign') + */ + element: AttachNode; + /** + * 用户自定义的高亮框 (仅当 `mode` 为 `popover` 时生效) + */ + highlightContent?: TNode; + /** + * 高亮框的内边距 + */ + highlightPadding?: number; + /** + * 引导框的类型 + */ + mode?: 'popover' | 'dialog'; + /** + * 用于自定义当前引导框的下一步按钮的内容 + */ + nextButtonProps?: ButtonProps; + /** + * 【讨论确认中】相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px'] + */ + offset?: Array; + /** + * 引导框相对于高亮元素出现的位置,(仅当 `mode` 为 `popover` 时生效) + * @default 'top' + */ + placement?: StepPopoverPlacement; + /** + * 透传全部属性到 Popover 组件。`mode=popover` 时有效 + */ + popoverProps?: PopoverProps; + /** + * 是否出现遮罩层 + * @default true + */ + showOverlay?: boolean; + /** + * 用于自定义当前步骤引导框的跳过按钮的内容 + */ + skipButtonProps?: ButtonProps; + /** + * 当前步骤的标题内容 + */ + title?: string | TNode; +} + +export type StepPopoverPlacement = + | 'top' + | 'left' + | 'right' + | 'bottom' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + | 'left-top' + | 'left-bottom' + | 'right-top' + | 'right-bottom' + | 'center'; + +export type GuideCrossProps = Pick< + GuideStep, + 'mode' | 'skipButtonProps' | 'nextButtonProps' | 'backButtonProps' | 'showOverlay' | 'highlightPadding' +>; diff --git a/src/guide/utils/dom.ts b/src/guide/utils/dom.ts new file mode 100644 index 00000000..3884629d --- /dev/null +++ b/src/guide/utils/dom.ts @@ -0,0 +1,137 @@ +import isString from 'lodash/isString'; +import isFunction from 'lodash/isFunction'; +import { AttachNode } from '../../common'; +import { elementInViewport, getWindowScroll, getWindowSize } from './shared'; +/** + * 获取元素某个 css 对应的值 + * @param element 元素 + * @param propName css 名 + * @returns string + */ +export function getElmCssPropValue(element: HTMLElement, propName: string): string { + let propValue = ''; + + if (document.defaultView && document.defaultView.getComputedStyle) { + propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName); + } + + if (propValue && propValue.toLowerCase) { + return propValue.toLowerCase(); + } + + return propValue; +} + +/** + * 判断元素是否处在 position fixed 中 + * @param element 元素 + * @returns boolean + */ +export function isFixed(element: HTMLElement): boolean { + const p = element.parentNode as HTMLElement; + + if (!p || p.nodeName === 'HTML') { + return false; + } + + if (getElmCssPropValue(element, 'position') === 'fixed') { + return true; + } + + return isFixed(p); +} + +/** + * 获取元素相对于另一个元素的位置(或者说相对于 body) + * 感谢 `meouw`: http://stackoverflow.com/a/442474/375966 + */ +export function getRelativePosition(elm: HTMLElement, relativeElm: HTMLElement = document.body) { + const { scrollTop, scrollLeft } = getWindowScroll(); + const { top: elmTop, left: elmLeft } = elm.getBoundingClientRect(); + const { top: relElmTop, left: relElmLeft } = relativeElm.getBoundingClientRect(); + const relativeElmPosition = getElmCssPropValue(relativeElm, 'position'); + + if ( + (relativeElm.tagName.toLowerCase() !== 'body' && relativeElmPosition === 'relative') || + relativeElmPosition === 'sticky' + ) { + return { + top: elmTop - relElmTop, + left: elmLeft - relElmLeft, + }; + } + + if (isFixed(elm)) { + return { + top: elmTop, + left: elmLeft, + }; + } + + return { + top: elmTop + scrollTop, + left: elmLeft + scrollLeft, + }; +} + +export function getTargetElm(elm: AttachNode): HTMLElement { + if (elm) { + let targetElement: HTMLElement; + if (isString(elm)) { + targetElement = document.querySelector(elm) as HTMLElement; + } else if (isFunction(elm)) { + targetElement = elm() as HTMLElement; + } else { + throw new Error('elm should be string or function'); + } + if (targetElement) { + return targetElement as HTMLElement; + } + if (process?.env?.NODE_ENV !== 'test') { + throw new Error('There is no element with given.'); + } + } else { + return document.body; + } + return document.body; +} + +export function getScrollParent(element: HTMLElement) { + let style = window.getComputedStyle(element); + const excludeStaticParent = style.position === 'absolute'; + const overflowRegex = /(auto|scroll)/; + + if (style.position === 'fixed') return document.body; + + for (let parent = element; parent.parentElement; ) { + parent = parent.parentElement; + style = window.getComputedStyle(parent); + if (excludeStaticParent && style.position === 'static') { + continue; + } + if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; + } + + return document.body; +} + +export function scrollToParentVisibleArea(element: HTMLElement) { + const parent = getScrollParent(element); + if (parent === document.body) return; + // !todo 逻辑待验证 + if (elementInViewport(element, parent)) return; + parent.scrollTop = element.offsetTop - parent.offsetTop; +} + +export function scrollToElm(elm: HTMLElement) { + const rect = elm.getBoundingClientRect(); + + if (!elementInViewport(elm)) { + const winHeight = getWindowSize().height; + // const top = rect.bottom - (rect.bottom - rect.top); + window.scrollTo({ + top: rect.top - (winHeight / 2 - rect.height / 2), + behavior: 'smooth', + }); + } +} diff --git a/src/guide/utils/index.ts b/src/guide/utils/index.ts new file mode 100644 index 00000000..ae897bea --- /dev/null +++ b/src/guide/utils/index.ts @@ -0,0 +1,10 @@ +import { + getElmCssPropValue, + isFixed, + getRelativePosition, + getTargetElm, + scrollToParentVisibleArea, + scrollToElm, +} from './dom'; + +export { getElmCssPropValue, isFixed, getRelativePosition, getTargetElm, scrollToParentVisibleArea, scrollToElm }; diff --git a/src/guide/utils/shared.ts b/src/guide/utils/shared.ts new file mode 100644 index 00000000..c383cce7 --- /dev/null +++ b/src/guide/utils/shared.ts @@ -0,0 +1,134 @@ +import isFunction from 'lodash/isFunction'; +import isString from 'lodash/isString'; + +const trim = (str: string): string => (str || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, ''); + +export const getAttach = (node: any, triggerNode?: any): HTMLElement | Element => { + const attachNode = isFunction(node) ? node(triggerNode) : node; + if (!attachNode) { + return document.body; + } + if (isString(attachNode)) { + return document.querySelector(attachNode) as Element; + } + if (attachNode instanceof HTMLElement) { + return attachNode; + } + return document.body; +}; + +export const getSSRAttach = () => { + if (process.env.NODE_ENV === 'test-snap') return 'body'; +}; + +export function stopPropagation(event: Event) { + event.stopPropagation(); +} + +export function preventDefault(event: Event, isStopPropagation?: boolean) { + if (typeof event.cancelable !== 'boolean' || event.cancelable) { + // The event can be canceled, so we do so. + event.preventDefault(); + } + + if (isStopPropagation) { + stopPropagation(event); + } +} + +export function hasClass(el: Element, cls: string): any { + if (!el || !cls) return false; + if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.'); + if (el.classList) { + return el.classList.contains(cls); + } + return ` ${el.className} `.indexOf(` ${cls} `) > -1; +} + +export function addClass(el: Element, cls: string): any { + if (!el) return; + let curClass = el.className; + const classes = (cls || '').split(' '); + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.add(clsName); + } else if (!hasClass(el, clsName)) { + curClass += ` ${clsName}`; + } + } + if (!el.classList) { + // eslint-disable-next-line no-param-reassign + el.className = curClass; + } +} + +export function removeClass(el: Element, cls: string): any { + if (!el || !cls) return; + const classes = cls.split(' '); + let curClass = ` ${el.className} `; + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.remove(clsName); + } else if (hasClass(el, clsName)) { + curClass = curClass.replace(` ${clsName} `, ' '); + } + } + if (!el.classList) { + // eslint-disable-next-line no-param-reassign + el.className = trim(curClass); + } +} + +/** + * 检查元素是否在父元素视图 + * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport + * @param elm 元素 + * @param parent + * @returns boolean + */ +export function elementInViewport(elm: HTMLElement, parent?: HTMLElement): boolean { + const rect = elm.getBoundingClientRect(); + if (parent) { + const parentRect = parent.getBoundingClientRect(); + return ( + rect.top >= parentRect.top && + rect.left >= parentRect.left && + rect.bottom <= parentRect.bottom && + rect.right <= parentRect.right + ); + } + return rect.top >= 0 && rect.left >= 0 && rect.bottom + 80 <= window.innerHeight && rect.right <= window.innerWidth; +} + +/** + * 获取当前视图滑动的距离 + * @returns { scrollTop: number, scrollLeft: number } + */ +export function getWindowScroll(): { scrollTop: number; scrollLeft: number } { + const { body } = document; + const docElm = document.documentElement; + const scrollTop = window.pageYOffset || docElm.scrollTop || body.scrollTop; + const scrollLeft = window.pageXOffset || docElm.scrollLeft || body.scrollLeft; + + return { scrollTop, scrollLeft }; +} + +/** + * 获取当前视图的大小 + * @returns { width: number, height: number } + */ +export function getWindowSize(): { width: number; height: number } { + if (window.innerWidth !== undefined) { + return { width: window.innerWidth, height: window.innerHeight }; + } + const doc = document.documentElement; + return { width: doc.clientWidth, height: doc.clientHeight }; +} diff --git a/src/index.ts b/src/index.ts index 678cb0fa..918f0aa4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,7 @@ export * from './pull-down-refresh'; export * from './toast'; export * from './drawer'; export * from './popover'; +export * from './guide'; /** * 二期组件 diff --git a/src/popup/index.tsx b/src/popup/index.tsx index a4f768c5..400e1a0a 100644 --- a/src/popup/index.tsx +++ b/src/popup/index.tsx @@ -2,7 +2,7 @@ import _Popup from './Popup'; import './style'; +export type { PopupProps } from './Popup'; export const Popup = _Popup; export default Popup; - diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 625b72d4..6aac3274 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -17015,6 +17015,1504 @@ exports[`csr snapshot test > csr test src/grid/_example/scroll.tsx 1`] = ` `; +exports[`csr snapshot test > csr test src/guide/_example/base.tsx 1`] = ` +
+
+ + +
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/custom-popover.tsx 1`] = ` +
+
+ + +
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/dialog.tsx 1`] = ` +
+
+ + +
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/dialog-body.tsx 1`] = ` +
+
+

+ 用户引导的说明文案 +

+
+ demo +
+
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/index.tsx 1`] = ` +
+
+
+

+ Guide 引导 +

+

+ 逐步骤进行指引或解释说明的组件,常用于用户不熟悉的或需进行特别强调的页面。 +

+
+
+
+

+ 01 组件类型 +

+

+ 基础引导 +

+
+
+
+ + +
+
+
+
+
+

+ 不带遮罩的引导 +

+
+
+
+ + +
+
+
+
+
+

+ 弹窗形式的引导 +

+
+
+
+ + +
+
+
+
+
+

+ 气泡与弹窗混合的引导 +

+
+
+
+ + +
+
+
+
+
+

+ 自定义气泡 +

+
+
+
+ + +
+
+
+
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/my-popover.tsx 1`] = ` +
+
+ + + +

+ 自定义的图形或说明文案,用来解释或指导该功能使用。 +

+
+ +
+
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/no-mask.tsx 1`] = ` +
+
+ + +
+
+`; + +exports[`csr snapshot test > csr test src/guide/_example/popover-dialog.tsx 1`] = ` +
+
+ + +
+
+`; + exports[`csr snapshot test > csr test src/icon/_example/base.tsx 1`] = `
ssr test src/grid/_example/index.tsx 1`] = `"
ssr test src/grid/_example/scroll.tsx 1`] = `"
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
"`; +exports[`ssr snapshot test > ssr test src/guide/_example/base.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/custom-popover.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/dialog.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/dialog-body.tsx 1`] = `"

用户引导的说明文案

demo
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/index.tsx 1`] = `"

Guide 引导

逐步骤进行指引或解释说明的组件,常用于用户不熟悉的或需进行特别强调的页面。

01 组件类型

基础引导

不带遮罩的引导

弹窗形式的引导

气泡与弹窗混合的引导

自定义气泡

"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/my-popover.tsx 1`] = `"

自定义的图形或说明文案,用来解释或指导该功能使用。

"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/no-mask.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/popover-dialog.tsx 1`] = `"
"`; + exports[`ssr snapshot test > ssr test src/icon/_example/base.tsx 1`] = `"

How do you feel today?


What is your favourite food?


How much icons does TDesign Icon includes?

"`; exports[`ssr snapshot test > ssr test src/icon/_example/enhanced.tsx 1`] = `"

"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index bec660d5..f8684e7c 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -120,6 +120,22 @@ exports[`ssr snapshot test > ssr test src/grid/_example/index.tsx 1`] = `"
ssr test src/grid/_example/scroll.tsx 1`] = `"
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
标题文字
"`; +exports[`ssr snapshot test > ssr test src/guide/_example/base.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/custom-popover.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/dialog.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/dialog-body.tsx 1`] = `"

用户引导的说明文案

demo
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/index.tsx 1`] = `"

Guide 引导

逐步骤进行指引或解释说明的组件,常用于用户不熟悉的或需进行特别强调的页面。

01 组件类型

基础引导

不带遮罩的引导

弹窗形式的引导

气泡与弹窗混合的引导

自定义气泡

"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/my-popover.tsx 1`] = `"

自定义的图形或说明文案,用来解释或指导该功能使用。

"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/no-mask.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/guide/_example/popover-dialog.tsx 1`] = `"
"`; + exports[`ssr snapshot test > ssr test src/icon/_example/base.tsx 1`] = `"

How do you feel today?


What is your favourite food?


How much icons does TDesign Icon includes?

"`; exports[`ssr snapshot test > ssr test src/icon/_example/enhanced.tsx 1`] = `"

"`;