feat: Tour add actionsRender prop (#53067)

* feat: Tour add buttonsRender prop

* chore: Revert unnecessary format modify

* fix(test): Update snapshots
This commit is contained in:
诸岳 2025-04-29 16:02:50 +08:00 committed by GitHub
parent b3af097e8c
commit 53965b88cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 373 additions and 31 deletions

View File

@ -1,5 +1,178 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/tour/demo/actions-render.tsx extend context correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
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 ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-color-default ant-btn-variant-outlined"
type="button"
>
<span>
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-icon-only"
type="button"
>
<span
class="ant-btn-icon"
>
<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>
</span>
</button>
</div>
</div>,
<div
class="ant-tour ant-tour-placement-bottom"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box; z-index: 1001;"
>
<div
class="ant-tour-arrow"
style="position: absolute; top: 0px; left: 0px;"
/>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<button
aria-label="Close"
class="ant-tour-close"
type="button"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</button>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Upload File
</div>
</div>
<div
class="ant-tour-description"
>
Put your files here.
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-indicators"
>
<span
class="ant-tour-indicator-active ant-tour-indicator"
/>
<span
class="ant-tour-indicator"
/>
<span
class="ant-tour-indicator"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-sm"
type="button"
>
<span>
Skip
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid ant-btn-sm ant-tour-next-btn"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders components/tour/demo/actions-render.tsx extend context correctly 2`] = `[]`;
exports[`renders components/tour/demo/basic.tsx extend context correctly 1`] = `
Array [
<button
@ -25,7 +198,7 @@ Array [
type="button"
>
<span>
Upload
Upload
</span>
</button>
</div>
@ -726,7 +899,7 @@ Array [
type="button"
>
<span>
Upload
Upload
</span>
</button>
</div>
@ -899,7 +1072,7 @@ Array [
type="button"
>
<span>
Upload
Upload
</span>
</button>
</div>

View File

@ -25,7 +25,84 @@ Array [
type="button"
>
<span>
Upload
Upload
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Save
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-icon-only"
type="button"
>
<span
class="ant-btn-icon"
>
<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>
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/tour/demo/actions-render.tsx correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
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 ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-btn-color-default ant-btn-variant-outlined"
type="button"
>
<span>
Upload
</span>
</button>
</div>
@ -385,7 +462,7 @@ Array [
type="button"
>
<span>
Upload
Upload
</span>
</button>
</div>
@ -462,7 +539,7 @@ Array [
type="button"
>
<span>
Upload
Upload
</span>
</button>
</div>

View File

@ -0,0 +1,7 @@
## zh-CN
自定义操作按钮。
## en-US
Custom action.

View File

@ -0,0 +1,68 @@
import React, { useRef, useState } from 'react';
import { EllipsisOutlined } from '@ant-design/icons';
import type { GetRef, TourProps } from 'antd';
import { Button, Divider, Space, Tour } from 'antd';
const App: React.FC = () => {
const ref1 = useRef<GetRef<typeof Button>>(null);
const ref2 = useRef<GetRef<typeof Button>>(null);
const ref3 = useRef<GetRef<typeof Button>>(null);
const [open, setOpen] = useState<boolean>(false);
const steps: TourProps['steps'] = [
{
title: 'Upload File',
description: 'Put your files here.',
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}
actionsRender={(originNode, { current, total }) => (
<>
{current !== total - 1 && (
<Button
size="small"
onClick={() => {
setOpen(false);
}}
>
Skip
</Button>
)}
{originNode}
</>
)}
/>
</>
);
};
export default App;

View File

@ -40,7 +40,7 @@ const App: React.FC = () => {
</Button>
<Divider />
<Space>
<Button ref={ref1}> Upload</Button>
<Button ref={ref1}>Upload</Button>
<Button ref={ref2} type="primary">
Save
</Button>

View File

@ -50,7 +50,7 @@ const App: React.FC = () => {
<Divider />
<Space>
<Button ref={ref1}> Upload</Button>
<Button ref={ref1}>Upload</Button>
<Button ref={ref2} type="primary">
Save
</Button>

View File

@ -43,7 +43,7 @@ const App: React.FC = () => {
<Divider />
<Space>
<Button ref={ref1}> Upload</Button>
<Button ref={ref1}>Upload</Button>
<Button ref={ref2} type="primary">
Save
</Button>

View File

@ -22,6 +22,7 @@ Use when you want to guide users through a product.
<code src="./demo/placement.tsx">Placement</code>
<code src="./demo/mask.tsx">Custom mask style</code>
<code src="./demo/indicator.tsx">Custom indicator</code>
<code src="./demo/actions-render.tsx" version="5.25.0">Custom action</code>
<code src="./demo/gap.tsx">Custom highlighted area style</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
@ -46,6 +47,7 @@ Common props ref[Common props](/docs/react/common-props)
| current | What is the current step | `number` | - | |
| scrollIntoViewOptions | support pass custom scrollIntoView options | `boolean \| ScrollIntoViewOptions` | `true` | 5.2.0 |
| indicatorsRender | custom indicator | `(current: number, total: number) => ReactNode` | - | 5.2.0 |
| actionsRender | custom action | `(originNode: ReactNode, info: { current: number, total: number }) => ReactNode` | - | 5.25.0 |
| zIndex | Tour's zIndex | number | 1001 | 5.3.0 |
| getPopupContainer | Set the rendering node of Tour floating layer | `(node: HTMLElement) => HTMLElement` | body | 5.12.0 |

View File

@ -22,6 +22,7 @@ const Tour: React.FC<TourProps> & { _InternalPanelDoNotUseOrYouWillBeFired: type
type,
rootClassName,
indicatorsRender,
actionsRender,
steps,
closeIcon,
...restProps
@ -66,6 +67,7 @@ const Tour: React.FC<TourProps> & { _InternalPanelDoNotUseOrYouWillBeFired: type
stepProps={stepProps}
current={stepCurrent}
indicatorsRender={indicatorsRender}
actionsRender={actionsRender}
/>
);

View File

@ -23,6 +23,7 @@ tag: 5.0.0
<code src="./demo/placement.tsx">位置</code>
<code src="./demo/mask.tsx">自定义遮罩样式</code>
<code src="./demo/indicator.tsx">自定义指示器</code>
<code src="./demo/actions-render.tsx" version="5.25.0">自定义操作按钮</code>
<code src="./demo/gap.tsx">自定义高亮区域的样式</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
@ -48,6 +49,7 @@ tag: 5.0.0
| current | 当前处于哪一步 | `number` | - | |
| scrollIntoViewOptions | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数 | `boolean \| ScrollIntoViewOptions` | `true` | 5.2.0 |
| indicatorsRender | 自定义指示器 | `(current: number, total: number) => ReactNode` | - | 5.2.0 |
| actionsRender | 自定义操作按钮 | `(originNode: ReactNode, info: { current: number, total: number }) => ReactNode` | - | 5.25.0 |
| zIndex | Tour 的层级 | number | 1001 | 5.3.0 |
| getPopupContainer | 设置 Tour 浮层的渲染节点,默认是 body | `(node: HTMLElement) => HTMLElement` | body | 5.12.0 |

View File

@ -9,6 +9,7 @@ export interface TourProps extends Omit<RCTourProps, 'renderPanel'> {
prefixCls?: string;
current?: number;
indicatorsRender?: (current: number, total: number) => ReactNode;
actionsRender?: TourStepProps['actionsRender'];
type?: 'default' | 'primary'; // default type, affects the background color and text color
}
@ -27,6 +28,7 @@ export interface TourStepProps extends RCTourStepProps {
style?: React.CSSProperties;
};
indicatorsRender?: (current: number, total: number) => ReactNode;
actionsRender?: (originNode: ReactNode, info: { current: number; total: number }) => ReactNode;
type?: 'default' | 'primary'; // default type, affects the background color and text color
}

View File

@ -21,12 +21,13 @@ interface TourPanelProps {
current: number;
type: TourStepProps['type'];
indicatorsRender?: TourStepProps['indicatorsRender'];
actionsRender?: TourStepProps['actionsRender'];
}
// Due to the independent design of Panel, it will be too coupled to put in rc-tour,
// so a set of Panel logic is implemented separately in antd.
const TourPanel: React.FC<TourPanelProps> = (props) => {
const { stepProps, current, type, indicatorsRender } = props;
const { stepProps, current, type, indicatorsRender, actionsRender } = props;
const {
prefixCls,
total = 1,
@ -115,6 +116,32 @@ const TourPanel: React.FC<TourPanelProps> = (props) => {
ghost: mergedType === 'primary',
};
const defaultActionsNode = (
<>
{current !== 0 ? (
<Button
size="small"
{...secondaryBtnProps}
{...prevButtonProps}
onClick={prevBtnClick}
className={classNames(`${prefixCls}-prev-btn`, prevButtonProps?.className)}
>
{prevButtonProps?.children ?? contextLocaleTour?.Previous}
</Button>
) : null}
<Button
size="small"
type={mainBtnType}
{...nextButtonProps}
onClick={nextBtnClick}
className={classNames(`${prefixCls}-next-btn`, nextButtonProps?.className)}
>
{nextButtonProps?.children ??
(isLastStep ? contextLocaleTour?.Finish : contextLocaleTour?.Next)}
</Button>
</>
);
return (
<div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-inner`}>
@ -125,27 +152,9 @@ const TourPanel: React.FC<TourPanelProps> = (props) => {
<div className={`${prefixCls}-footer`}>
{total > 1 && <div className={`${prefixCls}-indicators`}>{mergedIndicatorNode}</div>}
<div className={`${prefixCls}-buttons`}>
{current !== 0 ? (
<Button
{...secondaryBtnProps}
{...prevButtonProps}
onClick={prevBtnClick}
size="small"
className={classNames(`${prefixCls}-prev-btn`, prevButtonProps?.className)}
>
{prevButtonProps?.children ?? contextLocaleTour?.Previous}
</Button>
) : null}
<Button
type={mainBtnType}
{...nextButtonProps}
onClick={nextBtnClick}
size="small"
className={classNames(`${prefixCls}-next-btn`, nextButtonProps?.className)}
>
{nextButtonProps?.children ??
(isLastStep ? contextLocaleTour?.Finish : contextLocaleTour?.Next)}
</Button>
{actionsRender
? actionsRender(defaultActionsNode, { current, total })
: defaultActionsNode}
</div>
</div>
</div>