Skip to content

Commit

Permalink
feat: Add classNames and styles prop for card (#46811)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
zh-lx and vagusX committed Jan 30, 2024
1 parent a11d3b4 commit 6ed0254
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 22 deletions.
97 changes: 88 additions & 9 deletions components/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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']);

Expand Down
59 changes: 59 additions & 0 deletions components/card/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`;
18 changes: 18 additions & 0 deletions components/card/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
15 changes: 13 additions & 2 deletions components/card/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | |
Expand All @@ -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
Expand All @@ -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>
15 changes: 13 additions & 2 deletions components/card/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | |
Expand All @@ -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
Expand All @@ -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>
14 changes: 12 additions & 2 deletions components/config-provider/__tests__/style.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
8 changes: 7 additions & 1 deletion components/config-provider/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'>;

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion components/config-provider/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down

0 comments on commit 6ed0254

Please sign in to comment.