mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-06 00:44:17 +08:00
fix: Typography error on context ellipsis (#33725)
* chore: init measure * chore: out of space * refactor: Multiple render * chore: auto cut * feat: render split * fix: ellipsis logic of suffix * fix: ref missing * fix: Tooltip missing * test: snapshot * chore: opt for textarea * test: back part of ellipsis * chore: back of ellipsis logic * ellipsis logic * fix: init ellipsis measure * fix: ellipsis event * chore: clean up * test: Update snapshot * fix: test * test: Update snapshot * chore: lazy ellipsis * fix: check css ellipsis logic * test: Update snapshot * test: back of coverage * chore: clean up * test: ignore else * test: clean up
This commit is contained in:
parent
2ae15a6de2
commit
395c549049
@ -2979,9 +2979,26 @@ exports[`renders ./components/form/demo/label-debug.md extend context correctly
|
||||
title=""
|
||||
>
|
||||
<span
|
||||
aria-label="longtextlongtextlongtextlongtextlongtextlongtextlongtext"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
longtextlongtextlongtextlongtextlongtextlongtextlongtext
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -3016,9 +3033,26 @@ exports[`renders ./components/form/demo/label-debug.md extend context correctly
|
||||
title=""
|
||||
>
|
||||
<span
|
||||
aria-label="longtext longtext longtext longtext longtext longtext longtext"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
longtext longtext longtext longtext longtext longtext longtext
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -2617,9 +2617,26 @@ exports[`renders ./components/form/demo/label-debug.md correctly 1`] = `
|
||||
title=""
|
||||
>
|
||||
<span
|
||||
aria-label="longtextlongtextlongtextlongtextlongtextlongtextlongtext"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
longtextlongtextlongtextlongtextlongtextlongtextlongtext
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -2654,9 +2671,26 @@ exports[`renders ./components/form/demo/label-debug.md correctly 1`] = `
|
||||
title=""
|
||||
>
|
||||
<span
|
||||
aria-label="longtext longtext longtext longtext longtext longtext longtext"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
longtext longtext longtext longtext longtext longtext longtext
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -31,7 +31,12 @@ export interface Locale {
|
||||
global?: Record<string, any>;
|
||||
PageHeader?: { back: string };
|
||||
Icon?: Record<string, any>;
|
||||
Text?: Record<string, any>;
|
||||
Text?: {
|
||||
edit?: any;
|
||||
copy?: any;
|
||||
copied?: any;
|
||||
expand?: any;
|
||||
};
|
||||
Form?: {
|
||||
optional?: string;
|
||||
defaultValidateMessages: ValidateMessages;
|
||||
|
@ -1,604 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import toArray from 'rc-util/lib/Children/toArray';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import CheckOutlined from '@ant-design/icons/CheckOutlined';
|
||||
import CopyOutlined from '@ant-design/icons/CopyOutlined';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import { AutoSizeType } from 'rc-textarea/lib/ResizableTextArea';
|
||||
import { ConfigConsumerProps, configConsumerProps, ConfigContext } from '../config-provider';
|
||||
import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
||||
import devWarning from '../_util/devWarning';
|
||||
import TransButton from '../_util/transButton';
|
||||
import raf from '../_util/raf';
|
||||
import { isStyleSupport } from '../_util/styleChecker';
|
||||
import Tooltip from '../tooltip';
|
||||
import Typography, { TypographyProps } from './Typography';
|
||||
import Editable from './Editable';
|
||||
import measure from './util';
|
||||
|
||||
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
|
||||
|
||||
const isLineClampSupport = isStyleSupport('webkitLineClamp');
|
||||
const isTextOverflowSupport = isStyleSupport('textOverflow');
|
||||
|
||||
interface CopyConfig {
|
||||
text?: string;
|
||||
onCopy?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
tooltips?: boolean | React.ReactNode;
|
||||
}
|
||||
|
||||
interface EditConfig {
|
||||
editing?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
tooltip?: boolean | React.ReactNode;
|
||||
onStart?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
onCancel?: () => void;
|
||||
onEnd?: () => void;
|
||||
maxLength?: number;
|
||||
autoSize?: boolean | AutoSizeType;
|
||||
triggerType?: ('icon' | 'text')[];
|
||||
enterIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface EllipsisConfig {
|
||||
rows?: number;
|
||||
expandable?: boolean;
|
||||
suffix?: string;
|
||||
symbol?: React.ReactNode;
|
||||
onExpand?: React.MouseEventHandler<HTMLElement>;
|
||||
onEllipsis?: (ellipsis: boolean) => void;
|
||||
tooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface BlockProps extends TypographyProps {
|
||||
title?: string;
|
||||
editable?: boolean | EditConfig;
|
||||
copyable?: boolean | CopyConfig;
|
||||
type?: BaseType;
|
||||
disabled?: boolean;
|
||||
ellipsis?: boolean | EllipsisConfig;
|
||||
// decorations
|
||||
code?: boolean;
|
||||
mark?: boolean;
|
||||
underline?: boolean;
|
||||
delete?: boolean;
|
||||
strong?: boolean;
|
||||
keyboard?: boolean;
|
||||
italic?: boolean;
|
||||
}
|
||||
|
||||
function wrapperDecorations(
|
||||
{ mark, code, underline, delete: del, strong, keyboard, italic }: BlockProps,
|
||||
content: React.ReactNode,
|
||||
) {
|
||||
let currentContent = content;
|
||||
|
||||
function wrap(needed: boolean | undefined, tag: string) {
|
||||
if (!needed) return;
|
||||
|
||||
currentContent = React.createElement(tag, {}, currentContent);
|
||||
}
|
||||
|
||||
wrap(strong, 'strong');
|
||||
wrap(underline, 'u');
|
||||
wrap(del, 'del');
|
||||
wrap(code, 'code');
|
||||
wrap(mark, 'mark');
|
||||
wrap(keyboard, 'kbd');
|
||||
wrap(italic, 'i');
|
||||
|
||||
return currentContent;
|
||||
}
|
||||
|
||||
function getNode(dom: React.ReactNode, defaultNode: React.ReactNode, needDom?: boolean) {
|
||||
if (dom === true || dom === undefined) {
|
||||
return defaultNode;
|
||||
}
|
||||
return dom || (needDom && defaultNode);
|
||||
}
|
||||
|
||||
interface InternalBlockProps extends BlockProps {
|
||||
component: string;
|
||||
}
|
||||
|
||||
interface BaseState {
|
||||
edit: boolean;
|
||||
copied: boolean;
|
||||
ellipsisText: string;
|
||||
ellipsisContent: React.ReactNode;
|
||||
isEllipsis: boolean;
|
||||
isNativeEllipsis: boolean;
|
||||
expanded: boolean;
|
||||
clientRendered: boolean;
|
||||
}
|
||||
|
||||
interface Locale {
|
||||
edit?: string;
|
||||
copy?: string;
|
||||
copied?: string;
|
||||
expand?: string;
|
||||
}
|
||||
|
||||
const ELLIPSIS_STR = '...';
|
||||
|
||||
class Base extends React.Component<InternalBlockProps, BaseState> {
|
||||
static contextType = ConfigContext;
|
||||
|
||||
static defaultProps = {
|
||||
children: '',
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: BlockProps) {
|
||||
const { children, editable } = nextProps;
|
||||
|
||||
devWarning(
|
||||
!editable || typeof children === 'string',
|
||||
'Typography',
|
||||
'When `editable` is enabled, the `children` should use string.',
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
context: ConfigConsumerProps;
|
||||
|
||||
editIcon?: HTMLDivElement;
|
||||
|
||||
contentRef = React.createRef<HTMLElement>();
|
||||
|
||||
copyId?: number;
|
||||
|
||||
rafId?: number;
|
||||
|
||||
// Locale
|
||||
expandStr?: string;
|
||||
|
||||
copyStr?: string;
|
||||
|
||||
copiedStr?: string;
|
||||
|
||||
editStr?: string;
|
||||
|
||||
state: BaseState = {
|
||||
edit: false,
|
||||
copied: false,
|
||||
ellipsisText: '',
|
||||
ellipsisContent: null,
|
||||
isEllipsis: false,
|
||||
isNativeEllipsis: false,
|
||||
expanded: false,
|
||||
clientRendered: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ clientRendered: true });
|
||||
this.resizeOnNextFrame();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: BlockProps) {
|
||||
const { children } = this.props;
|
||||
const { isNativeEllipsis } = this.state;
|
||||
|
||||
const ellipsis = this.getEllipsis();
|
||||
const prevEllipsis = this.getEllipsis(prevProps);
|
||||
if (children !== prevProps.children || ellipsis.rows !== prevEllipsis.rows) {
|
||||
this.resizeOnNextFrame();
|
||||
}
|
||||
|
||||
// If use native ellipsis, we should check if ellipsis changed
|
||||
const textEle = this.contentRef.current;
|
||||
if (this.canUseCSSEllipsis() && textEle) {
|
||||
const currentEllipsis = textEle.offsetWidth < textEle.scrollWidth;
|
||||
if (isNativeEllipsis !== currentEllipsis) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
isNativeEllipsis: currentEllipsis,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearTimeout(this.copyId);
|
||||
raf.cancel(this.rafId);
|
||||
}
|
||||
|
||||
getPrefixCls = () => {
|
||||
const { prefixCls: customizePrefixCls } = this.props;
|
||||
const { getPrefixCls } = this.context;
|
||||
return getPrefixCls('typography', customizePrefixCls);
|
||||
};
|
||||
|
||||
// =============== Expand ===============
|
||||
onExpandClick: React.MouseEventHandler<HTMLElement> = e => {
|
||||
const { onExpand } = this.getEllipsis();
|
||||
this.setState({ expanded: true });
|
||||
(onExpand as React.MouseEventHandler<HTMLElement>)?.(e);
|
||||
};
|
||||
|
||||
// ================ Edit ================
|
||||
onEditClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
this.triggerEdit(true);
|
||||
};
|
||||
|
||||
onEditChange = (value: string) => {
|
||||
const { onChange } = this.getEditable();
|
||||
onChange?.(value);
|
||||
this.triggerEdit(false);
|
||||
};
|
||||
|
||||
onEditCancel = () => {
|
||||
this.getEditable().onCancel?.();
|
||||
this.triggerEdit(false);
|
||||
};
|
||||
|
||||
// ================ Copy ================
|
||||
onCopyClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const { children, copyable } = this.props;
|
||||
const copyConfig: CopyConfig = {
|
||||
...(typeof copyable === 'object' ? copyable : null),
|
||||
};
|
||||
|
||||
if (copyConfig.text === undefined) {
|
||||
copyConfig.text = String(children);
|
||||
}
|
||||
copy(copyConfig.text || '');
|
||||
|
||||
this.setState({ copied: true }, () => {
|
||||
if (copyConfig.onCopy) {
|
||||
copyConfig.onCopy();
|
||||
}
|
||||
|
||||
this.copyId = window.setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
|
||||
getEditable(props?: BlockProps): EditConfig {
|
||||
const { edit } = this.state;
|
||||
const { editable } = props || this.props;
|
||||
if (!editable) return { editing: edit };
|
||||
|
||||
return {
|
||||
editing: edit,
|
||||
...(typeof editable === 'object' ? editable : null),
|
||||
};
|
||||
}
|
||||
|
||||
getEllipsis(props?: BlockProps): EllipsisConfig {
|
||||
const { ellipsis } = props || this.props;
|
||||
if (!ellipsis) return {};
|
||||
|
||||
return {
|
||||
rows: 1,
|
||||
expandable: false,
|
||||
...(typeof ellipsis === 'object' ? ellipsis : null),
|
||||
};
|
||||
}
|
||||
|
||||
setEditRef = (node: HTMLDivElement) => {
|
||||
this.editIcon = node;
|
||||
};
|
||||
|
||||
triggerEdit = (edit: boolean) => {
|
||||
const { onStart } = this.getEditable();
|
||||
if (edit && onStart) {
|
||||
onStart();
|
||||
}
|
||||
|
||||
this.setState({ edit }, () => {
|
||||
if (!edit && this.editIcon) {
|
||||
this.editIcon.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ============== Ellipsis ==============
|
||||
resizeOnNextFrame = () => {
|
||||
raf.cancel(this.rafId);
|
||||
this.rafId = raf(() => {
|
||||
// Do not bind `syncEllipsis`. It need for test usage on prototype
|
||||
this.syncEllipsis();
|
||||
});
|
||||
};
|
||||
|
||||
canUseCSSEllipsis(): boolean {
|
||||
const { clientRendered } = this.state;
|
||||
const { editable, copyable } = this.props;
|
||||
const { rows, expandable, suffix, onEllipsis } = this.getEllipsis();
|
||||
|
||||
if (suffix) return false;
|
||||
// Can't use css ellipsis since we need to provide the place for button
|
||||
if (editable || copyable || expandable || !clientRendered || onEllipsis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rows === 1) {
|
||||
return isTextOverflowSupport;
|
||||
}
|
||||
|
||||
return isLineClampSupport;
|
||||
}
|
||||
|
||||
syncEllipsis() {
|
||||
const { ellipsisText, isEllipsis, expanded } = this.state;
|
||||
const { rows, suffix, onEllipsis } = this.getEllipsis();
|
||||
const { children } = this.props;
|
||||
if (!rows || rows < 0 || !this.contentRef.current || expanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not measure if css already support ellipsis
|
||||
if (this.canUseCSSEllipsis()) {
|
||||
return;
|
||||
}
|
||||
|
||||
devWarning(
|
||||
toArray(children).every((child: React.ReactNode) => typeof child === 'string'),
|
||||
'Typography',
|
||||
'`ellipsis` should use string as children only.',
|
||||
);
|
||||
|
||||
const { content, text, ellipsis } = measure(
|
||||
this.contentRef.current,
|
||||
{ rows, suffix },
|
||||
children,
|
||||
this.renderOperations(true),
|
||||
ELLIPSIS_STR,
|
||||
);
|
||||
if (ellipsisText !== text || isEllipsis !== ellipsis) {
|
||||
this.setState({ ellipsisText: text, ellipsisContent: content, isEllipsis: ellipsis });
|
||||
if (isEllipsis !== ellipsis && onEllipsis) {
|
||||
onEllipsis(ellipsis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderExpand(forceRender?: boolean) {
|
||||
const { expandable, symbol } = this.getEllipsis();
|
||||
const { expanded, isEllipsis } = this.state;
|
||||
|
||||
if (!expandable) return null;
|
||||
|
||||
// force render expand icon for measure usage or it will cause dead loop
|
||||
if (!forceRender && (expanded || !isEllipsis)) return null;
|
||||
|
||||
let expandContent: React.ReactNode;
|
||||
if (symbol) {
|
||||
expandContent = symbol;
|
||||
} else {
|
||||
expandContent = this.expandStr;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key="expand"
|
||||
className={`${this.getPrefixCls()}-expand`}
|
||||
onClick={this.onExpandClick}
|
||||
aria-label={this.expandStr}
|
||||
>
|
||||
{expandContent}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
renderEdit() {
|
||||
const { editable } = this.props;
|
||||
if (!editable) return;
|
||||
|
||||
const { icon, tooltip, triggerType = ['icon'] } = editable as EditConfig;
|
||||
|
||||
const title = toArray(tooltip)[0] || this.editStr;
|
||||
const ariaLabel = typeof title === 'string' ? title : '';
|
||||
|
||||
return triggerType.indexOf('icon') !== -1 ? (
|
||||
<Tooltip key="edit" title={tooltip === false ? '' : title}>
|
||||
<TransButton
|
||||
ref={this.setEditRef}
|
||||
className={`${this.getPrefixCls()}-edit`}
|
||||
onClick={this.onEditClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{icon || <EditOutlined role="button" />}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
renderCopy() {
|
||||
const { copied } = this.state;
|
||||
const { copyable } = this.props;
|
||||
if (!copyable) return;
|
||||
|
||||
const prefixCls = this.getPrefixCls();
|
||||
|
||||
const { tooltips, icon } = copyable as CopyConfig;
|
||||
|
||||
const tooltipNodes = Array.isArray(tooltips) ? tooltips : [tooltips];
|
||||
const iconNodes = Array.isArray(icon) ? icon : [icon];
|
||||
|
||||
const title = copied
|
||||
? getNode(tooltipNodes[1], this.copiedStr)
|
||||
: getNode(tooltipNodes[0], this.copyStr);
|
||||
const systemStr = copied ? this.copiedStr : this.copyStr;
|
||||
const ariaLabel = typeof title === 'string' ? title : systemStr;
|
||||
|
||||
return (
|
||||
<Tooltip key="copy" title={title}>
|
||||
<TransButton
|
||||
className={classNames(`${prefixCls}-copy`, copied && `${prefixCls}-copy-success`)}
|
||||
onClick={this.onCopyClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{copied
|
||||
? getNode(iconNodes[1], <CheckOutlined />, true)
|
||||
: getNode(iconNodes[0], <CopyOutlined />, true)}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditInput() {
|
||||
const { children, className, style } = this.props;
|
||||
const { direction } = this.context;
|
||||
const { maxLength, autoSize, onEnd, enterIcon } = this.getEditable();
|
||||
return (
|
||||
<Editable
|
||||
value={typeof children === 'string' ? children : ''}
|
||||
onSave={this.onEditChange}
|
||||
onCancel={this.onEditCancel}
|
||||
onEnd={onEnd}
|
||||
prefixCls={this.getPrefixCls()}
|
||||
className={className}
|
||||
style={style}
|
||||
direction={direction}
|
||||
maxLength={maxLength}
|
||||
autoSize={autoSize}
|
||||
enterIcon={enterIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderOperations(forceRenderExpanded?: boolean) {
|
||||
return [this.renderExpand(forceRenderExpanded), this.renderEdit(), this.renderCopy()].filter(
|
||||
node => node,
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { ellipsisContent, isEllipsis, isNativeEllipsis, expanded } = this.state;
|
||||
const { component, children, className, type, disabled, style, ...restProps } = this.props;
|
||||
const { direction } = this.context;
|
||||
const { rows, suffix, tooltip } = this.getEllipsis();
|
||||
const { triggerType = ['icon'] } = this.getEditable() as EditConfig;
|
||||
|
||||
const prefixCls = this.getPrefixCls();
|
||||
|
||||
const textProps = omit(restProps, [
|
||||
'prefixCls',
|
||||
'editable',
|
||||
'copyable',
|
||||
'ellipsis',
|
||||
'mark',
|
||||
'code',
|
||||
'delete',
|
||||
'underline',
|
||||
'strong',
|
||||
'keyboard',
|
||||
'italic',
|
||||
...(configConsumerProps as any),
|
||||
]) as any;
|
||||
|
||||
const cssEllipsis = this.canUseCSSEllipsis();
|
||||
const cssTextOverflow = rows === 1 && cssEllipsis;
|
||||
const cssLineClamp = rows && rows > 1 && cssEllipsis;
|
||||
|
||||
let textNode: React.ReactNode = children;
|
||||
|
||||
// Only use js ellipsis when css ellipsis not support
|
||||
if (rows && isEllipsis && !expanded && !cssEllipsis) {
|
||||
const { title } = restProps;
|
||||
let restContent = title || '';
|
||||
if (!title && (typeof children === 'string' || typeof children === 'number')) {
|
||||
restContent = String(children);
|
||||
}
|
||||
|
||||
// show rest content as title on symbol
|
||||
restContent = restContent.slice(String(ellipsisContent || '').length);
|
||||
|
||||
// We move full content to outer element to avoid repeat read the content by accessibility
|
||||
textNode = (
|
||||
<>
|
||||
{ellipsisContent}
|
||||
<span title={restContent} aria-hidden="true">
|
||||
{ELLIPSIS_STR}
|
||||
</span>
|
||||
{suffix}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
textNode = (
|
||||
<>
|
||||
{children}
|
||||
{suffix}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
textNode = wrapperDecorations(this.props, textNode);
|
||||
|
||||
return (
|
||||
<LocaleReceiver componentName="Text">
|
||||
{({ edit, copy: copyStr, copied, expand }: Locale) => {
|
||||
this.editStr = edit;
|
||||
this.copyStr = copyStr;
|
||||
this.copiedStr = copied;
|
||||
this.expandStr = expand;
|
||||
|
||||
return (
|
||||
<ResizeObserver onResize={this.resizeOnNextFrame} disabled={cssEllipsis}>
|
||||
{resizeRef => {
|
||||
let typography = (
|
||||
<Typography
|
||||
className={classNames(
|
||||
{
|
||||
[`${prefixCls}-${type}`]: type,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[`${prefixCls}-ellipsis`]: rows,
|
||||
[`${prefixCls}-single-line`]: rows === 1 && !isEllipsis,
|
||||
[`${prefixCls}-ellipsis-single-line`]: cssTextOverflow,
|
||||
[`${prefixCls}-ellipsis-multiple-line`]: cssLineClamp,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
WebkitLineClamp: cssLineClamp ? rows : undefined,
|
||||
}}
|
||||
component={component}
|
||||
ref={composeRef(this.contentRef, resizeRef)}
|
||||
direction={direction}
|
||||
onClick={triggerType.indexOf('text') !== -1 ? this.onEditClick : () => {}}
|
||||
{...textProps}
|
||||
>
|
||||
{textNode}
|
||||
{this.renderOperations()}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
// If provided tooltip, we need wrap with span to let Tooltip inject events
|
||||
if (cssEllipsis ? isNativeEllipsis : isEllipsis) {
|
||||
typography = (
|
||||
<Tooltip title={tooltip === true ? children : tooltip}>{typography}</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return typography;
|
||||
}}
|
||||
</ResizeObserver>
|
||||
);
|
||||
}}
|
||||
</LocaleReceiver>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { editing } = this.getEditable();
|
||||
|
||||
if (editing) {
|
||||
return this.renderEditInput();
|
||||
}
|
||||
return this.renderContent();
|
||||
}
|
||||
}
|
||||
|
||||
export default Base;
|
211
components/typography/Base/Ellipsis.tsx
Normal file
211
components/typography/Base/Ellipsis.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
import * as React from 'react';
|
||||
import toArray from 'rc-util/lib/Children/toArray';
|
||||
|
||||
export interface EllipsisProps {
|
||||
enabledMeasure?: boolean;
|
||||
text?: React.ReactNode;
|
||||
width: number;
|
||||
rows: number;
|
||||
children: (cutChildren: React.ReactNode[], needEllipsis: boolean) => React.ReactNode;
|
||||
onEllipsis: (isEllipsis: boolean) => void;
|
||||
}
|
||||
|
||||
function cuttable(node: React.ReactElement) {
|
||||
const type = typeof node;
|
||||
return type === 'string' || type === 'number';
|
||||
}
|
||||
|
||||
function getNodesLen(nodeList: React.ReactElement[]) {
|
||||
let totalLen = 0;
|
||||
|
||||
nodeList.forEach(node => {
|
||||
if (cuttable(node)) {
|
||||
totalLen += String(node).length;
|
||||
} else {
|
||||
totalLen += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return totalLen;
|
||||
}
|
||||
|
||||
function sliceNodes(nodeList: React.ReactElement[], len: number) {
|
||||
let currLen = 0;
|
||||
const currentNodeList: React.ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < nodeList.length; i += 1) {
|
||||
// Match to return
|
||||
if (currLen === len) {
|
||||
return currentNodeList;
|
||||
}
|
||||
|
||||
const node = nodeList[i];
|
||||
const canCut = cuttable(node);
|
||||
const nodeLen = canCut ? String(node).length : 1;
|
||||
const nextLen = currLen + nodeLen;
|
||||
|
||||
// Exceed but current not which means we need cut this
|
||||
// This will not happen on validate ReactElement
|
||||
if (nextLen > len) {
|
||||
const restLen = len - currLen;
|
||||
currentNodeList.push(String(node).slice(0, restLen));
|
||||
return currentNodeList;
|
||||
}
|
||||
|
||||
currentNodeList.push(node);
|
||||
currLen = nextLen;
|
||||
}
|
||||
|
||||
return nodeList;
|
||||
}
|
||||
|
||||
const NONE = 0;
|
||||
const PREPARE = 1;
|
||||
const WALKING = 2;
|
||||
const DONE_WITH_ELLIPSIS = 3;
|
||||
const DONE_WITHOUT_ELLIPSIS = 4;
|
||||
|
||||
const Ellipsis = ({ enabledMeasure, children, text, width, rows, onEllipsis }: EllipsisProps) => {
|
||||
const [cutLength, setCutLength] = React.useState<[number, number, number]>([0, 0, 0]);
|
||||
const [walkingState, setWalkingState] = React.useState<
|
||||
| typeof NONE
|
||||
| typeof PREPARE
|
||||
| typeof WALKING
|
||||
| typeof DONE_WITH_ELLIPSIS
|
||||
| typeof DONE_WITHOUT_ELLIPSIS
|
||||
>(NONE);
|
||||
const [startLen, midLen, endLen] = cutLength;
|
||||
|
||||
const [singleRowHeight, setSingleRowHeight] = React.useState(0);
|
||||
|
||||
const singleRowRef = React.useRef<HTMLSpanElement>(null);
|
||||
const midRowRef = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
const nodeList = React.useMemo(() => toArray(text), [text]);
|
||||
const totalLen = React.useMemo(() => getNodesLen(nodeList), [nodeList]);
|
||||
|
||||
const mergedChildren = React.useMemo(() => {
|
||||
if (!enabledMeasure || walkingState !== DONE_WITH_ELLIPSIS) {
|
||||
return children(nodeList, false);
|
||||
}
|
||||
|
||||
return children(sliceNodes(nodeList, midLen), midLen < totalLen);
|
||||
}, [enabledMeasure, walkingState, children, nodeList, midLen, totalLen]);
|
||||
|
||||
// ======================== Walk ========================
|
||||
React.useLayoutEffect(() => {
|
||||
if (enabledMeasure && width && totalLen) {
|
||||
setWalkingState(PREPARE);
|
||||
setCutLength([0, Math.ceil(totalLen / 2), totalLen]);
|
||||
}
|
||||
}, [enabledMeasure, width, text, totalLen, rows]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (walkingState === PREPARE) {
|
||||
setSingleRowHeight(singleRowRef.current?.offsetHeight || 0);
|
||||
}
|
||||
}, [walkingState]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (singleRowHeight) {
|
||||
if (walkingState === PREPARE) {
|
||||
// Ignore if position is enough
|
||||
const midHeight = midRowRef.current?.offsetHeight || 0;
|
||||
const maxHeight = rows * singleRowHeight;
|
||||
|
||||
if (midHeight <= maxHeight) {
|
||||
setWalkingState(DONE_WITHOUT_ELLIPSIS);
|
||||
onEllipsis(false);
|
||||
} else {
|
||||
setWalkingState(WALKING);
|
||||
}
|
||||
} else if (walkingState === WALKING) {
|
||||
if (startLen !== endLen) {
|
||||
const midHeight = midRowRef.current?.offsetHeight || 0;
|
||||
const maxHeight = rows * singleRowHeight;
|
||||
|
||||
let nextStartLen = startLen;
|
||||
let nextEndLen = endLen;
|
||||
|
||||
// We reach the last round
|
||||
if (startLen === endLen - 1) {
|
||||
nextEndLen = startLen;
|
||||
} else if (midHeight <= maxHeight) {
|
||||
nextStartLen = midLen;
|
||||
} else {
|
||||
nextEndLen = midLen;
|
||||
}
|
||||
|
||||
const nextMidLen = Math.ceil((nextStartLen + nextEndLen) / 2);
|
||||
|
||||
setCutLength([nextStartLen, nextMidLen, nextEndLen]);
|
||||
} else {
|
||||
setWalkingState(DONE_WITH_ELLIPSIS);
|
||||
onEllipsis(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [walkingState, startLen, endLen, rows, singleRowHeight]);
|
||||
|
||||
// ======================= Render =======================
|
||||
const measureStyle: React.CSSProperties = {
|
||||
width,
|
||||
whiteSpace: 'normal',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const renderMeasure = (
|
||||
content: React.ReactNode,
|
||||
ref: React.Ref<HTMLSpanElement>,
|
||||
style: React.CSSProperties,
|
||||
) => (
|
||||
<span
|
||||
aria-hidden
|
||||
ref={ref}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
display: 'block',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: -9999,
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderMeasureSlice = (len: number, ref: React.Ref<HTMLSpanElement>) => {
|
||||
const sliceNodeList = sliceNodes(nodeList, len);
|
||||
|
||||
return renderMeasure(children(sliceNodeList, true), ref, measureStyle);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{mergedChildren}
|
||||
{/* Measure usage */}
|
||||
{enabledMeasure &&
|
||||
walkingState !== DONE_WITH_ELLIPSIS &&
|
||||
walkingState !== DONE_WITHOUT_ELLIPSIS && (
|
||||
<>
|
||||
{/* `l` for top & `g` for bottom measure */}
|
||||
{renderMeasure('lg', singleRowRef, { width: 9999 })}
|
||||
{/* {renderMeasureSlice(midLen, midRowRef)} */}
|
||||
{walkingState === PREPARE
|
||||
? renderMeasure(children(nodeList, false), midRowRef, measureStyle)
|
||||
: renderMeasureSlice(midLen, midRowRef)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Ellipsis.displayName = 'Ellipsis';
|
||||
}
|
||||
|
||||
export default Ellipsis;
|
32
components/typography/Base/EllipsisTooltip.tsx
Normal file
32
components/typography/Base/EllipsisTooltip.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import Tooltip from '../../tooltip';
|
||||
|
||||
export interface EllipsisTooltipProps {
|
||||
title?: React.ReactNode;
|
||||
enabledEllipsis: boolean;
|
||||
isEllipsis?: boolean;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
const EllipsisTooltip = ({
|
||||
title,
|
||||
enabledEllipsis,
|
||||
isEllipsis,
|
||||
children,
|
||||
}: EllipsisTooltipProps) => {
|
||||
if (!title || !enabledEllipsis) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={title} visible={isEllipsis ? undefined : false}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
EllipsisTooltip.displayName = 'EllipsisTooltip';
|
||||
}
|
||||
|
||||
export default EllipsisTooltip;
|
509
components/typography/Base/index.tsx
Normal file
509
components/typography/Base/index.tsx
Normal file
@ -0,0 +1,509 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import toArray from 'rc-util/lib/Children/toArray';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import CheckOutlined from '@ant-design/icons/CheckOutlined';
|
||||
import CopyOutlined from '@ant-design/icons/CopyOutlined';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import { AutoSizeType } from 'rc-textarea/lib/ResizableTextArea';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import { useLocaleReceiver } from '../../locale-provider/LocaleReceiver';
|
||||
import TransButton from '../../_util/transButton';
|
||||
import { isStyleSupport } from '../../_util/styleChecker';
|
||||
import Tooltip from '../../tooltip';
|
||||
import Typography, { TypographyProps } from '../Typography';
|
||||
import Editable from '../Editable';
|
||||
import useMergedConfig from '../hooks/useMergedConfig';
|
||||
import useUpdatedEffect from '../hooks/useUpdatedEffect';
|
||||
import Ellipsis from './Ellipsis';
|
||||
import EllipsisTooltip from './EllipsisTooltip';
|
||||
|
||||
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
|
||||
|
||||
interface CopyConfig {
|
||||
text?: string;
|
||||
onCopy?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
tooltips?: boolean | React.ReactNode;
|
||||
}
|
||||
|
||||
interface EditConfig {
|
||||
editing?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
tooltip?: boolean | React.ReactNode;
|
||||
onStart?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
onCancel?: () => void;
|
||||
onEnd?: () => void;
|
||||
maxLength?: number;
|
||||
autoSize?: boolean | AutoSizeType;
|
||||
triggerType?: ('icon' | 'text')[];
|
||||
enterIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface EllipsisConfig {
|
||||
rows?: number;
|
||||
expandable?: boolean;
|
||||
suffix?: string;
|
||||
symbol?: React.ReactNode;
|
||||
onExpand?: React.MouseEventHandler<HTMLElement>;
|
||||
onEllipsis?: (ellipsis: boolean) => void;
|
||||
tooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface BlockProps extends TypographyProps {
|
||||
title?: string;
|
||||
editable?: boolean | EditConfig;
|
||||
copyable?: boolean | CopyConfig;
|
||||
type?: BaseType;
|
||||
disabled?: boolean;
|
||||
ellipsis?: boolean | EllipsisConfig;
|
||||
// decorations
|
||||
code?: boolean;
|
||||
mark?: boolean;
|
||||
underline?: boolean;
|
||||
delete?: boolean;
|
||||
strong?: boolean;
|
||||
keyboard?: boolean;
|
||||
italic?: boolean;
|
||||
}
|
||||
|
||||
function wrapperDecorations(
|
||||
{ mark, code, underline, delete: del, strong, keyboard, italic }: BlockProps,
|
||||
content: React.ReactNode,
|
||||
) {
|
||||
let currentContent = content;
|
||||
|
||||
function wrap(needed: boolean | undefined, tag: string) {
|
||||
if (!needed) return;
|
||||
|
||||
currentContent = React.createElement(tag, {}, currentContent);
|
||||
}
|
||||
|
||||
wrap(strong, 'strong');
|
||||
wrap(underline, 'u');
|
||||
wrap(del, 'del');
|
||||
wrap(code, 'code');
|
||||
wrap(mark, 'mark');
|
||||
wrap(keyboard, 'kbd');
|
||||
wrap(italic, 'i');
|
||||
|
||||
return currentContent;
|
||||
}
|
||||
|
||||
function getNode(dom: React.ReactNode, defaultNode: React.ReactNode, needDom?: boolean) {
|
||||
if (dom === true || dom === undefined) {
|
||||
return defaultNode;
|
||||
}
|
||||
return dom || (needDom && defaultNode);
|
||||
}
|
||||
|
||||
function toList<T>(val: T | T[]): T[] {
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}
|
||||
|
||||
interface InternalBlockProps extends BlockProps {
|
||||
component: string;
|
||||
}
|
||||
|
||||
const ELLIPSIS_STR = '...';
|
||||
|
||||
const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
style,
|
||||
type,
|
||||
disabled,
|
||||
children,
|
||||
ellipsis,
|
||||
editable,
|
||||
copyable,
|
||||
component,
|
||||
title,
|
||||
...restProps
|
||||
} = props;
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const textLocale = useLocaleReceiver('Text')[0]!; // Force TS get this
|
||||
|
||||
const typographyRef = React.useRef<HTMLElement>(null);
|
||||
const editIconRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// ============================ MISC ============================
|
||||
const prefixCls = getPrefixCls('typography', customizePrefixCls);
|
||||
|
||||
const textProps = omit(restProps, [
|
||||
'mark',
|
||||
'code',
|
||||
'delete',
|
||||
'underline',
|
||||
'strong',
|
||||
'keyboard',
|
||||
'italic',
|
||||
]) as any;
|
||||
|
||||
// ========================== Editable ==========================
|
||||
const [enableEdit, editConfig] = useMergedConfig<EditConfig>(editable);
|
||||
const [editing, setEditing] = useMergedState(false, {
|
||||
value: editConfig.editing,
|
||||
});
|
||||
const { triggerType = ['icon'] } = editConfig;
|
||||
|
||||
const triggerEdit = (edit: boolean) => {
|
||||
if (edit) {
|
||||
editConfig.onStart?.();
|
||||
}
|
||||
|
||||
setEditing(edit);
|
||||
};
|
||||
|
||||
// Focus edit icon when back
|
||||
useUpdatedEffect(() => {
|
||||
if (!editing) {
|
||||
editIconRef.current?.focus();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
const onEditClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
triggerEdit(true);
|
||||
};
|
||||
|
||||
const onEditChange = (value: string) => {
|
||||
editConfig.onChange?.(value);
|
||||
triggerEdit(false);
|
||||
};
|
||||
|
||||
const onEditCancel = () => {
|
||||
editConfig.onCancel?.();
|
||||
triggerEdit(false);
|
||||
};
|
||||
|
||||
// ========================== Copyable ==========================
|
||||
const [enableCopy, copyConfig] = useMergedConfig<CopyConfig>(copyable);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const copyIdRef = React.useRef<NodeJS.Timeout>();
|
||||
|
||||
const cleanCopyId = () => {
|
||||
clearTimeout(copyIdRef.current!);
|
||||
};
|
||||
|
||||
const onCopyClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (copyConfig.text === undefined) {
|
||||
copyConfig.text = String(children);
|
||||
}
|
||||
copy(copyConfig.text || '');
|
||||
|
||||
setCopied(true);
|
||||
|
||||
// Trigger tips update
|
||||
cleanCopyId();
|
||||
copyIdRef.current = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
|
||||
copyConfig.onCopy?.();
|
||||
};
|
||||
|
||||
React.useEffect(() => cleanCopyId, []);
|
||||
|
||||
// ========================== Ellipsis ==========================
|
||||
const [isLineClampSupport, setIsLineClampSupport] = React.useState(false);
|
||||
const [isTextOverflowSupport, setIsTextOverflowSupport] = React.useState(false);
|
||||
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [isJsEllipsis, setIsJsEllipsis] = React.useState(false);
|
||||
const [isNativeEllipsis, setIsNativeEllipsis] = React.useState(false);
|
||||
const [enableEllipsis, ellipsisConfig] = useMergedConfig<EllipsisConfig>(ellipsis, {
|
||||
expandable: false,
|
||||
});
|
||||
|
||||
const mergedEnableEllipsis = enableEllipsis && !expanded;
|
||||
|
||||
// Shared prop to reduce bundle size
|
||||
const { rows = 1 } = ellipsisConfig;
|
||||
|
||||
const needMeasureEllipsis = React.useMemo(
|
||||
() =>
|
||||
// Disable ellipsis
|
||||
!mergedEnableEllipsis ||
|
||||
// Provide suffix
|
||||
ellipsisConfig.suffix !== undefined ||
|
||||
ellipsisConfig.onEllipsis ||
|
||||
// Can't use css ellipsis since we need to provide the place for button
|
||||
ellipsisConfig.expandable ||
|
||||
enableEdit ||
|
||||
enableCopy,
|
||||
[mergedEnableEllipsis, ellipsisConfig, enableEdit, enableCopy],
|
||||
);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (enableEllipsis && !needMeasureEllipsis) {
|
||||
setIsLineClampSupport(isStyleSupport('webkitLineClamp'));
|
||||
setIsTextOverflowSupport(isStyleSupport('textOverflow'));
|
||||
}
|
||||
}, [needMeasureEllipsis, enableEllipsis]);
|
||||
|
||||
const cssEllipsis = React.useMemo(() => {
|
||||
if (needMeasureEllipsis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rows === 1) {
|
||||
return isTextOverflowSupport;
|
||||
}
|
||||
|
||||
return isLineClampSupport;
|
||||
}, [needMeasureEllipsis, isTextOverflowSupport, isLineClampSupport]);
|
||||
|
||||
const isMergedEllipsis = mergedEnableEllipsis && (cssEllipsis ? isNativeEllipsis : isJsEllipsis);
|
||||
|
||||
const cssTextOverflow = mergedEnableEllipsis && rows === 1 && cssEllipsis;
|
||||
const cssLineClamp = mergedEnableEllipsis && rows > 1 && cssEllipsis;
|
||||
|
||||
// >>>>> Expand
|
||||
const onExpandClick: React.MouseEventHandler<HTMLElement> = e => {
|
||||
setExpanded(true);
|
||||
ellipsisConfig.onExpand?.(e);
|
||||
};
|
||||
|
||||
const [ellipsisWidth, setEllipsisWidth] = React.useState(0);
|
||||
const onResize = ({ offsetWidth }: { offsetWidth: number }) => {
|
||||
setEllipsisWidth(offsetWidth);
|
||||
};
|
||||
|
||||
// >>>>> JS Ellipsis
|
||||
const onJsEllipsis = (jsEllipsis: boolean) => {
|
||||
setIsJsEllipsis(jsEllipsis);
|
||||
|
||||
// Trigger if changed
|
||||
if (isJsEllipsis !== jsEllipsis) {
|
||||
ellipsisConfig.onEllipsis?.(jsEllipsis);
|
||||
}
|
||||
};
|
||||
|
||||
// >>>>> Native ellipsis
|
||||
React.useEffect(() => {
|
||||
const textEle = typographyRef.current;
|
||||
|
||||
if (enableEllipsis && cssEllipsis && textEle) {
|
||||
const currentEllipsis = textEle.offsetWidth < textEle.scrollWidth;
|
||||
if (isNativeEllipsis !== currentEllipsis) {
|
||||
setIsNativeEllipsis(currentEllipsis);
|
||||
}
|
||||
}
|
||||
}, [enableEllipsis, cssEllipsis, children]);
|
||||
|
||||
// ========================== Tooltip ===========================
|
||||
const tooltipTitle = ellipsisConfig.tooltip === true ? children : ellipsisConfig.tooltip;
|
||||
const topAriaLabel = React.useMemo(() => {
|
||||
const isValid = (val: any) => ['string', 'number'].includes(typeof val);
|
||||
|
||||
if (!enableEllipsis || cssEllipsis) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isValid(children)) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (isValid(title)) {
|
||||
return title;
|
||||
}
|
||||
|
||||
if (isValid(tooltipTitle)) {
|
||||
return tooltipTitle;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [enableEllipsis, cssEllipsis, title, tooltipTitle, isMergedEllipsis]);
|
||||
|
||||
// =========================== Render ===========================
|
||||
// >>>>>>>>>>> Editing input
|
||||
if (editing) {
|
||||
return (
|
||||
<Editable
|
||||
value={typeof children === 'string' ? children : ''}
|
||||
onSave={onEditChange}
|
||||
onCancel={onEditCancel}
|
||||
onEnd={editConfig.onEnd}
|
||||
prefixCls={prefixCls}
|
||||
className={className}
|
||||
style={style}
|
||||
direction={direction}
|
||||
maxLength={editConfig.maxLength}
|
||||
autoSize={editConfig.autoSize}
|
||||
enterIcon={editConfig.enterIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// >>>>>>>>>>> Typography
|
||||
// Expand
|
||||
const renderExpand = () => {
|
||||
const { expandable, symbol } = ellipsisConfig;
|
||||
|
||||
if (!expandable) return null;
|
||||
|
||||
let expandContent: React.ReactNode;
|
||||
if (symbol) {
|
||||
expandContent = symbol;
|
||||
} else {
|
||||
expandContent = textLocale.expand;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key="expand"
|
||||
className={`${prefixCls}-expand`}
|
||||
onClick={onExpandClick}
|
||||
aria-label={textLocale.expand}
|
||||
>
|
||||
{expandContent}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
// Edit
|
||||
const renderEdit = () => {
|
||||
if (!enableEdit) return;
|
||||
|
||||
const { icon, tooltip } = editConfig;
|
||||
|
||||
const editTitle = toArray(tooltip)[0] || textLocale.edit;
|
||||
const ariaLabel = typeof editTitle === 'string' ? editTitle : '';
|
||||
|
||||
return triggerType.includes('icon') ? (
|
||||
<Tooltip key="edit" title={tooltip === false ? '' : editTitle}>
|
||||
<TransButton
|
||||
ref={editIconRef}
|
||||
className={`${prefixCls}-edit`}
|
||||
onClick={onEditClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{icon || <EditOutlined role="button" />}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
};
|
||||
|
||||
// Copy
|
||||
const renderCopy = () => {
|
||||
if (!enableCopy) return;
|
||||
|
||||
const { tooltips, icon } = copyConfig;
|
||||
|
||||
const tooltipNodes = toList(tooltips);
|
||||
const iconNodes = toList(icon);
|
||||
|
||||
const copyTitle = copied
|
||||
? getNode(tooltipNodes[1], textLocale.copied)
|
||||
: getNode(tooltipNodes[0], textLocale.copy);
|
||||
const systemStr = copied ? textLocale.copied : textLocale.copy;
|
||||
const ariaLabel = typeof copyTitle === 'string' ? copyTitle : systemStr;
|
||||
|
||||
return (
|
||||
<Tooltip key="copy" title={copyTitle}>
|
||||
<TransButton
|
||||
className={classNames(`${prefixCls}-copy`, copied && `${prefixCls}-copy-success`)}
|
||||
onClick={onCopyClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{copied
|
||||
? getNode(iconNodes[1], <CheckOutlined />, true)
|
||||
: getNode(iconNodes[0], <CopyOutlined />, true)}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOperations = (renderExpanded: boolean) => [
|
||||
renderExpanded && renderExpand(),
|
||||
renderEdit(),
|
||||
renderCopy(),
|
||||
];
|
||||
|
||||
const renderEllipsis = (needEllipsis: boolean) => [
|
||||
needEllipsis && (
|
||||
<span aria-hidden key="ellipsis">
|
||||
{ELLIPSIS_STR}
|
||||
</span>
|
||||
),
|
||||
ellipsisConfig.suffix,
|
||||
renderOperations(needEllipsis),
|
||||
];
|
||||
|
||||
return (
|
||||
<ResizeObserver onResize={onResize} disabled={!mergedEnableEllipsis || cssEllipsis}>
|
||||
{resizeRef => (
|
||||
<EllipsisTooltip
|
||||
title={tooltipTitle}
|
||||
enabledEllipsis={mergedEnableEllipsis}
|
||||
isEllipsis={isMergedEllipsis}
|
||||
>
|
||||
<Typography
|
||||
className={classNames(
|
||||
{
|
||||
[`${prefixCls}-${type}`]: type,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[`${prefixCls}-ellipsis`]: enableEllipsis,
|
||||
[`${prefixCls}-single-line`]: mergedEnableEllipsis && rows === 1,
|
||||
[`${prefixCls}-ellipsis-single-line`]: cssTextOverflow,
|
||||
[`${prefixCls}-ellipsis-multiple-line`]: cssLineClamp,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
WebkitLineClamp: cssLineClamp ? rows : undefined,
|
||||
}}
|
||||
component={component}
|
||||
ref={composeRef(resizeRef, typographyRef, ref)}
|
||||
direction={direction}
|
||||
onClick={triggerType.includes('text') ? onEditClick : null}
|
||||
aria-label={topAriaLabel}
|
||||
title={title}
|
||||
{...textProps}
|
||||
>
|
||||
<Ellipsis
|
||||
enabledMeasure={mergedEnableEllipsis && !cssEllipsis}
|
||||
text={children}
|
||||
rows={rows}
|
||||
width={ellipsisWidth}
|
||||
onEllipsis={onJsEllipsis}
|
||||
>
|
||||
{(node, needEllipsis) => {
|
||||
let renderNode: React.ReactNode = node;
|
||||
if (node.length && needEllipsis && topAriaLabel) {
|
||||
renderNode = (
|
||||
<span key="show-content" aria-hidden>
|
||||
{renderNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const wrappedContext = wrapperDecorations(
|
||||
props,
|
||||
<>
|
||||
{renderNode}
|
||||
{renderEllipsis(needEllipsis)}
|
||||
</>,
|
||||
);
|
||||
|
||||
return wrappedContext;
|
||||
}}
|
||||
</Ellipsis>
|
||||
</Typography>
|
||||
</EllipsisTooltip>
|
||||
)}
|
||||
</ResizeObserver>
|
||||
);
|
||||
});
|
||||
|
||||
export default Base;
|
@ -130,6 +130,7 @@ const Editable: React.FC<EditableProps> = ({
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
onBlur={onBlur}
|
||||
aria-label={ariaLabel}
|
||||
rows={1}
|
||||
autoSize={autoSize}
|
||||
/>
|
||||
{enterIcon !== null
|
||||
|
@ -18,9 +18,9 @@ const Link: React.ForwardRefRenderFunction<HTMLElement, LinkProps> = (
|
||||
'`ellipsis` only supports boolean value.',
|
||||
);
|
||||
|
||||
const baseRef = React.useRef<Base>(null);
|
||||
const baseRef = React.useRef<any>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => baseRef.current?.contentRef.current!);
|
||||
React.useImperativeHandle(ref, () => baseRef.current);
|
||||
|
||||
const mergedProps = {
|
||||
...restProps,
|
||||
|
@ -231,21 +231,102 @@ Array [
|
||||
/>
|
||||
</button>,
|
||||
<div
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</div>,
|
||||
<div
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
<a
|
||||
aria-label="Expand"
|
||||
class="ant-typography-expand"
|
||||
>
|
||||
more
|
||||
</a>
|
||||
</span>
|
||||
</div>,
|
||||
<span
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
style="width:100px"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>,
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip"
|
||||
style="opacity:0;pointer-events:none"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
>
|
||||
I am ellipsis now!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
@ -372,25 +453,201 @@ Array [
|
||||
</code>
|
||||
</span>
|
||||
case. Bnt Design, a design language for background applications, is refined by Ant UED Team. Cnt Design, a design language for background applications, is refined by Ant UED Team. Dnt Design, a design language for background applications, is refined by Ant UED Team. Ent Design, a design language for background applications, is refined by Ant UED Team.
|
||||
</div>,
|
||||
<p>
|
||||
2333
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</div>,
|
||||
<span
|
||||
aria-label="Ant Design is a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
style="width:100px"
|
||||
>
|
||||
Ant Design is a design language for background applications, is refined by Ant UED Team.
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip"
|
||||
style="opacity:0;pointer-events:none"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
>
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip"
|
||||
style="opacity:0;pointer-events:none"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
>
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>,
|
||||
<p>
|
||||
[Before]
|
||||
<span
|
||||
aria-label="not ellipsis"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
2333
|
||||
not ellipsis
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
2333
|
||||
[After]
|
||||
</p>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/typography/demo/ellipsis-middle.md extend context correctly 1`] = `
|
||||
<span
|
||||
aria-label="In the process of internal desktop applications development, many different design specs and implementations would be involved, which might cause designers and developers difficulties and duplication and reduce the efficiency of"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
style="max-width:100%"
|
||||
>
|
||||
In the process of internal desktop applications development, many different design specs and implementations would be involved, which might cause designers and developers difficulties and duplication and reduce the efficiency ofdevelopment.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
development.
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -1329,10 +1586,34 @@ Array [
|
||||
/>
|
||||
</div>,
|
||||
<div
|
||||
aria-label="To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
title="To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life--William Shakespeare"
|
||||
>
|
||||
To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life--William Shakespeare
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
--William Shakespeare
|
||||
<a
|
||||
aria-label="Expand"
|
||||
class="ant-typography-expand"
|
||||
>
|
||||
Expand
|
||||
</a>
|
||||
</span>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
@ -231,20 +231,77 @@ Array [
|
||||
/>
|
||||
</button>,
|
||||
<div
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</div>,
|
||||
<div
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
<a
|
||||
aria-label="Expand"
|
||||
class="ant-typography-expand"
|
||||
>
|
||||
more
|
||||
</a>
|
||||
</span>
|
||||
</div>,
|
||||
<span
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
style="width:100px"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
@ -348,25 +405,153 @@ Array [
|
||||
</code>
|
||||
</span>
|
||||
case. Bnt Design, a design language for background applications, is refined by Ant UED Team. Cnt Design, a design language for background applications, is refined by Ant UED Team. Dnt Design, a design language for background applications, is refined by Ant UED Team. Ent Design, a design language for background applications, is refined by Ant UED Team.
|
||||
</div>,
|
||||
<p>
|
||||
2333
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</div>,
|
||||
<span
|
||||
aria-label="Ant Design is a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
style="width:100px"
|
||||
>
|
||||
Ant Design is a design language for background applications, is refined by Ant UED Team.
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</span>,
|
||||
<p>
|
||||
[Before]
|
||||
<span
|
||||
aria-label="not ellipsis"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
2333
|
||||
not ellipsis
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
2333
|
||||
[After]
|
||||
</p>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/typography/demo/ellipsis-middle.md correctly 1`] = `
|
||||
<span
|
||||
aria-label="In the process of internal desktop applications development, many different design specs and implementations would be involved, which might cause designers and developers difficulties and duplication and reduce the efficiency of"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
style="max-width:100%"
|
||||
>
|
||||
In the process of internal desktop applications development, many different design specs and implementations would be involved, which might cause designers and developers difficulties and duplication and reduce the efficiency ofdevelopment.
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
development.
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -1021,10 +1206,34 @@ Array [
|
||||
/>
|
||||
</div>,
|
||||
<div
|
||||
aria-label="To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life"
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
title="To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life--William Shakespeare"
|
||||
>
|
||||
To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life--William Shakespeare
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:9999px"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
--William Shakespeare
|
||||
<a
|
||||
aria-label="Expand"
|
||||
class="ant-typography-expand"
|
||||
>
|
||||
Expand
|
||||
</a>
|
||||
</span>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
280
components/typography/__tests__/ellipsis.test.js
Normal file
280
components/typography/__tests__/ellipsis.test.js
Normal file
@ -0,0 +1,280 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
|
||||
import Base from '../Base';
|
||||
import Typography from '../Typography';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import * as styleChecker from '../../_util/styleChecker';
|
||||
|
||||
jest.mock('copy-to-clipboard');
|
||||
|
||||
jest.mock('../../_util/styleChecker', () => ({
|
||||
isStyleSupport: () => true,
|
||||
}));
|
||||
|
||||
describe('Typography.Ellipsis', () => {
|
||||
const LINE_STR_COUNT = 20;
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
let mockRectSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
mockRectSpy = spyElementPrototypes(HTMLElement, {
|
||||
offsetHeight: {
|
||||
get() {
|
||||
let html = this.innerHTML;
|
||||
html = html.replace(/<[^>]*>/g, '');
|
||||
const lines = Math.ceil(html.length / LINE_STR_COUNT);
|
||||
return lines * 16;
|
||||
},
|
||||
},
|
||||
offsetWidth: {
|
||||
get: () => 100,
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
let html = this.innerHTML;
|
||||
html = html.replace(/<[^>]*>/g, '');
|
||||
const lines = Math.ceil(html.length / LINE_STR_COUNT);
|
||||
return { height: lines * 16 };
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
errorSpy.mockReset();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
errorSpy.mockRestore();
|
||||
mockRectSpy.mockRestore();
|
||||
});
|
||||
|
||||
const fullStr =
|
||||
'Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light';
|
||||
|
||||
it('should trigger update', async () => {
|
||||
const onEllipsis = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ onEllipsis }} component="p" editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
// First resize
|
||||
wrapper.triggerResize();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little ...');
|
||||
expect(onEllipsis).toHaveBeenCalledWith(true);
|
||||
onEllipsis.mockReset();
|
||||
|
||||
// Second resize
|
||||
wrapper.setProps({ ellipsis: { rows: 2, onEllipsis } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little Light Bamboo is Litt...');
|
||||
expect(onEllipsis).not.toHaveBeenCalled();
|
||||
|
||||
// Third resize
|
||||
wrapper.setProps({ ellipsis: { rows: 99, onEllipsis } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr);
|
||||
expect(onEllipsis).toHaveBeenCalledWith(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('support css multiple lines', async () => {
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ rows: 2 }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
expect(wrapper.exists('.ant-typography-ellipsis-multiple-line')).toBeTruthy();
|
||||
expect(wrapper.find(Typography).prop('style').WebkitLineClamp).toEqual(2);
|
||||
});
|
||||
|
||||
it('string with parentheses', async () => {
|
||||
const parenthesesStr = `Ant Design, a design language (for background applications, is refined by
|
||||
Ant UED Team. Ant Design, a design language for background applications,
|
||||
is refined by Ant UED Team. Ant Design, a design language for background
|
||||
applications, is refined by Ant UED Team. Ant Design, a design language
|
||||
for background applications, is refined by Ant UED Team. Ant Design, a
|
||||
design language for background applications, is refined by Ant UED Team.
|
||||
Ant Design, a design language for background applications, is refined by
|
||||
Ant UED Team.`;
|
||||
const onEllipsis = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ onEllipsis }} component="p" editable>
|
||||
{parenthesesStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
wrapper.triggerResize();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toEqual('Ant Design, a des...');
|
||||
const ellipsisSpan = wrapper.find('span[aria-hidden]').last();
|
||||
expect(ellipsisSpan.text()).toEqual('...');
|
||||
onEllipsis.mockReset();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should middle ellipsis', async () => {
|
||||
const suffix = '--suffix';
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
wrapper.triggerResize();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual('Bamboo is...--suffix');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should front or middle ellipsis', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
wrapper.triggerResize();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual('...--The information is very important');
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 2, suffix } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual('Ba...--The information is very important');
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 99, suffix } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr + suffix);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('connect children', async () => {
|
||||
const bamboo = 'Bamboo';
|
||||
const is = ' is ';
|
||||
|
||||
const wrapper = mount(
|
||||
<Base ellipsis component="p" editable>
|
||||
{bamboo}
|
||||
{is}
|
||||
<code>Little</code>
|
||||
<code>Light</code>
|
||||
</Base>,
|
||||
);
|
||||
|
||||
wrapper.triggerResize();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little...');
|
||||
});
|
||||
|
||||
it('should expandable work', async () => {
|
||||
const onExpand = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ expandable: true, onExpand }} component="p" copyable editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find('.ant-typography-expand').simulate('click');
|
||||
expect(onExpand).toHaveBeenCalled();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr);
|
||||
});
|
||||
|
||||
it('should have custom expand style', async () => {
|
||||
const symbol = 'more';
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ expandable: true, symbol }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('.ant-typography-expand').text()).toEqual('more');
|
||||
});
|
||||
|
||||
it('can use css ellipsis', () => {
|
||||
const wrapper = mount(<Base ellipsis component="p" />);
|
||||
expect(wrapper.find('.ant-typography-ellipsis-single-line').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should calculate padding', () => {
|
||||
const wrapper = mount(
|
||||
<Base ellipsis component="p" style={{ paddingTop: '12px', paddingBottom: '12px' }} />,
|
||||
);
|
||||
expect(wrapper.find('.ant-typography-ellipsis-single-line').length).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('should tooltip support', () => {
|
||||
let domSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
domSpy = spyElementPrototypes(HTMLElement, {
|
||||
offsetWidth: {
|
||||
get: () => 100,
|
||||
},
|
||||
scrollWidth: {
|
||||
get: () => 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
domSpy.mockRestore();
|
||||
});
|
||||
|
||||
function getWrapper(tooltip) {
|
||||
return mount(
|
||||
<Base ellipsis={{ tooltip }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
}
|
||||
|
||||
it('boolean', async () => {
|
||||
const wrapper = getWrapper(true);
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Tooltip').prop('title')).toEqual(fullStr);
|
||||
});
|
||||
|
||||
it('customize', async () => {
|
||||
const wrapper = getWrapper('Bamboo is Light');
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Tooltip').prop('title')).toEqual('Bamboo is Light');
|
||||
});
|
||||
});
|
||||
|
||||
it('js ellipsis should show aria-label', () => {
|
||||
const titleWrapper = mount(<Base title="bamboo" ellipsis={{ expandable: true }} />);
|
||||
expect(titleWrapper.find('.ant-typography').prop('aria-label')).toEqual('bamboo');
|
||||
|
||||
const tooltipWrapper = mount(<Base ellipsis={{ expandable: true, tooltip: 'little' }} />);
|
||||
expect(tooltipWrapper.find('.ant-typography').prop('aria-label')).toEqual('little');
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { SmileOutlined, LikeOutlined, HighlightOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import KeyCode from 'rc-util/lib/KeyCode';
|
||||
import { resetWarned } from 'rc-util/lib/warning';
|
||||
import { spyElementPrototype, spyElementPrototypes } from 'rc-util/lib/test/domHook';
|
||||
import { spyElementPrototype } from 'rc-util/lib/test/domHook';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import Title from '../Title';
|
||||
import Link from '../Link';
|
||||
@ -88,219 +88,6 @@ describe('Typography', () => {
|
||||
});
|
||||
|
||||
describe('Base', () => {
|
||||
describe('trigger ellipsis update', () => {
|
||||
const fullStr =
|
||||
'Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light';
|
||||
|
||||
it('should trigger update', async () => {
|
||||
const onEllipsis = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ onEllipsis }} component="p" editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little ...');
|
||||
expect(onEllipsis).toHaveBeenCalledWith(true);
|
||||
onEllipsis.mockReset();
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 2, onEllipsis } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little Light Bamboo is Litt...');
|
||||
expect(onEllipsis).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 99, onEllipsis } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr);
|
||||
expect(onEllipsis).toHaveBeenCalledWith(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('string with parentheses', async () => {
|
||||
const parenthesesStr = `Ant Design, a design language (for background applications, is refined by
|
||||
Ant UED Team. Ant Design, a design language for background applications,
|
||||
is refined by Ant UED Team. Ant Design, a design language for background
|
||||
applications, is refined by Ant UED Team. Ant Design, a design language
|
||||
for background applications, is refined by Ant UED Team. Ant Design, a
|
||||
design language for background applications, is refined by Ant UED Team.
|
||||
Ant Design, a design language for background applications, is refined by
|
||||
Ant UED Team.`;
|
||||
const onEllipsis = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ onEllipsis }} component="p" editable>
|
||||
{parenthesesStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toEqual('Ant Design, a des...');
|
||||
const ellipsisSpan = wrapper.find('span[title]');
|
||||
expect(ellipsisSpan.text()).toEqual('...');
|
||||
expect(ellipsisSpan.props().title)
|
||||
.toEqual(`ign language (for background applications, is refined by
|
||||
Ant UED Team. Ant Design, a design language for background applications,
|
||||
is refined by Ant UED Team. Ant Design, a design language for background
|
||||
applications, is refined by Ant UED Team. Ant Design, a design language
|
||||
for background applications, is refined by Ant UED Team. Ant Design, a
|
||||
design language for background applications, is refined by Ant UED Team.
|
||||
Ant Design, a design language for background applications, is refined by
|
||||
Ant UED Team.`);
|
||||
onEllipsis.mockReset();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should middle ellipsis', async () => {
|
||||
const suffix = '--suffix';
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual('Bamboo is...--suffix');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should front or middle ellipsis', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual('...--The information is very important');
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 2, suffix } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual('Ba...--The information is very important');
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 99, suffix } });
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr + suffix);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('connect children', async () => {
|
||||
const bamboo = 'Bamboo';
|
||||
const is = ' is ';
|
||||
|
||||
const wrapper = mount(
|
||||
<Base ellipsis component="p" editable>
|
||||
{bamboo}
|
||||
{is}
|
||||
<code>Little</code>
|
||||
<code>Light</code>
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little...');
|
||||
});
|
||||
|
||||
it('should expandable work', async () => {
|
||||
const onExpand = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ expandable: true, onExpand }} component="p" copyable editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find('.ant-typography-expand').simulate('click');
|
||||
expect(onExpand).toHaveBeenCalled();
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr);
|
||||
});
|
||||
|
||||
it('should have custom expand style', async () => {
|
||||
const symbol = 'more';
|
||||
const wrapper = mount(
|
||||
<Base ellipsis={{ expandable: true, symbol }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('.ant-typography-expand').text()).toEqual('more');
|
||||
});
|
||||
|
||||
it('can use css ellipsis', () => {
|
||||
const wrapper = mount(<Base ellipsis component="p" />);
|
||||
expect(wrapper.find('.ant-typography-ellipsis-single-line').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should calculate padding', () => {
|
||||
const wrapper = mount(
|
||||
<Base ellipsis component="p" style={{ paddingTop: '12px', paddingBottom: '12px' }} />,
|
||||
);
|
||||
expect(wrapper.find('.ant-typography-ellipsis-single-line').length).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('should tooltip support', () => {
|
||||
let domSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
domSpy = spyElementPrototypes(HTMLElement, {
|
||||
offsetWidth: {
|
||||
get: () => 100,
|
||||
},
|
||||
scrollWidth: {
|
||||
get: () => 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
domSpy.mockRestore();
|
||||
});
|
||||
|
||||
function getWrapper(tooltip) {
|
||||
return mount(
|
||||
<Base ellipsis={{ tooltip }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
}
|
||||
|
||||
it('boolean', async () => {
|
||||
const wrapper = getWrapper(true);
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Tooltip').prop('title')).toEqual(fullStr);
|
||||
});
|
||||
|
||||
it('customize', async () => {
|
||||
const wrapper = getWrapper('Bamboo is Light');
|
||||
await sleep(20);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Tooltip').prop('title')).toEqual('Bamboo is Light');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyable', () => {
|
||||
function copyTest(name, text, target, icon, tooltips) {
|
||||
it(name, async () => {
|
||||
|
@ -41,9 +41,9 @@ class Demo extends React.Component {
|
||||
checkedChildren="Long Text"
|
||||
onChange={val => this.setState({ longText: val })}
|
||||
/>
|
||||
<Switch onChange={val => this.setState({ copyable: val })} />
|
||||
<Switch onChange={val => this.setState({ editable: val })} />
|
||||
<Switch onChange={val => this.setState({ expandable: val })} />
|
||||
<Switch checked={copyable} onChange={val => this.setState({ copyable: val })} />
|
||||
<Switch checked={editable} onChange={val => this.setState({ editable: val })} />
|
||||
<Switch checked={expandable} onChange={val => this.setState({ expandable: val })} />
|
||||
<Slider value={rows} min={1} max={10} onChange={this.onChange} />
|
||||
{longText ? (
|
||||
<Paragraph ellipsis={{ rows, expandable }} copyable={copyable} editable={editable}>
|
||||
@ -64,8 +64,12 @@ class Demo extends React.Component {
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<Text style={{ width: 100 }} ellipsis copyable>
|
||||
Ant Design is a design language for background applications, is refined by Ant UED Team.
|
||||
</Text>
|
||||
|
||||
<p>
|
||||
2333<Text ellipsis>2333</Text>2333
|
||||
[Before]<Text ellipsis>not ellipsis</Text>[After]
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
18
components/typography/hooks/useMergedConfig.ts
Normal file
18
components/typography/hooks/useMergedConfig.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function useMergedConfig<Target>(
|
||||
propConfig: any,
|
||||
templateConfig?: Target,
|
||||
): [boolean, Target] {
|
||||
return React.useMemo(() => {
|
||||
const support = !!propConfig;
|
||||
|
||||
return [
|
||||
support,
|
||||
{
|
||||
...templateConfig,
|
||||
...(support && typeof propConfig === 'object' ? propConfig : null),
|
||||
},
|
||||
];
|
||||
}, [propConfig]);
|
||||
}
|
14
components/typography/hooks/useUpdatedEffect.ts
Normal file
14
components/typography/hooks/useUpdatedEffect.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/** Similar with `useEffect` but only trigger after mounted */
|
||||
export default (callback: () => void, conditions: any[]) => {
|
||||
const mountRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mountRef.current) {
|
||||
callback();
|
||||
} else {
|
||||
mountRef.current = true;
|
||||
}
|
||||
}, conditions);
|
||||
};
|
@ -1,250 +0,0 @@
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import * as React from 'react';
|
||||
import toArray from 'rc-util/lib/Children/toArray';
|
||||
|
||||
interface MeasureResult {
|
||||
finished: boolean;
|
||||
reactNode: React.ReactNode;
|
||||
}
|
||||
interface Option {
|
||||
rows: number;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
// We only handle element & text node.
|
||||
const ELEMENT_NODE = 1;
|
||||
const TEXT_NODE = 3;
|
||||
const COMMENT_NODE = 8;
|
||||
|
||||
let ellipsisContainer: HTMLParagraphElement;
|
||||
|
||||
const wrapperStyle: React.CSSProperties = {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: 'inline',
|
||||
lineHeight: 'inherit',
|
||||
};
|
||||
|
||||
function styleToString(style: CSSStyleDeclaration) {
|
||||
// There are some different behavior between Firefox & Chrome.
|
||||
// We have to handle this ourself.
|
||||
const styleNames: string[] = Array.prototype.slice.apply(style);
|
||||
return styleNames.map(name => `${name}: ${style.getPropertyValue(name)};`).join('');
|
||||
}
|
||||
|
||||
function mergeChildren(children: React.ReactNode[]): React.ReactNode[] {
|
||||
const childList: React.ReactNode[] = [];
|
||||
|
||||
children.forEach((child: React.ReactNode) => {
|
||||
const prevChild = childList[childList.length - 1];
|
||||
if (typeof child === 'string' && typeof prevChild === 'string') {
|
||||
childList[childList.length - 1] += child;
|
||||
} else {
|
||||
childList.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return childList;
|
||||
}
|
||||
|
||||
function resetDomStyles(target: HTMLElement, origin: HTMLElement) {
|
||||
target.setAttribute('aria-hidden', 'true');
|
||||
const originStyle = window.getComputedStyle(origin);
|
||||
const originCSS = styleToString(originStyle);
|
||||
// Set shadow
|
||||
target.setAttribute('style', originCSS);
|
||||
target.style.position = 'fixed';
|
||||
target.style.left = '0';
|
||||
target.style.height = 'auto';
|
||||
target.style.minHeight = 'auto';
|
||||
target.style.maxHeight = 'auto';
|
||||
target.style.paddingTop = '0';
|
||||
target.style.paddingBottom = '0';
|
||||
target.style.borderTopWidth = '0';
|
||||
target.style.borderBottomWidth = '0';
|
||||
target.style.top = '-999999px';
|
||||
target.style.zIndex = '-1000';
|
||||
// clean up css overflow
|
||||
target.style.textOverflow = 'clip';
|
||||
target.style.whiteSpace = 'normal';
|
||||
(target.style as any).webkitLineClamp = 'none';
|
||||
}
|
||||
|
||||
function getRealLineHeight(originElement: HTMLElement) {
|
||||
const heightContainer = document.createElement('div');
|
||||
resetDomStyles(heightContainer, originElement);
|
||||
heightContainer.appendChild(document.createTextNode('text'));
|
||||
document.body.appendChild(heightContainer);
|
||||
// The element real height is always less than multiple of line-height
|
||||
// Use getBoundingClientRect to get actual single row height of the element
|
||||
const realHeight = heightContainer.getBoundingClientRect().height;
|
||||
document.body.removeChild(heightContainer);
|
||||
return realHeight;
|
||||
}
|
||||
|
||||
export default (
|
||||
originElement: HTMLElement,
|
||||
option: Option,
|
||||
content: React.ReactNode,
|
||||
fixedContent: React.ReactNode[],
|
||||
ellipsisStr: string,
|
||||
): { content: React.ReactNode; text: string; ellipsis: boolean } => {
|
||||
if (!ellipsisContainer) {
|
||||
ellipsisContainer = document.createElement('div');
|
||||
ellipsisContainer.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
// HMR will remove this from body which should patch back
|
||||
if (!ellipsisContainer.parentNode) {
|
||||
document.body.appendChild(ellipsisContainer);
|
||||
}
|
||||
|
||||
const { rows, suffix = '' } = option;
|
||||
const lineHeight = getRealLineHeight(originElement);
|
||||
const maxHeight = Math.round(lineHeight * rows * 100 ) / 100;
|
||||
|
||||
resetDomStyles(ellipsisContainer, originElement);
|
||||
|
||||
// Render in the fake container
|
||||
const contentList: React.ReactNode[] = mergeChildren(toArray(content));
|
||||
render(
|
||||
<div style={wrapperStyle}>
|
||||
<span style={wrapperStyle}>
|
||||
{contentList}
|
||||
{suffix}
|
||||
</span>
|
||||
<span style={wrapperStyle}>{fixedContent}</span>
|
||||
</div>,
|
||||
ellipsisContainer,
|
||||
); // wrap in an div for old version react
|
||||
|
||||
// Check if ellipsis in measure div is height enough for content
|
||||
function inRange() {
|
||||
const currentHeight =
|
||||
Math.round(ellipsisContainer.getBoundingClientRect().height * 100) / 100
|
||||
return currentHeight - .1 <= maxHeight; // -.1 for firefox
|
||||
}
|
||||
|
||||
// Skip ellipsis if already match
|
||||
if (inRange()) {
|
||||
unmountComponentAtNode(ellipsisContainer);
|
||||
return { content, text: ellipsisContainer.innerHTML, ellipsis: false };
|
||||
}
|
||||
|
||||
// We should clone the childNode since they're controlled by React and we can't reuse it without warning
|
||||
const childNodes: ChildNode[] = Array.prototype.slice
|
||||
.apply(ellipsisContainer.childNodes[0].childNodes[0].cloneNode(true).childNodes)
|
||||
.filter(({ nodeType }: ChildNode) => nodeType !== COMMENT_NODE);
|
||||
const fixedNodes: ChildNode[] = Array.prototype.slice.apply(
|
||||
ellipsisContainer.childNodes[0].childNodes[1].cloneNode(true).childNodes,
|
||||
);
|
||||
unmountComponentAtNode(ellipsisContainer);
|
||||
|
||||
// ========================= Find match ellipsis content =========================
|
||||
const ellipsisChildren: React.ReactNode[] = [];
|
||||
ellipsisContainer.innerHTML = '';
|
||||
|
||||
// Create origin content holder
|
||||
const ellipsisContentHolder = document.createElement('span');
|
||||
ellipsisContainer.appendChild(ellipsisContentHolder);
|
||||
const ellipsisTextNode = document.createTextNode(ellipsisStr + suffix);
|
||||
ellipsisContentHolder.appendChild(ellipsisTextNode);
|
||||
|
||||
fixedNodes.forEach(childNode => {
|
||||
ellipsisContainer.appendChild(childNode);
|
||||
});
|
||||
|
||||
// Append before fixed nodes
|
||||
function appendChildNode(node: ChildNode) {
|
||||
ellipsisContentHolder.insertBefore(node, ellipsisTextNode);
|
||||
}
|
||||
|
||||
// Get maximum text
|
||||
function measureText(
|
||||
textNode: Text,
|
||||
fullText: string,
|
||||
startLoc = 0,
|
||||
endLoc = fullText.length,
|
||||
lastSuccessLoc = 0,
|
||||
): MeasureResult {
|
||||
const midLoc = Math.floor((startLoc + endLoc) / 2);
|
||||
const currentText = fullText.slice(0, midLoc);
|
||||
textNode.textContent = currentText;
|
||||
|
||||
if (startLoc >= endLoc - 1) {
|
||||
// Loop when step is small
|
||||
for (let step = endLoc; step >= startLoc; step -= 1) {
|
||||
const currentStepText = fullText.slice(0, step);
|
||||
textNode.textContent = currentStepText;
|
||||
|
||||
if (inRange() || !currentStepText) {
|
||||
return step === fullText.length
|
||||
? {
|
||||
finished: false,
|
||||
reactNode: fullText,
|
||||
}
|
||||
: {
|
||||
finished: true,
|
||||
reactNode: currentStepText,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange()) {
|
||||
return measureText(textNode, fullText, midLoc, endLoc, midLoc);
|
||||
}
|
||||
return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc);
|
||||
}
|
||||
|
||||
function measureNode(childNode: ChildNode, index: number): MeasureResult {
|
||||
const type = childNode.nodeType;
|
||||
|
||||
if (type === ELEMENT_NODE) {
|
||||
// We don't split element, it will keep if whole element can be displayed.
|
||||
appendChildNode(childNode);
|
||||
if (inRange()) {
|
||||
return {
|
||||
finished: false,
|
||||
reactNode: contentList[index],
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up if can not pull in
|
||||
ellipsisContentHolder.removeChild(childNode);
|
||||
return {
|
||||
finished: true,
|
||||
reactNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === TEXT_NODE) {
|
||||
const fullText = childNode.textContent || '';
|
||||
const textNode = document.createTextNode(fullText);
|
||||
appendChildNode(textNode);
|
||||
return measureText(textNode, fullText);
|
||||
}
|
||||
|
||||
// Not handle other type of content
|
||||
// PS: This code should not be attached after react 16
|
||||
/* istanbul ignore next */
|
||||
return {
|
||||
finished: false,
|
||||
reactNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
childNodes.some((childNode, index) => {
|
||||
const { finished, reactNode } = measureNode(childNode, index);
|
||||
if (reactNode) {
|
||||
ellipsisChildren.push(reactNode);
|
||||
}
|
||||
return finished;
|
||||
});
|
||||
|
||||
return {
|
||||
content: ellipsisChildren,
|
||||
text: ellipsisContainer.innerHTML,
|
||||
ellipsis: true,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user