base.tsx 27 KB

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