mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 02:59:04 +08:00
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:
parent
a11d3b4b63
commit
6ed0254ad5
@ -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']);
|
||||
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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<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>
|
||||
|
@ -42,12 +42,10 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a-8zR6rrupgAAA
|
||||
| --- | --- | --- | --- | --- |
|
||||
| actions | 卡片操作组,位置在卡片底部 | Array<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>
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
|
@ -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 |
|
||||
|
@ -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;
|
||||
|
@ -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 |
|
||||
|
@ -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"
|
||||
|
@ -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`;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user