Skip to content

Commit e0b9d9d

Browse files
committed
start from main and add debounce
Signed-off-by: gitdallas <5322142+gitdallas@users.noreply.github.com>
1 parent 2cf0532 commit e0b9d9d

File tree

3 files changed

+83
-73
lines changed

3 files changed

+83
-73
lines changed

packages/react-core/src/components/Truncate/Truncate.tsx

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Fragment, useEffect, useRef, useState, forwardRef, useImperativeHandle
22
import styles from '@patternfly/react-styles/css/components/Truncate/truncate';
33
import { css } from '@patternfly/react-styles';
44
import { Tooltip, TooltipPosition, TooltipProps } from '../Tooltip';
5+
import { getReferenceElement } from '../../helpers';
56
import { getResizeObserver } from '../../helpers/resizeObserver';
67
import { debounce } from '../../helpers/util';
78

@@ -76,94 +77,82 @@ const TruncateBase: React.FunctionComponent<TruncateProps> = ({
7677
omissionContent = '\u2026',
7778
content,
7879
innerRef,
79-
onMouseEnter,
80-
onMouseLeave,
81-
onFocus,
82-
onBlur,
8380
...props
8481
}: TruncateProps) => {
85-
const [isTruncated, setIsTruncated] = useState(false);
82+
const [isTruncated, setIsTruncated] = useState(true);
83+
const [parentElement, setParentElement] = useState<HTMLElement>(null);
84+
const [textElement, setTextElement] = useState<HTMLElement>(null);
8685
const [shouldRenderByMaxChars, setShouldRenderByMaxChars] = useState(maxCharsDisplayed > 0);
87-
const [showTooltip, setShowTooltip] = useState(false);
8886

8987
const textRef = useRef<HTMLElement>(null);
9088
useImperativeHandle(innerRef, () => textRef.current!);
9189
const defaultSubParentRef = useRef<any>(null);
9290
const subParentRef = tooltipProps?.triggerRef || defaultSubParentRef;
91+
const observer = useRef(null);
9392

9493
if (maxCharsDisplayed <= 0) {
9594
// eslint-disable-next-line no-console
9695
console.warn('Truncate: the maxCharsDisplayed must be greater than 0, otherwise no content will be visible.');
9796
}
9897

99-
useEffect(() => {
100-
if (shouldRenderByMaxChars) {
101-
setIsTruncated(content.length > maxCharsDisplayed);
102-
}
103-
}, [shouldRenderByMaxChars, content.length, maxCharsDisplayed]);
98+
const getActualWidth = (element: Element) => {
99+
const computedStyle = getComputedStyle(element);
104100

105-
useEffect(() => {
106-
setShouldRenderByMaxChars(maxCharsDisplayed > 0);
107-
}, [maxCharsDisplayed]);
101+
return (
102+
parseFloat(computedStyle.width) -
103+
parseFloat(computedStyle.paddingLeft) -
104+
parseFloat(computedStyle.paddingRight) -
105+
parseFloat(computedStyle.borderRight) -
106+
parseFloat(computedStyle.borderLeft)
107+
);
108+
};
108109

109-
// Check truncation on mount for without maxChars
110-
useEffect(() => {
111-
if (!shouldRenderByMaxChars && textRef.current) {
112-
setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth);
113-
}
114-
}, [shouldRenderByMaxChars, content]);
110+
const calculateTotalTextWidth = (element: Element, trailingNumChars: number, content: string) => {
111+
const firstTextWidth = element.scrollWidth;
112+
const firstTextLength = content.length;
113+
return (firstTextWidth / firstTextLength) * trailingNumChars + firstTextWidth;
114+
};
115115

116-
const debouncedHandleResize = debounce(() => {
117-
if (!shouldRenderByMaxChars && textRef.current) {
118-
const isCurrentlyTruncated = textRef.current.scrollWidth > textRef.current.clientWidth;
119-
setIsTruncated(isCurrentlyTruncated);
116+
useEffect(() => {
117+
if (textRef && textRef.current && !textElement) {
118+
setTextElement(textRef.current);
120119
}
121-
}, 500);
120+
}, [textRef, textElement]);
122121

123-
// Set up ResizeObserver for non-maxChars truncation
124122
useEffect(() => {
125-
if (!shouldRenderByMaxChars && textRef.current) {
126-
const observer = getResizeObserver(textRef.current, debouncedHandleResize, false);
127-
return observer;
123+
const refElement = getReferenceElement(subParentRef);
124+
if (refElement?.parentElement && !parentElement) {
125+
setParentElement(refElement.parentElement);
128126
}
129-
}, [shouldRenderByMaxChars, debouncedHandleResize]);
127+
}, [subParentRef, parentElement]);
130128

131-
// Check if content is truncated (called on hover/focus)
132-
const checkTruncation = (): boolean => {
133-
if (shouldRenderByMaxChars) {
134-
return isTruncated;
135-
}
129+
useEffect(() => {
130+
if (textElement && parentElement && !observer.current && !shouldRenderByMaxChars) {
131+
const totalTextWidth = calculateTotalTextWidth(textElement, trailingNumChars, content);
132+
const textWidth = position === 'middle' ? totalTextWidth : textElement.scrollWidth;
136133

137-
if (!textRef.current) {
138-
return false;
139-
}
134+
const debouncedHandleResize = debounce(() => {
135+
const parentWidth = getActualWidth(parentElement);
136+
setIsTruncated(textWidth >= parentWidth);
137+
}, 500);
140138

141-
return textRef.current.scrollWidth > textRef.current.clientWidth;
142-
};
139+
const observer = getResizeObserver(parentElement, debouncedHandleResize);
143140

144-
const handleMouseEnter = (e: React.MouseEvent<HTMLElement>) => {
145-
if (checkTruncation()) {
146-
setShowTooltip(true);
141+
return () => {
142+
observer();
143+
};
147144
}
148-
onMouseEnter?.(e);
149-
};
150-
151-
const handleMouseLeave = (e: React.MouseEvent<HTMLElement>) => {
152-
setShowTooltip(false);
153-
onMouseLeave?.(e);
154-
};
145+
}, [textElement, parentElement, trailingNumChars, content, position, shouldRenderByMaxChars]);
155146

156-
const handleFocus = (e: React.FocusEvent<HTMLElement>) => {
157-
if (checkTruncation()) {
158-
setShowTooltip(true);
147+
useEffect(() => {
148+
if (shouldRenderByMaxChars) {
149+
setIsTruncated(content.length > maxCharsDisplayed);
159150
}
160-
onFocus?.(e);
161-
};
151+
}, [shouldRenderByMaxChars, content.length, maxCharsDisplayed]);
162152

163-
const handleBlur = (e: React.FocusEvent<HTMLElement>) => {
164-
setShowTooltip(false);
165-
onBlur?.(e);
166-
};
153+
useEffect(() => {
154+
setShouldRenderByMaxChars(maxCharsDisplayed > 0);
155+
}, [maxCharsDisplayed]);
167156

168157
const lrmEntity = <Fragment>&lrm;</Fragment>;
169158
const isStartPosition = position === TruncatePosition.start;
@@ -252,10 +241,6 @@ const TruncateBase: React.FunctionComponent<TruncateProps> = ({
252241
href={href}
253242
className={css(styles.truncate, shouldRenderByMaxChars && styles.modifiers.fixed, className)}
254243
{...(isTruncated && !href && !tooltipProps?.triggerRef && { tabIndex: 0 })}
255-
onMouseEnter={handleMouseEnter}
256-
onMouseLeave={handleMouseLeave}
257-
onFocus={handleFocus}
258-
onBlur={handleBlur}
259244
{...props}
260245
>
261246
{!shouldRenderByMaxChars ? renderResizeObserverContent() : renderMaxDisplayContent()}
@@ -264,14 +249,15 @@ const TruncateBase: React.FunctionComponent<TruncateProps> = ({
264249

265250
return (
266251
<>
267-
<Tooltip
268-
position={tooltipPosition}
269-
content={content}
270-
triggerRef={subParentRef}
271-
trigger="manual"
272-
isVisible={showTooltip}
273-
{...tooltipProps}
274-
/>
252+
{isTruncated && (
253+
<Tooltip
254+
hidden={!isTruncated}
255+
position={tooltipPosition}
256+
content={content}
257+
triggerRef={subParentRef}
258+
{...tooltipProps}
259+
/>
260+
)}
275261
{truncateBody}
276262
</>
277263
);

packages/react-core/src/components/Truncate/__tests__/Truncate.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { render, screen } from '@testing-library/react';
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
22
import { Truncate } from '../Truncate';
33
import styles from '@patternfly/react-styles/css/components/Truncate/truncate';
44
import '@testing-library/jest-dom';
55

66
jest.mock('../../Tooltip', () => ({
77
Tooltip: ({ content, position, children, triggerRef, trigger, isVisible, ...props }) => (
8-
<div data-testid="Tooltip-mock" {...props}>
8+
<div data-testid="Tooltip-mock" data-trigger-ref={triggerRef ? 'true' : 'false'} {...props}>
99
<div data-testid="Tooltip-mock-content-container">Test {content}</div>
1010
<p>{`position: ${position}`}</p>
1111
{children}
@@ -242,3 +242,22 @@ describe('Truncation with maxCharsDisplayed', () => {
242242
expect(asFragment()).toMatchSnapshot();
243243
});
244244
});
245+
246+
test('Tooltip appears on keyboard focus when external triggerRef is provided (ClipboardCopy regression test)', () => {
247+
const mockTriggerRef = { current: document.createElement('div') };
248+
249+
render(
250+
<Truncate
251+
content="This is a very long piece of content that should be truncated when the container is too small"
252+
tooltipProps={{ triggerRef: mockTriggerRef }}
253+
data-testid="truncate-element"
254+
/>
255+
);
256+
257+
// Simulate keyboard focus on the external trigger element
258+
fireEvent.focus(mockTriggerRef.current);
259+
260+
// The tooltip should be present and visible
261+
const tooltip = screen.getByTestId('Tooltip-mock');
262+
expect(tooltip).toBeInTheDocument();
263+
});

packages/react-core/src/components/Truncate/__tests__/__snapshots__/Truncate.test.tsx.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ exports[`Truncation with maxCharsDisplayed Matches snapshot with default positio
44
<DocumentFragment>
55
<div
66
data-testid="Tooltip-mock"
7+
data-trigger-ref="true"
78
>
89
<div
910
data-testid="Tooltip-mock-content-container"
@@ -42,6 +43,7 @@ exports[`renders default truncation 1`] = `
4243
<DocumentFragment>
4344
<div
4445
data-testid="Tooltip-mock"
46+
data-trigger-ref="true"
4547
>
4648
<div
4749
data-testid="Tooltip-mock-content-container"
@@ -54,6 +56,7 @@ exports[`renders default truncation 1`] = `
5456
</div>
5557
<span
5658
class="pf-v6-c-truncate"
59+
tabindex="0"
5760
>
5861
<span
5962
class="pf-v6-c-truncate__start"
@@ -68,6 +71,7 @@ exports[`renders start truncation with &lrm; at start and end 1`] = `
6871
<DocumentFragment>
6972
<div
7073
data-testid="Tooltip-mock"
74+
data-trigger-ref="true"
7175
>
7276
<div
7377
data-testid="Tooltip-mock-content-container"
@@ -80,6 +84,7 @@ exports[`renders start truncation with &lrm; at start and end 1`] = `
8084
</div>
8185
<span
8286
class="pf-v6-c-truncate"
87+
tabindex="0"
8388
>
8489
<span
8590
class="pf-v6-c-truncate__end"

0 commit comments

Comments
 (0)