diff --git a/site/site.config.mjs b/site/site.config.mjs index 48fe71275..b0631bb30 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -97,6 +97,14 @@ export const docs = [ component: () => import('tdesign-react/link/link.md'), componentEn: () => import('tdesign-react/link/link.en-US.md'), }, + { + title: 'Typography 排版', + titleEn: 'Typography', + name: 'typography', + path: '/react/components/typography', + component: () => import('tdesign-react/typography/typography.md'), + componentEn: () => import('tdesign-react/typography/typography.en-US.md'), + }, ], }, { diff --git a/site/test-coverage.js b/site/test-coverage.js index 5a8d2a648..7f84f6181 100644 --- a/site/test-coverage.js +++ b/site/test-coverage.js @@ -1,9 +1,9 @@ module.exports = { "Util": { - "statements": "61.53%", - "branches": "48.77%", + "statements": "61.88%", + "branches": "48.57%", "functions": "69.29%", - "lines": "63.26%" + "lines": "63.77%" }, "affix": { "statements": "84.84%", @@ -18,10 +18,10 @@ module.exports = { "lines": "100%" }, "anchor": { - "statements": "92.98%", + "statements": "93.85%", "branches": "69.56%", "functions": "88%", - "lines": "97.08%" + "lines": "98.05%" }, "autoComplete": { "statements": "96.17%", @@ -114,10 +114,10 @@ module.exports = { "lines": "68.75%" }, "datePicker": { - "statements": "62.47%", - "branches": "43.3%", - "functions": "60%", - "lines": "66.16%" + "statements": "62.55%", + "branches": "44.05%", + "functions": "60.6%", + "lines": "66.24%" }, "descriptions": { "statements": "98.82%", @@ -127,7 +127,7 @@ module.exports = { }, "dialog": { "statements": "85%", - "branches": "71.42%", + "branches": "71.92%", "functions": "85%", "lines": "87.24%" }, @@ -162,10 +162,10 @@ module.exports = { "lines": "84.21%" }, "guide": { - "statements": "100%", - "branches": "94.2%", + "statements": "99.32%", + "branches": "92.85%", "functions": "100%", - "lines": "100%" + "lines": "99.31%" }, "hooks": { "statements": "55.22%", @@ -174,10 +174,10 @@ module.exports = { "lines": "55.19%" }, "image": { - "statements": "88.76%", + "statements": "88.88%", "branches": "82.53%", "functions": "80%", - "lines": "91.76%" + "lines": "91.86%" }, "imageViewer": { "statements": "77.85%", @@ -186,10 +186,10 @@ module.exports = { "lines": "77.62%" }, "input": { - "statements": "93.78%", + "statements": "93.82%", "branches": "92.24%", "functions": "89.47%", - "lines": "94.11%" + "lines": "94.15%" }, "inputAdornment": { "statements": "86.95%", @@ -204,10 +204,10 @@ module.exports = { "lines": "80.16%" }, "layout": { - "statements": "91.3%", + "statements": "91.48%", "branches": "41.66%", "functions": "85.71%", - "lines": "91.3%" + "lines": "91.48%" }, "link": { "statements": "100%", @@ -240,9 +240,9 @@ module.exports = { "lines": "90.51%" }, "message": { - "statements": "88.31%", + "statements": "88.96%", "branches": "86.66%", - "functions": "68.18%", + "functions": "70.45%", "lines": "94.28%" }, "notification": { @@ -282,10 +282,10 @@ module.exports = { "lines": "83.58%" }, "rangeInput": { - "statements": "75%", - "branches": "67.44%", + "statements": "74.02%", + "branches": "62.79%", "functions": "48.14%", - "lines": "74.66%" + "lines": "73.68%" }, "rate": { "statements": "96.22%", @@ -355,7 +355,7 @@ module.exports = { }, "tabs": { "statements": "90.96%", - "branches": "78.64%", + "branches": "79.04%", "functions": "86.36%", "lines": "91.17%" }, @@ -413,6 +413,12 @@ module.exports = { "functions": "97.61%", "lines": "97.22%" }, + "typography": { + "statements": "95.58%", + "branches": "76.31%", + "functions": "81.81%", + "lines": "98.46%" + }, "upload": { "statements": "96.66%", "branches": "95.65%", diff --git a/src/_common b/src/_common index 78a589aea..1d0df6ee8 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 78a589aea1301c99fab72de56d4439744a897afb +Subproject commit 1d0df6ee8ddee26c3f8543504a598c3edc466f06 diff --git a/src/_util/copyText.ts b/src/_util/copyText.ts index dfa4e44e1..4a1a26529 100644 --- a/src/_util/copyText.ts +++ b/src/_util/copyText.ts @@ -1,4 +1,8 @@ +import { canUseDocument } from './dom'; + export default function copyText(text: string) { + if (!canUseDocument) return; + if ('clipboard' in navigator) { navigator.clipboard.writeText(text); return; @@ -16,7 +20,7 @@ export default function copyText(text: string) { selection.removeAllRanges(); selection.addRange(range); - document.execCommand('copy'); + document.execCommand?.('copy'); selection.removeAllRanges(); document.body.removeChild(textarea); } diff --git a/src/config-provider/config-provider.en-US.md b/src/config-provider/config-provider.en-US.md index 4504d00c2..becaa7bcf 100644 --- a/src/config-provider/config-provider.en-US.md +++ b/src/config-provider/config-provider.en-US.md @@ -320,3 +320,11 @@ finishButtonProps | Object | - | finish button in last step. `{ content: 'Finish nextButtonProps | Object | - | next step button. `{ content: 'Next Button', theme: 'primary' }`。Typescript:`ButtonProps` | N prevButtonProps | Object | - | previous step button. `{ content: 'Previous Step', theme: 'default' }`。Typescript:`ButtonProps` | N skipButtonProps | Object | - | skip button. `{ content: 'Skip', theme: 'default' }`。Typescript:`ButtonProps` | N + +### TypographyConfig + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +collapseText | String | - | collapse text | N +expandText | String | - | expand text | N +copiedText | String | - | copied text | N \ No newline at end of file diff --git a/src/config-provider/config-provider.md b/src/config-provider/config-provider.md index bd97571d7..c73e8635d 100644 --- a/src/config-provider/config-provider.md +++ b/src/config-provider/config-provider.md @@ -63,6 +63,7 @@ timePicker | Object | - | 时间选择器全局配置。TS 类型:`TimePickerC transfer | Object | - | 穿梭框全局配置。TS 类型:`TransferConfig` | N tree | Object | - | 树组件全局配置。TS 类型:`TreeConfig` | N treeSelect | Object | - | 树选择器组件全局配置。TS 类型:`TreeSelectConfig` | N +typography | Object | - | 排版组件全局配置。TS 类型:`TypographyConfig` | N upload | Object | - | 上传组件全局配置。TS 类型:`UploadConfig` | N ### InputConfig @@ -350,3 +351,11 @@ finishButtonProps | Object | - | 最后一步中的完成按钮,示例:`{ co nextButtonProps | Object | - | 下一步按钮,示例:`{ content: '下一步', theme: 'primary' }`。TS 类型:`ButtonProps` | N prevButtonProps | Object | - | 上一步按钮,示例:`{ content: '上一步', theme: 'default' }`。TS 类型:`ButtonProps` | N skipButtonProps | Object | - | 跳过按钮,示例:`{ content: '跳过', theme: 'default' }`。TS 类型:`ButtonProps` | N + +### TypographyConfig + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +collapseText | String | - | 语言配置,“收起”描述文本 | N +expandText | String | - | 语言配置,“展开”描述文本 | N +copiedText | String | - | 语言配置,“复制成功”描述文本 | N \ No newline at end of file diff --git a/src/config-provider/type.ts b/src/config-provider/type.ts index f25337dbc..17de983e3 100644 --- a/src/config-provider/type.ts +++ b/src/config-provider/type.ts @@ -126,6 +126,10 @@ export interface GlobalConfigProvider { * 树选择器组件全局配置 */ treeSelect?: TreeSelectConfig; + /** + * 排版全局配置 + */ + typography?: TypographyConfig; /** * 上传组件全局配置 */ @@ -886,6 +890,24 @@ export interface GuideConfig { skipButtonProps?: ButtonProps; } +export interface TypographyConfig { + /** + * 语言配置,“收起”描述文本 + * @default '' + */ + collapseText?: string; + /** + * 语言配置,“展开”描述文本 + * @default '' + */ + expandText?: string; + /** + * 语言配置,“复制成功”描述文本 + * @default '' + */ + copiedText?: string; +} + export type AnimationType = 'ripple' | 'expand' | 'fade'; export type IconConfig = GlobalIconConfig; diff --git a/src/index.ts b/src/index.ts index 2b8142ca9..b6101a916 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,3 +66,4 @@ export * from './guide'; export * from './back-top'; export * from './statistic'; export * from './descriptions'; +export * from './typography'; diff --git a/src/table/_example/base.jsx b/src/table/_example/base.jsx index dcbface2e..4353f1dd2 100644 --- a/src/table/_example/base.jsx +++ b/src/table/_example/base.jsx @@ -70,7 +70,6 @@ export default function TableBasic() { rowClassName={({ rowIndex }) => `${rowIndex}-class`} cellEmptyContent={'-'} resizable - bordered // 与pagination对齐 pagination={{ defaultCurrent: 2, diff --git a/src/typography/Paragraph.tsx b/src/typography/Paragraph.tsx new file mode 100644 index 000000000..239084038 --- /dev/null +++ b/src/typography/Paragraph.tsx @@ -0,0 +1,45 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; +import Ellipsis from './ellipsis/Ellipsis'; +import { paragraphDefaultProps } from './defaultProps'; + +import useConfig from '../hooks/useConfig'; +import useEllipsis from './ellipsis/useEllipsis'; +import useDefaultProps from '../hooks/useDefaultProps'; + +import type { StyledProps } from '../common'; +import type { TdParagraphProps } from './type'; + +export type TypographyParagraphProps = TdParagraphProps & + StyledProps & { + children: React.ReactNode; + }; + +const Paragraph = forwardRef((originalProps, ref) => { + const { classPrefix } = useConfig(); + const props = useDefaultProps(originalProps, paragraphDefaultProps); + + const { ellipsis, children, className, content, ...rest } = props; + const prefixCls = `${classPrefix}-typography`; + + const { ellipsisProps } = useEllipsis(ellipsis); + + if (!ellipsis) { + return ( +
+ {children || content} +
+ ); + } + + return ( + + {children || content} + + ); +}); + +Paragraph.displayName = 'Paragraph'; +Paragraph.defaultProps = paragraphDefaultProps; + +export default Paragraph; diff --git a/src/typography/Text.tsx b/src/typography/Text.tsx new file mode 100644 index 000000000..ddb1595af --- /dev/null +++ b/src/typography/Text.tsx @@ -0,0 +1,176 @@ +import React, { ReactElement, useRef, forwardRef, useState } from 'react'; +import classNames from 'classnames'; +import { CheckIcon, CopyIcon } from 'tdesign-icons-react'; + +import Ellipsis from './ellipsis/Ellipsis'; +import useConfig from '../hooks/useConfig'; +import useEllipsis from './ellipsis/useEllipsis'; +import Button from '../button/Button'; +import Tooltip from '../tooltip'; +import { useLocaleReceiver } from '../locale/LocalReceiver'; +import useDefaultProps from '../hooks/useDefaultProps'; +import { textDefaultProps } from './defaultProps'; +import copyText from '../_util/copyText'; + +import type { StyledProps } from '../common'; +import type { TdTextProps } from './type'; + +export type TypographyTextProps = TdTextProps & + StyledProps & { + children: React.ReactNode; + }; + +const Text = forwardRef((originalProps, ref) => { + const { classPrefix } = useConfig(); + const props = useDefaultProps(originalProps, textDefaultProps); + + const prefixCls = `${classPrefix}-typography`; + + const [local, t] = useLocaleReceiver('typography'); + const copiedText = t(local.copiedText); + + const { + theme, + disabled, + className, + copyable, + strong, + mark, + code, + keyboard, + underline, + delete: deleteProp, + italic, + children, + ellipsis, + ...rest + } = props; + + const getComponent = () => { + const componentMap = { + strong: !!strong, + mark: !!mark, + code: !!code, + kbd: !!keyboard, + u: !!underline, + del: !!deleteProp, + i: !!italic, + }; + return Object.entries(componentMap).find(([, condition]) => !!condition)?.[0] as keyof HTMLElementTagNameMap; + }; + + const currentRef = useRef(); + const { ellipsisProps } = useEllipsis(ellipsis); + const Component = getComponent(); + + const textEllipsisProps = { + ...ellipsisProps, + }; + + const [isCopied, setIsCopied] = useState(false); + + const copyProps = + typeof copyable === 'boolean' + ? { + text: children.toString(), + onCopy: Function.prototype, + tooltipProps: isCopied + ? { + content: copiedText, + } + : null, + } + : { + text: copyable?.text || children.toString(), + onCopy: copyable?.onCopy?.(), + tooltipProps: { + ...copyable?.tooltipProps, + content: isCopied ? copiedText : copyable?.tooltipProps?.content, + }, + suffix: copyable?.suffix, + }; + + const handleCopy = () => { + copyText(copyProps?.text); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 1500); + if (typeof copyProps.onCopy === 'function') copyProps.onCopy(); + }; + + const renderContent = (withChildren: boolean) => { + const { tooltipProps } = copyProps; + const wrapWithTooltip = (wrapContent: React.ReactNode) => + tooltipProps ? {wrapContent} : wrapContent; + + const getSuffix = (): ReactElement => { + if (typeof copyProps?.suffix === 'function') { + return copyProps.suffix({ copied: isCopied }) as ReactElement; + } + return isCopied ? : ; + }; + + return ( + <> + {withChildren ? children : null} + {copyable + ? wrapWithTooltip( + + + ), + expandable: true, + collapsible: true, + }} + > + {textString} + + '', + expandable: false, + tooltipProps: { + content: 'long long long text', + }, + onExpand: handleExpand, + }} + style={{ width: isExpanded ? '100%' : 300, display: 'block' }} + > + {textString} + + + ( + + {expanded ? ( + + ) : ( + + )} + + ), + expandable: true, + }} + > + {textString} + + + ); +}; + +export default EllipsisExample; diff --git a/src/typography/_example/text.jsx b/src/typography/_example/text.jsx new file mode 100644 index 000000000..8bc3ec8b4 --- /dev/null +++ b/src/typography/_example/text.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Typography, Space } from 'tdesign-react'; + +const { Text } = Typography; + +export default function TextExample() { + return ( + + TDesign (primary) + TDesign (secondary) + TDesign (disabled) + TDesign (success) + TDesign (warning) + TDesign (error) + TDesign (mark) + TDesign (code) + TDesign (keyboard) + TDesign (underline) + TDesign (delete) + TDesign (strong) + TDesign (italic) + + ); +} diff --git a/src/typography/_example/title.jsx b/src/typography/_example/title.jsx new file mode 100644 index 000000000..3cbfeeb19 --- /dev/null +++ b/src/typography/_example/title.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Typography } from 'tdesign-react'; + +const { Title } = Typography; + +const TitleExample = () => ( + <> + H1. TDesign + H2. TDesign + H3. TDesign + H4. TDesign + H5. TDesign + H6. TDesign + +); + +export default TitleExample; diff --git a/src/typography/defaultProps.ts b/src/typography/defaultProps.ts new file mode 100644 index 000000000..2a427dc0d --- /dev/null +++ b/src/typography/defaultProps.ts @@ -0,0 +1,22 @@ +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdTextProps, TdTitleProps, TdParagraphProps } from './type'; + +export const textDefaultProps: TdTextProps = { + code: false, + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + italic: false, + keyboard: false, + mark: false, + strong: false, + underline: false, +}; + +export const titleDefaultProps: TdTitleProps = { ellipsis: false, level: 'h1' }; + +export const paragraphDefaultProps: TdParagraphProps = { ellipsis: false }; diff --git a/src/typography/ellipsis/Ellipsis.tsx b/src/typography/ellipsis/Ellipsis.tsx new file mode 100644 index 000000000..d0416252e --- /dev/null +++ b/src/typography/ellipsis/Ellipsis.tsx @@ -0,0 +1,106 @@ +import React, { ReactNode, useEffect, useRef, useState } from 'react'; + +import classNames from 'classnames'; + +import Truncate from './Truncate'; +import useConfig from '../../hooks/useConfig'; + +export type TdEllipsis = { + className?: string; + children: ReactNode; + lines: number; + ellipsisClassName?: string; + ellipsisPrefix?: ReactNode; + onToggleExpand?: (isExpanded: boolean, e: React.MouseEvent) => void; + width?: number; + onTruncate?: (truncated: boolean) => void; + component?: keyof HTMLElementTagNameMap; + collapsible: boolean; + expandable: boolean; + more: ReactNode; + less: ReactNode; +}; + +const Ellipsis = ({ + className, + children, + lines = 1, + ellipsisClassName, + ellipsisPrefix = '...', + onToggleExpand, + width = 0, + onTruncate, + component: Component = 'div', + collapsible = false, + expandable = false, + more, + less, + ...rest +}: TdEllipsis & { children: React.ReactNode }) => { + const { classPrefix } = useConfig(); + const symbolClassName = ellipsisClassName || `${classPrefix}-typography-ellipsis-symbol`; + + const isMountRef = useRef(false); + useEffect(() => { + isMountRef.current = true; + }, []); + + const [isExpanded, setIsExpanded] = useState(false); + const handleToggleExpand = (e: React.MouseEvent) => { + if (!expandable) return; + + if (isMountRef.current) { + setIsExpanded(!isExpanded); + onToggleExpand?.(!isExpanded, e); + } + }; + + const truncateRef = useRef(); + const [isTruncated, setTruncated] = useState(false); + const handleTruncate = (truncated) => { + if (isMountRef.current && truncated !== isTruncated) { + setTruncated(truncated); + + if (truncated && truncateRef.current) { + truncateRef.current.onResize?.(); + } + onTruncate?.(truncated); + } + }; + + const componentProps = { + className, + ...rest, + }; + + return ( + // @ts-ignore + + + {ellipsisPrefix} + + {more} + + + } + onTruncate={handleTruncate} + ref={truncateRef} + lineClassName={`${classPrefix}-typography-ellipsis-line`} + > + {children} + + {!isTruncated && collapsible && isExpanded && ( + + {less} + + )} + + ); +}; + +export default Ellipsis; diff --git a/src/typography/ellipsis/Truncate.tsx b/src/typography/ellipsis/Truncate.tsx new file mode 100644 index 000000000..42c758ef9 --- /dev/null +++ b/src/typography/ellipsis/Truncate.tsx @@ -0,0 +1,446 @@ +/** + * LICENSE + * https://github.com/pablosichert/react-truncate/blob/master/LICENSE.md + * + * ISC License + * +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is +hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +import React from 'react'; +import omit from 'lodash/omit'; +import PropTypes from 'prop-types'; + +export type TruncateProps = { + children: React.ReactNode; + ellipsis: React.ReactNode; + onTruncate: (truncated: boolean) => void; + lines: number; + trimWhitespace: boolean; + width: number; + className: string; + lineClassName: string; +}; + +export type TruncateState = { + targetWidth?: number; +}; + +export default class Truncate extends React.Component { + static propTypes = { + children: PropTypes.node, + ellipsis: PropTypes.node, + lines: PropTypes.oneOfType([PropTypes.oneOf([false]), PropTypes.number]), + trimWhitespace: PropTypes.bool, + width: PropTypes.number, + onTruncate: PropTypes.func, + className: PropTypes.string, + lineClassName: PropTypes.string, + }; + + static defaultProps = { + children: '', + ellipsis: '...', + lines: 1, + trimWhitespace: false, + width: 0, + lineClassName: 'truncate-line', + }; + + elements: Record; + + replacedLinks: Array>; + + calculatedEllipsisWidth: boolean; + + canvasContext: CanvasRenderingContext2D; + + timeout: number; + + state: TruncateState = {}; + + constructor(props: TruncateProps) { + super(props); + + this.elements = {}; + this.replacedLinks = []; + this.calculatedEllipsisWidth = false; + this.canvasContext; + } + + componentDidMount() { + const { + elements: { text }, + calcTargetWidth, + onResize, + } = this; + + const canvas = document.createElement('canvas'); + this.canvasContext = canvas.getContext('2d'); + + calcTargetWidth(() => { + // Node not needed in document tree to read its content + if (text && text.parentNode) { + text.parentNode.removeChild(text); + } + }); + + window.addEventListener('resize', onResize); + } + + componentDidUpdate(prevProps: TruncateProps) { + // Render was based on outdated refs and needs to be rerun + if (this.props.children !== prevProps.children) { + this.forceUpdate(); + } + + // If the width prop has changed, recalculate size of contents + if (this.props.width !== prevProps.width) { + this.calcTargetWidth(); + } + } + + componentWillUnmount() { + const { + elements: { ellipsis }, + onResize, + timeout, + } = this; + + if (ellipsis.parentNode && this.calculatedEllipsisWidth) { + ellipsis.parentNode.removeChild(ellipsis); + } + + window.removeEventListener('resize', onResize); + window.cancelAnimationFrame(timeout); + } + + extractReplaceLinksKeys = (content) => { + const i = 0; + this.replacedLinks = []; + + content.replace(/(]+)>((?:.(?!<\/a>))*.)<\/a>)/g, (...rest) => { + const item = Array.prototype.slice.call(rest, 1, 4); + item.key = `[${'@'.repeat(item[2].length - 1)}=${i + 1}]`; + this.replacedLinks.push(item); + + // eslint-disable-next-line no-param-reassign + content = content.replace(item[0], item.key); + }); + + return content; + }; + + restoreReplacedLinks = (content: string) => { + this.replacedLinks.forEach((item) => { + // eslint-disable-next-line no-param-reassign + content = content.replace(item.key, item[0]); + }); + + return this.createMarkup(content) as unknown as string; + }; + + // Shim innerText to consistently break lines at
but not at \n + innerText = (node: HTMLElement) => { + const div = document.createElement('div'); + const contentKey = 'innerText' in window.HTMLElement.prototype ? 'innerText' : 'textContent'; + + const content = node.innerHTML.replace(/\r\n|\r|\n/g, ' '); + div.innerHTML = this.extractReplaceLinksKeys(content); + + let text = div[contentKey]; + + const test = document.createElement('div'); + test.innerHTML = 'foo
bar'; + + if (test[contentKey].replace(/\r\n|\r/g, '\n') !== 'foo\nbar') { + div.innerHTML = div.innerHTML.replace(//gi, '\n'); + text = div[contentKey]; + } + + return text; + }; + + onResize = () => { + this.calcTargetWidth(); + }; + + onTruncate = (didTruncate: boolean) => { + const { onTruncate } = this.props; + + if (typeof onTruncate === 'function') { + this.timeout = window.requestAnimationFrame(() => { + onTruncate(didTruncate); + }); + } + }; + + calcTargetWidth = (callback?: () => void) => { + const { + elements: { target }, + calcTargetWidth, + canvasContext, + props: { width }, + } = this; + + // Calculation is no longer relevant, since node has been removed + if (!target) { + return; + } + + const targetWidth = + width || + // Floor the result to deal with browser subpixel precision + Math.floor(target.parentElement.getBoundingClientRect().width); + + // Delay calculation until parent node is inserted to the document + // Mounting order in React is ChildComponent, ParentComponent + if (!targetWidth) { + return window.requestAnimationFrame(() => calcTargetWidth(callback)); + } + + const style = window.getComputedStyle(target); + + const font = [style['font-weight'], style['font-style'], style['font-size'], style['font-family']].join(' '); + + canvasContext.font = font; + + this.setState( + { + targetWidth, + }, + callback, + ); + }; + + measureWidth = (text: string) => this.canvasContext.measureText(text).width; + + ellipsisWidth = (node: HTMLElement) => { + this.calculatedEllipsisWidth = true; + return node.offsetWidth; + }; + + trimRight = (text: string) => text.replace(/\s+$/, ''); + + createMarkup = (str: string) => ( + + ); + + getLines = () => { + const { + elements, + props: { lines: numLines, ellipsis, trimWhitespace, lineClassName }, + state: { targetWidth }, + innerText, + measureWidth, + onTruncate, + trimRight, + renderLine, + restoreReplacedLinks, + } = this; + + const lines = []; + const text = innerText(elements.text); + + const textLines = text.split('\n').map((line) => line.split(' ')); + let didTruncate = true; + const ellipsisWidth = this.ellipsisWidth(this.elements.ellipsis); + + for (let line = 1; line <= numLines; line++) { + const textWords = textLines[0]; + + // Handle newline + if (textWords.length === 0) { + lines.push(); + textLines.shift(); + line -= 1; + continue; + } + + let resultLine: string | React.JSX.Element = textWords.join(' '); + if (measureWidth(resultLine) <= targetWidth) { + if (textLines.length === 1) { + // Line is end of text and fits without truncating + didTruncate = false; + + resultLine = restoreReplacedLinks(resultLine); + lines.push(resultLine); + + break; + } + } + + if (line === numLines) { + // Binary search determining the longest possible line including truncate string + const textRest = textWords.join(' '); + + let lower = 0; + let upper = textRest.length - 1; + + while (lower <= upper) { + const middle = Math.floor((lower + upper) / 2); + + const testLine = textRest.slice(0, middle + 1); + + if (measureWidth(testLine) + ellipsisWidth <= targetWidth) { + lower = middle + 1; + } else { + upper = middle - 1; + } + } + + let lastLineText = textRest.slice(0, lower); + + if (trimWhitespace) { + lastLineText = trimRight(lastLineText); + + // Remove blank lines from the end of text + while (!lastLineText.length && lines.length) { + const prevLine = lines.pop(); + + lastLineText = trimRight(prevLine); + } + } + + if (lastLineText.substr(lastLineText.length - 2) === '][') { + lastLineText = lastLineText.substring(0, lastLineText.length - 1); + } + + lastLineText = lastLineText.replace(/\[@+$/, ''); + lastLineText = restoreReplacedLinks(lastLineText); + + resultLine = ( + + {lastLineText} + {ellipsis} + + ); + } else { + // Binary search determining when the line breaks + let lower = 0; + let upper = textWords.length - 1; + + while (lower <= upper) { + const middle = Math.floor((lower + upper) / 2); + + const testLine = textWords.slice(0, middle + 1).join(' '); + + if (measureWidth(testLine) <= targetWidth) { + lower = middle + 1; + } else { + upper = middle - 1; + } + } + + // The first word of this line is too long to fit it + if (lower === 0) { + // Jump to processing of last line + line = numLines - 1; + continue; + } + + resultLine = textWords.slice(0, lower).join(' '); + + resultLine = restoreReplacedLinks(resultLine); + + textLines[0].splice(0, lower); + } + + lines.push(resultLine); + } + + onTruncate(didTruncate); + + return lines.map(renderLine); + }; + + renderLine = (line: React.ReactNode, i: number, arr: Array) => { + const { lineClassName } = this.props; + + if (i === arr.length - 1) { + return ( + + {line} + + ); + } + const br =
; + if (line) { + return [ + + {line} + , + br, + ]; + } + return br; + }; + + render() { + const { + elements: { target }, + props: { children, ellipsis, lines, ...spanProps }, + state: { targetWidth }, + getLines, + onTruncate, + } = this; + + let text: React.ReactNode; + + const mounted = !!(target && targetWidth); + + if (typeof window !== 'undefined' && mounted) { + if (lines > 0) { + text = getLines(); + } else { + text = children; + + onTruncate(false); + } + } + + return ( + { + this.elements.target = targetEl; + }} + > + 0 ? `${spanProps.width}px` : 'unset', + }} + > + {text} + + { + this.elements.text = textEl; + }} + > + {children} + + { + this.elements.ellipsis = ellipsisEl; + }} + style={{ + position: 'fixed', + visibility: 'hidden', + top: 0, + left: 0, + }} + > + {ellipsis} + + + ); + } +} diff --git a/src/typography/ellipsis/style/css.js b/src/typography/ellipsis/style/css.js new file mode 100644 index 000000000..f388879ac --- /dev/null +++ b/src/typography/ellipsis/style/css.js @@ -0,0 +1 @@ +import '../../style/index.css'; diff --git a/src/typography/ellipsis/style/index.js b/src/typography/ellipsis/style/index.js new file mode 100644 index 000000000..4a2e7d583 --- /dev/null +++ b/src/typography/ellipsis/style/index.js @@ -0,0 +1 @@ +import '../../style/index.js'; diff --git a/src/typography/ellipsis/useEllipsis.tsx b/src/typography/ellipsis/useEllipsis.tsx new file mode 100644 index 000000000..37188347e --- /dev/null +++ b/src/typography/ellipsis/useEllipsis.tsx @@ -0,0 +1,81 @@ +/* eslint-disable no-nested-ternary */ +import React, { useState } from 'react'; +import isFunction from 'lodash/isFunction'; + +import { TypographyEllipsis } from '../type'; +import Tooltip from '../../tooltip'; +import { useLocaleReceiver } from '../../locale/LocalReceiver'; + +export default function useEllipsis(ellipsis: boolean | TypographyEllipsis) { + const [local, t] = useLocaleReceiver('typography'); + const expandText = t(local.expandText); + const collapseText = t(local.collapseText); + + let formattedEllipsis: TypographyEllipsis = {}; + if (ellipsis) { + formattedEllipsis = + ellipsis === true + ? { + row: 1, + expandable: false, + tooltipProps: null, + suffix: () => '', + collapsible: true, + } + : { + row: ellipsis.row || 1, + expandable: ellipsis.expandable ?? false, + tooltipProps: ellipsis.tooltipProps || null, + suffix: ({ expanded }) => + typeof ellipsis?.suffix === 'function' + ? ellipsis?.suffix({ expanded }) + : expanded + ? collapseText + : ellipsis?.expandable + ? `${expandText}` + : '...', + collapsible: ellipsis?.collapsible ?? false, + }; + } + + const [isClamped, setIsClamped] = useState(true); + const handleExpand = (expanded: boolean) => { + if (typeof expanded !== 'boolean') return; + setIsClamped(!expanded); + (ellipsis as TypographyEllipsis).onExpand?.(!expanded); + }; + + const getEllipsisSuffix = () => { + let moreOrLess: React.ReactNode; + if (isFunction(formattedEllipsis.suffix)) moreOrLess = formattedEllipsis.suffix?.({ expanded: !isClamped }); + else moreOrLess = formattedEllipsis.suffix; + + if (formattedEllipsis?.tooltipProps && !!moreOrLess) { + return {moreOrLess}; + } + return moreOrLess; + }; + + const getEllipsisPrefix = () => { + let moreOrLess: React.ReactNode; + if (isFunction(formattedEllipsis.suffix)) moreOrLess = formattedEllipsis.suffix?.({ expanded: !isClamped }); + else moreOrLess = formattedEllipsis.suffix; + + if (formattedEllipsis?.tooltipProps && !moreOrLess) { + return ...; + } + return '...'; + }; + + const ellipsisProps = { + lines: formattedEllipsis.row, + ellipsisPrefix: getEllipsisPrefix(), + more: getEllipsisSuffix(), + less: getEllipsisSuffix(), + onToggleExpand: handleExpand, + expandable: formattedEllipsis.expandable, + collapsible: formattedEllipsis.collapsible, + }; + + return { ellipsisProps }; +} diff --git a/src/typography/index.ts b/src/typography/index.ts new file mode 100644 index 000000000..7bd6e333b --- /dev/null +++ b/src/typography/index.ts @@ -0,0 +1,8 @@ +import _Typography from './Typography'; + +import './style/index.js'; + +export * from './type'; + +export const Typography = _Typography; +export default Typography; diff --git a/src/typography/style/css.js b/src/typography/style/css.js new file mode 100644 index 000000000..6a9a4b132 --- /dev/null +++ b/src/typography/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/typography/style/index.js b/src/typography/style/index.js new file mode 100644 index 000000000..df223430b --- /dev/null +++ b/src/typography/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/typography/_index.less'; diff --git a/src/typography/type.ts b/src/typography/type.ts new file mode 100644 index 000000000..51b33d6b6 --- /dev/null +++ b/src/typography/type.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TooltipProps } from '../tooltip'; +import { TNode } from '../common'; + +export interface TdTextProps { + /** + * 文本内容,同content + */ + children?: TNode; + /** + * 是否添加代码样式 + * @default false + */ + code?: boolean; + /** + * 是否可复制,可通过配置参数自定义复制操作的具体功能和样式 + * @default false + */ + copyable?: boolean | TypographyCopyable; + /** + * 是否添加删除线样式 + * @default false + */ + delete?: boolean; + /** + * 是否添加不可用样式 + * @default false + */ + disabled?: boolean; + /** + * 是否省略展示,可通过配置参数自定义省略操作的具体功能和样式 + * @default false + */ + ellipsis?: boolean | TypographyEllipsis; + /** + * 文本是否为斜体 + * @default false + */ + italic?: boolean; + /** + * 是否添加键盘样式 + * @default false + */ + keyboard?: boolean; + /** + * 是否添加标记样式,默认为黄色,可通过配置颜色修改标记样式,如#0052D9 + * @default false + */ + mark?: string | boolean; + /** + * 文本是否加粗 + * @default false + */ + strong?: boolean; + /** + * 主题 + */ + theme?: 'primary' | 'secondary' | 'success' | 'warning' | 'error'; + /** + * 是否添加下划线样式 + * @default false + */ + underline?: boolean; +} + +export interface TdTitleProps { + /** + * 段落内容,同 content + */ + children?: TNode; + /** + * 段落内容 + */ + content?: TNode; + /** + * 是否省略展示,可通过配置参数自定义省略操作的具体功能和样式 + * @default false + */ + ellipsis?: boolean | TypographyEllipsis; + /** + * 标题等级 + * @default h1 + */ + level?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; +} + +export interface TdParagraphProps { + /** + * 段落内容,同 content + */ + children?: TNode; + /** + * 段落内容 + */ + content?: TNode; + /** + * 是否省略展示,可通过配置参数自定义省略操作的具体功能和样式 + * @default false + */ + ellipsis?: boolean | TypographyEllipsis; +} + +export interface TypographyEllipsis { + /** + * 展开后是否可以重新收起 + * @default true + */ + collapsible?: boolean; + /** + * 是否可展开 + * @default true + */ + expandable?: boolean; + /** + * 省略配置默认展示行数 + * @default 1 + */ + row?: number; + /** + * 自定义省略触发元素,一般用于自定义折叠图标 + */ + suffix?: TNode<{ expanded: boolean }>; + /** + * 光标在省略图标上出现的tooltip的配置 + */ + tooltipProps?: TooltipProps; + /** + * 点击省略按钮的回调 + */ + onExpand?: (expanded: boolean) => void; +} + +export interface TypographyCopyable { + /** + * 复制的文本内容,默认为全部文本 + * @default '' + */ + text?: string; + /** + * 自定义复制触发元素,一般用于自定义复制图标 + */ + suffix?: TNode<{ copied: boolean }>; + /** + * 光标在复制图标上出现的tooltip的配置 + */ + tooltipProps?: TooltipProps; + /** + * 点击复制按钮的回调 + */ + onCopy?: () => void; +} diff --git a/src/typography/typography.en-US.md b/src/typography/typography.en-US.md new file mode 100644 index 000000000..27477926c --- /dev/null +++ b/src/typography/typography.en-US.md @@ -0,0 +1,62 @@ +:: BASE_DOC :: + +## API +### Text Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | N +children | TNode | - | children of text。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +code | Boolean | false | add code style | N +copyable | Boolean / Object | false | add copyable style。Typescript:`boolean \| TypographyCopyable` | N +delete | Boolean | false | add delete line style | N +disabled | Boolean | false | add disabled style | N +ellipsis | Boolean / Object | false | add ellipsis style。Typescript:`boolean \| TypographyEllipsis` | N +italic | Boolean | false | add italic style | N +keyboard | Boolean | false | add keyboard style | N +mark | String / Boolean | false | add mark style | N +strong | Boolean | false | add bold style | N +theme | String | - | theme of text。options: primary/secondary/success/warning/error | N +underline | Boolean | false | add underline style | N + +### Title Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | N +children | TNode | - | children of title。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +content | TNode | - | content of title。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +ellipsis | Boolean / Object | false | add ellipsis style。Typescript:`boolean \| TypographyEllipsis` | N +level | String | h1 | level of title。options: h1/h2/h3/h4/h5/h6 | N + +### Paragraph Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | N +children | TNode | - | children of paragraph。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +content | TNode | - | content of paragraph。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +ellipsis | Boolean / Object | false | add ellipsis style。Typescript:`boolean \| TypographyEllipsis` | N + +### TypographyEllipsis + +name | type | default | description | required +-- | -- | -- | -- | -- +collapsible | Boolean | true | collapsible after expanding | N +expandable | Boolean | true | expandable | N +row | Number | 1 | default row number of ellipsis | N +suffix | TElement | - | custom element configuration for ellipsis and collapse icon。Typescript:`TNode<{ expanded: boolean }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +tooltipProps | Object | - | Configuration of the tooltip that appears on the ellipsis icon when the cursor is over it.。Typescript:`tooltipProps`,[Tooltip API Documents](./tooltip?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/typography/type.ts) | N +onExpand | Function | | Typescript:`(expanded:boolean) => void`
| N + +### TypographyCopyable + +name | type | default | description | required +-- | -- | -- | -- | -- + text | String | - | copied content | N +suffix | TElement | - | custom element configuration for copy icon。Typescript:`TNode<{ copied: boolean }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +tooltipProps | Object | - | Configuration of the tooltip that appears on the copy icon when the cursor is over it.。Typescript:`tooltipProps`,[Tooltip API Documents](./tooltip?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/typography/type.ts) | N +onCopy | Function | | Typescript:`() => void`
| N \ No newline at end of file diff --git a/src/typography/typography.md b/src/typography/typography.md new file mode 100644 index 000000000..21a5a5a1a --- /dev/null +++ b/src/typography/typography.md @@ -0,0 +1,62 @@ +:: BASE_DOC :: + +## API +### Text Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +children | TNode | - | 文本内容,同content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +code | Boolean | false | 是否添加代码样式 | N +copyable | Boolean / Object | false | 是否可复制,可通过配置参数自定义复制操作的具体功能和样式。TS 类型:`boolean \| TypographyCopyable` | N +delete | Boolean | false | 是否添加删除线样式 | N +disabled | Boolean | false | 是否添加不可用样式 | N +ellipsis | Boolean / Object | false | 是否省略展示,可通过配置参数自定义省略操作的具体功能和样式。TS 类型:`boolean \| TypographyEllipsis` | N +italic | Boolean | false | 文本是否为斜体 | N +keyboard | Boolean | false | 是否添加键盘样式 | N +mark | String / Boolean | false | 是否添加标记样式,默认为黄色,可通过配置颜色修改标记样式,如#0052D9 | N +strong | Boolean | false | 文本是否加粗 | N +theme | String | - | 主题。可选项:primary/secondary/success/warning/error | N +underline | Boolean | false | 是否添加下划线样式 | N + +### Title Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +children | TNode | - | 段落内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +content | TNode | - | 段落内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +ellipsis | Boolean / Object | false | 是否省略展示,可通过配置参数自定义省略操作的具体功能和样式。TS 类型:`boolean \| TypographyEllipsis` | N +level | String | h1 | 标题等级。可选项:h1/h2/h3/h4/h5/h6 | N + +### Paragraph Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +children | TNode | - | 段落内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +content | TNode | - | 段落内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +ellipsis | Boolean / Object | false | 是否省略展示,可通过配置参数自定义省略操作的具体功能和样式。TS 类型:`boolean \| TypographyEllipsis` | N + +### TypographyEllipsis + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +collapsible | Boolean | true | 展开后是否可以重新收起 | N +expandable | Boolean | true | 是否可展开 | N +row | Number | 1 | 省略配置默认展示行数 | N +suffix | TElement | - | 自定义省略触发元素,一般用于自定义折叠图标。TS 类型:`TNode<{ expanded: boolean }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +tooltipProps | Object | - | 光标在省略图标上出现的tooltip的配置。TS 类型:`tooltipProps`,[Tooltip API Documents](./tooltip?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/typography/type.ts) | N +onExpand | Function | | TS 类型:`(expanded:boolean) => void`
点击省略按钮的回调 | N + +### TypographyCopyable + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- + text | String | - | 复制的文本内容,默认为全部文本 | N +suffix | TElement | - | 自定义复制触发元素,一般用于自定义复制图标。TS 类型:`TNode<{ copied: boolean }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +tooltipProps | Object | - | 光标在复制图标上出现的tooltip的配置。TS 类型:`tooltipProps`,[Tooltip API Documents](./tooltip?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/typography/type.ts) | N +onCopy | Function | | TS 类型:`() => void`
点击复制按钮的回调 | N \ No newline at end of file diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index d1357c91f..cfc27de02 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -198542,7 +198542,7 @@ exports[`csr snapshot test > csr test src/table/_example/base.jsx 1`] = ` class="t-space-item" >
csr test src/table/_example/base.jsx 1`] = ` class="t-space-item" >
csr test src/tree-select/_example/valuetype.jsx 1`] } `; +exports[`csr snapshot test > csr test src/typography/_example/base.jsx 1`] = ` +{ + "asFragment": [Function], + "baseElement": +