diff --git a/docs/demo/gap.md b/docs/demo/gap.md index 165f09b..940b3cf 100644 --- a/docs/demo/gap.md +++ b/docs/demo/gap.md @@ -16,3 +16,7 @@ nav: ## Radius + +## Offset + + diff --git a/docs/examples/gap-offset-array.tsx b/docs/examples/gap-offset-array.tsx new file mode 100644 index 0000000..e64a46e --- /dev/null +++ b/docs/examples/gap-offset-array.tsx @@ -0,0 +1,60 @@ +import { useRef, useState } from 'react'; +import Tour from '../../src/index'; +import './basic.less'; + +const App = () => { + const button1Ref = useRef(null); + const button2Ref = useRef(null); + const button3Ref = useRef(null); + + const [open, setOpen] = useState(false); + const offset: [number, number][] = [ + [10, 10], + [20, 20], + [30, 30], + ] + return ( +
+
+ + + + +
+ + +
+ button1Ref.current, + mask: true, + }, + { + title: '创建2', + description: '创建一条数据2', + target: () => button2Ref.current, + mask: true, + }, + { + title: '创建3', + description: '创建一条数据3', + target: () => button3Ref.current, + mask: true, + }, + ]} + onClose={() => { + setOpen(false); + }} + /> +
+ ); +}; + +export default App; diff --git a/src/Tour.tsx b/src/Tour.tsx index fd96306..f0d069e 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -128,6 +128,7 @@ const Tour: React.FC = props => { mergedScrollIntoViewOptions, inlineMode, placeholderRef, + mergedCurrent ); const mergedPlacement = getPlacement(targetElement, placement, stepPlacement); @@ -263,4 +264,4 @@ const Tour: React.FC = props => { ); }; -export default Tour; +export default Tour; \ No newline at end of file diff --git a/src/hooks/useTarget.ts b/src/hooks/useTarget.ts index 682a596..0e90716 100644 --- a/src/hooks/useTarget.ts +++ b/src/hooks/useTarget.ts @@ -5,7 +5,7 @@ import type { TourStepInfo } from '..'; import { isInViewPort } from '../util'; export interface Gap { - offset?: number | [number, number]; + offset?: number | [number, number] | [number, number][]; radius?: number; } @@ -16,6 +16,8 @@ export interface PosInfo { width: number; radius: number; } +const DEFAULT_GAP_OFFSET = 6; + function isValidNumber(val) { return typeof val === 'number' && !Number.isNaN(val); } @@ -27,6 +29,7 @@ export default function useTarget( scrollIntoViewOptions?: boolean | ScrollIntoViewOptions, inlineMode?: boolean, placeholderRef?: React.RefObject, + current: number = 0, ): [PosInfo, HTMLElement] { // ========================= Target ========================= // We trade `undefined` as not get target by function yet. @@ -78,8 +81,27 @@ export default function useTarget( } }); - const getGapOffset = (index: number) => - (Array.isArray(gap?.offset) ? gap?.offset[index] : gap?.offset) ?? 6; + const getGapOffset = (index: number): number => { + if (gap?.offset === undefined) return DEFAULT_GAP_OFFSET; + + if (typeof gap.offset === 'number') { + return gap.offset; + } + + if (Array.isArray(gap.offset)) { + if (typeof gap.offset[0] === 'number') { + const tuple = gap.offset as [number, number]; + return tuple[index] ?? DEFAULT_GAP_OFFSET; + } + if (Array.isArray(gap.offset[0])) { + const arrayOfTuples = gap.offset as [number, number][]; + const stepIndex = current ?? 0; + return arrayOfTuples[stepIndex]?.[index] ?? DEFAULT_GAP_OFFSET; + } + } + + return DEFAULT_GAP_OFFSET; + }; useLayoutEffect(() => { updatePos(); @@ -111,7 +133,7 @@ export default function useTarget( height: posInfo.height + gapOffsetY * 2, radius: gapRadius, }; - }, [posInfo, gap]); + }, [posInfo, gap, current]); return [mergedPosInfo, targetElement]; } diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 6f27163..f2d9f4b 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1289,4 +1289,86 @@ describe('Tour', () => { height: 0, }); }); + + it('should support gap.offset with array<[number, number]> and change on next step', async () => { + mockBtnRect({ + x: 100, + y: 100, + width: 230, + height: 180, + }); + + const Demo = () => { + const button1Ref = useRef(null); + const button2Ref = useRef(null); + const button3Ref = useRef(null); + + return ( +
+ + + + button1Ref.current, + }, + { + title: 'step 2', + description: 'description 2', + target: () => button2Ref.current, + }, + { + title: 'step 3', + description: 'description 3', + target: () => button3Ref.current, + }, + ]} + /> +
+ ); + }; + + render(); + await act(() => { + jest.runAllTimers(); + }); + + // gap [10, 15] -> width: 230 + 20 = 250, height: 180 + 30 = 210 + let targetRect = document + .getElementById('rc-tour-mask-test-id') + .querySelectorAll('rect')[1]; + expect(targetRect).toHaveAttribute('width', '250'); + expect(targetRect).toHaveAttribute('height', '210'); + + fireEvent.click(screen.getByRole('button', { name: 'Next' })); + await act(() => { + jest.runAllTimers(); + }); + + // gap [20, 25] -> width: 230 + 40 = 270, height: 180 + 50 = 230 + targetRect = document + .getElementById('rc-tour-mask-test-id') + .querySelectorAll('rect')[1]; + expect(targetRect).toHaveAttribute('width', '270'); + expect(targetRect).toHaveAttribute('height', '230'); + + fireEvent.click(screen.getByRole('button', { name: 'Next' })); + await act(() => { + jest.runAllTimers(); + }); + + // gap [30, 35] -> width: 230 + 60 = 290, height: 180 + 70 = 250 + targetRect = document + .getElementById('rc-tour-mask-test-id') + .querySelectorAll('rect')[1]; + expect(targetRect).toHaveAttribute('width', '290'); + expect(targetRect).toHaveAttribute('height', '250'); + }); });