base.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. import React, { Component, CSSProperties } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/typography/constants';
  5. import Typography from './typography';
  6. import Copyable from './copyable';
  7. import { IconSize as Size } from '../icons/index';
  8. import { isUndefined, omit, merge, isString, isNull, isFunction } from 'lodash';
  9. import Tooltip from '../tooltip/index';
  10. import Popover from '../popover/index';
  11. import getRenderText from './util';
  12. import warning from '@douyinfe/semi-foundation/utils/warning';
  13. import isEnterPress from '@douyinfe/semi-foundation/utils/isEnterPress';
  14. import LocaleConsumer from '../locale/localeConsumer';
  15. import type { Locale } from '../locale/interface';
  16. import type { Ellipsis, EllipsisPos, ShowTooltip, TypographyBaseSize, TypographyBaseType } from './interface';
  17. import { CopyableConfig, LinkType } from './title';
  18. import { BaseProps } from '../_base/baseComponent';
  19. import { isSemiIcon, runAfterTicks } from '../_utils';
  20. import SizeContext from './context';
  21. import ResizeObserver, { ObserverProperty, ResizeEntry } from '../resizeObserver';
  22. export interface BaseTypographyProps extends BaseProps {
  23. copyable?: CopyableConfig | boolean;
  24. delete?: boolean;
  25. disabled?: boolean;
  26. icon?: React.ReactNode;
  27. /**
  28. * ellipsis 用于设置截断相关参数.
  29. * Ellipsis is used to set ellipsis related parameters.
  30. * ellipsis 仅支持纯文本的截断,不支持 reactNode 等复杂类型,请确保 children 传入内容类型为 string.
  31. * Ellipsis only supports ellipsis of plain text, and does not support complex types such as reactNode.
  32. * Please ensure that the content type of children is string.
  33. * Semi 截断有两种策略, CSS 截断和 JS 截断。
  34. * Semi ellipsis has two strategies, CSS ellipsis and JS ellipsis.
  35. * - 当设置中间截断(pos='middle')、可展开(expandable)、有后缀(suffix 非空)、可复制(copyable),启用 JS 截断策略
  36. * - When setting middle ellipsis (pos='middle')、expandable、suffix is not empty string、copyable,
  37. * the JS ellipsis strategy is enabled
  38. * - 非以上场景,启用 CSS 截断策略
  39. * - Otherwise, enable the CSS ellipsis strategy
  40. *
  41. * 通常来说 CSS 截断的性能优于 JS 截断。在 children 不变, 容器尺寸不变的情况下,CSS 截断只涉及 1-2 次计算,js 截断基于二分法,可能涉及多次计算。
  42. * In general CSS ellipsis performs better than JS ellipsis. when the children and container size remain unchanged,
  43. * CSS ellipsis only involves 1-2 calculations, while JS ellipsis is based on dichotomy and may require multiple calculations.
  44. * 同时使用大量带有截断功能的 Typography 需注意性能消耗,如在 Table 中,可通过设置合理的页容量进行分页减少性能损耗
  45. * Pay attention to performance consumption when using a large number of Typography with ellipsis. For example, in Table,
  46. * you can reduce performance loss by setting a reasonable pageSize for paging
  47. */
  48. ellipsis?: Ellipsis | boolean;
  49. mark?: boolean;
  50. underline?: boolean;
  51. link?: LinkType;
  52. strong?: boolean;
  53. type?: TypographyBaseType;
  54. size?: TypographyBaseSize;
  55. style?: React.CSSProperties;
  56. className?: string;
  57. code?: boolean;
  58. children?: React.ReactNode;
  59. component?: React.ElementType;
  60. spacing?: string;
  61. heading?: string;
  62. weight?: string | number
  63. }
  64. interface BaseTypographyState {
  65. editable: boolean;
  66. copied: boolean;
  67. isOverflowed: boolean;
  68. ellipsisContent: React.ReactNode;
  69. expanded: boolean;
  70. isTruncated: boolean;
  71. prevChildren: React.ReactNode
  72. }
  73. const prefixCls = cssClasses.PREFIX;
  74. const ELLIPSIS_STR = '...';
  75. const wrapperDecorations = (props: BaseTypographyProps, content: React.ReactNode) => {
  76. const { mark, code, underline, strong, link, disabled } = props;
  77. let wrapped = content;
  78. const wrap = (isNeeded: boolean | LinkType, tag: string) => {
  79. let wrapProps = {};
  80. if (!isNeeded) {
  81. return;
  82. }
  83. if (typeof isNeeded === 'object') {
  84. wrapProps = { ...isNeeded };
  85. }
  86. wrapped = React.createElement(tag, wrapProps, wrapped);
  87. };
  88. wrap(mark, 'mark');
  89. wrap(code, 'code');
  90. wrap(underline && !link, 'u');
  91. wrap(strong, 'strong');
  92. wrap(props.delete, 'del');
  93. wrap(link, disabled ? 'span' : 'a');
  94. return wrapped;
  95. };
  96. export default class Base extends Component<BaseTypographyProps, BaseTypographyState> {
  97. static propTypes = {
  98. children: PropTypes.node,
  99. copyable: PropTypes.oneOfType([
  100. PropTypes.shape({
  101. text: PropTypes.string,
  102. onCopy: PropTypes.func,
  103. successTip: PropTypes.node,
  104. copyTip: PropTypes.node,
  105. }),
  106. PropTypes.bool,
  107. ]),
  108. delete: PropTypes.bool,
  109. disabled: PropTypes.bool,
  110. // editable: PropTypes.bool,
  111. ellipsis: PropTypes.oneOfType([
  112. PropTypes.shape({
  113. rows: PropTypes.number,
  114. expandable: PropTypes.bool,
  115. expandText: PropTypes.string,
  116. onExpand: PropTypes.func,
  117. suffix: PropTypes.string,
  118. showTooltip: PropTypes.oneOfType([
  119. PropTypes.shape({
  120. type: PropTypes.string,
  121. opts: PropTypes.object,
  122. }),
  123. PropTypes.bool,
  124. ]),
  125. collapsible: PropTypes.bool,
  126. collapseText: PropTypes.string,
  127. pos: PropTypes.oneOf(['end', 'middle']),
  128. }),
  129. PropTypes.bool,
  130. ]),
  131. mark: PropTypes.bool,
  132. underline: PropTypes.bool,
  133. link: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
  134. spacing: PropTypes.oneOf(strings.SPACING),
  135. strong: PropTypes.bool,
  136. size: PropTypes.oneOf(strings.SIZE),
  137. type: PropTypes.oneOf(strings.TYPE),
  138. style: PropTypes.object,
  139. className: PropTypes.string,
  140. icon: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
  141. heading: PropTypes.string,
  142. component: PropTypes.string,
  143. };
  144. static defaultProps = {
  145. children: null as React.ReactNode,
  146. copyable: false,
  147. delete: false,
  148. disabled: false,
  149. // editable: false,
  150. ellipsis: false,
  151. icon: '',
  152. mark: false,
  153. underline: false,
  154. strong: false,
  155. link: false,
  156. type: 'primary',
  157. spacing: 'normal',
  158. size: 'normal',
  159. style: {},
  160. className: '',
  161. };
  162. static contextType = SizeContext;
  163. context: TypographyBaseSize;
  164. wrapperRef: React.RefObject<any>;
  165. expandRef: React.RefObject<any>;
  166. copyRef: React.RefObject<any>;
  167. rafId: ReturnType<typeof requestAnimationFrame>;
  168. expandStr: string;
  169. collapseStr: string;
  170. observerTakingEffect: boolean = false
  171. constructor(props: BaseTypographyProps) {
  172. super(props);
  173. this.state = {
  174. editable: false,
  175. copied: false,
  176. // ellipsis
  177. // if text is overflow in container
  178. isOverflowed: false,
  179. ellipsisContent: props.children,
  180. expanded: false,
  181. // if text is truncated with js
  182. isTruncated: false,
  183. prevChildren: null,
  184. };
  185. this.wrapperRef = React.createRef();
  186. this.expandRef = React.createRef();
  187. this.copyRef = React.createRef();
  188. }
  189. componentDidMount() {
  190. if (this.props.ellipsis) {
  191. // runAfterTicks: make sure start observer on the next tick
  192. this.onResize().then(()=>runAfterTicks(()=>this.observerTakingEffect = true, 1));
  193. }
  194. }
  195. static getDerivedStateFromProps(props: BaseTypographyProps, prevState: BaseTypographyState) {
  196. const { prevChildren } = prevState;
  197. const newState: Partial<BaseTypographyState> = {};
  198. newState.prevChildren = props.children;
  199. if (props.ellipsis && prevChildren !== props.children) {
  200. // reset ellipsis state if children update
  201. newState.isOverflowed = false;
  202. newState.ellipsisContent = props.children;
  203. newState.expanded = false;
  204. newState.isTruncated = true;
  205. }
  206. return newState;
  207. }
  208. componentDidUpdate(prevProps: BaseTypographyProps) {
  209. // Render was based on outdated refs and needs to be rerun
  210. if (this.props.children !== prevProps.children) {
  211. this.forceUpdate();
  212. if (this.props.ellipsis) {
  213. this.onResize();
  214. }
  215. }
  216. }
  217. componentWillUnmount() {
  218. if (this.rafId) {
  219. window.cancelAnimationFrame(this.rafId);
  220. }
  221. }
  222. onResize = async (entries?: ResizeEntry[]) => {
  223. if (this.rafId) {
  224. window.cancelAnimationFrame(this.rafId);
  225. }
  226. return new Promise<void>(resolve => {
  227. this.rafId = window.requestAnimationFrame(async ()=>{
  228. await this.getEllipsisState();
  229. resolve();
  230. });
  231. });
  232. };
  233. // if it needs to use js overflowed:
  234. // 1. text is expandable 2. expandText need to be shown 3. has extra operation 4. text need to ellipse from mid
  235. canUseCSSEllipsis = () => {
  236. const { copyable } = this.props;
  237. const { expandable, expandText, pos, suffix } = this.getEllipsisOpt();
  238. return !expandable && isUndefined(expandText) && !copyable && pos === 'end' && !suffix.length;
  239. };
  240. /**
  241. * whether truncated
  242. * rows < = 1 if there is overflow content, return true
  243. * rows > 1 if there is overflow height, return true
  244. * @param {Number} rows
  245. * @returns {Boolean}
  246. */
  247. shouldTruncated = (rows: number) => {
  248. if (!rows || rows < 1) {
  249. return false;
  250. }
  251. const updateOverflow =
  252. rows <= 1 ?
  253. this.compareSingleRow() :
  254. this.wrapperRef.current.scrollHeight > this.wrapperRef.current.offsetHeight;
  255. return updateOverflow;
  256. };
  257. /**
  258. * 通过将 content 给到 Range 对象,借助 Range 的 getBoundingClientRect 拿到 content 的准确 width
  259. * 不受 css ellipsis 与否的影响
  260. * By giving the content to the Range object, get the exact width of the content with the help of Range's getBoundingClientRect
  261. * Not affected by css ellipsis or not
  262. * https://github.com/DouyinFE/semi-design/issues/1731
  263. */
  264. compareSingleRow = () => {
  265. if (!(document && document.createRange)) {
  266. return false;
  267. }
  268. const containerNode = this.wrapperRef.current;
  269. const containerWidth = containerNode.getBoundingClientRect().width;
  270. const childNodes = Array.from(containerNode.childNodes) as Node[];
  271. const range = document.createRange();
  272. const contentWidth = childNodes.reduce((acc: number, node: Node) => {
  273. range.selectNodeContents(node as Node);
  274. return acc + (range.getBoundingClientRect().width ?? 0);
  275. }, 0);
  276. range.detach();
  277. return contentWidth > containerWidth;
  278. }
  279. showTooltip = () => {
  280. const { isOverflowed, isTruncated, expanded } = this.state;
  281. const { showTooltip, expandable, expandText } = this.getEllipsisOpt();
  282. const canUseCSSEllipsis = this.canUseCSSEllipsis();
  283. // If the css is truncated, use isOverflowed to judge. If the css is truncated, use isTruncated to judge.
  284. const overflowed = !expanded && (canUseCSSEllipsis ? isOverflowed : isTruncated);
  285. const noExpandText = !expandable && isUndefined(expandText);
  286. const show = noExpandText && overflowed && showTooltip;
  287. if (!show) {
  288. return show;
  289. }
  290. const defaultOpts = {
  291. type: 'tooltip',
  292. };
  293. if (typeof showTooltip === 'object') {
  294. if (showTooltip.type && showTooltip.type.toLowerCase() === 'popover') {
  295. return merge(
  296. {
  297. opts: {
  298. // style: { width: '240px' },
  299. showArrow: true,
  300. },
  301. },
  302. showTooltip,
  303. {
  304. opts: {
  305. className: cls({
  306. [`${prefixCls}-ellipsis-popover`]: true,
  307. [showTooltip?.opts?.className]: Boolean(showTooltip?.opts?.className)
  308. }),
  309. }
  310. }
  311. );
  312. }
  313. return { ...defaultOpts, ...showTooltip };
  314. }
  315. return defaultOpts;
  316. };
  317. onHover = ()=>{
  318. const canUseCSSEllipsis = this.canUseCSSEllipsis();
  319. if (canUseCSSEllipsis) {
  320. const { rows, suffix, pos } = this.getEllipsisOpt();
  321. const updateOverflow = this.shouldTruncated(rows);
  322. // isOverflowed needs to be updated to show tooltip when using css ellipsis
  323. this.setState({
  324. isOverflowed: updateOverflow,
  325. isTruncated: false
  326. });
  327. return undefined;
  328. }
  329. }
  330. getEllipsisState = async ()=> {
  331. const { rows, suffix, pos } = this.getEllipsisOpt();
  332. const { children, strong } = this.props;
  333. // wait until element mounted
  334. if (!this.wrapperRef || !this.wrapperRef.current) {
  335. await this.onResize();
  336. return;
  337. }
  338. const { expanded } = this.state;
  339. const canUseCSSEllipsis = this.canUseCSSEllipsis();
  340. if (canUseCSSEllipsis) {
  341. // const updateOverflow = this.shouldTruncated(rows);
  342. // // isOverflowed needs to be updated to show tooltip when using css ellipsis
  343. // this.setState({
  344. // isOverflowed: updateOverflow,
  345. // isTruncated: false
  346. // });
  347. return ;
  348. }
  349. // If children is null, css/js truncated flag isTruncate is false
  350. if (isNull(children)) {
  351. return new Promise<void>(resolve=>{
  352. this.setState({
  353. isTruncated: false,
  354. isOverflowed: false
  355. }, resolve);
  356. });
  357. }
  358. // Currently only text truncation is supported, if there is non-text,
  359. // both css truncation and js truncation should throw a warning
  360. warning(
  361. 'children' in this.props && typeof children !== 'string',
  362. "[Semi Typography] Only children with pure text could be used with ellipsis at this moment."
  363. );
  364. if (!rows || rows < 0 || expanded) {
  365. return;
  366. }
  367. const extraNode = { expand: this.expandRef.current, copy: this.copyRef && this.copyRef.current };
  368. // Perform type conversion on children to prevent component crash due to non-string type of children
  369. // https://github.com/DouyinFE/semi-design/issues/2167
  370. const realChildren = Array.isArray(children) ? children.join('') : String(children);
  371. const content = getRenderText(
  372. this.wrapperRef.current,
  373. rows,
  374. realChildren,
  375. extraNode,
  376. ELLIPSIS_STR,
  377. suffix,
  378. pos,
  379. strong
  380. );
  381. return new Promise<void>(resolve=>{
  382. this.setState({
  383. isOverflowed: false,
  384. ellipsisContent: content,
  385. isTruncated: realChildren !== content,
  386. }, resolve);
  387. });
  388. }
  389. /**
  390. * Triggered when the fold button is clicked to save the latest expanded state
  391. * @param {Event} e
  392. */
  393. toggleOverflow = (e: React.MouseEvent<HTMLAnchorElement>) => {
  394. const { onExpand, expandable, collapsible } = this.getEllipsisOpt();
  395. const { expanded } = this.state;
  396. onExpand && onExpand(!expanded, e);
  397. if ((expandable && !expanded) || (collapsible && expanded)) {
  398. this.setState({ expanded: !expanded });
  399. }
  400. };
  401. getEllipsisOpt = (): Ellipsis => {
  402. const { ellipsis } = this.props;
  403. if (!ellipsis) {
  404. return {};
  405. }
  406. const opt = {
  407. rows: 1,
  408. expandable: false,
  409. pos: 'end' as EllipsisPos,
  410. suffix: '',
  411. showTooltip: false,
  412. collapsible: false,
  413. expandText: (ellipsis as Ellipsis).expandable ? this.expandStr : undefined,
  414. collapseText: (ellipsis as Ellipsis).collapsible ? this.collapseStr : undefined,
  415. ...(typeof ellipsis === 'object' ? ellipsis : null),
  416. };
  417. return opt;
  418. };
  419. renderExpandable = () => {
  420. const { expanded, isTruncated } = this.state;
  421. if (!isTruncated) return null;
  422. const { expandText, expandable, collapseText, collapsible } = this.getEllipsisOpt();
  423. const noExpandText = !expandable && isUndefined(expandText);
  424. const noCollapseText = !collapsible && isUndefined(collapseText);
  425. let text;
  426. if (!expanded && !noExpandText) {
  427. text = expandText;
  428. } else if (expanded && !noCollapseText) {
  429. text = collapseText;
  430. }
  431. if (!noExpandText || !noCollapseText) {
  432. return (
  433. // TODO: replace `a` tag with `span` in next major version
  434. // NOTE: may have effect on style
  435. // eslint-disable-next-line jsx-a11y/anchor-is-valid
  436. <a
  437. role="button"
  438. tabIndex={0}
  439. className={`${prefixCls}-ellipsis-expand`}
  440. key="expand"
  441. ref={this.expandRef}
  442. aria-label={text}
  443. onClick={this.toggleOverflow}
  444. onKeyPress={e => isEnterPress(e) && this.toggleOverflow(e as any)}
  445. >
  446. {text}
  447. </a>
  448. );
  449. }
  450. return null;
  451. };
  452. /**
  453. * 获取文本的缩略class和style
  454. *
  455. * 截断类型:
  456. * - 当设置中间截断(pos='middle')、可展开(expandable)、有后缀(suffix 非空)、可复制(copyable),启用 JS 截断策略
  457. * - 非以上场景,启用 CSS 截断策略
  458. * 相关变量
  459. * props:
  460. * - ellipsis:
  461. * - rows
  462. * - expandable
  463. * - pos
  464. * - suffix
  465. * state:
  466. * - isOverflowed,文本是否处于overflow状态
  467. * - expanded,文本是否处于折叠状态
  468. * - isTruncated,文本是否被js截断
  469. *
  470. * Get the abbreviated class and style of the text
  471. *
  472. * Truncation type:
  473. * -When setting middle ellipsis (pos='middle')、expandable、suffix is not empty、copyable, the JS ellipsis strategy is enabled
  474. * -Otherwise, enable the CSS ellipsis strategy
  475. * related variables
  476. * props:
  477. * -ellipsis:
  478. * -rows
  479. * -expandable
  480. * -pos
  481. * -suffix
  482. * state:
  483. * -isOverflowed, whether the text is in an overflow state
  484. * -expanded, whether the text is in a collapsed state
  485. * -isTruncated, whether the text is truncated by js
  486. * @returns {Object}
  487. */
  488. getEllipsisStyle = () => {
  489. const { ellipsis, component } = this.props;
  490. if (!ellipsis) {
  491. return {
  492. ellipsisCls: '',
  493. ellipsisStyle: {},
  494. // ellipsisAttr: {}
  495. };
  496. }
  497. const { rows } = this.getEllipsisOpt();
  498. const { expanded } = this.state;
  499. const useCSS = !expanded && this.canUseCSSEllipsis();
  500. const ellipsisCls = cls({
  501. [`${prefixCls}-ellipsis`]: true,
  502. [`${prefixCls}-ellipsis-single-line`]: rows === 1,
  503. [`${prefixCls}-ellipsis-multiple-line`]: rows > 1,
  504. // component === 'span', Text component, It should be externally displayed inline
  505. [`${prefixCls}-ellipsis-multiple-line-text`]: rows > 1 && component === 'span',
  506. [`${prefixCls}-ellipsis-overflow-ellipsis`]: rows === 1 && useCSS,
  507. // component === 'span', Text component, It should be externally displayed inline
  508. [`${prefixCls}-ellipsis-overflow-ellipsis-text`]: rows === 1 && useCSS && component === 'span',
  509. });
  510. const ellipsisStyle = useCSS && rows > 1 ? { WebkitLineClamp: rows } : {};
  511. return {
  512. ellipsisCls,
  513. ellipsisStyle,
  514. };
  515. };
  516. renderEllipsisText = (opt: Ellipsis) => {
  517. const { suffix } = opt;
  518. const { children } = this.props;
  519. const { isTruncated, expanded, ellipsisContent } = this.state;
  520. if (expanded || !isTruncated) {
  521. return (
  522. <span onMouseEnter={this.onHover}>
  523. {children}
  524. {suffix && suffix.length ? suffix : null}
  525. </span>
  526. );
  527. }
  528. return (
  529. <span onMouseEnter={this.onHover}>
  530. {ellipsisContent}
  531. {/* {ELLIPSIS_STR} */}
  532. {suffix}
  533. </span>
  534. );
  535. };
  536. renderOperations() {
  537. return (
  538. <>
  539. {this.renderExpandable()}
  540. {this.renderCopy()}
  541. </>
  542. );
  543. }
  544. renderCopy() {
  545. const { copyable, children } = this.props;
  546. if (!copyable) {
  547. return null;
  548. }
  549. // If it is configured in the content of copyable, the copied content will be the content in copyable
  550. const willCopyContent = (copyable as CopyableConfig)?.content ?? children;
  551. let copyContent: string;
  552. let hasObject = false;
  553. if (Array.isArray(willCopyContent)) {
  554. copyContent = '';
  555. willCopyContent.forEach(value => {
  556. if (typeof value === 'object') {
  557. hasObject = true;
  558. }
  559. copyContent += String(value);
  560. });
  561. } else if (typeof willCopyContent !== 'object') {
  562. copyContent = String(willCopyContent);
  563. } else {
  564. hasObject = true;
  565. copyContent = String(willCopyContent);
  566. }
  567. warning(
  568. hasObject,
  569. 'Content to be copied in Typography is a object, it will case a [object Object] mistake when copy to clipboard.'
  570. );
  571. const copyConfig = {
  572. content: copyContent,
  573. duration: 3,
  574. ...(typeof copyable === 'object' ? copyable : null),
  575. };
  576. return <Copyable {...copyConfig} forwardRef={this.copyRef}/>;
  577. }
  578. renderIcon() {
  579. const { icon, size } = this.props;
  580. const realSize = size === 'inherit' ? this.context : size;
  581. if (!icon) {
  582. return null;
  583. }
  584. const iconSize: Size = realSize === 'small' ? 'small' : 'default';
  585. return (
  586. <span className={`${prefixCls}-icon`} x-semi-prop="icon">
  587. {isSemiIcon(icon) ? React.cloneElement((icon as React.ReactElement), { size: iconSize }) : icon}
  588. </span>
  589. );
  590. }
  591. renderContent() {
  592. const {
  593. component,
  594. children,
  595. className,
  596. type,
  597. spacing,
  598. disabled,
  599. style,
  600. ellipsis,
  601. icon,
  602. size,
  603. link,
  604. heading,
  605. weight,
  606. ...rest
  607. } = this.props;
  608. const textProps = omit(rest, [
  609. 'strong',
  610. 'editable',
  611. 'mark',
  612. 'copyable',
  613. 'underline',
  614. 'code',
  615. // 'link',
  616. 'delete',
  617. ]);
  618. const realSize = size === 'inherit' ? this.context : size;
  619. const iconNode = this.renderIcon();
  620. const ellipsisOpt = this.getEllipsisOpt();
  621. const { ellipsisCls, ellipsisStyle } = this.getEllipsisStyle();
  622. let textNode = ellipsis ? this.renderEllipsisText(ellipsisOpt) : children;
  623. const linkCls = cls({
  624. [`${prefixCls}-link-text`]: link,
  625. [`${prefixCls}-link-underline`]: this.props.underline && link,
  626. });
  627. textNode = wrapperDecorations(
  628. this.props,
  629. <>
  630. {iconNode}
  631. {this.props.link ? <span className={linkCls}>{textNode}</span> : textNode}
  632. </>
  633. );
  634. const hTagReg = /^h[1-6]$/;
  635. const isHeader = isString(heading) && hTagReg.test(heading);
  636. const wrapperCls = cls(className, ellipsisCls, {
  637. // [`${prefixCls}-primary`]: !type || type === 'primary',
  638. [`${prefixCls}-${type}`]: type && !link,
  639. [`${prefixCls}-${realSize}`]: realSize,
  640. [`${prefixCls}-link`]: link,
  641. [`${prefixCls}-disabled`]: disabled,
  642. [`${prefixCls}-${spacing}`]: spacing,
  643. [`${prefixCls}-${heading}`]: isHeader,
  644. [`${prefixCls}-${heading}-weight-${weight}`]: isHeader && weight && isNaN(Number(weight)),
  645. });
  646. const textStyle: CSSProperties = {
  647. ...(
  648. isNaN(Number(weight)) ? {} : { fontWeight: weight }
  649. ),
  650. ...style
  651. };
  652. return (
  653. <Typography
  654. className={wrapperCls}
  655. style={{ ...textStyle, ...ellipsisStyle }}
  656. component={component}
  657. forwardRef={this.wrapperRef}
  658. {...textProps}
  659. >
  660. {textNode}
  661. {this.renderOperations()}
  662. </Typography>
  663. );
  664. }
  665. renderTipWrapper() {
  666. const { children } = this.props;
  667. const showTooltip = this.showTooltip();
  668. const content = this.renderContent();
  669. if (showTooltip) {
  670. const { type, opts, renderTooltip } = showTooltip as ShowTooltip;
  671. if (isFunction(renderTooltip)) {
  672. return renderTooltip(children, content);
  673. } else if (type.toLowerCase() === 'popover') {
  674. return (
  675. <Popover content={children} position="top" {...opts}>
  676. {content}
  677. </Popover>
  678. );
  679. }
  680. return (
  681. <Tooltip content={children} position="top" {...opts}>
  682. {content}
  683. </Tooltip>
  684. );
  685. } else {
  686. return content;
  687. }
  688. }
  689. render() {
  690. const { size } = this.props;
  691. const realSize = size === 'inherit' ? this.context : size;
  692. const content = (
  693. <SizeContext.Provider value={realSize}>
  694. <LocaleConsumer componentName="Typography">
  695. {(locale: Locale['Typography']) => {
  696. this.expandStr = locale.expand;
  697. this.collapseStr = locale.collapse;
  698. return this.renderTipWrapper();
  699. }}
  700. </LocaleConsumer>
  701. </SizeContext.Provider>
  702. );
  703. if (this.props.ellipsis) {
  704. return (
  705. <ResizeObserver onResize={(...args)=>{
  706. if (this.observerTakingEffect) {
  707. this.onResize(...args);
  708. }
  709. }} observeParent observerProperty={ObserverProperty.Width}>
  710. {content}
  711. </ResizeObserver>
  712. );
  713. }
  714. return content;
  715. }
  716. }