feat: Add classNames and styles prop for card (#46811)

* feat: support classNames and styles for card

* feat: add lost info

* docs: update context of card

* feat: optimize classNames and styles code of card

* test: update card test case

* feat: remove headWrapper

* test: correct test

* feat: remove redundant snapshot

* feat: update config provider for card

* feat: update classNames and styles of card

* feat: add warning for headStyle and bodyStyle of card

* test: replace bodyStyle to styles.body in demo of flex

* docs: add jsDoc about deprecated for headStyle and bodyStyle

* snap: update table counts of card from 3 to 4

* docs: update version for styles and classnames of card

---------

Signed-off-by: zhoulixiang <18366276315@163.com>
Co-authored-by: vagusX <vagusxl@gmail.com>
This commit is contained in:
zhoulixiang 2024-01-30 15:00:25 +08:00 committed by GitHub
parent a11d3b4b63
commit 6ed0254ad5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 217 additions and 22 deletions

View File

@ -3,6 +3,7 @@ import classNames from 'classnames';
import type { Tab } from 'rc-tabs/lib/interface';
import omit from 'rc-util/lib/omit';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import useSize from '../config-provider/hooks/useSize';
import Skeleton from '../skeleton';
@ -26,7 +27,9 @@ export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 't
title?: React.ReactNode;
extra?: React.ReactNode;
bordered?: boolean;
/** @deprecated Please use `styles.header` instead */
headStyle?: React.CSSProperties;
/** @deprecated Please use `styles.body` instead */
bodyStyle?: React.CSSProperties;
style?: React.CSSProperties;
loading?: boolean;
@ -45,12 +48,35 @@ export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 't
activeTabKey?: string;
defaultActiveTabKey?: string;
tabProps?: TabsProps;
classNames?: {
header?: string;
body?: string;
extra?: string;
title?: string;
actions?: string;
cover?: string;
};
styles?: {
header?: React.CSSProperties;
body?: React.CSSProperties;
extra?: React.CSSProperties;
title?: React.CSSProperties;
actions?: React.CSSProperties;
cover?: React.CSSProperties;
};
}
const ActionNode: React.FC<{ prefixCls: string; actions: React.ReactNode[] }> = (props) => {
const { prefixCls, actions = [] } = props;
type CardClassNamesModule = keyof Exclude<CardProps['classNames'], undefined>;
type CardStylesModule = keyof Exclude<CardProps['styles'], undefined>;
const ActionNode: React.FC<{
actionClasses: string;
actions: React.ReactNode[];
actionStyle: React.CSSProperties;
}> = (props) => {
const { actionClasses, actions = [], actionStyle } = props;
return (
<ul className={`${prefixCls}-actions`}>
<ul className={actionClasses} style={actionStyle}>
{actions.map<React.ReactNode>((action, index) => {
// Move this out since eslint not allow index key
// And eslint-disable makes conflict with rollup
@ -89,15 +115,36 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>((props, ref) => {
tabBarExtraContent,
hoverable,
tabProps = {},
classNames: customClassNames,
styles: customStyles,
...others
} = props;
const { getPrefixCls, direction, card } = React.useContext(ConfigContext);
// =================Warning===================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Card');
[
['headStyle', 'styles.header'],
['bodyStyle', 'styles.body'],
].forEach(([deprecatedName, newName]) => {
warning.deprecated(!(deprecatedName in props), deprecatedName, newName);
});
}
const onTabChange = (key: string) => {
props.onTabChange?.(key);
};
const moduleClass = (moduleName: CardClassNamesModule) =>
classNames(card?.classNames?.[moduleName], customClassNames?.[moduleName]);
const moduleStyle = (moduleName: CardStylesModule) => ({
...card?.styles?.[moduleName],
...customStyles?.[moduleName],
});
const isContainGrid = React.useMemo<boolean>(() => {
let containGrid = false;
React.Children.forEach(children, (element: JSX.Element) => {
@ -139,25 +186,57 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>((props, ref) => {
/>
) : null;
if (title || extra || tabs) {
const headClasses = classNames(`${prefixCls}-head`, moduleClass('header'));
const titleClasses = classNames(`${prefixCls}-head-title`, moduleClass('title'));
const extraClasses = classNames(`${prefixCls}-extra`, moduleClass('extra'));
const mergedHeadStyle: React.CSSProperties = {
...headStyle,
...moduleStyle('header'),
};
head = (
<div className={`${prefixCls}-head`} style={headStyle}>
<div className={headClasses} style={mergedHeadStyle}>
<div className={`${prefixCls}-head-wrapper`}>
{title && <div className={`${prefixCls}-head-title`}>{title}</div>}
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
{title && (
<div className={titleClasses} style={moduleStyle('title')}>
{title}
</div>
)}
{extra && (
<div className={extraClasses} style={moduleStyle('extra')}>
{extra}
</div>
)}
</div>
{tabs}
</div>
);
}
const coverDom = cover ? <div className={`${prefixCls}-cover`}>{cover}</div> : null;
const coverClasses = classNames(`${prefixCls}-cover`, moduleClass('cover'));
const coverDom = cover ? (
<div className={coverClasses} style={moduleStyle('cover')}>
{cover}
</div>
) : null;
const bodyClasses = classNames(`${prefixCls}-body`, moduleClass('body'));
const mergedBodyStyle: React.CSSProperties = {
...bodyStyle,
...moduleStyle('body'),
};
const body = (
<div className={`${prefixCls}-body`} style={bodyStyle}>
<div className={bodyClasses} style={mergedBodyStyle}>
{loading ? loadingBlock : children}
</div>
);
const actionClasses = classNames(`${prefixCls}-actions`, moduleClass('actions'));
const actionDom =
actions && actions.length ? <ActionNode prefixCls={prefixCls} actions={actions} /> : null;
actions && actions.length ? (
<ActionNode
actionClasses={actionClasses}
actionStyle={moduleStyle('actions')}
actions={actions}
/>
) : null;
const divProps = omit(others, ['onTabChange']);

View File

@ -338,3 +338,62 @@ exports[`Card title should be vertically aligned 1`] = `
</div>
</div>
`;
exports[`Card should support custom className 1`] = `
<div>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-head custom-head"
>
<div
class="ant-card-head-wrapper"
>
<div
class="ant-card-head-title"
>
Card title
</div>
</div>
</div>
<div
class="ant-card-body"
>
<p>
Card content
</p>
</div>
</div>
</div>
`;
exports[`Card should support custom styles 1`] = `
<div>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-head"
style="color: red;"
>
<div
class="ant-card-head-wrapper"
>
<div
class="ant-card-head-title"
>
Card title
</div>
</div>
</div>
<div
class="ant-card-body"
>
<p>
Card content
</p>
</div>
</div>
</div>
`;

View File

@ -174,4 +174,22 @@ describe('Card', () => {
expect(container.firstChild).toMatchSnapshot();
});
it('should support custom className', () => {
const { container } = render(
<Card title="Card title" classNames={{ header: 'custom-head' }}>
<p>Card content</p>
</Card>,
);
expect(container).toMatchSnapshot();
});
it('should support custom styles', () => {
const { container } = render(
<Card title="Card title" styles={{ header: { color: 'red' } }}>
<p>Card content</p>
</Card>,
);
expect(container).toMatchSnapshot();
});
});

View File

@ -41,12 +41,10 @@ Common props ref[Common props](/docs/react/common-props)
| --- | --- | --- | --- | --- |
| actions | The action list, shows at the bottom of the Card | Array&lt;ReactNode> | - | |
| activeTabKey | Current TabPane's key | string | - | |
| bodyStyle | Inline style to apply to the card content | CSSProperties | - | |
| bordered | Toggles rendering of the border around the card | boolean | true | |
| cover | Card cover | ReactNode | - | |
| defaultActiveTabKey | Initial active TabPane's key, if `activeTabKey` is not set | string | - | |
| extra | Content to render in the top-right corner of the card | ReactNode | - | |
| headStyle | Inline style to apply to the card head | CSSProperties | - | |
| hoverable | Lift up when hovering card | boolean | false | |
| loading | Shows a loading indicator while the contents of the card are being fetched | boolean | false | |
| size | Size of card | `default` \| `small` | `default` | |
@ -55,6 +53,8 @@ Common props ref[Common props](/docs/react/common-props)
| tabProps | [Tabs](/components/tabs/#tabs) | - | - | |
| title | Card title | ReactNode | - | |
| type | Card style type, can be set to `inner` or not set | string | - | |
| classNames | Config Card build-in module's className | Record<SemanticDOM, string> | - | 5.14.0 |
| styles | Config Card build-in module's style | Record<SemanticDOM, string> | - | 5.14.0 |
| onTabChange | Callback when tab is switched | (key) => void | - | |
### Card.Grid
@ -75,6 +75,17 @@ Common props ref[Common props](/docs/react/common-props)
| style | The style object of container | CSSProperties | - | |
| title | Title content | ReactNode | - | |
### `styles``classNames` attribute
| 名称 | 说明 | 版本 |
| ------- | --------------------- | ------ |
| header | set `header` of card | 5.14.0 |
| body | set `body` of card | 5.14.0 |
| extra | set `extra` of card | 5.14.0 |
| title | set `title` of card | 5.14.0 |
| actions | set `actions` of card | 5.14.0 |
| cover | set `cover` of card | 5.14.0 |
## Design Token
<ComponentTokenTable component="Card"></ComponentTokenTable>

View File

@ -42,12 +42,10 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a-8zR6rrupgAAA
| --- | --- | --- | --- | --- |
| actions | 卡片操作组,位置在卡片底部 | Array&lt;ReactNode> | - | |
| activeTabKey | 当前激活页签的 key | string | - | |
| bodyStyle | 内容区域自定义样式 | CSSProperties | - | |
| bordered | 是否有边框 | boolean | true | |
| cover | 卡片封面 | ReactNode | - | |
| defaultActiveTabKey | 初始化选中页签的 key如果没有设置 activeTabKey | string | `第一个页签` | |
| extra | 卡片右上角的操作区域 | ReactNode | - | |
| headStyle | 自定义标题区域样式 | CSSProperties | - | |
| hoverable | 鼠标移过时可浮起 | boolean | false | |
| loading | 当卡片内容还在加载中时,可以用 loading 展示一个占位 | boolean | false | |
| size | card 的尺寸 | `default` \| `small` | `default` | |
@ -56,6 +54,8 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a-8zR6rrupgAAA
| tabProps | [Tabs](/components/tabs-cn#tabs) | - | - | |
| title | 卡片标题 | ReactNode | - | |
| type | 卡片类型,可设置为 `inner` 或 不设置 | string | - | |
| classNames | 配置卡片内置模块的 className | Record<SemanticDOM, string> | - | 5.14.0 |
| styles | 配置卡片内置模块的 style | Record<SemanticDOM, string> | - | 5.14.0 |
| onTabChange | 页签切换的回调 | (key) => void | - | |
### Card.Grid
@ -76,6 +76,17 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a-8zR6rrupgAAA
| style | 定义容器类名的样式 | CSSProperties | - | |
| title | 标题内容 | ReactNode | - | |
### `styles``classNames` 属性
| 名称 | 说明 | 版本 |
| ------- | ------------------------ | ------ |
| header | 设置卡片头部区域 | 5.14.0 |
| body | 设置卡片内容区域 | 5.14.0 |
| extra | 设置卡片右上角的操作区域 | 5.14.0 |
| title | 设置卡片标题 | 5.14.0 |
| actions | 设置卡片底部操作组 | 5.14.0 |
| cover | 设置标题封面 | 5.14.0 |
## 主题变量Design Token
<ComponentTokenTable component="Card"></ComponentTokenTable>

View File

@ -1006,15 +1006,25 @@ describe('ConfigProvider support style and className props', () => {
);
});
it('Should Card className & style works', () => {
it('Should Card className & style & classNames & styles works', () => {
const { container } = render(
<ConfigProvider card={{ className: 'cp-card', style: { backgroundColor: 'blue' } }}>
<ConfigProvider
card={{
className: 'cp-card',
style: { backgroundColor: 'blue' },
classNames: { body: 'custom-body' },
styles: { body: { color: 'red' } },
}}
>
<Card>test</Card>
</ConfigProvider>,
);
const element = container.querySelector<HTMLDivElement>('.ant-card');
expect(element).toHaveClass('cp-card');
expect(element).toHaveStyle({ backgroundColor: 'blue' });
const head = container.querySelector<HTMLDivElement>('.ant-card-body');
expect(head).toHaveClass('custom-body');
expect(head).toHaveStyle({ color: 'red' });
});
it('Should Tabs className & style works', () => {

View File

@ -15,6 +15,7 @@ import type { SelectProps } from '../select';
import type { SpaceProps } from '../space';
import type { TableProps } from '../table';
import type { TabsProps } from '../tabs';
import type { CardProps } from '../card';
import type { AliasToken, MappingAlgorithm, OverrideToken } from '../theme/interface';
import type { TourProps } from '../tour/interface';
import type { RenderEmptyHandler } from './defaultRenderEmpty';
@ -82,6 +83,11 @@ export type BadgeConfig = ComponentStyleConfig & Pick<BadgeProps, 'classNames' |
export type ButtonConfig = ComponentStyleConfig & Pick<ButtonProps, 'classNames' | 'styles'>;
export interface CardConfig extends ComponentStyleConfig {
classNames?: CardProps['classNames'];
styles: CardProps['styles'];
}
export type DrawerConfig = ComponentStyleConfig &
Pick<DrawerProps, 'classNames' | 'styles' | 'closeIcon'>;
@ -152,7 +158,7 @@ export interface ConfigConsumerProps {
message?: ComponentStyleConfig;
tag?: ComponentStyleConfig;
table?: TableConfig;
card?: ComponentStyleConfig;
card?: CardConfig;
tabs?: ComponentStyleConfig & Pick<TabsProps, 'indicator' | 'indicatorSize'>;
timeline?: ComponentStyleConfig;
timePicker?: ComponentStyleConfig;

View File

@ -107,7 +107,7 @@ const {
| badge | Set Badge common props | { className?: string, style?: React.CSSProperties, classNames?: { count?: string, indicator?: string }, styles?: { count?: React.CSSProperties, indicator?: React.CSSProperties } } | - | 5.7.0 |
| breadcrumb | Set Breadcrumb common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| button | Set Button common props | { className?: string, style?: React.CSSProperties, classNames?: { icon: string }, styles?: { icon: React.CSSProperties } } | - | 5.6.0 |
| card | Set Card common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| card | Set Card common props | { className?: string, style?: React.CSSProperties, classNames?: [CardProps\["classNames"\]](/components/card#api), styles?: [CardProps\["styles"\]](/components/card#api) } | - | 5.7.0, `classNames` and `styles`: 5.14.0 |
| calendar | Set Calendar common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| carousel | Set Carousel common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| cascader | Set Cascader common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |

View File

@ -23,6 +23,7 @@ import defaultSeedToken from '../theme/themes/seed';
import type {
BadgeConfig,
ButtonConfig,
CardConfig,
ComponentStyleConfig,
ConfigConsumerProps,
CSPConfig,
@ -167,7 +168,7 @@ export interface ConfigProviderProps {
message?: ComponentStyleConfig;
tag?: ComponentStyleConfig;
table?: TableConfig;
card?: ComponentStyleConfig;
card?: CardConfig;
tabs?: ComponentStyleConfig & Pick<TabsProps, 'indicator' | 'indicatorSize'>;
timeline?: ComponentStyleConfig;
timePicker?: ComponentStyleConfig;

View File

@ -110,7 +110,7 @@ const {
| breadcrumb | 设置 Breadcrumb 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| button | 设置 Button 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: { icon: string }, styles?: { icon: React.CSSProperties } } | - | 5.6.0 |
| calendar | 设置 Calendar 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| card | 设置 Card 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| card | 设置 Card 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [CardProps\["classNames"\]](/components/card-cn#api), styles?: [CardProps\["styles"\]](/components/card-cn#api) } | - | 5.7.0, `classNames``styles`: 5.14.0 |
| carousel | 设置 Carousel 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| cascader | 设置 Cascader 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| checkbox | 设置 Checkbox 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |

View File

@ -11,7 +11,7 @@ const imgStyle: React.CSSProperties = {
};
const App: React.FC = () => (
<Card hoverable style={cardStyle} bodyStyle={{ padding: 0, overflow: 'hidden' }}>
<Card hoverable style={cardStyle} styles={{ body: { padding: 0, overflow: 'hidden' } }}>
<Flex justify="space-between">
<img
alt="avatar"

View File

@ -40,9 +40,9 @@ exports[`site test Component components/calendar en Page 1`] = `1`;
exports[`site test Component components/calendar zh Page 1`] = `1`;
exports[`site test Component components/card en Page 1`] = `3`;
exports[`site test Component components/card en Page 1`] = `4`;
exports[`site test Component components/card zh Page 1`] = `3`;
exports[`site test Component components/card zh Page 1`] = `4`;
exports[`site test Component components/carousel en Page 1`] = `2`;