refactor: reimplement Typography code (#50561)

Co-authored-by: lijianan <574980606@qq.com>
This commit is contained in:
afc163 2024-08-27 22:08:04 +08:00 committed by GitHub
parent 1cd79e3b0a
commit 9f274a0884
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 100 additions and 171 deletions

View File

@ -19,32 +19,26 @@ export interface CopyBtnProps extends Omit<CopyConfig, 'onCopy'> {
loading: boolean; loading: boolean;
} }
const CopyBtn: React.FC<CopyBtnProps> = (props) => { const CopyBtn: React.FC<CopyBtnProps> = ({
const { prefixCls,
prefixCls, copied,
copied, locale,
locale, iconOnly,
iconOnly, tooltips,
tooltips, icon,
icon, tabIndex,
loading: btnLoading, onCopy,
tabIndex, loading: btnLoading,
onCopy, }) => {
} = props;
const tooltipNodes = toList(tooltips); const tooltipNodes = toList(tooltips);
const iconNodes = toList(icon); const iconNodes = toList(icon);
const { copied: copiedText, copy: copyText } = locale ?? {}; const { copied: copiedText, copy: copyText } = locale ?? {};
const copyTitle = copied
? getNode(tooltipNodes[1], copiedText)
: getNode(tooltipNodes[0], copyText);
const systemStr = copied ? copiedText : copyText; const systemStr = copied ? copiedText : copyText;
const copyTitle = getNode(tooltipNodes[copied ? 1 : 0], systemStr);
const ariaLabel = typeof copyTitle === 'string' ? copyTitle : systemStr; const ariaLabel = typeof copyTitle === 'string' ? copyTitle : systemStr;
return ( return (
<Tooltip key="copy" title={copyTitle}> <Tooltip title={copyTitle}>
<TransButton <TransButton
className={classNames(`${prefixCls}-copy`, { className={classNames(`${prefixCls}-copy`, {
[`${prefixCls}-copy-success`]: copied, [`${prefixCls}-copy-success`]: copied,

View File

@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import toArray from 'rc-util/lib/Children/toArray'; import toArray from 'rc-util/lib/Children/toArray';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import { isValidText } from './util';
interface MeasureTextProps { interface MeasureTextProps {
style?: React.CSSProperties; style?: React.CSSProperties;
@ -44,24 +45,8 @@ const MeasureText = React.forwardRef<MeasureTextRef, MeasureTextProps>(
}, },
); );
function cuttable(node: React.ReactElement) { const getNodesLen = (nodeList: React.ReactElement[]) =>
const type = typeof node; nodeList.reduce((totalLen, node) => totalLen + (isValidText(node) ? String(node).length : 1), 0);
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) { function sliceNodes(nodeList: React.ReactElement[], len: number) {
let currLen = 0; let currLen = 0;
@ -74,7 +59,7 @@ function sliceNodes(nodeList: React.ReactElement[], len: number) {
} }
const node = nodeList[i]; const node = nodeList[i];
const canCut = cuttable(node); const canCut = isValidText(node);
const nodeLen = canCut ? String(node).length : 1; const nodeLen = canCut ? String(node).length : 1;
const nextLen = currLen + nodeLen; const nextLen = currLen + nodeLen;
@ -97,7 +82,6 @@ export interface EllipsisProps {
enableMeasure?: boolean; enableMeasure?: boolean;
text?: React.ReactNode; text?: React.ReactNode;
width: number; width: number;
// fontSize: number;
rows: number; rows: number;
children: ( children: (
cutChildren: React.ReactNode[], cutChildren: React.ReactNode[],
@ -142,9 +126,7 @@ export default function EllipsisMeasure(props: EllipsisProps) {
// ========================= NeedEllipsis ========================= // ========================= NeedEllipsis =========================
const measureWhiteSpaceRef = React.useRef<HTMLElement>(null); const measureWhiteSpaceRef = React.useRef<HTMLElement>(null);
const needEllipsisRef = React.useRef<MeasureTextRef>(null); const needEllipsisRef = React.useRef<MeasureTextRef>(null);
// Measure for `rows-1` height, to avoid operation exceed the line height // Measure for `rows-1` height, to avoid operation exceed the line height
const descRowsEllipsisRef = React.useRef<MeasureTextRef>(null); const descRowsEllipsisRef = React.useRef<MeasureTextRef>(null);
const symbolRowEllipsisRef = React.useRef<MeasureTextRef>(null); const symbolRowEllipsisRef = React.useRef<MeasureTextRef>(null);
@ -187,9 +169,11 @@ export default function EllipsisMeasure(props: EllipsisProps) {
// Get the height of `rows - 1` + symbol height // Get the height of `rows - 1` + symbol height
const descRowsEllipsisHeight = rows === 1 ? 0 : descRowsEllipsisRef.current?.getHeight() || 0; const descRowsEllipsisHeight = rows === 1 ? 0 : descRowsEllipsisRef.current?.getHeight() || 0;
const symbolRowEllipsisHeight = symbolRowEllipsisRef.current?.getHeight() || 0; const symbolRowEllipsisHeight = symbolRowEllipsisRef.current?.getHeight() || 0;
const rowsWithEllipsisHeight = descRowsEllipsisHeight + symbolRowEllipsisHeight; const maxRowsHeight = Math.max(
baseRowsEllipsisHeight,
const maxRowsHeight = Math.max(baseRowsEllipsisHeight, rowsWithEllipsisHeight); // height of rows with ellipsis
descRowsEllipsisHeight + symbolRowEllipsisHeight,
);
setEllipsisHeight(maxRowsHeight + 1); setEllipsisHeight(maxRowsHeight + 1);
@ -209,16 +193,10 @@ export default function EllipsisMeasure(props: EllipsisProps) {
const isOverflow = midHeight > ellipsisHeight; const isOverflow = midHeight > ellipsisHeight;
let targetMidIndex = cutMidIndex; let targetMidIndex = cutMidIndex;
if (maxIndex - minIndex === 1) { if (maxIndex - minIndex === 1) {
targetMidIndex = isOverflow ? minIndex : maxIndex; targetMidIndex = isOverflow ? minIndex : maxIndex;
} }
setEllipsisCutIndex(isOverflow ? [minIndex, targetMidIndex] : [targetMidIndex, maxIndex]);
if (isOverflow) {
setEllipsisCutIndex([minIndex, targetMidIndex]);
} else {
setEllipsisCutIndex([targetMidIndex, maxIndex]);
}
} }
}, [ellipsisCutIndex, cutMidIndex]); }, [ellipsisCutIndex, cutMidIndex]);
@ -235,26 +213,21 @@ export default function EllipsisMeasure(props: EllipsisProps) {
ellipsisCutIndex[0] !== ellipsisCutIndex[1] ellipsisCutIndex[0] !== ellipsisCutIndex[1]
) { ) {
const content = children(nodeList, false); const content = children(nodeList, false);
// Limit the max line count to avoid scrollbar blink unless no need ellipsis
// Limit the max line count to avoid scrollbar blink
// https://github.com/ant-design/ant-design/issues/42958 // https://github.com/ant-design/ant-design/issues/42958
if ( if ([STATUS_MEASURE_NO_NEED_ELLIPSIS, STATUS_MEASURE_NONE].includes(needEllipsis)) {
needEllipsis !== STATUS_MEASURE_NO_NEED_ELLIPSIS && return content;
needEllipsis !== STATUS_MEASURE_NONE
) {
return (
<span
style={{
...lineClipStyle,
WebkitLineClamp: rows,
}}
>
{content}
</span>
);
} }
return (
return content; <span
style={{
...lineClipStyle,
WebkitLineClamp: rows,
}}
>
{content}
</span>
);
} }
return children(expanded ? nodeList : sliceNodes(nodeList, ellipsisCutIndex[0]), canEllipsis); return children(expanded ? nodeList : sliceNodes(nodeList, ellipsisCutIndex[0]), canEllipsis);

View File

@ -4,7 +4,7 @@ import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
import type { AutoSizeType } from 'rc-textarea'; import type { AutoSizeType } from 'rc-textarea';
import toArray from 'rc-util/lib/Children/toArray'; import toArray from 'rc-util/lib/Children/toArray';
import useIsomorphicLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import useMergedState from 'rc-util/lib/hooks/useMergedState'; import useMergedState from 'rc-util/lib/hooks/useMergedState';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
@ -19,13 +19,13 @@ import Editable from '../Editable';
import useCopyClick from '../hooks/useCopyClick'; import useCopyClick from '../hooks/useCopyClick';
import useMergedConfig from '../hooks/useMergedConfig'; import useMergedConfig from '../hooks/useMergedConfig';
import usePrevious from '../hooks/usePrevious'; import usePrevious from '../hooks/usePrevious';
import useUpdatedEffect from '../hooks/useUpdatedEffect'; import useTooltipProps from '../hooks/useTooltipProps';
import type { TypographyProps } from '../Typography'; import type { TypographyProps } from '../Typography';
import Typography from '../Typography'; import Typography from '../Typography';
import CopyBtn from './CopyBtn'; import CopyBtn from './CopyBtn';
import Ellipsis from './Ellipsis'; import Ellipsis from './Ellipsis';
import EllipsisTooltip from './EllipsisTooltip'; import EllipsisTooltip from './EllipsisTooltip';
import { isEleEllipsis } from './util'; import { isEleEllipsis, isValidText } from './util';
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger'; export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
@ -162,7 +162,7 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
// Focus edit icon when back // Focus edit icon when back
const prevEditing = usePrevious(editing); const prevEditing = usePrevious(editing);
useUpdatedEffect(() => { useLayoutEffect(() => {
if (!editing && prevEditing) { if (!editing && prevEditing) {
editIconRef.current?.focus(); editIconRef.current?.focus();
} }
@ -223,7 +223,7 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
[mergedEnableEllipsis, ellipsisConfig, enableEdit, enableCopy], [mergedEnableEllipsis, ellipsisConfig, enableEdit, enableCopy],
); );
useIsomorphicLayoutEffect(() => { useLayoutEffect(() => {
if (enableEllipsis && !needMeasureEllipsis) { if (enableEllipsis && !needMeasureEllipsis) {
setIsLineClampSupport(isStyleSupport('webkitLineClamp')); setIsLineClampSupport(isStyleSupport('webkitLineClamp'));
setIsTextOverflowSupport(isStyleSupport('textOverflow')); setIsTextOverflowSupport(isStyleSupport('textOverflow'));
@ -246,7 +246,7 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
// We use effect to change from css ellipsis to js ellipsis. // We use effect to change from css ellipsis to js ellipsis.
// To make SSR still can see the ellipsis. // To make SSR still can see the ellipsis.
useIsomorphicLayoutEffect(() => { useLayoutEffect(() => {
setCssEllipsis(canUseCssEllipsis && mergedEnableEllipsis); setCssEllipsis(canUseCssEllipsis && mergedEnableEllipsis);
}, [canUseCssEllipsis, mergedEnableEllipsis]); }, [canUseCssEllipsis, mergedEnableEllipsis]);
@ -314,40 +314,13 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
}, [cssEllipsis, mergedEnableEllipsis]); }, [cssEllipsis, mergedEnableEllipsis]);
// ========================== Tooltip =========================== // ========================== Tooltip ===========================
let tooltipProps: TooltipProps = {}; const tooltipProps = useTooltipProps(ellipsisConfig.tooltip, editConfig.text, children);
if (ellipsisConfig.tooltip === true) {
tooltipProps = { title: editConfig.text ?? children };
} else if (React.isValidElement(ellipsisConfig.tooltip)) {
tooltipProps = { title: ellipsisConfig.tooltip };
} else if (typeof ellipsisConfig.tooltip === 'object') {
tooltipProps = { title: editConfig.text ?? children, ...ellipsisConfig.tooltip };
} else {
tooltipProps = { title: ellipsisConfig.tooltip };
}
const topAriaLabel = React.useMemo(() => {
const isValid = (val: any): val is string | number => ['string', 'number'].includes(typeof val);
const topAriaLabel = React.useMemo(() => {
if (!enableEllipsis || cssEllipsis) { if (!enableEllipsis || cssEllipsis) {
return undefined; return undefined;
} }
return [editConfig.text, children, title, tooltipProps.title].find(isValidText);
if (isValid(editConfig.text)) {
return editConfig.text;
}
if (isValid(children)) {
return children;
}
if (isValid(title)) {
return title;
}
if (isValid(tooltipProps.title)) {
return tooltipProps.title;
}
return undefined;
}, [enableEllipsis, cssEllipsis, title, tooltipProps.title, isMergedEllipsis]); }, [enableEllipsis, cssEllipsis, title, tooltipProps.title, isMergedEllipsis]);
// =========================== Render =========================== // =========================== Render ===========================

View File

@ -35,16 +35,15 @@ export function isEleEllipsis(ele: HTMLElement): boolean {
ele.removeChild(childDiv); ele.removeChild(childDiv);
// Range checker // Range checker
if ( return (
// Horizontal in range // Horizontal out of range
rect.left <= childRect.left && rect.left > childRect.left ||
childRect.right <= rect.right && childRect.right > rect.right ||
// Vertical in range // Vertical out of range
rect.top <= childRect.top && rect.top > childRect.top ||
childRect.bottom <= rect.bottom childRect.bottom > rect.bottom
) { );
return false;
}
return true;
} }
export const isValidText = (val: any): val is string | number =>
['string', 'number'].includes(typeof val);

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import EnterOutlined from '@ant-design/icons/EnterOutlined'; import EnterOutlined from '@ant-design/icons/EnterOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import type { AutoSizeType } from 'rc-textarea'; import type { TextAreaProps } from 'rc-textarea';
import KeyCode from 'rc-util/lib/KeyCode'; import KeyCode from 'rc-util/lib/KeyCode';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
@ -21,7 +21,7 @@ interface EditableProps {
style?: React.CSSProperties; style?: React.CSSProperties;
direction?: DirectionType; direction?: DirectionType;
maxLength?: number; maxLength?: number;
autoSize?: boolean | AutoSizeType; autoSize?: TextAreaProps['autoSize'];
enterIcon?: React.ReactNode; enterIcon?: React.ReactNode;
component?: string; component?: string;
} }
@ -94,19 +94,20 @@ const Editable: React.FC<EditableProps> = (props) => {
}) => { }) => {
// Check if it's a real key // Check if it's a real key
if ( if (
lastKeyCode.current === keyCode && lastKeyCode.current !== keyCode ||
!inComposition.current && inComposition.current ||
!ctrlKey && ctrlKey ||
!altKey && altKey ||
!metaKey && metaKey ||
!shiftKey shiftKey
) { ) {
if (keyCode === KeyCode.ENTER) { return;
confirmChange(); }
onEnd?.(); if (keyCode === KeyCode.ENTER) {
} else if (keyCode === KeyCode.ESC) { confirmChange();
onCancel(); onEnd?.();
} } else if (keyCode === KeyCode.ESC) {
onCancel();
} }
}; };
@ -114,8 +115,6 @@ const Editable: React.FC<EditableProps> = (props) => {
confirmChange(); confirmChange();
}; };
const textClassName = component ? `${prefixCls}-${component}` : '';
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const textAreaClassName = classNames( const textAreaClassName = classNames(
@ -123,9 +122,9 @@ const Editable: React.FC<EditableProps> = (props) => {
`${prefixCls}-edit-content`, `${prefixCls}-edit-content`,
{ {
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-${component}`]: !!component,
}, },
className, className,
textClassName,
hashId, hashId,
cssVarCls, cssVarCls,
); );

View File

@ -19,7 +19,6 @@ const Text: React.ForwardRefRenderFunction<HTMLSpanElement, TextProps> = (
if (ellipsis && typeof ellipsis === 'object') { if (ellipsis && typeof ellipsis === 'object') {
return omit(ellipsis as EllipsisConfig, ['expandable', 'rows']); return omit(ellipsis as EllipsisConfig, ['expandable', 'rows']);
} }
return ellipsis; return ellipsis;
}, [ellipsis]); }, [ellipsis]);

View File

@ -17,8 +17,6 @@ export interface TitleProps
const Title = React.forwardRef<HTMLElement, TitleProps>((props, ref) => { const Title = React.forwardRef<HTMLElement, TitleProps>((props, ref) => {
const { level = 1, ...restProps } = props; const { level = 1, ...restProps } = props;
let component: keyof JSX.IntrinsicElements;
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Typography.Title'); const warning = devUseWarning('Typography.Title');
@ -28,13 +26,9 @@ const Title = React.forwardRef<HTMLElement, TitleProps>((props, ref) => {
'Title only accept `1 | 2 | 3 | 4 | 5` as `level` value. And `5` need 4.6.0+ version.', 'Title only accept `1 | 2 | 3 | 4 | 5` as `level` value. And `5` need 4.6.0+ version.',
); );
} }
const component: keyof JSX.IntrinsicElements = TITLE_ELE_LIST.includes(level)
if (TITLE_ELE_LIST.includes(level)) { ? `h${level}`
component = `h${level}`; : `h1`;
} else {
component = 'h1';
}
return <Base ref={ref} {...restProps} component={component} />; return <Base ref={ref} {...restProps} component={component} />;
}); });

View File

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
import { devUseWarning } from '../_util/warning'; import { devUseWarning } from '../_util/warning';
import type { ConfigConsumerProps, DirectionType } from '../config-provider'; import type { ConfigConsumerProps, DirectionType } from '../config-provider';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
@ -42,6 +41,7 @@ const Typography = React.forwardRef<
style, style,
...restProps ...restProps
} = props; } = props;
const { const {
getPrefixCls, getPrefixCls,
direction: contextDirection, direction: contextDirection,
@ -49,23 +49,16 @@ const Typography = React.forwardRef<
} = React.useContext<ConfigConsumerProps>(ConfigContext); } = React.useContext<ConfigConsumerProps>(ConfigContext);
const direction = typographyDirection ?? contextDirection; const direction = typographyDirection ?? contextDirection;
const mergedRef = setContentRef ? composeRef(ref, setContentRef) : ref;
let mergedRef = ref; const prefixCls = getPrefixCls('typography', customizePrefixCls);
if (setContentRef) {
mergedRef = composeRef(ref, setContentRef);
}
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Typography'); const warning = devUseWarning('Typography');
warning.deprecated(!setContentRef, 'setContentRef', 'ref'); warning.deprecated(!setContentRef, 'setContentRef', 'ref');
} }
const prefixCls = getPrefixCls('typography', customizePrefixCls);
// Style // Style
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const componentClassName = classNames( const componentClassName = classNames(
prefixCls, prefixCls,
typography?.className, typography?.className,
@ -92,5 +85,4 @@ if (process.env.NODE_ENV !== 'production') {
Typography.displayName = 'Typography'; Typography.displayName = 'Typography';
} }
// es default export should use const instead of let
export default Typography; export default Typography;

View File

@ -0,0 +1,22 @@
import { isValidElement, useMemo } from 'react';
import type { TooltipProps } from '../../tooltip';
const useTooltipProps = (
tooltip: React.ReactNode | TooltipProps,
editConfigText: React.ReactNode,
children: React.ReactNode,
) =>
useMemo(() => {
if (tooltip === true) {
return { title: editConfigText ?? children };
}
if (isValidElement(tooltip)) {
return { title: tooltip };
}
if (typeof tooltip === 'object') {
return { title: editConfigText ?? children, ...tooltip };
}
return { title: tooltip };
}, [typeof tooltip === 'object' ? JSON.stringify(tooltip) : tooltip, editConfigText, children]);
export default useTooltipProps;

View File

@ -1,16 +0,0 @@
import * as React from 'react';
/** Similar with `useEffect` but only trigger after mounted */
const useUpdatedEffect = (callback: () => void, conditions?: React.DependencyList) => {
const mountRef = React.useRef(false);
React.useEffect(() => {
if (mountRef.current) {
callback();
} else {
mountRef.current = true;
}
}, conditions);
};
export default useUpdatedEffect;