index.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import React, { PureComponent, ReactNode, CSSProperties } from 'react';
  2. import { omit, isString } from 'lodash';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/card/constants';
  5. import '@douyinfe/semi-foundation/card/card.scss';
  6. import Meta from './meta';
  7. import cls from 'classnames';
  8. import Skeleton from '../skeleton';
  9. import Typography from '../typography';
  10. import Space from '../space';
  11. const prefixcls = cssClasses.PREFIX;
  12. export type Shadows = 'hover' | 'always';
  13. export { MetaProps } from './meta';
  14. export { CardGroupProps } from './cardGroup';
  15. export interface CardProps {
  16. /** Operation group at the bottom of the card content area */
  17. actions?: ReactNode[];
  18. /** Card content area inline style */
  19. bodyStyle?: CSSProperties;
  20. /** Whether there is an outer border */
  21. bordered?: boolean;
  22. /** Style class name */
  23. className?: string;
  24. children?: React.ReactNode;
  25. /** Cover */
  26. cover?: ReactNode;
  27. /** Additional additions to the right of the title */
  28. headerExtraContent?: ReactNode;
  29. /** Custom end of page */
  30. footer?: ReactNode;
  31. /** Whether there is an edge between the bottom of the page and the content area */
  32. footerLine?: boolean;
  33. /** Inline style at the end of the page */
  34. footerStyle?: CSSProperties;
  35. /** Custom head */
  36. header?: ReactNode;
  37. /** Whether there is an edge line between the head and the content area */
  38. headerLine?: boolean;
  39. /** Head inline style */
  40. headerStyle?: CSSProperties;
  41. /** Whether to preload */
  42. loading?: boolean;
  43. /** Set shadow */
  44. shadows?: Shadows;
  45. /** Card inline style */
  46. style?: CSSProperties;
  47. /** Title */
  48. title?: ReactNode;
  49. /** aria label */
  50. 'aria-label'?: string;
  51. }
  52. class Card extends PureComponent<CardProps> {
  53. static Meta = Meta;
  54. static propTypes = {
  55. actions: PropTypes.array,
  56. bodyStyle: PropTypes.object,
  57. bordered: PropTypes.bool,
  58. children: PropTypes.node,
  59. className: PropTypes.string,
  60. cover: PropTypes.node,
  61. footer: PropTypes.node,
  62. footerLine: PropTypes.bool,
  63. footerStyle: PropTypes.object,
  64. header: PropTypes.node,
  65. headerExtraContent: PropTypes.node,
  66. headerLine: PropTypes.bool,
  67. headerStyle: PropTypes.object,
  68. loading: PropTypes.bool,
  69. shadows: PropTypes.oneOf(strings.SHADOWS),
  70. style: PropTypes.object,
  71. title: PropTypes.node,
  72. 'aria-label': PropTypes.string,
  73. };
  74. static defaultProps = {
  75. bordered: true,
  76. footerLine: false,
  77. headerLine: true,
  78. loading: false
  79. };
  80. renderHeader = (): ReactNode => {
  81. const {
  82. title,
  83. headerExtraContent,
  84. header,
  85. headerLine,
  86. headerStyle
  87. } = this.props;
  88. const headerCls = cls(`${prefixcls}-header`, {
  89. [`${prefixcls}-header-bordered`]: Boolean(headerLine)
  90. });
  91. const headerWrapperCls = cls(`${prefixcls}-header-wrapper`);
  92. const titleCls = cls(`${prefixcls}-header-wrapper-title`, {
  93. [`${prefixcls}-header-wrapper-spacing`]: Boolean(headerExtraContent)
  94. });
  95. if (header || headerExtraContent || title) {
  96. return (
  97. <div style={headerStyle} className={headerCls}>
  98. {header || ( // Priority of header over title and headerExtraContent
  99. <div className={headerWrapperCls}>
  100. {headerExtraContent && (
  101. <div
  102. className={`${prefixcls}-header-wrapper-extra`}
  103. x-semi-prop="headerExtraContent"
  104. >
  105. {headerExtraContent}
  106. </div>
  107. )}
  108. {title && (
  109. <div className={titleCls}>
  110. {isString(title) ? (
  111. <Typography.Title
  112. heading={6}
  113. ellipsis={{ showTooltip: true, rows: 1 }}
  114. x-semi-prop="title"
  115. >
  116. {title}
  117. </Typography.Title>
  118. ) : (
  119. title
  120. )}
  121. </div>
  122. )}
  123. </div>
  124. )}
  125. </div>
  126. );
  127. }
  128. return null;
  129. };
  130. renderCover = (): ReactNode => {
  131. const {
  132. cover
  133. } = this.props;
  134. const coverCls = cls(`${prefixcls}-cover`);
  135. return (
  136. cover && (
  137. <div className={coverCls} x-semi-prop="cover">
  138. {cover}
  139. </div>
  140. )
  141. );
  142. };
  143. renderBody = (): ReactNode => {
  144. const { bodyStyle, children, actions, loading } = this.props;
  145. const bodyCls = cls(`${prefixcls}-body`);
  146. const actionsCls = cls(`${prefixcls}-body-actions`);
  147. const actionsItemCls = cls(`${prefixcls}-body-actions-item`);
  148. const placeholder = (
  149. <div>
  150. <Skeleton.Title />
  151. <br />
  152. <Skeleton.Paragraph rows={3} />
  153. </div>
  154. );
  155. return (
  156. <div style={bodyStyle} className={bodyCls}>
  157. {children && (
  158. <Skeleton placeholder={placeholder} loading={loading} active>
  159. {children}
  160. </Skeleton>
  161. )}
  162. {
  163. Array.isArray(actions) &&
  164. (
  165. <div className={actionsCls}>
  166. <Space spacing={12}>
  167. {actions.map((item, idx) => (
  168. <div key={idx} className={actionsItemCls} x-semi-prop={`actions.${idx}`}>{item}</div>
  169. ))}
  170. </Space>
  171. </div>
  172. )
  173. }
  174. </div>
  175. );
  176. };
  177. renderFooter = (): ReactNode => {
  178. const {
  179. footer,
  180. footerLine,
  181. footerStyle
  182. } = this.props;
  183. const footerCls = cls(`${prefixcls}-footer`, {
  184. [`${prefixcls}-footer-bordered`]: footerLine
  185. });
  186. return (
  187. footer && (
  188. <div style={footerStyle} className={footerCls} x-semi-prop="footer">
  189. {footer}
  190. </div>
  191. )
  192. );
  193. };
  194. render(): ReactNode {
  195. const {
  196. bordered,
  197. shadows,
  198. style,
  199. className,
  200. ...otherProps
  201. } = this.props;
  202. const others = omit(otherProps, [ // Remove APIs in otherProps that do not need to be hung on the outer node
  203. 'actions',
  204. 'bodyStyle',
  205. 'cover',
  206. 'headerExtraContent',
  207. 'footer',
  208. 'footerLine',
  209. 'footerStyle',
  210. 'header',
  211. 'headerLine',
  212. 'headerStyle',
  213. 'loading',
  214. 'title'
  215. ]);
  216. const cardCls = cls(prefixcls, className, {
  217. [`${prefixcls}-bordered`]: bordered,
  218. [`${prefixcls}-shadows`]: shadows,
  219. [`${prefixcls}-shadows-${shadows}`]: shadows
  220. });
  221. return (
  222. <div {...others} aria-busy={this.props.loading} className={cardCls} style={style}>
  223. {this.renderHeader()}
  224. {this.renderCover()}
  225. {this.renderBody()}
  226. {this.renderFooter()}
  227. </div>
  228. );
  229. }
  230. }
  231. export default Card;