From 61fa52561987e6d369190df742b0596d5760f5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Wed, 24 Sep 2025 22:55:56 +0800 Subject: [PATCH] feat(steps): Support better customization with semantic classNames/styles as function (#54956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(steps): Support better customization with semantic classNames/styles as function * test: update demo snapshot --------- Co-authored-by: 二货机器人 --- .../__snapshots__/demo-extend.test.ts.snap | 275 ++++++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 273 +++++++++++++++++ components/steps/__tests__/semantic.test.tsx | 30 ++ components/steps/demo/style-class.md | 7 + components/steps/demo/style-class.tsx | 60 ++++ components/steps/index.en-US.md | 5 +- components/steps/index.tsx | 58 +++- components/steps/index.zh-CN.md | 5 +- 8 files changed, 698 insertions(+), 15 deletions(-) create mode 100644 components/steps/demo/style-class.md create mode 100644 components/steps/demo/style-class.tsx diff --git a/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap index 68cf002590..e55a4ae37b 100644 --- a/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -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`] = ` +
+
+
+
+
+ + + +
+
+
+
+ Finished +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 2 + +
+
+
+
+ In Progress +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 3 + +
+
+
+
+ Waiting +
+
+
+ This is a content. +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+ Finished +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 2 + +
+
+
+
+ In Progress +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 3 + +
+
+
+
+ Waiting +
+
+
+ This is a content. +
+
+
+
+
+
+`; + +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 [
+
+
+
+
+ + + +
+
+
+
+ Finished +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 2 + +
+
+
+
+ In Progress +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 3 + +
+
+
+
+ Waiting +
+
+
+ This is a content. +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+ Finished +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 2 + +
+
+
+
+ In Progress +
+
+
+
+ This is a content. +
+
+
+
+
+
+
+ + 3 + +
+
+
+
+ Waiting +
+
+
+ This is a content. +
+
+
+
+
+
+`; + exports[`renders components/steps/demo/title-placement.tsx correctly 1`] = ` Array [
{ 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('.custom-navigation-root'); + expect(rootElement).toBeTruthy(); + expect(rootElement).toHaveClass('ant-steps'); + expect(rootElement).toHaveStyle({ backgroundColor: 'rgb(255, 0, 0)' }); + }); }); diff --git a/components/steps/demo/style-class.md b/components/steps/demo/style-class.md new file mode 100644 index 0000000000..00ec95685b --- /dev/null +++ b/components/steps/demo/style-class.md @@ -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`. diff --git a/components/steps/demo/style-class.tsx b/components/steps/demo/style-class.tsx new file mode 100644 index 0000000000..fec73ca2fc --- /dev/null +++ b/components/steps/demo/style-class.tsx @@ -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 ( + + + + + ); +}; + +export default App; diff --git a/components/steps/index.en-US.md b/components/steps/index.en-US.md index 289d4fc56b..3af2b446e1 100644 --- a/components/steps/index.en-US.md +++ b/components/steps/index.en-US.md @@ -30,6 +30,7 @@ When a given task is complicated or has a certain sequence in the series of subt Steps inside Steps Inline Steps Inline Style Combination +Custom semantic dom styling Variant Debug Component Token @@ -43,7 +44,7 @@ The whole of the step bar. | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | -| classNames | Semantic DOM class | [Record](#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](#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` | | diff --git a/components/steps/index.tsx b/components/steps/index.tsx index 4d4314540c..691a9e4f48 100644 --- a/components/steps/index.tsx +++ b/components/steps/index.tsx @@ -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, ) => React.ReactNode; +export type StepsSemanticName = + | 'root' + | 'item' + | 'itemWrapper' + | 'itemIcon' + | 'itemSection' + | 'itemHeader' + | 'itemTitle' + | 'itemSubtitle' + | 'itemContent' + | 'itemRail'; + +export type StepsClassNamesType = SemanticClassNamesType; +export type StepsStylesType = SemanticStylesType; + 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 { diff --git a/components/steps/index.zh-CN.md b/components/steps/index.zh-CN.md index a4394d6633..d597f2e7d0 100644 --- a/components/steps/index.zh-CN.md +++ b/components/steps/index.zh-CN.md @@ -33,6 +33,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cFsBQLA0b7UAAA 内联样式组合 变体 Debug 组件 Token +自定义各种语义结构的样式和类 ## API @@ -44,7 +45,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cFsBQLA0b7UAAA | 参数 | 说明 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | -| classNames | 语义化结构 className | [Record](#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](#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` | |