Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<!-- prettier-ignore -->
| 属性 | 类型 | 说明 | 版本 |
| --- | --- | --- | --- |
| next | `() => void` | 跳转到下一步 | 2.2.0 |
| prev | `() => void` | 跳转到上一步 | 2.2.0 |
| close | `() => void` | 关闭步骤 | 2.2.0 |
| finish | `() => void` | 结束步骤 | 2.2.0 |


## 🤝 Contributing

<a href="https://openomy.app/github/react-component/tour" target="_blank" style="display: block; width: 100%;" align="center">
Expand Down
4 changes: 4 additions & 0 deletions docs/demo/gap.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ nav:
## Radius

<code src="../examples/gap-radius.tsx"></code>

## Offset

<code src="../examples/gap-offset-array.tsx"></code>
59 changes: 59 additions & 0 deletions docs/examples/gap-offset-array.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useRef, useState } from 'react';
import Tour from '../../src/index';
import './basic.less';

const App = () => {
const button1Ref = useRef<HTMLButtonElement>(null);
const button2Ref = useRef<HTMLButtonElement>(null);
const button3Ref = useRef<HTMLButtonElement>(null);

const [open, setOpen] = useState(false);
const offset: [number, number][] = [
[10, 10],
[20, 20],
]
return (
<div style={{ margin: 20 }}>
<div style={{ display: 'flex', gap: 10 }}>
<button onClick={() => setOpen(true)}>Open</button>
<button ref={button1Ref}>button 1</button>
<button ref={button2Ref}>button 2</button>
<button ref={button3Ref}>button 3</button>
</div>


<div style={{ height: 200 }} />
<Tour
open={open}
gap={{
offset
}}
steps={[
{
title: '创建1',
description: '创建一条数据1',
target: () => 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);
}}
/>
</div>
);
};

export default App;
59 changes: 37 additions & 22 deletions src/Tour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,7 +29,8 @@ const defaultScrollIntoViewOptions: ScrollIntoViewOptions = {

export type { TourProps };

const Tour: React.FC<TourProps> = props => {
const Tour = React.forwardRef<TourRef, TourProps>((props: TourProps, ref) => {

const {
prefixCls = 'rc-tour',
steps = [],
Expand Down Expand Up @@ -76,6 +77,35 @@ const Tour: React.FC<TourProps> = 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);

Expand Down Expand Up @@ -128,6 +158,7 @@ const Tour: React.FC<TourProps> = props => {
mergedScrollIntoViewOptions,
inlineMode,
placeholderRef,
mergedCurrent,
);
const mergedPlacement = getPlacement(targetElement, placement, stepPlacement);

Expand All @@ -144,11 +175,6 @@ const Tour: React.FC<TourProps> = props => {
triggerRef.current?.forceAlign();
}, [arrowPointAtCenter, mergedCurrent]);

// ========================= Change =========================
const onInternalChange = (nextCurrent: number) => {
setMergedCurrent(nextCurrent);
onChange?.(nextCurrent);
};

const mergedBuiltinPlacements = useMemo(() => {
if (builtinPlacements) {
Expand All @@ -165,10 +191,6 @@ const Tour: React.FC<TourProps> = props => {
return null;
}

const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent);
};

const getPopupElement = () => (
<TourStep
Expand All @@ -179,18 +201,11 @@ const Tour: React.FC<TourProps> = 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}
/>
Expand Down Expand Up @@ -261,6 +276,6 @@ const Tour: React.FC<TourProps> = props => {
</Trigger>
</>
);
};
});

export default Tour;
32 changes: 28 additions & 4 deletions src/hooks/useTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
}
Expand All @@ -27,6 +29,7 @@ export default function useTarget(
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions,
inlineMode?: boolean,
placeholderRef?: React.RefObject<HTMLDivElement>,
current?: number,
): [PosInfo, HTMLElement] {
// ========================= Target =========================
// We trade `undefined` as not get target by function yet.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -111,7 +135,7 @@ export default function useTarget(
height: posInfo.height + gapOffsetY * 2,
radius: gapRadius,
};
}, [posInfo, gap]);
}, [posInfo, gap, current]);

return [mergedPosInfo, targetElement];
}
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
15 changes: 11 additions & 4 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@ export type SemanticName =
| 'mask';


export type HTMLAriaDataAttributes = React.AriaAttributes & {
[key: `data-${string}`]: unknown;
} & Pick<React.HTMLAttributes<HTMLDivElement>, 'role'>;
export type HTMLAriaDataAttributes = React.AriaAttributes & Record<`data-${string}`, unknown> & Pick<React.HTMLAttributes<HTMLDivElement>, '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?:
Expand All @@ -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 {
Expand All @@ -49,6 +55,7 @@ export interface TourStepProps extends TourStepInfo {
onNext?: () => void;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
contentRender?: (next: () => void, prev: () => void, close: () => void, finish: () => void) => ReactNode;
}

export interface TourProps extends Pick<TriggerProps, 'onPopupAlign'> {
Expand Down
Loading