Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
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>
60 changes: 60 additions & 0 deletions docs/examples/gap-offset-array.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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],
[30, 30],
]
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;
45 changes: 26 additions & 19 deletions src/Tour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ 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?.();
};

// Record if already rended in the DOM to avoid `findDOMNode` issue
const [hasOpened, setHasOpened] = React.useState(mergedOpen);

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

Expand All @@ -144,11 +167,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 +183,6 @@ const Tour: React.FC<TourProps> = props => {
return null;
}

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

const getPopupElement = () => (
<TourStep
Expand All @@ -179,18 +193,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
30 changes: 26 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,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();
Expand Down Expand Up @@ -111,7 +133,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;
5 changes: 1 addition & 4 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export type SemanticName =
| 'description'
| '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 TourStepInfo {
arrow?: boolean | { pointAtCenter: boolean };
Expand Down
84 changes: 83 additions & 1 deletion tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<HTMLButtonElement>(null);
const button2Ref = useRef<HTMLButtonElement>(null);
const button3Ref = useRef<HTMLButtonElement>(null);

return (
<div style={{ margin: 20 }}>
<button ref={button1Ref}>button 1</button>
<button ref={button2Ref}>button 2</button>
<button ref={button3Ref}>button 3</button>
<Tour
open
gap={{
offset: [[10, 15], [20, 25], [30, 35]] as [number, number][],
}}
steps={[
{
title: 'step 1',
description: 'description 1',
target: () => button1Ref.current,
},
{
title: 'step 2',
description: 'description 2',
target: () => button2Ref.current,
},
{
title: 'step 3',
description: 'description 3',
target: () => button3Ref.current,
},
]}
/>
</div>
);
};

render(<Demo />);
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');
});
});