diff --git a/docs/demo/inline.md b/docs/demo/inline.md new file mode 100644 index 0000000..8c8124c --- /dev/null +++ b/docs/demo/inline.md @@ -0,0 +1,8 @@ +--- +title: inline +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/inline.tsx b/docs/examples/inline.tsx new file mode 100644 index 0000000..7d6e40c --- /dev/null +++ b/docs/examples/inline.tsx @@ -0,0 +1,59 @@ +import React, { useRef } from 'react'; +import Tour from '../../src/index'; +import './basic.less'; + +const App = () => { + const [open, setOpen] = React.useState(true); + const createBtnRef = useRef(null); + + return ( +
+
+ + + { + setOpen(false); + }} + // style={{ background: 'red' }} + steps={[ + { + title: '创建', + description: '创建一条数据', + target: () => createBtnRef.current, + mask: true, + }, + ]} + /> +
+ +
+
+ ); +}; + +export default App; diff --git a/src/Mask.tsx b/src/Mask.tsx index 3fc21d4..b2ca95f 100644 --- a/src/Mask.tsx +++ b/src/Mask.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import Portal from '@rc-component/portal'; import type { PosInfo } from './hooks/useTarget'; import useId from 'rc-util/lib/hooks/useId'; -import { SemanticName } from './interface'; +import type { SemanticName, TourProps } from './interface'; const COVER_PROPS = { fill: 'transparent', @@ -24,6 +24,7 @@ export interface MaskProps { disabledInteraction?: boolean; classNames?: Partial>; styles?: Partial>; + getPopupContainer?: TourProps['getPopupContainer']; } const Mask = (props: MaskProps) => { @@ -33,13 +34,14 @@ const Mask = (props: MaskProps) => { pos, showMask, style = {}, - fill = "rgba(0,0,0,0.5)", + fill = 'rgba(0,0,0,0.5)', open, animated, zIndex, disabledInteraction, styles, classNames: tourClassNames, + getPopupContainer, } = props; const id = useId(); @@ -47,15 +49,29 @@ const Mask = (props: MaskProps) => { 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 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 ( - +
{ {/* Block click region */} {pos && ( <> + {/* Top */} + + {/* Left */} + {/* Bottom */} + {/* Right */} diff --git a/src/Tour.tsx b/src/Tour.tsx index c54fd16..26b67d9 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -38,6 +38,7 @@ const Tour: React.FC = props => { onClose, onFinish, open, + defaultOpen, mask = true, arrow = true, rootClassName, @@ -55,6 +56,7 @@ const Tour: React.FC = props => { classNames: tourClassNames, className, style, + getPopupContainer, ...restProps } = props; @@ -65,7 +67,7 @@ const Tour: React.FC = props => { defaultValue: defaultCurrent, }); - const [mergedOpen, setMergedOpen] = useMergedState(undefined, { + const [mergedOpen, setMergedOpen] = useMergedState(defaultOpen, { value: open, postState: origin => mergedCurrent < 0 || mergedCurrent >= steps.length @@ -112,11 +114,19 @@ const Tour: React.FC = props => { 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); @@ -198,6 +208,7 @@ const Tour: React.FC = props => { return ( <> = props => { /> = props => { getTriggerDOMNode={getTriggerDOMNode} arrow={!!mergedArrow} > - +
= props => { )} style={{ ...(posInfo || CENTER_PLACEHOLDER), - position: 'fixed', + position: inlineMode ? 'absolute' : 'fixed', pointerEvents: 'none', ...style, }} diff --git a/src/hooks/useTarget.ts b/src/hooks/useTarget.ts index 3d262c7..73976fa 100644 --- a/src/hooks/useTarget.ts +++ b/src/hooks/useTarget.ts @@ -25,6 +25,8 @@ export default function useTarget( open: boolean, gap?: Gap, scrollIntoViewOptions?: boolean | ScrollIntoViewOptions, + inlineMode?: boolean, + placeholderRef?: React.RefObject, ): [PosInfo, HTMLElement] { // ========================= Target ========================= // We trade `undefined` as not get target by function yet. @@ -53,6 +55,17 @@ export default function useTarget( targetElement.getBoundingClientRect(); const nextPosInfo: PosInfo = { 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; diff --git a/src/interface.ts b/src/interface.ts index ce7754b..3cfa6b3 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -53,6 +53,7 @@ export interface TourProps extends Pick { style?: React.CSSProperties; steps?: TourStepInfo[]; open?: boolean; + defaultOpen?: boolean; defaultCurrent?: number; current?: number; onChange?: (current: number) => void; @@ -76,7 +77,7 @@ export interface TourProps extends Pick { animated?: boolean | { placeholder: boolean }; scrollIntoViewOptions?: boolean | ScrollIntoViewOptions; zIndex?: number; - getPopupContainer?: TriggerProps['getPopupContainer']; + getPopupContainer?: TriggerProps['getPopupContainer'] | false; builtinPlacements?: | TriggerProps['builtinPlacements'] | ((config?: { diff --git a/tests/__snapshots__/index.test.tsx.snap b/tests/__snapshots__/index.test.tsx.snap index aa16fd2..823f744 100644 --- a/tests/__snapshots__/index.test.tsx.snap +++ b/tests/__snapshots__/index.test.tsx.snap @@ -51,7 +51,7 @@ exports[`Tour animated placeholder true 1`] = ` /> @@ -204,7 +204,7 @@ exports[`Tour animated true 1`] = ` /> @@ -385,7 +385,7 @@ exports[`Tour renderPanel basic 1`] = ` /> @@ -496,7 +496,7 @@ exports[`Tour rootClassName 1`] = ` /> @@ -780,7 +780,7 @@ exports[`Tour run in strict mode 2`] = ` /> @@ -1194,7 +1194,7 @@ exports[`Tour should update position when window resize 1`] = ` /> @@ -1331,7 +1331,7 @@ exports[`Tour showArrow should show tooltip arrow default 1`] = ` /> @@ -1480,7 +1480,7 @@ exports[`Tour single 1`] = ` /> @@ -1635,7 +1635,7 @@ exports[`Tour use custom builtinPlacements 1`] = ` /> @@ -1790,7 +1790,7 @@ exports[`Tour use custom builtinPlacements 2`] = ` /> @@ -1956,7 +1956,7 @@ exports[`Tour use custom builtinPlacements 3`] = ` /> diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 1b18036..a213561 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1197,4 +1197,32 @@ describe('Tour', () => { expect(footerElement.style.borderTop).toBe('1px solid black'); expect(descriptionElement.style.fontStyle).toBe('italic'); }); + + it('inline', async () => { + const Demo = () => { + const btnRef = useRef(null); + return ( +
+ + , + target: () => btnRef.current, + }, + ]} + /> +
+ ); + }; + render(); + const container = document.querySelector('.bamboo'); + const children = container.querySelector('.little'); + + // Check container has children + expect(children).toBeTruthy(); + }); });