feat(result): Support better customization with semantic classNames/styles as function (#55044)

* Support better customization with semantic classNames/styles

* Support better customization with semantic classNames/styles

* Support better customization with semantic classNames/styles

* Support better customization with semantic classNames/styles

* Revert "Merge branch 'next' of github.com:ccc1018/ant-design into feature/result"

This reverts commit a64cb1b935, reversing
changes made to 2343dd9463.

* Revert "Support better customization with semantic classNames/styles"

This reverts commit 7d3a4defb4.

* Revert "Support better customization with semantic classNames/styles"

This reverts commit cfe65dda2d.

* Support better customization with semantic classNames/styles

* Update components/alert/Alert.tsx

Signed-off-by: thinkasany <480968828@qq.com>

* Support better customization with semantic classNames/styles

* Support better customization with semantic classNames/styles

* Support better customization with semantic classNames/styles

* Support better customization with semantic classNames/styles

* fix: remove alert component changes from result branch

* fix: reset alert component to next branch state, keep only result changes

* Support better customization with semantic classNames/styles

* fix: remove unwanted files from git commands

* docs: add missing Version column header in API tables

* Fix errors in the file

* Fix modify  in the file

* format doc table

* fix .snap

* Refactor Result component to use props directly

Signed-off-by: lijianan <574980606@qq.com>

* Fix formatting in result component props destructuring

Signed-off-by: lijianan <574980606@qq.com>

* Update components/result/index.zh-CN.md

Signed-off-by: thinkasany <480968828@qq.com>

---------

Signed-off-by: thinkasany <480968828@qq.com>
Signed-off-by: lijianan <574980606@qq.com>
Co-authored-by: thinkasany <480968828@qq.com>
Co-authored-by: 遇见同学 <1875694521@qq.com>
Co-authored-by: lijianan <574980606@qq.com>
This commit is contained in:
ccc1018
2025-09-30 14:16:30 +08:00
committed by GitHub
parent 38fc1dc4ab
commit 446c4a67d8
8 changed files with 403 additions and 35 deletions

View File

@@ -1258,6 +1258,126 @@ exports[`renders components/result/demo/info.tsx extend context correctly 1`] =
exports[`renders components/result/demo/info.tsx extend context correctly 2`] = `[]`;
exports[`renders components/result/demo/style-class.tsx extend context correctly 1`] = `
Array [
<div
class="ant-result ant-result-info css-var-test-id demo-result-root"
style="border-width: 2px; border-style: dashed; padding: 16px;"
>
<div
class="ant-result-icon demo-result-icon"
style="opacity: 0.8;"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</div>
<div
class="ant-result-title demo-result-title"
style="font-style: italic; color: rgb(24, 144, 255);"
>
classNames Object
</div>
<div
class="ant-result-subtitle demo-result-subtitle"
style="font-weight: bold;"
>
This is a subtitle
</div>
<div
class="ant-result-extra demo-result-extra"
style="background-color: rgb(240, 240, 240); padding: 8px;"
>
<button
class="ant-btn css-var-test-id ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Action
</span>
</button>
</div>
<div
class="ant-result-body demo-result-body"
style="background-color: rgb(250, 250, 250); padding: 12px;"
>
<div>
Content area
</div>
</div>
</div>,
<div
class="ant-result ant-result-success css-var-test-id demo-result-root--success"
style="background-color: rgb(246, 255, 237); border-color: rgb(82, 196, 26);"
>
<div
class="ant-result-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</div>
<div
class="ant-result-title"
style="color: rgb(82, 196, 26);"
>
classNames Function
</div>
<div
class="ant-result-subtitle"
>
Dynamic class names
</div>
<div
class="ant-result-extra"
>
<button
class="ant-btn css-var-test-id ant-btn-default ant-btn-color-default ant-btn-variant-outlined"
type="button"
>
<span>
Action
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/result/demo/style-class.tsx extend context correctly 2`] = `[]`;
exports[`renders components/result/demo/success.tsx extend context correctly 1`] = `
<div
class="ant-result ant-result-success css-var-test-id"

View File

@@ -1244,6 +1244,124 @@ exports[`renders components/result/demo/info.tsx correctly 1`] = `
</div>
`;
exports[`renders components/result/demo/style-class.tsx correctly 1`] = `
Array [
<div
class="ant-result ant-result-info css-var-test-id demo-result-root"
style="border-width:2px;border-style:dashed;padding:16px"
>
<div
class="ant-result-icon demo-result-icon"
style="opacity:0.8"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</div>
<div
class="ant-result-title demo-result-title"
style="font-style:italic;color:#1890ff"
>
classNames Object
</div>
<div
class="ant-result-subtitle demo-result-subtitle"
style="font-weight:bold"
>
This is a subtitle
</div>
<div
class="ant-result-extra demo-result-extra"
style="background-color:#f0f0f0;padding:8px"
>
<button
class="ant-btn css-var-test-id ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Action
</span>
</button>
</div>
<div
class="ant-result-body demo-result-body"
style="background-color:#fafafa;padding:12px"
>
<div>
Content area
</div>
</div>
</div>,
<div
class="ant-result ant-result-success css-var-test-id demo-result-root--success"
style="background-color:#f6ffed;border-color:#52c41a"
>
<div
class="ant-result-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</div>
<div
class="ant-result-title"
style="color:#52c41a"
>
classNames Function
</div>
<div
class="ant-result-subtitle"
>
Dynamic class names
</div>
<div
class="ant-result-extra"
>
<button
class="ant-btn css-var-test-id ant-btn-default ant-btn-color-default ant-btn-variant-outlined"
type="button"
>
<span>
Action
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/result/demo/success.tsx correctly 1`] = `
<div
class="ant-result ant-result-success css-var-test-id"

View File

@@ -5,6 +5,7 @@ import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render } from '../../../tests/utils';
import Button from '../../button';
import type { ResultProps } from '../index';
describe('Result', () => {
mountTest(Result);
@@ -77,7 +78,7 @@ describe('Result', () => {
});
it('should apply custom styles to Result', () => {
const customClassNames = {
const customClassNames: ResultProps['classNames'] = {
root: 'custom-root',
title: 'custom-title',
subTitle: 'custom-subTitle',
@@ -86,7 +87,7 @@ describe('Result', () => {
icon: 'custom-icon',
};
const customStyles = {
const customStyles: ResultProps['styles'] = {
root: { color: 'red' },
title: { color: 'green' },
subTitle: { color: 'yellow' },
@@ -130,4 +131,34 @@ describe('Result', () => {
expect(resultExtraElement.style.backgroundColor).toBe('blue');
expect(resultIconElement.style.backgroundColor).toBe('black');
});
it('should support function-based classNames and styles', () => {
const classNamesFn: ResultProps['classNames'] = (info) => {
if (info.props.status === 'success') {
return { root: 'success-result' };
}
return { root: 'default-result' };
};
const stylesFn: ResultProps['styles'] = (info) => {
if (info.props.status === 'error') {
return { root: { backgroundColor: 'red' } };
}
return { root: { backgroundColor: 'green' } };
};
const { container, rerender } = render(
<Result status="success" title="Success" classNames={classNamesFn} styles={stylesFn} />,
);
let resultElement = container.querySelector('.ant-result') as HTMLElement;
expect(resultElement.classList).toContain('success-result');
expect(resultElement.style.backgroundColor).toBe('green');
rerender(<Result status="error" title="Error" classNames={classNamesFn} styles={stylesFn} />);
resultElement = container.querySelector('.ant-result') as HTMLElement;
expect(resultElement.classList).toContain('default-result');
expect(resultElement.style.backgroundColor).toBe('red');
});
});

View File

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

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { Button, Result } from 'antd';
import type { ResultProps } from 'antd';
const classNamesObject: ResultProps['classNames'] = {
root: 'demo-result-root',
title: 'demo-result-title',
subTitle: 'demo-result-subtitle',
icon: 'demo-result-icon',
extra: 'demo-result-extra',
body: 'demo-result-body',
};
const classNamesFn: ResultProps['classNames'] = (info) => {
if (info.props.status === 'success') {
return { root: 'demo-result-root--success' };
}
return { root: 'demo-result-root--default' };
};
const stylesObject: ResultProps['styles'] = {
root: { borderWidth: 2, borderStyle: 'dashed', padding: 16 },
title: { fontStyle: 'italic', color: '#1890ff' },
subTitle: { fontWeight: 'bold' },
icon: { opacity: 0.8 },
extra: { backgroundColor: '#f0f0f0', padding: 8 },
body: { backgroundColor: '#fafafa', padding: 12 },
};
const stylesFn: ResultProps['styles'] = (info) => {
if (info.props.status === 'error') {
return {
root: { backgroundColor: '#fff2f0', borderColor: '#ff4d4f' },
title: { color: '#ff4d4f' },
};
}
return {
root: { backgroundColor: '#f6ffed', borderColor: '#52c41a' },
title: { color: '#52c41a' },
};
};
const App: React.FC = () => {
return (
<>
<Result
status="info"
title="classNames Object"
subTitle="This is a subtitle"
styles={stylesObject}
classNames={classNamesObject}
extra={<Button type="primary">Action</Button>}
>
<div>Content area</div>
</Result>
<Result
status="success"
title="classNames Function"
subTitle="Dynamic class names"
styles={stylesFn}
classNames={classNamesFn}
extra={<Button>Action</Button>}
/>
</>
);
};
export default App;

View File

@@ -22,19 +22,22 @@ Use when important operations need to inform the user to process the results and
<code src="./demo/500.tsx">500</code>
<code src="./demo/error.tsx">Error</code>
<code src="./demo/customIcon.tsx">Custom icon</code>
<code src="./demo/style-class.tsx" version="6.0.0">Custom semantic dom styling</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>
## API
Common props ref[Common props](/docs/react/common-props)
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| extra | Operating area | ReactNode | - |
| icon | Custom back icon | ReactNode | - |
| status | Result status, decide icons and colors | `success` \| `error` \| `info` \| `warning` \| `404` \| `403` \| `500` | `info` |
| subTitle | The subTitle | ReactNode | - |
| title | The title | ReactNode | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| classNames | Customize class for each semantic structure inside the component. Supports object or function | [Record<SemanticDOM, string> \| (info: { props }) => Record<SemanticDOM, string>](#semantic-dom) | - | |
| extra | Operating area | ReactNode | - | |
| icon | Custom back icon | ReactNode | - | |
| status | Result status, decide icons and colors | `success` \| `error` \| `info` \| `warning` \| `404` \| `403` \| `500` | `info` | |
| styles | Customize inline style for each semantic structure inside the component. Supports object or function | [Record<SemanticDOM, CSSProperties> \| (info: { props }) => Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | |
| subTitle | The subTitle | ReactNode | - | |
| title | The title | ReactNode | - | |
## Semantic DOM

View File

@@ -6,6 +6,7 @@ import WarningFilled from '@ant-design/icons/WarningFilled';
import classNames from 'classnames';
import useMergeSemantic from '../_util/hooks/useMergeSemantic';
import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks/useMergeSemantic';
import { devUseWarning } from '../_util/warning';
import { useComponentConfig } from '../config-provider/context';
import noFound from './noFound';
@@ -27,9 +28,15 @@ export const ExceptionMap = {
};
export type ExceptionStatusType = 403 | 404 | 500 | '403' | '404' | '500';
export type ResultStatusType = ExceptionStatusType | keyof typeof IconMap;
type SemanticName = 'root' | 'title' | 'subTitle' | 'body' | 'extra' | 'icon';
export type ResultClassNamesType = SemanticClassNamesType<ResultProps, SemanticName>;
export type ResultStylesType = SemanticStylesType<ResultProps, SemanticName>;
export interface ResultProps {
icon?: React.ReactNode;
status?: ResultStatusType;
@@ -41,8 +48,8 @@ export interface ResultProps {
rootClassName?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
classNames?: ResultClassNamesType;
styles?: ResultStylesType;
}
// ExceptionImageMap keys
@@ -120,20 +127,22 @@ export interface ResultType extends React.FC<ResultProps> {
PRESENTED_IMAGE_500: React.FC;
}
const Result: ResultType = ({
prefixCls: customizePrefixCls,
className: customizeClassName,
rootClassName,
subTitle,
title,
style,
children,
status = 'info',
icon,
extra,
styles,
classNames: resultClassNames,
}) => {
const Result: ResultType = (props) => {
const {
prefixCls: customizePrefixCls,
className: customizeClassName,
rootClassName,
subTitle,
title,
style,
children,
status = 'info',
icon,
extra,
styles,
classNames: resultClassNames,
} = props;
const {
getPrefixCls,
direction,
@@ -143,10 +152,19 @@ const Result: ResultType = ({
styles: contextStyles,
} = useComponentConfig('result');
const [mergedClassNames, mergedStyles] = useMergeSemantic(
[contextClassNames, resultClassNames],
[contextStyles, styles],
);
// =========== Merged Props for Semantic ==========
const mergedProps: ResultProps = {
...props,
status,
};
const [mergedClassNames, mergedStyles] = useMergeSemantic<
ResultClassNamesType,
ResultStylesType,
ResultProps
>([contextClassNames, resultClassNames], [contextStyles, styles], undefined, {
props: mergedProps,
});
const prefixCls = getPrefixCls('result', customizePrefixCls);

View File

@@ -23,19 +23,22 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*-0kxQrbHx2kAAA
<code src="./demo/500.tsx">500</code>
<code src="./demo/error.tsx">Error</code>
<code src="./demo/customIcon.tsx">自定义 icon</code>
<code src="./demo/style-class.tsx" version="6.0.0">自定义各种语义结构的样式和类</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
## API
通用属性参考:[通用属性](/docs/react/common-props)
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| extra | 操作区 | ReactNode | - |
| icon | 自定义 icon | ReactNode | - |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| classNames | 自定义组件内部各语义化结构的类名。支持对象或函数 | [Record<SemanticDOM, string> \| (info: { props }) => Record<SemanticDOM, string>](#semantic-dom) | - | |
| extra | 操作区 | ReactNode | - | |
| icon | 自定义 icon | ReactNode | - | |
| status | 结果的状态,决定图标和颜色 | `success` \| `error` \| `info` \| `warning` \| `404` \| `403` \| `500` | `info` |
| subTitle | subTitle 文字 | ReactNode | - |
| title | title 文字 | ReactNode | - |
| styles | 自定义组件内部各语义化结构的内联样式。支持对象或函数 | [Record<SemanticDOM, CSSProperties> \| (info: { props }) => Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | |
| subTitle | subTitle 文字 | ReactNode | - | |
| title | title 文字 | ReactNode | - | |
## Semantic DOM