[new component] Next tour (#37867)

* feat: init

* feat: update

* feat: upate

* feat: update

* feat: update

* feat: init

* feat: init

* feat: init

* feat: update

* feat: update

* feat: update

* feat: update rc-tour

* feat: init component

* feat: init component

* chore: update pck

* doc: update doc

* doc: update reviewer

* doc: update reviewer

* doc: update reviewer

* feat: update reviewer

* feat: update reviewer

* feat: update doc

* feat: update deme

* feat: update demo doc

* feat: update demo

* feat: update demo

* feat: update style

* feat: update dom & style

* feat: update dome

* feat: update dome

* docs: update demo

* feat: update doc

* feat: update dome

* feat: add locale

* doc: update locale

* doc: add test

* feat: add test case

* feat: add test case

* feat: update package

* feat: update ts

* feat: update ts

* feat: update snapshots

* feat: update demo

* feat: update demo

* feat: update demo

* feat: edit maxSize

* feat: edit maxSize

* feat: update lint

* feat: update lint

* feat: update style reviewer

* feat: update style

* feat: merge next

* feat: add locale

* feat: reset bundleSize

* feat: change maxSize

* feat: update test coverage

* feat: update test coverage

* feat: add type

* chore: simplify en locale

* feat: update

* feat: update test snap

* docs: demo update

* chore: adjust style

* chore: adjust style

* chore: bump rc-tour

* Update package.json

* feat: update package

* feat: update package

* feat: update cover

* docs: update api

* docs: update overview snap

* feat: update token

* feat: delete repeat ts

* feat: remove finishButtonProps

* chore: update demo

* feat: tour style

* test: fix lint

* chore: code clean

Co-authored-by: lijianan <574980606@qq.com>
Co-authored-by: 二货机器人 <smith3816@gmail.com>
Co-authored-by: MadCcc <1075746765@qq.com>
This commit is contained in:
黑雨 2022-11-02 16:25:28 +08:00 committed by GitHub
parent 8f6f223f2d
commit b0850139f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1421 additions and 4 deletions

View File

@ -59,6 +59,7 @@ Array [
"TimePicker",
"Timeline",
"Tooltip",
"Tour",
"Transfer",
"Tree",
"TreeSelect",

View File

@ -67,7 +67,7 @@ const genBaseStyle: GenerateStyle<DropdownToken> = token => {
// A placeholder out of dropdown visible range to avoid close when user moving
'&::before': {
position: 'absolute',
insetBlock: -dropdownArrowDistance + sizePopupArrow,
insetBlock: -dropdownArrowDistance + sizePopupArrow / 2,
// insetInlineStart: -7, // FIXME: Seems not work for hidden element
zIndex: -9999,
opacity: 0.0001,
@ -449,7 +449,7 @@ export default genComponentStyleHook(
const dropdownToken = mergeToken<DropdownToken>(token, {
menuCls: `${componentCls}-menu`,
rootPrefixCls,
dropdownArrowDistance: sizePopupArrow + marginXXS,
dropdownArrowDistance: sizePopupArrow / 2 + marginXXS,
dropdownArrowOffset,
dropdownPaddingVertical,
dropdownEdgeChildPadding: paddingXXS,

View File

@ -133,6 +133,8 @@ export { default as Timeline } from './timeline';
export type { TimelineItemProps, TimelineProps } from './timeline';
export { default as Tooltip } from './tooltip';
export type { TooltipProps } from './tooltip';
export { default as Tour } from './tour';
export type { TourProps, TourStepProps } from './tour/interface';
export { default as Transfer } from './transfer';
export type { TransferProps } from './transfer';
export { default as Tree } from './tree';

View File

@ -4,6 +4,7 @@ import warning from '../_util/warning';
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
import type { TransferLocale as TransferLocaleForEmpty } from '../empty';
import type { ModalLocale } from '../modal/locale';
import type { TourLocale } from '../tour/interface';
import { changeConfirmLocale } from '../modal/locale';
import type { PaginationLocale } from '../pagination/Pagination';
import type { PopconfirmLocale } from '../popconfirm/PurePanel';
@ -23,6 +24,7 @@ export interface Locale {
Calendar?: Record<string, any>;
Table?: TableLocale;
Modal?: ModalLocale;
Tour?: TourLocale;
Popconfirm?: PopconfirmLocale;
Transfer?: TransferLocale;
Select?: Record<string, any>;

View File

@ -35,6 +35,11 @@ const localeValues: Locale = {
triggerAsc: 'Click to sort ascending',
cancelSort: 'Click to cancel sorting',
},
Tour: {
Next: 'Next',
Previous: 'Previous',
Finish: 'Finish',
},
Modal: {
okText: 'OK',
cancelText: 'Cancel',

View File

@ -40,6 +40,11 @@ const localeValues: Locale = {
cancelText: '取消',
justOkText: '知道了',
},
Tour: {
Next: '下一步',
Previous: '上一步',
Finish: '结束导览',
},
Popconfirm: {
cancelText: '取消',
okText: '确定',

View File

@ -37,6 +37,11 @@ const localeValues: Locale = {
cancelText: '取消',
justOkText: '知道了',
},
Tour: {
Next: '下一步',
Previous: '上一步',
Finish: '結束導覽',
},
Popconfirm: {
okText: '確定',
cancelText: '取消',

View File

@ -37,6 +37,11 @@ const localeValues: Locale = {
cancelText: '取消',
justOkText: '知道了',
},
Tour: {
Next: '下一步',
Previous: '上一步',
Finish: '結束導覽',
},
Popconfirm: {
okText: '確定',
cancelText: '取消',

View File

@ -56,7 +56,7 @@ export default function getArrowStyle<Token extends TokenWithCommonCls<AliasToke
borderRadiusOuter,
limitVerticalRadius,
});
const dropdownArrowDistance = sizePopupArrow + marginXXS;
const dropdownArrowDistance = sizePopupArrow / 2 + marginXXS;
return {
[componentCls]: {

View File

@ -46,6 +46,7 @@ import type { ComponentToken as TooltipComponentToken } from '../tooltip/style';
import type { ComponentToken as TransferComponentToken } from '../transfer/style';
import type { ComponentToken as TypographyComponentToken } from '../typography/style';
import type { ComponentToken as UploadComponentToken } from '../upload/style';
import type { ComponentToken as TourComponentToken } from '../tour/style';
export const PresetColors = [
'blue',
@ -133,6 +134,7 @@ export interface ComponentTokenMap {
Table?: TableComponentToken;
Space?: SpaceComponentToken;
Progress?: ProgressComponentToken;
Tour?: TourComponentToken;
}
export type OverrideToken = {

View File

@ -0,0 +1,162 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/tour/demo/basic.md extend context correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/non-modal.md extend context correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin non-modal Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/placement.md extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>
`;

View File

@ -0,0 +1,162 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/tour/demo/basic.md correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/non-modal.md correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin non-modal Tour
</span>
</button>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-icon-only"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders ./components/tour/demo/placement.md correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Begin Tour
</span>
</button>
`;

View File

@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tour Primary 1`] = `
<button
disabled=""
type="button"
>
Cover
</button>
`;
exports[`Tour basic 1`] = `
<div>
<button
type="button"
>
Show
</button>
<button
disabled=""
type="button"
>
Cover
</button>
<button
disabled=""
type="button"
>
Placement
</button>
</div>
`;
exports[`Tour rtl render component should be rendered correctly in RTL direction 1`] = `null`;
exports[`Tour single 1`] = `
<button
disabled=""
type="button"
>
Cover
</button>
`;
exports[`Tour steps is empty 1`] = `
<button
disabled=""
type="button"
>
Cover
</button>
`;
exports[`Tour steps props stepRender 1`] = `
<button
disabled=""
type="button"
>
Cover
</button>
`;

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('tour');

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('tour');

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Tooltip tour', () => {
imageDemoTest('tour');
});

View File

@ -0,0 +1,257 @@
import React, { useRef, useEffect } from 'react';
import Tour from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, screen } from '../../../tests/utils';
describe('Tour', () => {
mountTest(Tour);
rtlTest(Tour);
it('single', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<any>();
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour
steps={[
{
title: 'cover title',
description: 'cover description.',
target: () => coverBtnRef.current,
},
]}
/>
</>
);
};
const { getByText, container } = render(<App />);
expect(getByText('cover title')).toBeTruthy();
expect(getByText('cover description.')).toBeTruthy();
expect(container.firstChild).toMatchSnapshot();
});
it('steps is empty', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<any>();
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour steps={[]} />
<Tour />
</>
);
};
const { container } = render(<App />);
expect(container.firstChild).toMatchSnapshot();
});
it('steps props stepRender', () => {
const onClickMock = jest.fn();
const stepRenderMock = jest.fn();
const App: React.FC = () => {
const coverBtnRef = useRef<any>();
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour
type="default"
stepRender={stepRenderMock}
steps={[
{
title: 'With Cover',
nextButtonProps: {
onClick: onClickMock,
},
},
{
title: 'With Cover',
nextButtonProps: {
onClick: onClickMock,
},
prevButtonProps: {
onClick: onClickMock,
},
},
{
title: 'With Cover',
prevButtonProps: {
onClick: onClickMock,
},
nextButtonProps: {
onClick: onClickMock,
},
},
]}
/>
</>
);
};
const { container } = render(<App />);
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
fireEvent.click(screen.getByRole('button', { name: 'Finish' }));
expect(onClickMock).toHaveBeenCalledTimes(5);
expect(container.firstChild).toMatchSnapshot();
});
it('button props onClick', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<any>();
const [btnName, steBtnName] = React.useState<string>('defaultBtn');
return (
<>
<span id="btnName">{btnName}</span>
<button disabled ref={coverBtnRef} type="button">
target
</button>
<Tour
steps={[
{
title: '',
description: '',
target: () => coverBtnRef.current,
nextButtonProps: {
onClick: () => steBtnName('nextButton'),
},
},
{
title: '',
target: () => coverBtnRef.current,
prevButtonProps: {
onClick: () => steBtnName('prevButton'),
},
nextButtonProps: {
onClick: () => steBtnName('finishButton'),
},
},
]}
/>
</>
);
};
const { container } = render(<App />);
expect(container.querySelector('#btnName')).toHaveTextContent('defaultBtn');
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(container.querySelector('#btnName')).toHaveTextContent('nextButton');
fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
expect(container.querySelector('#btnName')).toHaveTextContent('prevButton');
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
fireEvent.click(screen.getByRole('button', { name: 'Finish' }));
expect(container.querySelector('#btnName')).toHaveTextContent('finishButton');
});
it('Primary', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<any>();
return (
<>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<Tour
type="primary"
steps={[
{
title: 'primary title',
description: 'primary description.',
target: () => coverBtnRef.current,
},
]}
/>
</>
);
};
const { getByText, container } = render(<App />);
expect(getByText('primary description.')).toBeTruthy();
expect(document.querySelector('.ant-tour')).toHaveClass('ant-tour-primary');
expect(container.firstChild).toMatchSnapshot();
});
it('basic', () => {
const App: React.FC = () => {
const coverBtnRef = useRef<any>(null);
const placementBtnRef = useRef<any>(null);
const [show, setShow] = React.useState<boolean | undefined>();
useEffect(() => {
if (show === false) {
setShow(true);
}
}, [show]);
return (
<>
<div>
<button
type="button"
onClick={() => {
setShow(false);
}}
>
Show
</button>
<button disabled ref={coverBtnRef} type="button">
Cover
</button>
<button disabled ref={placementBtnRef} type="button">
Placement
</button>
</div>
{show && (
<Tour
steps={[
{
title: 'Show in Center',
description: 'Here is the content of Tour.',
target: null,
},
{
title: 'With Cover',
description: 'Here is the content of Tour.',
target: () => coverBtnRef.current,
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
),
},
{
title: 'Adjust Placement',
description: 'Here is the content of Tour which show on the right.',
placement: 'right',
target: () => placementBtnRef.current,
},
]}
/>
)}
</>
);
};
const { getByText, container } = render(<App />);
fireEvent.click(screen.getByRole('button', { name: 'Show' }));
expect(getByText('Show in Center')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(getByText('Here is the content of Tour.')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(getByText('Adjust Placement')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Finish' }));
expect(document.querySelector('.rc-tour')).toBeFalsy();
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,75 @@
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---
## zh-CN
最简单的用法。
## en-US
The most basic usage.
```tsx
import React, { useRef, useState } from 'react';
import { Button, Divider, Space, Tour } from 'antd';
import type { TourProps } from 'antd';
import { EllipsisOutlined } from '@ant-design/icons';
const App: React.FC = () => {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
const [open, setOpen] = useState<boolean>(false);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
),
target: () => ref1.current,
},
{
title: 'Save',
description: 'Save your changes.',
target: () => ref2.current,
},
{
title: 'Other Actions',
description: 'Click to see other actions.',
target: () => ref3.current,
},
];
return (
<>
<Button type="primary" onClick={() => setOpen(true)}>
Begin Tour
</Button>
<Divider />
<Space>
<Button ref={ref1}> Upload</Button>
<Button ref={ref2} type="primary">
Save
</Button>
<Button ref={ref3} icon={<EllipsisOutlined />} />
</Space>
<Tour open={open} onClose={() => setOpen(false)} steps={steps} />
</>
);
};
export default App;
```

View File

@ -0,0 +1,75 @@
---
order: 1
title:
zh-CN: 非模态
en-US: Non-modal
---
## zh-CN
使用 `mask={false}` 可以将引导变为非模态,同时为了强调引导本身,建议与 `type="primary"` 组合使用。
## en-US
Use `mask={false}` to make Tour non-modal. At the meantime it is recommended to use with `type="primary"` to emphasize the guide itself.
```tsx
import React, { useRef, useState } from 'react';
import { Button, Divider, Space, Tour } from 'antd';
import type { TourProps } from 'antd';
import { EllipsisOutlined } from '@ant-design/icons';
const App: React.FC = () => {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
const [open, setOpen] = useState<boolean>(false);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
),
target: () => ref1.current,
},
{
title: 'Save',
description: 'Save your changes.',
target: () => ref2.current,
},
{
title: 'Other Actions',
description: 'Click to see other actions.',
target: () => ref3.current,
},
];
return (
<>
<Button type="primary" onClick={() => setOpen(true)}>
Begin non-modal Tour
</Button>
<Divider />
<Space>
<Button ref={ref1}> Upload</Button>
<Button ref={ref2} type="primary">
Save
</Button>
<Button ref={ref3} icon={<EllipsisOutlined />} />
</Space>
<Tour open={open} onClose={() => setOpen(false)} mask={false} type="primary" steps={steps} />
</>
);
};
export default App;
```

View File

@ -0,0 +1,58 @@
---
order: 2
title:
zh-CN: 位置
en-US: Placement
---
## zh-CN
改变引导相对于目标的位置,共有 12 种位置可供选择。当 `target={null}` 时引导将会展示在正中央。
## en-US
Change the placement of the guide relative to the target, there are 12 placements available. When `target={null}` the guide will show in the center.
```tsx
import React, { useRef, useState } from 'react';
import { Button, Tour } from 'antd';
import type { TourProps } from 'antd';
const App: React.FC = () => {
const ref = useRef(null);
const [open, setOpen] = useState<boolean>(false);
const steps: TourProps['steps'] = [
{
title: 'Center',
description: 'Displayed in the center of screen.',
target: null,
},
{
title: 'Right',
description: 'On the right of target.',
placement: 'right',
target: () => ref.current,
},
{
title: 'Top',
description: 'On the top of target.',
placement: 'top',
target: () => ref.current,
},
];
return (
<>
<Button type="primary" onClick={() => setOpen(true)} ref={ref}>
Begin Tour
</Button>
<Tour open={open} onClose={() => setOpen(false)} steps={steps} />
</>
);
};
export default App;
```

View File

@ -0,0 +1,41 @@
---
category: Components
subtitle: Walk Through
type: Data Entry
title: Tour
cover: https://gw.alipayobjects.com/zos/bmw-prod/cc3fcbfa-bf5b-4c8c-8a3d-c3f8388c75e8.svg
---
A popup component for guiding users through a product. Available since `5.0.0`.
## When To Use
Use when you want to guide users through a product.
## API
### Tour
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| arrow | Whether to show the arrow, including the configuration whether to point to the center of the elemen | `boolean`\|`{ pointAtCenter: boolean}` | `true` | |
| placement | Position of the guide card relative to the target element | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | `bottom` | |
| onClose | Callback function on shutdown | `Function` | - | |
| mask | Whether to enable masking | `boolean` | `true` | |
| type | Type, affects the background color and text color | `default` `primary` | `default` | |
### TourStep
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| target | Get the element the guide card points to. Empty makes it show in center of screen | `() => HTMLElement` `HTMLElement` | - | |
| arrow | Whether to show the arrow, including the configuration whether to point to the center of the element | `boolean` `{ pointAtCenter: boolean}` | `true` | |
| cover | Displayed pictures or videos | `ReactNode` | - | |
| title | title | `ReactNode` | - | |
| description | description | `ReactNode` | - | |
| placement | Position of the guide card relative to the target element | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | `bottom` | |
| onClose | Callback function on shutdown | `Function` | - | |
| mask | Whether to enable masking, the default follows the `mask` property of Tour | `boolean` | `true` | |
| type | Type, affects the background color and text color | `default` `primary` | `default` | |
| nextButtonProps | Properties of the Next button | `{ children: ReactNode; onClick: Function }` | - | |
| prevButtonProps | Properties of the previous button | `{ children: ReactNode; onClick: Function }` | - | |

53
components/tour/index.tsx Normal file
View File

@ -0,0 +1,53 @@
import React, { useContext } from 'react';
import RCTour from '@rc-component/tour';
import classNames from 'classnames';
import panelRender from './panelRender';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import useStyle from './style';
import type { TourProps, TourStepProps } from './interface';
const Tour: React.ForwardRefRenderFunction<HTMLDivElement, TourProps> = props => {
const {
prefixCls: customizePrefixCls,
steps,
current,
type,
rootClassName,
...restProps
} = props;
const { getPrefixCls, direction } = useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('tour', customizePrefixCls);
const [wrapSSR, hashId] = useStyle(prefixCls);
const customClassName = classNames(
{
[`${prefixCls}-rtl`]: direction === 'rtl',
},
{
[`${prefixCls}-primary`]: type === 'primary',
},
hashId,
rootClassName,
);
const mergedRenderPanel = (stepProps: TourStepProps, stepCurrent: number) =>
panelRender(stepProps, stepCurrent, type);
return wrapSSR(
<RCTour
{...restProps}
rootClassName={customClassName}
prefixCls={prefixCls}
steps={steps}
current={current}
renderPanel={mergedRenderPanel}
/>,
);
};
if (process.env.NODE_ENV !== 'production') {
Tour.displayName = 'Tour';
}
export default Tour;

View File

@ -0,0 +1,42 @@
---
category: Components
subtitle: 漫游式引导
type: 数据展示
title: Tour
cover: https://gw.alipayobjects.com/zos/bmw-prod/cc3fcbfa-bf5b-4c8c-8a3d-c3f8388c75e8.svg
---
用于分步引导用户了解产品功能的气泡组件。自 `5.0.0` 版本开始提供该组件。
## 何时使用
常用于引导用户了解产品功能。
## API
### Tour
| 属性 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| arrow | 是否显示箭头,包含是否指向元素中心的配置 | `boolean` \| `{ pointAtCenter: boolean}` | `true` | |
| placement | 引导卡片相对于目标元素的位置 | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | `bottom` | |
| onClose | 关闭引导时的回调函数 | `Function` | - | |
| onFinish | 引导完成时的回调 | `Function` | - | |
| mask | 是否启用蒙层 | `boolean` | `true` | |
| type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` | |
### TourStep 引导步骤卡片
| 属性 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| target | 获取引导卡片指向的元素,为空时居中于屏幕 | `() => HTMLElement` \| `HTMLElement` | - | |
| arrow | 是否显示箭头,包含是否指向元素中心的配置 | `boolean` \| `{ pointAtCenter: boolean}` | `true` | |
| cover | 展示的图片或者视频 | `ReactNode` | - | |
| title | 标题 | `ReactNode` | - | |
| description | 主要描述部分 | `ReactNode` | - | |
| placement | 引导卡片相对于目标元素的位置 | `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | | |
| onClose | 关闭引导时的回调函数 | `Function` | - | |
| mask | 是否启用蒙层,默认跟随 Tour 的 `mask` 属性 | `boolean` | `true` | |
| type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` | |
| nextButtonProps | 下一步按钮的属性 | `{ children: ReactNode; onClick: Function }` | - | |
| prevButtonProps | 上一步按钮的属性 | `{ children: ReactNode; onClick: Function }` | - | |

View File

@ -0,0 +1,28 @@
import type { ReactNode } from 'react';
import type {
TourProps as RCTourProps,
TourStepProps as RCTourStepProps,
} from '@rc-component/tour';
export type TourProps = Omit<RCTourProps, 'renderPanel'> & {
steps?: TourStepProps[];
className?: string;
prefixCls?: string;
current?: number;
stepRender?: (current: number, total: number) => ReactNode;
type?: 'default' | 'primary'; // default 类型,影响底色与文字颜色
};
export interface TourStepProps extends RCTourStepProps {
cover?: ReactNode; // 展示的图片或者视频
nextButtonProps?: { children?: ReactNode; onClick?: () => void };
prevButtonProps?: { children?: ReactNode; onClick?: () => void };
stepRender?: (current: number, total: number) => ReactNode;
type?: 'default' | 'primary'; // default 类型,影响底色与文字颜色
}
export interface TourLocale {
Next: string;
Previous: string;
Finish: string;
}

View File

@ -0,0 +1,128 @@
import React from 'react';
import type { ReactNode } from 'react';
import classNames from 'classnames';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import type { TourStepProps } from './interface';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import Button from '../button';
import type { ButtonProps } from '../button';
import defaultLocale from '../locale/en_US';
const panelRender: (
step: TourStepProps,
current: number,
type: TourStepProps['type'],
) => ReactNode = (props: TourStepProps, current: number, type) => {
const {
prefixCls,
total = 1,
title,
onClose,
onPrev,
onNext,
onFinish,
cover,
description,
nextButtonProps,
prevButtonProps,
stepRender,
} = props;
const isLastStep = current === total - 1;
const prevBtnClick = () => {
onPrev?.();
if (typeof prevButtonProps?.onClick === 'function') {
prevButtonProps?.onClick();
}
};
const nextBtnClick = () => {
onNext?.();
if (isLastStep) {
onFinish?.();
}
if (typeof nextButtonProps?.onClick === 'function') {
nextButtonProps?.onClick();
}
};
let headerNode: ReactNode;
if (title) {
headerNode = (
<div className={`${prefixCls}-header`}>
<div className={`${prefixCls}-title`}>{title}</div>
</div>
);
}
let descriptionNode: ReactNode;
if (description) {
descriptionNode = <div className={`${prefixCls}-description`}>{description}</div>;
}
let coverNode: ReactNode;
if (cover) {
coverNode = <div className={`${prefixCls}-cover`}>{cover}</div>;
}
const mergedSlickNode =
(typeof stepRender === 'function' && stepRender(current, total)) ||
[...Array.from({ length: total }).keys()].map((stepItem, index) => (
<span
key={stepItem}
className={classNames(
index === current && `${prefixCls}-slider-active`,
`${prefixCls}-slider`,
)}
/>
));
const slickNode: ReactNode = total > 1 ? mergedSlickNode : null;
const mainBtnType = type === 'primary' ? 'default' : 'primary';
const secondaryBtnProps: ButtonProps = {
type: 'default',
ghost: type === 'primary',
};
return (
<LocaleReceiver componentName="Tour" defaultLocale={defaultLocale.Tour}>
{contextLocale => (
<>
<CloseOutlined className={`${prefixCls}-close`} onClick={onClose} />
{coverNode}
{headerNode}
{descriptionNode}
<div className={`${prefixCls}-footer`}>
<div className={`${prefixCls}-sliders`}>{slickNode}</div>
<div className={`${prefixCls}-buttons`}>
{current !== 0 ? (
<Button
{...secondaryBtnProps}
{...prevButtonProps}
onClick={prevBtnClick}
size="small"
className={`${prefixCls}-prev-btn`}
>
{prevButtonProps?.children ?? contextLocale.Previous}
</Button>
) : null}
<Button
type={mainBtnType}
{...nextButtonProps}
onClick={nextBtnClick}
size="small"
className={`${prefixCls}-next-btn`}
>
{nextButtonProps?.children ??
(isLastStep ? contextLocale.Finish : contextLocale.Next)}
</Button>
</div>
</div>
</>
)}
</LocaleReceiver>
);
};
export default panelRender;

View File

@ -0,0 +1,235 @@
import { TinyColor } from '@ctrl/tinycolor';
import type { FullToken, GenerateStyle } from '../../theme';
import { genComponentStyleHook, mergeToken } from '../../theme';
import { resetComponent } from '../../style';
import getArrowStyle, { MAX_VERTICAL_CONTENT_RADIUS } from '../../style/placementArrow';
export interface ComponentToken {}
interface TourToken extends FullToken<'Tour'> {
tourZIndexPopup: number;
sliderWidth: number;
sliderHeight: number;
tourBorderRadius: number;
tourCloseSize: number;
}
// =============================== Base ===============================
const genBaseStyle: GenerateStyle<TourToken> = token => {
const {
componentCls,
lineHeight,
padding,
paddingXS,
borderRadius,
borderRadiusXS,
colorPrimary,
colorText,
colorFill,
sliderHeight,
sliderWidth,
boxShadow,
tourZIndexPopup,
fontSize,
colorBgContainer,
fontWeightStrong,
marginXS,
colorTextLightSolid,
tourBorderRadius,
colorWhite,
colorBgTextHover,
tourCloseSize,
} = token;
return [
{
[componentCls]: {
...resetComponent(token),
color: colorText,
position: 'absolute',
zIndex: tourZIndexPopup,
display: 'block',
visibility: 'visible',
fontSize,
lineHeight,
width: 520,
'--antd-arrow-background-color': colorBgContainer,
[`&${componentCls}-hidden`]: {
display: 'none',
},
// ============================= panel content ===========================
[`${componentCls}-content`]: {
position: 'relative',
},
[`${componentCls}-inner`]: {
textAlign: 'start',
textDecoration: 'none',
borderRadius: tourBorderRadius,
boxShadow,
position: 'relative',
backgroundColor: colorBgContainer,
border: 'none',
backgroundClip: 'padding-box',
[`${componentCls}-close`]: {
position: 'absolute',
top: padding,
insetInlineEnd: padding,
color: token.colorIcon,
outline: 'none',
width: tourCloseSize,
height: tourCloseSize,
borderRadius: token.borderRadiusSM,
transition: `background-color ${token.motionDurationFast}, color ${token.motionDurationFast}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
color: token.colorIconHover,
backgroundColor: token.wireframe ? 'transparent' : token.colorFillContent,
},
},
[`${componentCls}-cover`]: {
textAlign: 'center',
padding: `${padding + tourCloseSize + paddingXS}px ${padding}px 0`,
img: {
width: '100%',
},
},
[`${componentCls}-header`]: {
padding: `${padding}px ${padding}px ${paddingXS}px`,
[`${componentCls}-title`]: {
lineHeight,
fontSize,
fontWeight: fontWeightStrong,
},
},
[`${componentCls}-description`]: {
padding: `0 ${padding}px`,
lineHeight,
wordWrap: 'break-word',
},
[`${componentCls}-footer`]: {
padding: `${paddingXS}px ${padding}px ${padding}px`,
textAlign: 'end',
borderRadius: `0 0 ${borderRadiusXS}px ${borderRadiusXS}px`,
display: 'flex',
justifyContent: 'space-between',
[`${componentCls}-sliders`]: {
display: 'inline-block',
[`${componentCls}-slider`]: {
width: `${sliderWidth}px`,
height: `${sliderHeight}px`,
display: 'inline-block',
borderRadius: '50%',
background: colorFill,
marginInlineEnd: sliderHeight,
'&-active': {
background: colorPrimary,
},
},
},
[`${componentCls}-buttons button`]: {
marginInlineStart: marginXS,
},
},
},
// ============================= primary type ===========================
[`&${componentCls}-primary`]: {
'--antd-arrow-background-color': colorPrimary,
[`${componentCls}-inner`]: {
color: colorTextLightSolid,
textAlign: 'start',
textDecoration: 'none',
backgroundColor: colorPrimary,
borderRadius,
boxShadow,
[`${componentCls}-close`]: {
color: colorTextLightSolid,
},
[`${componentCls}-sliders`]: {
[`${componentCls}-slider`]: {
background: new TinyColor(colorTextLightSolid).setAlpha(0.15).toRgbString(),
'&-active': {
background: colorTextLightSolid,
},
},
},
[`${componentCls}-prev-btn`]: {
color: colorTextLightSolid,
borderColor: new TinyColor(colorTextLightSolid).setAlpha(0.15).toRgbString(),
'&:hover': {
backgroundColor: new TinyColor(colorTextLightSolid).setAlpha(0.15).toRgbString(),
borderColor: 'transparent',
},
},
[`${componentCls}-next-btn`]: {
color: colorPrimary,
borderColor: 'transparent',
background: colorWhite,
'&:hover': {
background: new TinyColor(colorBgTextHover).onBackground(colorWhite).toRgbString(),
},
},
},
},
},
// Limit left and right placement radius
[[
`&-placement-left`,
`&-placement-leftTop`,
`&-placement-leftBottom`,
`&-placement-right`,
`&-placement-rightTop`,
`&-placement-rightBottom`,
].join(',')]: {
[`${componentCls}-inner`]: {
borderRadius:
tourBorderRadius > MAX_VERTICAL_CONTENT_RADIUS
? MAX_VERTICAL_CONTENT_RADIUS
: tourBorderRadius,
},
},
},
// Arrow Style
getArrowStyle<TourToken>(token, {
colorBg: 'var(--antd-arrow-background-color)',
showArrowCls: '',
contentRadius: tourBorderRadius,
limitVerticalRadius: true,
}),
];
};
// ============================== Export ==============================
export default genComponentStyleHook('Tour', token => {
const { borderRadiusLG, fontSize, lineHeight } = token;
const TourToken = mergeToken<TourToken>(token, {
tourZIndexPopup: token.zIndexPopupBase + 70,
sliderWidth: 6,
sliderHeight: 6,
tourBorderRadius: borderRadiusLG,
tourCloseSize: fontSize * lineHeight,
});
return [genBaseStyle(TourToken)];
});

View File

@ -114,6 +114,7 @@
"@ant-design/react-slick": "~0.29.1",
"@babel/runtime": "^7.18.3",
"@ctrl/tinycolor": "^3.4.0",
"@rc-component/tour": "~1.0.0-9",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"dayjs": "^1.11.1",
@ -314,7 +315,7 @@
"bundlesize": [
{
"path": "./dist/antd.min.js",
"maxSize": "375 kB"
"maxSize": "377 kB"
}
],
"tnpm": {

View File

@ -59,6 +59,7 @@ Array [
"TimePicker",
"Timeline",
"Tooltip",
"Tour",
"Transfer",
"Tree",
"TreeSelect",