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:
二货机器人 2022-01-18 14:13:41 +08:00 committed by GitHub
parent 2ae15a6de2
commit 395c549049
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1650 additions and 1085 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View 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;

View File

@ -130,6 +130,7 @@ const Editable: React.FC<EditableProps> = ({
onCompositionEnd={onCompositionEnd}
onBlur={onBlur}
aria-label={ariaLabel}
rows={1}
autoSize={autoSize}
/>
{enterIcon !== null

View File

@ -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,

View File

@ -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>,
]
`;

View File

@ -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>,
]
`;

View 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');
});
});

View File

@ -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 () => {

View File

@ -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>
</>
);

View 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]);
}

View 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);
};

View File

@ -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,
};
};