feat: ConfigProvider support classNames and styles for Descriptions (#52120)
Some checks are pending
Publish Any Commit / build (push) Waiting to run
🔀 Sync mirror to Gitee / mirror (push) Waiting to run
✅ test / lint (push) Waiting to run
✅ test / test-react-legacy (16, 1/2) (push) Waiting to run
✅ test / test-react-legacy (16, 2/2) (push) Waiting to run
✅ test / test-react-legacy (17, 1/2) (push) Waiting to run
✅ test / test-react-legacy (17, 2/2) (push) Waiting to run
✅ test / test-node (push) Waiting to run
✅ test / test-react-latest (dom, 1/2) (push) Waiting to run
✅ test / test-react-latest (dom, 2/2) (push) Waiting to run
✅ test / test-react-latest-dist (dist, 1/2) (push) Blocked by required conditions
✅ test / test-react-latest-dist (dist, 2/2) (push) Blocked by required conditions
✅ test / test-react-latest-dist (dist-min, 1/2) (push) Blocked by required conditions
✅ test / test-react-latest-dist (dist-min, 2/2) (push) Blocked by required conditions
✅ test / test-coverage (push) Blocked by required conditions
✅ test / build (push) Waiting to run
✅ test / test lib/es module (es, 1/2) (push) Waiting to run
✅ test / test lib/es module (es, 2/2) (push) Waiting to run
✅ test / test lib/es module (lib, 1/2) (push) Waiting to run
✅ test / test lib/es module (lib, 2/2) (push) Waiting to run
👁️ Visual Regression Persist Start / test image (push) Waiting to run

* draft

* fix

* fix

* add header, title, extra

* add border preview

* update

* update demo

* add divider

* update width

* update SemanticPreview demo
This commit is contained in:
thinkasany 2025-01-02 14:40:48 +08:00 committed by GitHub
parent 3c1f56c99d
commit 0745996d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 335 additions and 24 deletions

View File

@ -37,6 +37,7 @@ import type { RenderEmptyHandler } from './defaultRenderEmpty';
import type { TooltipProps } from '../tooltip';
import type { PopoverProps } from '../popover';
import type { PopconfirmProps } from '../popconfirm';
import type { DescriptionsProps } from '../descriptions';
export const defaultPrefixCls = 'ant';
export const defaultIconPrefixCls = 'anticon';
@ -136,6 +137,9 @@ export type MenuConfig = ComponentStyleConfig & Pick<MenuProps, 'expandIcon'>;
export type TourConfig = Pick<TourProps, 'closeIcon'>;
export type DescriptionsConfig = ComponentStyleConfig &
Pick<DescriptionsProps, 'classNames' | 'styles'>;
export type ModalConfig = ComponentStyleConfig &
Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon' | 'closable'>;
@ -285,7 +289,7 @@ export interface ConfigConsumerProps {
breadcrumb?: ComponentStyleConfig;
menu?: MenuConfig;
checkbox?: ComponentStyleConfig;
descriptions?: ComponentStyleConfig;
descriptions?: DescriptionsConfig;
empty?: ComponentStyleConfig;
badge?: BadgeConfig;
radio?: ComponentStyleConfig;

View File

@ -123,7 +123,7 @@ const {
| colorPicker | Set ColorPicker common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| datePicker | Set datePicker common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| rangePicker | Set rangePicker common props | { className?: string, style?: React.CSSProperties } | - | 5.11.0 |
| descriptions | Set Descriptions common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| descriptions | Set Descriptions common props | { className?: string, style?: React.CSSProperties, classNames?: [DescriptionsProps\["classNames"\]](/components/descriptions#api), styles?: [DescriptionsProps\["styles"\]](/components/descriptions#api) } | - | 5.7.0, `classNames` and `styles`: 5.23.0 |
| divider | Set Divider common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| drawer | Set Drawer common props | { className?: string, style?: React.CSSProperties, classNames?: [DrawerProps\["classNames"\]](/components/drawer#api), styles?: [DrawerProps\["styles"\]](/components/drawer#api), closeIcon?: ReactNode } | - | 5.7.0, `classNames` and `styles`: 5.10.0, `closeIcon`: 5.14.0 |
| dropdown | Set Dropdown common props | { className?: string, style?: React.CSSProperties } | - | 5.11.0 |

View File

@ -125,7 +125,7 @@ const {
| colorPicker | 设置 ColorPicker 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| datePicker | 设置 DatePicker 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| rangePicker | 设置 RangePicker 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.11.0 |
| descriptions | 设置 Descriptions 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| descriptions | 设置 Descriptions 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [DescriptionsProps\["classNames"\]](/components/descriptions-cn#api), styles?: [DescriptionsProps\["styles"\]](/components/descriptions-cn#api) } | - | 5.7.0, `classNames``styles`: 5.23.0 |
| divider | 设置 Divider 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| drawer | 设置 Drawer 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [DrawerProps\["classNames"\]](/components/drawer-cn#api), styles?: [DrawerProps\["styles"\]](/components/drawer-cn#api), closeIcon?: ReactNode } | - | 5.7.0, `classNames``styles`: 5.10.0, `closeIcon`: 5.14.0 |
| dropdown | 设置 Dropdown 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.11.0 |

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import type { JSX } from 'react';
import classNames from 'classnames';
import DescriptionsContext from './DescriptionsContext';
function notEmpty(val: any) {
return val !== undefined && val !== null;
@ -12,8 +13,18 @@ export interface CellProps {
className?: string;
component: string;
style?: React.CSSProperties;
/** @deprecated Please use `styles={{ label: {} }}` instead */
labelStyle?: React.CSSProperties;
/** @deprecated Please use `styles={{ content: {} }}` instead */
contentStyle?: React.CSSProperties;
styles?: {
label?: React.CSSProperties;
content?: React.CSSProperties;
};
classNames?: {
label?: string;
content?: string;
};
bordered?: boolean;
label?: React.ReactNode;
content?: React.ReactNode;
@ -35,9 +46,12 @@ const Cell: React.FC<CellProps> = (props) => {
content,
colon,
type,
styles,
} = props;
const Component = component as keyof JSX.IntrinsicElements;
const descContext = React.useContext(DescriptionsContext);
const { classNames: descriptionsClassNames } = descContext;
if (bordered) {
return (
@ -46,14 +60,16 @@ const Cell: React.FC<CellProps> = (props) => {
{
[`${itemPrefixCls}-item-label`]: type === 'label',
[`${itemPrefixCls}-item-content`]: type === 'content',
[`${descriptionsClassNames?.label}`]: type === 'label',
[`${descriptionsClassNames?.content}`]: type === 'content',
},
className,
)}
style={style}
colSpan={span}
>
{notEmpty(label) && <span style={labelStyle}>{label}</span>}
{notEmpty(content) && <span style={contentStyle}>{content}</span>}
{notEmpty(label) && <span style={{ ...labelStyle, ...styles?.label }}>{label}</span>}
{notEmpty(content) && <span style={{ ...labelStyle, ...styles?.content }}>{content}</span>}
</Component>
);
}
@ -67,16 +83,19 @@ const Cell: React.FC<CellProps> = (props) => {
<div className={`${itemPrefixCls}-item-container`}>
{(label || label === 0) && (
<span
className={classNames(`${itemPrefixCls}-item-label`, {
className={classNames(`${itemPrefixCls}-item-label`, descriptionsClassNames?.label, {
[`${itemPrefixCls}-item-no-colon`]: !colon,
})}
style={labelStyle}
style={{ ...labelStyle, ...styles?.label }}
>
{label}
</span>
)}
{(content || content === 0) && (
<span className={classNames(`${itemPrefixCls}-item-content`)} style={contentStyle}>
<span
className={classNames(`${itemPrefixCls}-item-content`, descriptionsClassNames?.content)}
style={{ ...contentStyle, ...styles?.content }}
>
{content}
</span>
)}

View File

@ -1,8 +1,18 @@
import React from 'react';
export interface DescriptionsContextProps {
/** @deprecated Please use `styles={{ label: {} }}` instead */
labelStyle?: React.CSSProperties;
/** @deprecated Please use `styles={{ content: {} }}` instead */
contentStyle?: React.CSSProperties;
styles?: {
label?: React.CSSProperties;
content?: React.CSSProperties;
};
classNames?: {
label?: string;
content?: string;
};
}
const DescriptionsContext = React.createContext<DescriptionsContextProps>({});

View File

@ -5,8 +5,18 @@ export interface DescriptionsItemProps {
className?: string;
style?: React.CSSProperties;
label?: React.ReactNode;
/** @deprecated Please use `styles={{ label: {} }}` instead */
labelStyle?: React.CSSProperties;
/** @deprecated Please use `styles={{ content: {} }}` instead */
contentStyle?: React.CSSProperties;
styles?: {
label?: React.CSSProperties;
content?: React.CSSProperties;
};
classNames?: {
label?: string;
content?: string;
};
children: React.ReactNode;
span?: number;
}

View File

@ -22,6 +22,7 @@ function renderCells(
showContent,
labelStyle: rootLabelStyle,
contentStyle: rootContentStyle,
styles: rootStyles,
}: CellConfig & DescriptionsContextProps,
) {
return items.map(
@ -36,6 +37,7 @@ function renderCells(
contentStyle,
span = 1,
key,
styles,
},
index,
) => {
@ -45,8 +47,20 @@ function renderCells(
key={`${type}-${key || index}`}
className={className}
style={style}
labelStyle={{ ...rootLabelStyle, ...labelStyle }}
contentStyle={{ ...rootContentStyle, ...contentStyle }}
styles={{
label: {
...rootLabelStyle,
...rootStyles?.label,
...labelStyle,
...styles?.label,
},
content: {
...rootContentStyle,
...rootStyles?.content,
...contentStyle,
...styles?.content,
},
}}
span={span}
colon={colon}
component={component}
@ -63,7 +77,13 @@ function renderCells(
<Cell
key={`label-${key || index}`}
className={className}
style={{ ...rootLabelStyle, ...style, ...labelStyle }}
style={{
...rootLabelStyle,
...rootStyles?.label,
...style,
...labelStyle,
...styles?.label,
}}
span={1}
colon={colon}
component={component[0]}
@ -75,7 +95,13 @@ function renderCells(
<Cell
key={`content-${key || index}`}
className={className}
style={{ ...rootContentStyle, ...style, ...contentStyle }}
style={{
...rootContentStyle,
...rootStyles?.content,
...style,
...contentStyle,
...styles?.content,
}}
span={span * 2 - 1}
component={component[1]}
itemPrefixCls={itemPrefixCls}

View File

@ -415,4 +415,83 @@ describe('Descriptions', () => {
expect(wrapper.container.querySelectorAll('.ant-descriptions-item-label')).toHaveLength(1);
expect(wrapper.container.querySelectorAll('.ant-descriptions-item-content')).toHaveLength(1);
});
it('should apply custom styles to Descriptions', () => {
const customClassNames = {
root: 'custom-root',
header: 'custom-header',
title: 'custom-title',
extra: 'custom-extra',
label: 'custom-label',
content: 'custom-content',
};
const customStyles = {
root: { backgroundColor: 'red' },
header: { backgroundColor: 'black' },
title: { backgroundColor: 'yellow' },
extra: { backgroundColor: 'purple' },
label: { backgroundColor: 'blue' },
content: { backgroundColor: 'green' },
};
const { container } = render(
<Descriptions
classNames={customClassNames}
styles={customStyles}
extra={'extra'}
title="User Info"
items={[
{
key: '1',
label: 'UserName',
children: '1',
},
{
key: '2',
label: 'UserName',
children: '2',
styles: {
content: { color: 'yellow' },
label: { color: 'orange' },
},
},
]}
/>,
);
const rootElement = container.querySelector('.ant-descriptions') as HTMLElement;
const headerElement = container.querySelector('.ant-descriptions-header') as HTMLElement;
const titleElement = container.querySelector('.ant-descriptions-title') as HTMLElement;
const extraElement = container.querySelector('.ant-descriptions-extra') as HTMLElement;
const labelElement = container.querySelector('.ant-descriptions-item-label') as HTMLElement;
const contentElement = container.querySelector('.ant-descriptions-item-content') as HTMLElement;
const labelElements = container.querySelectorAll(
'.ant-descriptions-item-label',
) as NodeListOf<HTMLElement>;
const contentElements = container.querySelectorAll(
'.ant-descriptions-item-content',
) as NodeListOf<HTMLElement>;
// check classNames
expect(rootElement.classList).toContain('custom-root');
expect(headerElement.classList).toContain('custom-header');
expect(titleElement.classList).toContain('custom-title');
expect(extraElement.classList).toContain('custom-extra');
expect(labelElement.classList).toContain('custom-label');
expect(contentElement.classList).toContain('custom-content');
// check styles
expect(rootElement.style.backgroundColor).toBe('red');
expect(headerElement.style.backgroundColor).toBe('black');
expect(titleElement.style.backgroundColor).toBe('yellow');
expect(extraElement.style.backgroundColor).toBe('purple');
expect(labelElement.style.backgroundColor).toBe('blue');
expect(contentElement.style.backgroundColor).toBe('green');
expect(labelElements[1].style.color).toBe('orange');
expect(contentElements[1].style.color).toBe('yellow');
expect(labelElements[0].style.color).not.toBe('orange');
expect(contentElements[0].style.color).not.toBe('yellow');
});
});

View File

@ -0,0 +1,75 @@
import React from 'react';
import { Button, Descriptions, DescriptionsProps, Divider, Switch } from 'antd';
import SemanticPreview from '../../../.dumi/components/SemanticPreview';
import useLocale from '../../../.dumi/hooks/useLocale';
const locales = {
cn: {
root: '根节点',
header: '头部元素',
title: '标题元素',
extra: '额外内容',
label: '标签元素',
content: '内容元素',
},
en: {
root: 'root element',
header: 'header element',
title: 'title element',
extra: 'extra element',
label: 'label element',
content: 'content element',
},
};
const items: DescriptionsProps['items'] = [
{
key: '1',
label: 'Telephone',
children: '1810000000',
},
];
const BlockList: React.FC<React.PropsWithChildren> = (props: any) => {
const divRef = React.useRef<HTMLDivElement>(null);
const [bordered, setBordered] = React.useState(false);
const handleBorderChange = (checked: boolean) => {
setBordered(checked);
};
return (
<div ref={divRef} style={{ width: '100%', height: '100%' }}>
<Switch checked={bordered} onChange={handleBorderChange} /> Toggle Border
<Divider />
<Descriptions
title="User Info"
items={items}
extra={<Button type="primary">Edit</Button>}
bordered={bordered}
{...props}
/>
</div>
);
};
const App: React.FC = () => {
const [locale] = useLocale(locales);
return (
<SemanticPreview
semantics={[
{ name: 'root', desc: locale.root, version: '5.23.0' },
{ name: 'header', desc: locale.header, version: '5.23.0' },
{ name: 'title', desc: locale.title, version: '5.23.0' },
{ name: 'extra', desc: locale.extra, version: '5.23.0' },
{ name: 'label', desc: locale.label, version: '5.23.0' },
{ name: 'content', desc: locale.content, version: '5.23.0' },
]}
>
<BlockList />
</SemanticPreview>
);
};
export default App;

View File

@ -12,8 +12,10 @@ const items: DescriptionsProps['items'] = [
key: '1',
label: 'Product',
children: 'Cloud Database',
labelStyle,
contentStyle,
styles: {
label: labelStyle,
content: contentStyle,
},
},
{
key: '2',
@ -42,8 +44,10 @@ const rootStyleItems: DescriptionsProps['items'] = [
key: '3',
label: 'Automatic Renewal',
children: 'YES',
labelStyle: { color: 'orange' },
contentStyle: { color: 'blue' },
styles: {
label: { color: 'orange' },
content: { color: 'blue' },
},
},
];
@ -69,8 +73,10 @@ const App: React.FC = () => {
<Divider />
<Descriptions
title="Root style"
labelStyle={labelStyle}
contentStyle={contentStyle}
styles={{
label: labelStyle,
content: contentStyle,
}}
bordered={border}
layout={layout}
items={rootStyleItems}

View File

@ -103,6 +103,10 @@ Common props ref[Common props](/docs/react/common-props)
> The number of span Description.Item. Span={2} takes up the width of two DescriptionItems. When both `style` and `labelStyle`(or `contentStyle`) configured, both of them will work. And next one will overwrite first when conflict.
## Semantic DOM
<code src="./demo/_semantic.tsx" simplify="true"></code>
## Design Token
<ComponentTokenTable component="Descriptions"></ComponentTokenTable>

View File

@ -4,6 +4,7 @@ import classNames from 'classnames';
import type { Breakpoint } from '../_util/responsiveObserver';
import { matchScreen } from '../_util/responsiveObserver';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import useSize from '../config-provider/hooks/useSize';
import useBreakpoint from '../grid/hooks/useBreakpoint';
@ -48,6 +49,22 @@ export interface DescriptionsProps {
colon?: boolean;
labelStyle?: React.CSSProperties;
contentStyle?: React.CSSProperties;
styles?: {
root?: React.CSSProperties;
header?: React.CSSProperties;
title?: React.CSSProperties;
extra?: React.CSSProperties;
label?: React.CSSProperties;
content?: React.CSSProperties;
};
classNames?: {
root?: string;
header?: string;
title?: string;
extra?: string;
label?: string;
content?: string;
};
items?: DescriptionsItemType[];
id?: string;
}
@ -68,13 +85,25 @@ const Descriptions: React.FC<DescriptionsProps> & CompoundedComponent = (props)
size: customizeSize,
labelStyle,
contentStyle,
styles,
items,
classNames: descriptionsClassNames,
...restProps
} = props;
const { getPrefixCls, direction, descriptions } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('descriptions', customizePrefixCls);
const screens = useBreakpoint();
// ============================== Warn ==============================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Descriptions');
[
['labelStyle', 'styles={{ label: {} }}'],
['contentStyle', 'styles={{ content: {} }}'],
].forEach(([deprecatedName, newName]) => {
warning.deprecated(!(deprecatedName in props), deprecatedName, newName);
});
}
// Column count
const mergedColumn = React.useMemo(() => {
if (typeof column === 'number') {
@ -99,8 +128,19 @@ const Descriptions: React.FC<DescriptionsProps> & CompoundedComponent = (props)
// ======================== Render ========================
const contextValue = React.useMemo(
() => ({ labelStyle, contentStyle }),
[labelStyle, contentStyle],
() => ({
labelStyle,
contentStyle,
styles: {
content: { ...descriptions?.styles?.content, ...styles?.content },
label: { ...descriptions?.styles?.label, ...styles?.label },
},
classNames: {
label: classNames(descriptions?.classNames?.label, descriptionsClassNames?.label),
content: classNames(descriptions?.classNames?.content, descriptionsClassNames?.content),
},
}),
[labelStyle, contentStyle, styles, descriptionsClassNames, descriptions],
);
return wrapCSSVar(
@ -109,6 +149,8 @@ const Descriptions: React.FC<DescriptionsProps> & CompoundedComponent = (props)
className={classNames(
prefixCls,
descriptions?.className,
descriptions?.classNames?.root,
descriptionsClassNames?.root,
{
[`${prefixCls}-${mergedSize}`]: mergedSize && mergedSize !== 'default',
[`${prefixCls}-bordered`]: !!bordered,
@ -119,13 +161,45 @@ const Descriptions: React.FC<DescriptionsProps> & CompoundedComponent = (props)
hashId,
cssVarCls,
)}
style={{ ...descriptions?.style, ...style }}
style={{ ...descriptions?.style, ...descriptions?.styles?.root, ...styles?.root, ...style }}
{...restProps}
>
{(title || extra) && (
<div className={`${prefixCls}-header`}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
<div
className={classNames(
`${prefixCls}-header`,
descriptions?.classNames?.header,
descriptionsClassNames?.header,
)}
style={{ ...descriptions?.styles?.header, ...styles?.header }}
>
{title && (
<div
className={classNames(
`${prefixCls}-title`,
descriptions?.classNames?.title,
descriptionsClassNames?.title,
)}
style={{
...descriptions?.styles?.title,
...styles?.title,
}}
>
{title}
</div>
)}
{extra && (
<div
className={classNames(
`${prefixCls}-extra`,
descriptions?.classNames?.extra,
descriptionsClassNames?.extra,
)}
style={{ ...descriptions?.styles?.extra, ...styles?.extra }}
>
{extra}
</div>
)}
</div>
)}

View File

@ -104,6 +104,10 @@ const items: DescriptionsProps['items'] = [
> span 是 Description.Item 的数量。 span={2} 会占用两个 DescriptionItem 的宽度。当同时配置 `style``labelStyle`(或 `contentStyle`)时,两者会同时作用。样式冲突时,后者会覆盖前者。
## Semantic DOM
<code src="./demo/_semantic.tsx" simplify="true"></code>
## 主题变量Design Token
<ComponentTokenTable component="Descriptions"></ComponentTokenTable>