From a1949202f3ae1d7ad9970a32242af03ff6d42729 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Mon, 14 Jul 2025 16:06:48 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20Tour=20=E7=BB=84=E7=BB=87=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20ref=20=E5=BC=95=E7=94=A8=E5=B9=B6=E4=B8=94=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=20next=20/=20prev=20/=20close=20/=20finish=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Tour.tsx | 58 ++++++++++++++++++----------- src/index.tsx | 2 +- src/interface.ts | 15 ++++++-- tests/index.test.tsx | 88 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/src/Tour.tsx b/src/Tour.tsx index fd96306..bd521a2 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -9,7 +9,7 @@ import useMergedState from '@rc-component/util/lib/hooks/useMergedState'; import { useMemo } from 'react'; import { useClosable } from './hooks/useClosable'; import useTarget from './hooks/useTarget'; -import type { TourProps } from './interface'; +import type { TourProps, TourRef } from './interface'; import Mask from './Mask'; import { getPlacements } from './placements'; import TourStep from './TourStep'; @@ -29,7 +29,8 @@ const defaultScrollIntoViewOptions: ScrollIntoViewOptions = { export type { TourProps }; -const Tour: React.FC = props => { +const Tour = React.forwardRef((props: TourProps, ref) => { + const { prefixCls = 'rc-tour', steps = [], @@ -76,6 +77,35 @@ const Tour: React.FC = props => { : (origin ?? true), }); + // ========================= Change ========================= + const onInternalChange = (nextCurrent: number) => { + setMergedCurrent(nextCurrent); + onChange?.(nextCurrent); + }; + + const handleClose = () => { + setMergedOpen(false); + onClose?.(mergedCurrent); + }; + + const handlePrev = () => { + onInternalChange(mergedCurrent - 1); + }; + const handleNext = () => { + onInternalChange(mergedCurrent + 1); + }; + const handleFinish = () => { + handleClose(); + onFinish?.(); + }; + + React.useImperativeHandle(ref, () => ({ + next: handleNext, + prev: handlePrev, + close: handleClose, + finish: handleFinish, + })); + // Record if already rended in the DOM to avoid `findDOMNode` issue const [hasOpened, setHasOpened] = React.useState(mergedOpen); @@ -144,11 +174,6 @@ const Tour: React.FC = props => { triggerRef.current?.forceAlign(); }, [arrowPointAtCenter, mergedCurrent]); - // ========================= Change ========================= - const onInternalChange = (nextCurrent: number) => { - setMergedCurrent(nextCurrent); - onChange?.(nextCurrent); - }; const mergedBuiltinPlacements = useMemo(() => { if (builtinPlacements) { @@ -165,10 +190,6 @@ const Tour: React.FC = props => { return null; } - const handleClose = () => { - setMergedOpen(false); - onClose?.(mergedCurrent); - }; const getPopupElement = () => ( = props => { prefixCls={prefixCls} total={steps.length} renderPanel={renderPanel} - onPrev={() => { - onInternalChange(mergedCurrent - 1); - }} - onNext={() => { - onInternalChange(mergedCurrent + 1); - }} + onPrev={handlePrev} + onNext={handleNext} onClose={handleClose} current={mergedCurrent} - onFinish={() => { - handleClose(); - onFinish?.(); - }} + onFinish={handleFinish} {...steps[mergedCurrent]} closable={mergedClosable} /> @@ -261,6 +275,6 @@ const Tour: React.FC = props => { ); -}; +}); export default Tour; diff --git a/src/index.tsx b/src/index.tsx index 01e76e9..51205af 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ import Tour from './Tour'; -export type { TourProps, TourStepInfo, TourStepProps } from './interface'; +export type { TourProps, TourStepInfo, TourStepProps, TourRef } from './interface'; export default Tour; diff --git a/src/interface.ts b/src/interface.ts index ec02414..48c61b3 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -14,14 +14,19 @@ export type SemanticName = | 'mask'; -export type HTMLAriaDataAttributes = React.AriaAttributes & { - [key: `data-${string}`]: unknown; -} & Pick, 'role'>; +export type HTMLAriaDataAttributes = React.AriaAttributes & Record<`data-${string}`, unknown> & Pick, 'role'>; + +export interface TourRef { + next: () => void; + prev: () => void; + close: () => void; + finish: () => void; +} export interface TourStepInfo { arrow?: boolean | { pointAtCenter: boolean }; target?: HTMLElement | (() => HTMLElement) | null | (() => null); - title: ReactNode; + title?: ReactNode; description?: ReactNode; placement?: PlacementType; mask?: @@ -36,6 +41,7 @@ export interface TourStepInfo { scrollIntoViewOptions?: boolean | ScrollIntoViewOptions; closeIcon?: ReactNode; closable?: boolean | ({ closeIcon?: ReactNode } & HTMLAriaDataAttributes); + contentRender?: (next: () => void, prev: () => void, close: () => void, finish: () => void) => ReactNode; } export interface TourStepProps extends TourStepInfo { @@ -49,6 +55,7 @@ export interface TourStepProps extends TourStepInfo { onNext?: () => void; classNames?: Partial>; styles?: Partial>; + contentRender?: (next: () => void, prev: () => void, close: () => void, finish: () => void) => ReactNode; } export interface TourProps extends Pick { diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 6f27163..4b9569f 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -3,7 +3,7 @@ import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; import type { ReactNode } from 'react'; import React, { StrictMode, useRef, useState } from 'react'; import { act } from 'react-dom/test-utils'; -import type { TourProps } from '../src/index'; +import type { TourProps, TourRef } from '../src/index'; import Tour from '../src/index'; import { getPlacements, placements } from '../src/placements'; import { getPlacement } from '../src/util'; @@ -1289,4 +1289,90 @@ describe('Tour', () => { height: 0, }); }); + + it('tour ref', () => { + const Demo = () => { + const ref = useRef(null); + React.useEffect(() => { + // 判断 ref 属性是否存在 next / prev / close / finish + if (ref.current) { + expect('next' in ref.current).toBe(true); + expect('prev' in ref.current).toBe(true); + expect('close' in ref.current).toBe(true); + expect('finish' in ref.current).toBe(true); + } + }, []); + return ; + }; + render(); + }); + + it('ref.next / ref.prev / ref.close can be called', () => { + const changeHandleSpy = jest.fn(); + const onCloseSpy = jest.fn(); + let tourRef: TourRef | null = null; + + const Demo = () => { + const ref = useRef(null); + const [open, setOpen] = React.useState(true); + + React.useEffect(() => { + // 保存ref到外部变量,用于测试 + tourRef = ref.current; + }, []); + + const handleClose = (current: number) => { + setOpen(false); + onCloseSpy(current); + }; + + return ( + + ); + }; + + // 渲染组件 + render(); + + // 测试ref是否正确设置 + expect(tourRef).toBeTruthy(); + + if (tourRef) { + // 调用 next 方法 + act(() => { + tourRef!.next(); + }); + expect(changeHandleSpy).toHaveBeenCalledWith(1); + + // 调用 prev 方法 + act(() => { + tourRef!.prev(); + }); + expect(changeHandleSpy).toHaveBeenCalledWith(1); + + // 调用 close 方法 + act(() => { + tourRef!.close(); + }); + expect(document.querySelector('.rc-tour-pannel')).toBeFalsy(); + } + }); }); From 6af685581498898fee399eb772904fc7105a0851 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Mon, 14 Jul 2025 16:15:10 +0800 Subject: [PATCH 2/8] =?UTF-8?q?docs:=20=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index b82c575..f54add5 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,16 @@ We use typescript to create the Type definition. You can view directly in IDE. B | style | `React.CSSProperties` | - | - | | scrollIntoViewOptions | `boolean \| ScrollIntoViewOptions` | `true` | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数,默认跟随 Tour 的 `scrollIntoViewOptions` 属性 | +### TourInstance + +| 属性 | 类型 | 说明 | 版本 | +| --- | --- | --- | --- | +| next | `() => void` | 跳转到下一步 | 2.2.0 | +| prev | `() => void` | 跳转到上一步 | 2.2.0 | +| close | `() => void` | 关闭步骤 | 2.2.0 | +| finish | `() => void` | 结束步骤 | 2.2.0 | + + ## 🤝 Contributing From 4d9b2795f5b3d663d538bd7e406263fb94968786 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Wed, 20 Aug 2025 22:35:08 +0800 Subject: [PATCH 3/8] feat: support array of gap offsets for Tour steps --- docs/demo/gap.md | 4 ++ docs/examples/gap-offset-array.tsx | 59 +++++++++++++++++++++ src/Tour.tsx | 1 + src/hooks/useTarget.ts | 32 ++++++++++-- tests/index.test.tsx | 82 ++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 docs/examples/gap-offset-array.tsx 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..2c848d5 --- /dev/null +++ b/docs/examples/gap-offset-array.tsx @@ -0,0 +1,59 @@ +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], + ] + 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 bd521a2..e575d9b 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -158,6 +158,7 @@ const Tour = React.forwardRef((props: TourProps, ref) => { mergedScrollIntoViewOptions, inlineMode, placeholderRef, + mergedCurrent, ); const mergedPlacement = getPlacement(targetElement, placement, stepPlacement); diff --git a/src/hooks/useTarget.ts b/src/hooks/useTarget.ts index 682a596..8de54b2 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, ): [PosInfo, HTMLElement] { // ========================= Target ========================= // We trade `undefined` as not get target by function yet. @@ -78,8 +81,29 @@ 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)) { + // 如果是 [number, number] 格式 + if (typeof gap.offset[0] === 'number') { + const tuple = gap.offset as [number, number]; + return tuple[index] ?? DEFAULT_GAP_OFFSET; + } + // 如果是 Array<[number, number]> 格式,根据当前 tour step 索引取对应元素 + 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 +135,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 4b9569f..ec42e2f 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1375,4 +1375,86 @@ describe('Tour', () => { expect(document.querySelector('.rc-tour-pannel')).toBeFalsy(); } }); + + 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'); + }); }); From 6490259c64a7cacc4884df2d59b5c7c8d88ec541 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Thu, 21 Aug 2025 16:32:48 +0800 Subject: [PATCH 4/8] fix: remove Tour ref API and related tests --- README.md | 10 ---- docs/examples/gap-offset-array.tsx | 1 + src/Tour.tsx | 14 ++--- src/hooks/useTarget.ts | 2 - src/interface.ts | 12 +---- tests/index.test.tsx | 86 ------------------------------ 6 files changed, 5 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index f54add5..b82c575 100644 --- a/README.md +++ b/README.md @@ -141,16 +141,6 @@ We use typescript to create the Type definition. You can view directly in IDE. B | style | `React.CSSProperties` | - | - | | scrollIntoViewOptions | `boolean \| ScrollIntoViewOptions` | `true` | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数,默认跟随 Tour 的 `scrollIntoViewOptions` 属性 | -### TourInstance - -| 属性 | 类型 | 说明 | 版本 | -| --- | --- | --- | --- | -| next | `() => void` | 跳转到下一步 | 2.2.0 | -| prev | `() => void` | 跳转到上一步 | 2.2.0 | -| close | `() => void` | 关闭步骤 | 2.2.0 | -| finish | `() => void` | 结束步骤 | 2.2.0 | - - ## 🤝 Contributing
diff --git a/docs/examples/gap-offset-array.tsx b/docs/examples/gap-offset-array.tsx index 2c848d5..e64a46e 100644 --- a/docs/examples/gap-offset-array.tsx +++ b/docs/examples/gap-offset-array.tsx @@ -11,6 +11,7 @@ const App = () => { const offset: [number, number][] = [ [10, 10], [20, 20], + [30, 30], ] return (
diff --git a/src/Tour.tsx b/src/Tour.tsx index e575d9b..9070759 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -9,7 +9,7 @@ import useMergedState from '@rc-component/util/lib/hooks/useMergedState'; import { useMemo } from 'react'; import { useClosable } from './hooks/useClosable'; import useTarget from './hooks/useTarget'; -import type { TourProps, TourRef } from './interface'; +import type { TourProps } from './interface'; import Mask from './Mask'; import { getPlacements } from './placements'; import TourStep from './TourStep'; @@ -29,8 +29,7 @@ const defaultScrollIntoViewOptions: ScrollIntoViewOptions = { export type { TourProps }; -const Tour = React.forwardRef((props: TourProps, ref) => { - +const Tour: React.FC = props => { const { prefixCls = 'rc-tour', steps = [], @@ -99,13 +98,6 @@ const Tour = React.forwardRef((props: TourProps, ref) => { onFinish?.(); }; - React.useImperativeHandle(ref, () => ({ - next: handleNext, - prev: handlePrev, - close: handleClose, - finish: handleFinish, - })); - // Record if already rended in the DOM to avoid `findDOMNode` issue const [hasOpened, setHasOpened] = React.useState(mergedOpen); @@ -276,6 +268,6 @@ const Tour = React.forwardRef((props: TourProps, ref) => { ); -}); +}; export default Tour; diff --git a/src/hooks/useTarget.ts b/src/hooks/useTarget.ts index 8de54b2..4d6e372 100644 --- a/src/hooks/useTarget.ts +++ b/src/hooks/useTarget.ts @@ -89,12 +89,10 @@ export default function useTarget( } if (Array.isArray(gap.offset)) { - // 如果是 [number, number] 格式 if (typeof gap.offset[0] === 'number') { const tuple = gap.offset as [number, number]; return tuple[index] ?? DEFAULT_GAP_OFFSET; } - // 如果是 Array<[number, number]> 格式,根据当前 tour step 索引取对应元素 if (Array.isArray(gap.offset[0])) { const arrayOfTuples = gap.offset as [number, number][]; const stepIndex = current ?? 0; diff --git a/src/interface.ts b/src/interface.ts index 48c61b3..b686f5c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -13,20 +13,12 @@ export type SemanticName = | 'description' | 'mask'; - export type HTMLAriaDataAttributes = React.AriaAttributes & Record<`data-${string}`, unknown> & Pick, 'role'>; -export interface TourRef { - next: () => void; - prev: () => void; - close: () => void; - finish: () => void; -} - export interface TourStepInfo { arrow?: boolean | { pointAtCenter: boolean }; target?: HTMLElement | (() => HTMLElement) | null | (() => null); - title?: ReactNode; + title: ReactNode; description?: ReactNode; placement?: PlacementType; mask?: @@ -41,7 +33,6 @@ export interface TourStepInfo { scrollIntoViewOptions?: boolean | ScrollIntoViewOptions; closeIcon?: ReactNode; closable?: boolean | ({ closeIcon?: ReactNode } & HTMLAriaDataAttributes); - contentRender?: (next: () => void, prev: () => void, close: () => void, finish: () => void) => ReactNode; } export interface TourStepProps extends TourStepInfo { @@ -55,7 +46,6 @@ export interface TourStepProps extends TourStepInfo { onNext?: () => void; classNames?: Partial>; styles?: Partial>; - contentRender?: (next: () => void, prev: () => void, close: () => void, finish: () => void) => ReactNode; } export interface TourProps extends Pick { diff --git a/tests/index.test.tsx b/tests/index.test.tsx index ec42e2f..3bad030 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1290,92 +1290,6 @@ describe('Tour', () => { }); }); - it('tour ref', () => { - const Demo = () => { - const ref = useRef(null); - React.useEffect(() => { - // 判断 ref 属性是否存在 next / prev / close / finish - if (ref.current) { - expect('next' in ref.current).toBe(true); - expect('prev' in ref.current).toBe(true); - expect('close' in ref.current).toBe(true); - expect('finish' in ref.current).toBe(true); - } - }, []); - return ; - }; - render(); - }); - - it('ref.next / ref.prev / ref.close can be called', () => { - const changeHandleSpy = jest.fn(); - const onCloseSpy = jest.fn(); - let tourRef: TourRef | null = null; - - const Demo = () => { - const ref = useRef(null); - const [open, setOpen] = React.useState(true); - - React.useEffect(() => { - // 保存ref到外部变量,用于测试 - tourRef = ref.current; - }, []); - - const handleClose = (current: number) => { - setOpen(false); - onCloseSpy(current); - }; - - return ( - - ); - }; - - // 渲染组件 - render(); - - // 测试ref是否正确设置 - expect(tourRef).toBeTruthy(); - - if (tourRef) { - // 调用 next 方法 - act(() => { - tourRef!.next(); - }); - expect(changeHandleSpy).toHaveBeenCalledWith(1); - - // 调用 prev 方法 - act(() => { - tourRef!.prev(); - }); - expect(changeHandleSpy).toHaveBeenCalledWith(1); - - // 调用 close 方法 - act(() => { - tourRef!.close(); - }); - expect(document.querySelector('.rc-tour-pannel')).toBeFalsy(); - } - }); - it('should support gap.offset with array<[number, number]> and change on next step', async () => { mockBtnRect({ x: 100, From ae22a5fa67a578d0e63fbf80adf79ac839835d2d Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Thu, 21 Aug 2025 16:39:01 +0800 Subject: [PATCH 5/8] fix: refactor Tour step navigation handlers --- src/Tour.tsx | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/Tour.tsx b/src/Tour.tsx index 9070759..f0d069e 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -76,28 +76,6 @@ const Tour: React.FC = props => { : (origin ?? true), }); - // ========================= Change ========================= - const onInternalChange = (nextCurrent: number) => { - setMergedCurrent(nextCurrent); - onChange?.(nextCurrent); - }; - - const handleClose = () => { - setMergedOpen(false); - onClose?.(mergedCurrent); - }; - - const handlePrev = () => { - onInternalChange(mergedCurrent - 1); - }; - const handleNext = () => { - onInternalChange(mergedCurrent + 1); - }; - const handleFinish = () => { - handleClose(); - onFinish?.(); - }; - // Record if already rended in the DOM to avoid `findDOMNode` issue const [hasOpened, setHasOpened] = React.useState(mergedOpen); @@ -150,7 +128,7 @@ const Tour: React.FC = props => { mergedScrollIntoViewOptions, inlineMode, placeholderRef, - mergedCurrent, + mergedCurrent ); const mergedPlacement = getPlacement(targetElement, placement, stepPlacement); @@ -167,6 +145,11 @@ const Tour: React.FC = props => { triggerRef.current?.forceAlign(); }, [arrowPointAtCenter, mergedCurrent]); + // ========================= Change ========================= + const onInternalChange = (nextCurrent: number) => { + setMergedCurrent(nextCurrent); + onChange?.(nextCurrent); + }; const mergedBuiltinPlacements = useMemo(() => { if (builtinPlacements) { @@ -183,6 +166,10 @@ const Tour: React.FC = props => { return null; } + const handleClose = () => { + setMergedOpen(false); + onClose?.(mergedCurrent); + }; const getPopupElement = () => ( = props => { prefixCls={prefixCls} total={steps.length} renderPanel={renderPanel} - onPrev={handlePrev} - onNext={handleNext} + onPrev={() => { + onInternalChange(mergedCurrent - 1); + }} + onNext={() => { + onInternalChange(mergedCurrent + 1); + }} onClose={handleClose} current={mergedCurrent} - onFinish={handleFinish} + onFinish={() => { + handleClose(); + onFinish?.(); + }} {...steps[mergedCurrent]} closable={mergedClosable} /> @@ -270,4 +264,4 @@ const Tour: React.FC = props => { ); }; -export default Tour; +export default Tour; \ No newline at end of file From a9bd27e3756b77a8a32daca5c506d7a3196ef404 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Thu, 21 Aug 2025 16:41:18 +0800 Subject: [PATCH 6/8] fix: set default value for current parameter in useTarget --- src/hooks/useTarget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useTarget.ts b/src/hooks/useTarget.ts index 4d6e372..0e90716 100644 --- a/src/hooks/useTarget.ts +++ b/src/hooks/useTarget.ts @@ -29,7 +29,7 @@ export default function useTarget( scrollIntoViewOptions?: boolean | ScrollIntoViewOptions, inlineMode?: boolean, placeholderRef?: React.RefObject, - current?: number, + current: number = 0, ): [PosInfo, HTMLElement] { // ========================= Target ========================= // We trade `undefined` as not get target by function yet. From 7ded05bd078b6533db759e1a4f1131f7938cfed5 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Thu, 21 Aug 2025 16:47:29 +0800 Subject: [PATCH 7/8] fix: remove TourRef export and update related types --- src/index.tsx | 2 +- src/interface.ts | 4 +++- tests/index.test.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 51205af..01e76e9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ import Tour from './Tour'; -export type { TourProps, TourStepInfo, TourStepProps, TourRef } from './interface'; +export type { TourProps, TourStepInfo, TourStepProps } from './interface'; export default Tour; diff --git a/src/interface.ts b/src/interface.ts index b686f5c..141d16a 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -13,7 +13,9 @@ export type SemanticName = | 'description' | 'mask'; -export type HTMLAriaDataAttributes = React.AriaAttributes & Record<`data-${string}`, unknown> & Pick, 'role'>; +export type HTMLAriaDataAttributes = React.AriaAttributes & { + [key: `data-${string}`]: unknown; +} & Pick, 'role'>; export interface TourStepInfo { arrow?: boolean | { pointAtCenter: boolean }; diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 3bad030..f2d9f4b 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -3,7 +3,7 @@ import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; import type { ReactNode } from 'react'; import React, { StrictMode, useRef, useState } from 'react'; import { act } from 'react-dom/test-utils'; -import type { TourProps, TourRef } from '../src/index'; +import type { TourProps } from '../src/index'; import Tour from '../src/index'; import { getPlacements, placements } from '../src/placements'; import { getPlacement } from '../src/util'; From 8ff9c28558f8326db76e673da36355b207e90b84 Mon Sep 17 00:00:00 2001 From: zhengjihao <2714499297@qq.com> Date: Thu, 21 Aug 2025 16:48:52 +0800 Subject: [PATCH 8/8] chore: code format --- src/interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interface.ts b/src/interface.ts index 141d16a..ec02414 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -13,6 +13,7 @@ export type SemanticName = | 'description' | 'mask'; + export type HTMLAriaDataAttributes = React.AriaAttributes & { [key: `data-${string}`]: unknown; } & Pick, 'role'>;