Skip to content

Commit fd2d7f6

Browse files
committed
fix: extract useTextResize hook
1 parent 7a40fef commit fd2d7f6

File tree

4 files changed

+341
-188
lines changed

4 files changed

+341
-188
lines changed
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React from 'react';
2+
import { cleanup, render } from '@testing-library/react';
3+
import { act, renderHook } from '@testing-library/react-hooks';
4+
5+
import useTextStyle from '../useTextStyle';
6+
import { getContainerWidth, getRangeWidth, getStyle } from '../utils';
7+
8+
jest.mock('../utils', () => ({
9+
getContainerWidth: jest.fn(),
10+
getRangeWidth: jest.fn(),
11+
getStyle: jest.fn(),
12+
}));
13+
14+
describe('Test useTextStyle', () => {
15+
const mockGetContainerWidth = getContainerWidth as jest.Mock;
16+
const mockGetRangeWidth = getRangeWidth as jest.Mock;
17+
const mockGetStyle = getStyle as jest.Mock;
18+
19+
beforeEach(() => {
20+
cleanup();
21+
jest.clearAllMocks();
22+
});
23+
it('should return a ref, overflow state, style, and trigger function', () => {
24+
const { result } = renderHook(() => useTextStyle('Test Text'));
25+
const [ref, isOverflow, style, updateTextStyle] = result.current;
26+
27+
expect(ref).toBeInstanceOf(Object);
28+
expect(typeof isOverflow).toBe('boolean');
29+
expect(style).toBeInstanceOf(Object);
30+
expect(typeof updateTextStyle).toBe('function');
31+
});
32+
33+
it('should calculate overflow correctly', () => {
34+
const { result } = renderHook(() => useTextStyle('Test Text'));
35+
const [ref, , , updateTextStyle] = result.current;
36+
37+
render(<span ref={ref}></span>);
38+
39+
mockGetRangeWidth.mockReturnValue(150);
40+
mockGetContainerWidth.mockReturnValue(100);
41+
42+
act(() => {
43+
updateTextStyle();
44+
});
45+
46+
const [, isOverflow, style] = result.current;
47+
48+
expect(mockGetRangeWidth).toHaveBeenCalled();
49+
expect(mockGetContainerWidth).toHaveBeenCalled();
50+
expect(isOverflow).toBe(true);
51+
expect(style.maxWidth).toBe(100);
52+
});
53+
54+
it('should not overflow if the container width is sufficient', () => {
55+
const { result } = renderHook(() => useTextStyle('Test Text'));
56+
const [ref, , , updateTextStyle] = result.current;
57+
58+
render(<span ref={ref}></span>);
59+
60+
mockGetRangeWidth.mockReturnValue(80);
61+
mockGetContainerWidth.mockReturnValue(100);
62+
63+
act(() => {
64+
updateTextStyle();
65+
});
66+
const [, isOverflow, style] = result.current;
67+
68+
expect(isOverflow).toBe(false);
69+
expect(style.maxWidth).toBe(100);
70+
});
71+
72+
it('should inherit cursor style from parent', () => {
73+
const { result } = renderHook(() => useTextStyle('Test Text'));
74+
const [ref, , , updateTextStyle] = result.current;
75+
76+
render(<span ref={ref}></span>);
77+
78+
mockGetStyle.mockReturnValue('pointer');
79+
80+
act(() => {
81+
updateTextStyle();
82+
});
83+
const [, , style] = result.current;
84+
85+
expect(style.cursor).toBe('pointer');
86+
});
87+
88+
it('should have cursor style as default when text is not overflowing and parent cursor is default', () => {
89+
const { result } = renderHook(() => useTextStyle('Test Text'));
90+
const [ref, , , updateTextStyle] = result.current;
91+
92+
render(<span ref={ref}></span>);
93+
94+
mockGetRangeWidth.mockReturnValue(80);
95+
mockGetContainerWidth.mockReturnValue(100);
96+
mockGetStyle.mockReturnValue('default');
97+
98+
act(() => {
99+
updateTextStyle();
100+
});
101+
const [, isOverflow, style] = result.current;
102+
103+
expect(isOverflow).toBe(false);
104+
expect(style.cursor).toBe('default');
105+
});
106+
107+
it('should have cursor style as pointer when text is overflowing and parent cursor is default', () => {
108+
const { result } = renderHook(() => useTextStyle('Test Text'));
109+
const [ref, , , updateTextStyle] = result.current;
110+
111+
render(<span ref={ref}></span>);
112+
113+
mockGetRangeWidth.mockReturnValue(150);
114+
mockGetContainerWidth.mockReturnValue(100);
115+
mockGetStyle.mockReturnValue('default');
116+
117+
act(() => {
118+
updateTextStyle();
119+
});
120+
const [, isOverflow, style] = result.current;
121+
122+
expect(isOverflow).toBe(true);
123+
expect(style.cursor).toBe('pointer');
124+
});
125+
});

src/ellipsisText/index.tsx

+8-188
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
import React, {
2-
CSSProperties,
3-
ReactNode,
4-
useCallback,
5-
useLayoutEffect,
6-
useRef,
7-
useState,
8-
} from 'react';
1+
import React, { ReactNode, useCallback } from 'react';
92
import { Tooltip } from 'antd';
103
import { AbstractTooltipProps, RenderFunction } from 'antd/lib/tooltip';
114
import classNames from 'classnames';
125

136
import Resize from '../resize';
7+
import useTextStyle from './useTextStyle';
148
import './style.scss';
159

1610
export interface IEllipsisTextProps extends AbstractTooltipProps {
1711
/**
1812
* 文本内容
1913
*/
20-
value: string | number | ReactNode | RenderFunction;
14+
value: ReactNode | RenderFunction;
2115
/**
2216
* 提示内容
2317
* @default value
@@ -41,12 +35,6 @@ export interface IEllipsisTextProps extends AbstractTooltipProps {
4135
[propName: string]: any;
4236
}
4337

44-
export interface NewHTMLElement extends HTMLElement {
45-
currentStyle?: CSSStyleDeclaration;
46-
}
47-
48-
const DEFAULT_MAX_WIDTH = 120;
49-
5038
const EllipsisText = (props: IEllipsisTextProps) => {
5139
const {
5240
value,
@@ -56,190 +44,22 @@ const EllipsisText = (props: IEllipsisTextProps) => {
5644
watchParentSizeChange = false,
5745
...otherProps
5846
} = props;
47+
const [ref, isOverflow, style, onResize] = useTextStyle(value, maxWidth);
5948

60-
const ellipsisRef = useRef<HTMLSpanElement>(null);
6149
const observerEle =
62-
watchParentSizeChange && ellipsisRef.current?.parentElement
63-
? ellipsisRef.current?.parentElement
64-
: null;
65-
66-
const [visible, setVisible] = useState(false);
67-
const [width, setWidth] = useState<number | string>(DEFAULT_MAX_WIDTH);
68-
const [cursor, setCursor] = useState('default');
69-
70-
useLayoutEffect(() => {
71-
onResize();
72-
}, [value, maxWidth]);
73-
74-
/**
75-
* @description: 根据属性名,获取dom的属性值
76-
* @param {NewHTMLElement} dom
77-
* @param {string} attr
78-
* @return {*}
79-
*/
80-
const getStyle = (dom: NewHTMLElement, attr: string) => {
81-
// Compatible width IE8
82-
// @ts-ignore
83-
return window.getComputedStyle(dom)[attr] || dom.currentStyle[attr];
84-
};
85-
86-
/**
87-
* @description: 根据属性名,获取dom的属性值为number的属性。如: height、width。。。
88-
* @param {NewHTMLElement} dom
89-
* @param {string} attr
90-
* @return {*}
91-
*/
92-
const getNumTypeStyleValue = (dom: NewHTMLElement, attr: string) => {
93-
return parseInt(getStyle(dom, attr));
94-
};
95-
96-
/**
97-
* @description: 10 -> 10,
98-
* @description: 10px -> 10,
99-
* @description: 90% -> ele.width * 0.9
100-
* @description: calc(100% - 32px) -> ele.width - 32
101-
* @param {*} ele
102-
* @param {string & number} maxWidth
103-
* @return {*}
104-
*/
105-
const transitionWidth = (ele: HTMLElement, maxWidth: string | number) => {
106-
const eleWidth = getActualWidth(ele);
107-
108-
if (typeof maxWidth === 'number') {
109-
return maxWidth > eleWidth ? eleWidth : maxWidth; // 如果父元素的宽度小于传入的最大宽度,返回父元素的宽度
110-
}
111-
112-
const numMatch = maxWidth.match(/^(\d+)(px)?$/);
113-
if (numMatch) {
114-
return +numMatch[1] > eleWidth ? eleWidth : +numMatch[1]; // 如果父元素的宽度小于传入的最大宽度,返回父元素的宽度
115-
}
116-
117-
const percentMatch = maxWidth.match(/^(\d+)%$/);
118-
if (percentMatch) {
119-
return eleWidth * (parseInt(percentMatch[1]) / 100);
120-
}
121-
122-
const relativeMatch = maxWidth.match(/^calc\(100% - (\d+)px\)$/);
123-
if (relativeMatch) {
124-
return eleWidth - parseInt(relativeMatch[1]);
125-
}
126-
127-
return eleWidth;
128-
};
129-
130-
const hideEleContent = (node: HTMLElement) => {
131-
node.style.display = 'none';
132-
};
133-
134-
const showEleContent = (node: HTMLElement) => {
135-
node.style.display = 'inline-block';
136-
};
137-
138-
/**
139-
* @description: 获取能够得到宽度的最近父元素宽度。行内元素无法获得宽度,需向上查找父元素
140-
* @param {HTMLElement} ele
141-
* @return {*}
142-
*/
143-
const getContainerWidth = (ele: HTMLElement): number | string => {
144-
if (!ele) return DEFAULT_MAX_WIDTH;
145-
146-
const { scrollWidth, parentElement } = ele;
147-
148-
// 如果是行内元素,获取不到宽度,则向上寻找父元素
149-
if (scrollWidth === 0) {
150-
return getContainerWidth(parentElement!);
151-
}
152-
// 如果设置了最大宽度,则直接返回宽度
153-
if (maxWidth) {
154-
return transitionWidth(ele, maxWidth);
155-
}
156-
157-
hideEleContent(ellipsisRef.current!);
158-
159-
const availableWidth = getAvailableWidth(ele);
160-
161-
return availableWidth < 0 ? 0 : availableWidth;
162-
};
163-
164-
/**
165-
* @description: 获取dom元素的内容宽度
166-
* @param {HTMLElement} ele
167-
* @return {*}
168-
*/
169-
const getRangeWidth = (ele: HTMLElement): any => {
170-
const range = document.createRange();
171-
range.selectNodeContents(ele);
172-
const rangeWidth = range.getBoundingClientRect().width;
173-
174-
return rangeWidth;
175-
};
176-
177-
/**
178-
* @description: 获取元素不包括 padding 的宽度
179-
* @param {HTMLElement} ele
180-
* @return {*}
181-
*/
182-
const getActualWidth = (ele: HTMLElement) => {
183-
const width = ele.getBoundingClientRect().width;
184-
const paddingLeft = getNumTypeStyleValue(ele, 'paddingLeft');
185-
const paddingRight = getNumTypeStyleValue(ele, 'paddingRight');
186-
return width - paddingLeft - paddingRight;
187-
};
188-
189-
/**
190-
* @description: 获取dom的可用宽度
191-
* @param {HTMLElement} ele
192-
* @return {*}
193-
*/
194-
const getAvailableWidth = (ele: HTMLElement) => {
195-
const width = getActualWidth(ele);
196-
const contentWidth = getRangeWidth(ele);
197-
const ellipsisWidth = width - contentWidth;
198-
return ellipsisWidth;
199-
};
200-
201-
/**
202-
* @description: 计算父元素的宽度是否满足内容的大小
203-
* @return {*}
204-
*/
205-
const onResize = () => {
206-
const ellipsisNode = ellipsisRef.current!;
207-
const parentElement = ellipsisNode.parentElement!;
208-
const rangeWidth = getRangeWidth(ellipsisNode);
209-
const containerWidth = getContainerWidth(parentElement);
210-
const visible = rangeWidth > containerWidth;
211-
setVisible(visible);
212-
setWidth(containerWidth);
213-
const parentCursor = getStyle(parentElement, 'cursor');
214-
if (parentCursor !== 'default') {
215-
// 继承父元素的 hover 手势
216-
setCursor(parentCursor);
217-
} else {
218-
// 截取文本时,则改变 hover 手势为 pointer
219-
visible && setCursor('pointer');
220-
}
221-
showEleContent(ellipsisNode);
222-
};
50+
watchParentSizeChange && ref.current?.parentElement ? ref.current?.parentElement : null;
22351

22452
const renderText = useCallback(() => {
225-
const style: CSSProperties = {
226-
maxWidth: width,
227-
cursor,
228-
};
22953
return (
230-
<span
231-
ref={ellipsisRef}
232-
className={classNames('dtc-ellipsis-text', className)}
233-
style={style}
234-
>
54+
<span ref={ref} className={classNames('dtc-ellipsis-text', className)} style={style}>
23555
{typeof value === 'function' ? value() : value}
23656
</span>
23757
);
238-
}, [width, cursor, value]);
58+
}, [style, value]);
23959

24060
return (
24161
<Resize onResize={onResize} observerEle={observerEle}>
242-
{visible ? (
62+
{isOverflow ? (
24363
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0} {...otherProps}>
24464
{renderText()}
24565
</Tooltip>

0 commit comments

Comments
 (0)