feat(steps): Support better customization with semantic classNames/styles as function (#54956)

* feat(steps): Support better customization with semantic classNames/styles as function

* test: update demo snapshot

---------

Co-authored-by: 二货机器人 <smith3816@gmail.com>
This commit is contained in:
遇见同学
2025-09-24 22:55:56 +08:00
committed by GitHub
parent 37d7afa1e5
commit 61fa525619
8 changed files with 698 additions and 15 deletions

View File

@@ -6282,6 +6282,281 @@ Array [
exports[`renders components/steps/demo/steps-in-steps.tsx extend context correctly 2`] = `[]`;
exports[`renders components/steps/demo/style-class.tsx extend context correctly 1`] = `
<div
class="ant-flex css-var-test-id ant-flex-align-stretch ant-flex-gap-middle ant-flex-vertical"
>
<div
class="ant-steps ant-steps-vertical ant-steps-title-horizontal ant-steps-filled css-var-test-id acss-1tcbvg1"
style="--steps-items-offset: 0;"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
style="border-radius: 30%;"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-item-icon-finish"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-process"
/>
</div>
<div
class="ant-steps-item-content"
style="font-style: italic;"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
style="border-radius: 30%;"
>
<span
class="ant-steps-item-icon-number"
>
2
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-wait"
/>
</div>
<div
class="ant-steps-item-content"
style="font-style: italic;"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
style="border-radius: 30%;"
>
<span
class="ant-steps-item-icon-number"
>
3
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
</div>
<div
class="ant-steps-item-content"
style="font-style: italic;"
>
This is a content.
</div>
</div>
</div>
</div>
</div>
<div
class="ant-steps ant-steps-vertical ant-steps-title-horizontal ant-steps-filled ant-steps-navigation css-var-test-id acss-1tcbvg1"
style="--steps-items-offset: 0; border-color: rgb(24, 144, 255);"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-item-icon-finish"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-process"
/>
</div>
<div
class="ant-steps-item-content"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
>
<span
class="ant-steps-item-icon-number"
>
2
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-wait"
/>
</div>
<div
class="ant-steps-item-content"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
>
<span
class="ant-steps-item-icon-number"
>
3
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
</div>
<div
class="ant-steps-item-content"
>
This is a content.
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders components/steps/demo/style-class.tsx extend context correctly 2`] = `[]`;
exports[`renders components/steps/demo/title-placement.tsx extend context correctly 1`] = `
Array [
<div

View File

@@ -5640,6 +5640,279 @@ Array [
]
`;
exports[`renders components/steps/demo/style-class.tsx correctly 1`] = `
<div
class="ant-flex css-var-test-id ant-flex-align-stretch ant-flex-gap-middle ant-flex-vertical"
>
<div
class="ant-steps ant-steps-horizontal ant-steps-title-horizontal ant-steps-filled css-var-test-id acss-1tcbvg1"
style="--steps-items-offset:0"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
style="border-radius:30%"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-item-icon-finish"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-process"
/>
</div>
<div
class="ant-steps-item-content"
style="font-style:italic"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
style="border-radius:30%"
>
<span
class="ant-steps-item-icon-number"
>
2
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-wait"
/>
</div>
<div
class="ant-steps-item-content"
style="font-style:italic"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
style="border-radius:30%"
>
<span
class="ant-steps-item-icon-number"
>
3
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
</div>
<div
class="ant-steps-item-content"
style="font-style:italic"
>
This is a content.
</div>
</div>
</div>
</div>
</div>
<div
class="ant-steps ant-steps-horizontal ant-steps-title-horizontal ant-steps-filled ant-steps-navigation css-var-test-id acss-1tcbvg1"
style="--steps-items-offset:0;border-color:#1890ff"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-item-icon-finish"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-process"
/>
</div>
<div
class="ant-steps-item-content"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
>
<span
class="ant-steps-item-icon-number"
>
2
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-rail ant-steps-item-rail-wait"
/>
</div>
<div
class="ant-steps-item-content"
>
This is a content.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-wrapper"
>
<div
class="ant-steps-item-icon ant-wave-target"
>
<span
class="ant-steps-item-icon-number"
>
3
</span>
</div>
<div
class="ant-steps-item-section"
>
<div
class="ant-steps-item-header"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
</div>
<div
class="ant-steps-item-content"
>
This is a content.
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders components/steps/demo/title-placement.tsx correctly 1`] = `
Array [
<div

View File

@@ -77,4 +77,34 @@ describe('Steps.Semantic', () => {
expect(element).toHaveStyle(style);
});
});
it('semantic structure with function classNames and styles', () => {
const classNamesFn: StepsProps['classNames'] = (info) => {
if (info.props.type === 'navigation') {
return { root: 'custom-navigation-root' };
}
return { root: 'custom-default-root' };
};
const stylesFn: StepsProps['styles'] = (info) => {
if (info.props.current === 1) {
return { root: { backgroundColor: 'rgb(255, 0, 0)' } };
}
return { root: { backgroundColor: 'rgb(0, 255, 0)' } };
};
const { container } = render(
renderSteps({
type: 'navigation',
current: 1,
classNames: classNamesFn,
styles: stylesFn,
}),
);
const rootElement = container.querySelector<HTMLElement>('.custom-navigation-root');
expect(rootElement).toBeTruthy();
expect(rootElement).toHaveClass('ant-steps');
expect(rootElement).toHaveStyle({ backgroundColor: 'rgb(255, 0, 0)' });
});
});

View File

@@ -0,0 +1,7 @@
## zh-CN
通过 `classNames``styles` 传入对象/函数可以自定义 Steps 的[语义化结构](#semantic-dom)样式。
## en-US
You can customize the [semantic dom](#semantic-dom) style of Steps by passing objects/functions through `classNames` and `styles`.

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { Flex, Steps } from 'antd';
import type { StepsProps } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => ({
root: {
border: `2px dashed ${token.colorBorder}`,
borderRadius: token.borderRadius,
padding: token.padding,
},
}));
const stylesObject: StepsProps['styles'] = {
itemIcon: { borderRadius: '30%' },
itemContent: { fontStyle: 'italic' },
};
const stylesFn: StepsProps['styles'] = (info) => {
if (info.props.type === 'navigation') {
return {
root: { borderColor: '#1890ff' },
};
}
return {};
};
const App: React.FC = () => {
const { styles } = useStyles();
const sharedProps: StepsProps = {
items: [
{
title: 'Finished',
content: 'This is a content.',
},
{
title: 'In Progress',
content: 'This is a content.',
},
{
title: 'Waiting',
content: 'This is a content.',
},
],
current: 1,
classNames: {
root: styles.root,
},
};
return (
<Flex vertical gap="middle">
<Steps {...sharedProps} styles={stylesObject} />
<Steps {...sharedProps} type="navigation" styles={stylesFn} />
</Flex>
);
};
export default App;

View File

@@ -30,6 +30,7 @@ When a given task is complicated or has a certain sequence in the series of subt
<code src="./demo/steps-in-steps.tsx" debug>Steps inside Steps</code>
<code src="./demo/inline.tsx">Inline Steps</code>
<code src="./demo/inline-variant.tsx">Inline Style Combination</code>
<code src="./demo/style-class.tsx" version="6.0.0">Custom semantic dom styling</code>
<code src="./demo/variant-debug.tsx" debug>Variant Debug</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>
@@ -43,7 +44,7 @@ The whole of the step bar.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| classNames | Semantic DOM class | [Record<SemanticDOM, string>](#semantic-dom) | - | |
| classNames | Customize class for each semantic structure inside the component. Supports object or function. | Record<[SemanticDOM](#semantic-dom), string> \| (info: { props })=> Record<[SemanticDOM](#semantic-dom), string> | - | |
| current | To set the current step, counting from 0. You can overwrite this state by using `status` of `Step` | number | 0 | |
| ~~direction~~ | To specify the direction of the step bar, `horizontal` or `vertical` | string | `horizontal` | |
| iconRender | Custom render icon, please use `items.icon` first | (oriNode, info: { index, active, item }) => ReactNode | - | |
@@ -55,7 +56,7 @@ The whole of the step bar.
| responsive | Change to vertical direction when screen width smaller than `532px` | boolean | true | |
| size | To specify the size of the step bar, `default` and `small` are currently supported | string | `default` | |
| status | To specify the status of current step, can be set to one of the following values: `wait` `process` `finish` `error` | string | `process` | |
| styles | Semantic DOM style | [Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | |
| styles | Customize inline style for each semantic structure inside the component. Supports object or function. | Record<[SemanticDOM](#semantic-dom), CSSProperties> \| (info: { props })=> Record<[SemanticDOM](#semantic-dom), CSSProperties> | - | |
| titlePlacement | Place title and content with `horizontal` or `vertical` direction | string | `horizontal` | |
| type | Type of steps, can be set to one of the following values: `default` `dot` `inline` `navigation` `panel` | string | `default` | |
| variant | Config style variant | `filled` \| `outlined` | `filled` | |

View File

@@ -6,6 +6,7 @@ import type { StepsProps as RcStepsProps } from '@rc-component/steps/lib/Steps';
import cls from 'classnames';
import useMergeSemantic from '../_util/hooks/useMergeSemantic';
import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks/useMergeSemantic';
import type { GetProp } from '../_util/type';
import { devUseWarning } from '../_util/warning';
import Wave from '../_util/wave';
@@ -26,6 +27,21 @@ export type IconRenderType = (
info: Pick<RcIconRenderTypeInfo, 'index' | 'active' | 'item' | 'components'>,
) => React.ReactNode;
export type StepsSemanticName =
| 'root'
| 'item'
| 'itemWrapper'
| 'itemIcon'
| 'itemSection'
| 'itemHeader'
| 'itemTitle'
| 'itemSubtitle'
| 'itemContent'
| 'itemRail';
export type StepsClassNamesType = SemanticClassNamesType<StepsProps, StepsSemanticName>;
export type StepsStylesType = SemanticStylesType<StepsProps, StepsSemanticName>;
interface StepItem {
className?: string;
style?: React.CSSProperties;
@@ -55,14 +71,12 @@ export type ProgressDotRender = (
},
) => React.ReactNode;
export interface StepsProps {
export interface BaseStepsProps {
// Style
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
rootClassName?: string;
classNames?: RcStepsProps['classNames'];
styles?: RcStepsProps['styles'];
classNames?: StepsClassNamesType;
styles?: StepsStylesType;
variant?: 'filled' | 'outlined';
size?: 'default' | 'small';
@@ -97,6 +111,11 @@ export interface StepsProps {
onChange?: (current: number) => void;
}
export interface StepsProps extends BaseStepsProps {
prefixCls?: string;
style?: React.CSSProperties;
}
const waveEffectClassNames: StepsProps['classNames'] = {
itemIcon: TARGET_CLS,
};
@@ -171,12 +190,6 @@ const Steps = (props: StepsProps) => {
// ============================= Item =============================
const mergedItems = React.useMemo(() => (items || []).filter(Boolean), [items]);
// ============================ Styles ============================
const [mergedClassNames, mergedStyles] = useMergeSemantic(
[waveEffectClassNames, contextClassNames, classNames],
[contextStyles, styles],
);
// ============================ Layout ============================
const { xs } = useBreakpoint(responsive);
@@ -225,6 +238,29 @@ const Steps = (props: StepsProps) => {
// ========================== Percentage ==========================
const mergedPercent = isInline ? undefined : percent;
// =========== Merged Props for Semantic ===========
const mergedProps: StepsProps = {
...props,
variant,
size: mergedSize,
type: mergedType,
orientation: mergedOrientation,
titlePlacement: mergedTitlePlacement,
current,
percent: mergedPercent,
responsive,
offset,
};
// ============================ Styles ============================
const [mergedClassNames, mergedStyles] = useMergeSemantic<
StepsClassNamesType,
StepsStylesType,
StepsProps
>([waveEffectClassNames, contextClassNames, classNames], [contextStyles, styles], undefined, {
props: mergedProps,
});
// ============================= Icon =============================
const internalIconRender: RcStepsProps['iconRender'] = (_, info) => {
const {

View File

@@ -33,6 +33,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cFsBQLA0b7UAAA
<code src="./demo/inline-variant.tsx">内联样式组合</code>
<code src="./demo/variant-debug.tsx" debug>变体 Debug</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
<code src="./demo/style-class.tsx" version="6.0.0">自定义各种语义结构的样式和类</code>
## API
@@ -44,7 +45,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cFsBQLA0b7UAAA
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| classNames | 语义化结构 className | [Record<SemanticDOM, string>](#semantic-dom) | - | |
| classNames | 用于自定义组件内部各语义化结构 class,支持对象或函数 | Record<[SemanticDOM](#semantic-dom), string> \| (info: { props })=> Record<[SemanticDOM](#semantic-dom), string> | - | |
| current | 指定当前步骤,从 0 开始记数。在子 Step 元素中,可以通过 `status` 属性覆盖状态 | number | 0 | |
| ~~direction~~ | 指定步骤条方向。目前支持水平(`horizontal`)和竖直(`vertical`)两种方向 | string | `horizontal` | |
| iconRender | 自定义渲染图标,请优先使用 `items.icon` | (oriNode, info: { index, active, item }) => ReactNode | - | |
@@ -56,7 +57,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cFsBQLA0b7UAAA
| responsive | 当屏幕宽度小于 `532px` 时自动变为垂直模式 | boolean | true | |
| size | 指定大小,目前支持普通(`default`)和迷你(`small` | string | `default` | |
| status | 指定当前步骤的状态,可选 `wait` `process` `finish` `error` | string | `process` | |
| styles | 语义化结构 style | [Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | |
| styles | 用于自定义组件内部各语义化结构的行内 style,支持对象或函数 | Record<[SemanticDOM](#semantic-dom), CSSProperties> \| (info: { props })=> Record<[SemanticDOM](#semantic-dom), CSSProperties> | - | |
| titlePlacement | 指定标签放置位置,默认水平放图标右侧,可选 `vertical` 放图标下方 | string | `horizontal` | |
| type | 步骤条类型,可选 `default` `dot` `inline` `navigation` `panel` | string | `default` | |
| variant | 设置样式变体 | `filled` \| `outlined` | `filled` | |