Skip to content

Commit e8c5538

Browse files
feat(Truncate): added tooltip props and anchor usage (#11801)
* feat(Truncate): added tooltip props and anchor usage * Added tests * Forwarded ref
1 parent 2d58acf commit e8c5538

File tree

6 files changed

+113
-20
lines changed

6 files changed

+113
-20
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
204204
const shouldTruncate = variant === ClipboardCopyVariant.inlineCompact && truncation;
205205
const inlineCompactContent = shouldTruncate ? (
206206
<Truncate
207-
refToGetParent={this.clipboardRef}
207+
tooltipProps={{ triggerRef: this.clipboardRef }}
208208
content={copyableText}
209209
{...(typeof truncation === 'object' && truncation)}
210210
/>
@@ -223,6 +223,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
223223
className
224224
)}
225225
ref={this.clipboardRef}
226+
{...(shouldTruncate && { tabIndex: 0 })}
226227
{...divProps}
227228
{...getOUIAProps(ClipboardCopy.displayName, ouiaId, ouiaSafe)}
228229
>

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

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Fragment, useEffect, useRef, useState } from 'react';
1+
import { Fragment, useEffect, useRef, useState, forwardRef } from 'react';
22
import styles from '@patternfly/react-styles/css/components/Truncate/truncate';
33
import { css } from '@patternfly/react-styles';
4-
import { Tooltip, TooltipPosition } from '../Tooltip';
4+
import { Tooltip, TooltipPosition, TooltipProps } from '../Tooltip';
5+
import { getReferenceElement } from '../../helpers';
56
import { getResizeObserver } from '../../helpers/resizeObserver';
67

78
export enum TruncatePosition {
@@ -17,11 +18,15 @@ const truncateStyles = {
1718

1819
const minWidthCharacters: number = 12;
1920

20-
export interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
21+
export interface TruncateProps extends Omit<React.HTMLProps<HTMLSpanElement | HTMLAnchorElement>, 'ref'> {
2122
/** Class to add to outer span */
2223
className?: string;
2324
/** Text to truncate */
2425
content: string;
26+
/** An HREF to turn the truncate wrapper into an anchor element. For more custom control, use the
27+
* tooltipProps with a triggerRef property passed in.
28+
*/
29+
href?: string;
2530
/** The number of characters displayed in the second half of a middle truncation. This will be overridden by
2631
* the maxCharsDisplayed prop.
2732
*/
@@ -52,24 +57,24 @@ export interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
5257
| 'left-end'
5358
| 'right-start'
5459
| 'right-end';
55-
/** @hide The element whose parent to reference when calculating whether truncation should occur. This must be an ancestor
56-
* of the ClipboardCopy, and must have a valid width value. For internal use only, do not use as it is not part of the public API
57-
* and is subject to change.
58-
*/
59-
refToGetParent?: React.RefObject<any>;
60+
/** Additional props to pass to the tooltip. */
61+
tooltipProps?: Omit<TooltipProps, 'content'>;
62+
/** @hide Forwarded ref */
63+
innerRef?: React.Ref<any>;
6064
}
6165

6266
const sliceTrailingContent = (str: string, slice: number) => [str.slice(0, str.length - slice), str.slice(-slice)];
6367

64-
export const Truncate: React.FunctionComponent<TruncateProps> = ({
68+
const TruncateBase: React.FunctionComponent<TruncateProps> = ({
6569
className,
70+
href,
6671
position = 'end',
6772
tooltipPosition = 'top',
73+
tooltipProps,
6874
trailingNumChars = 7,
6975
maxCharsDisplayed,
7076
omissionContent = '\u2026',
7177
content,
72-
refToGetParent,
7378
...props
7479
}: TruncateProps) => {
7580
const [isTruncated, setIsTruncated] = useState(true);
@@ -78,7 +83,8 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
7883
const [shouldRenderByMaxChars, setShouldRenderByMaxChars] = useState(maxCharsDisplayed > 0);
7984

8085
const textRef = useRef<HTMLElement>(null);
81-
const subParentRef = useRef<HTMLDivElement>(null);
86+
const defaultSubParentRef = useRef<any>(null);
87+
const subParentRef = tooltipProps?.triggerRef || defaultSubParentRef;
8288
const observer = useRef(null);
8389

8490
if (maxCharsDisplayed <= 0) {
@@ -108,11 +114,14 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
108114
if (textRef && textRef.current && !textElement) {
109115
setTextElement(textRef.current);
110116
}
117+
}, [textRef, textElement]);
111118

112-
if ((refToGetParent?.current || (subParentRef?.current && subParentRef.current.parentElement)) && !parentElement) {
113-
setParentElement(refToGetParent?.current.parentElement || subParentRef?.current.parentElement);
119+
useEffect(() => {
120+
const refElement = getReferenceElement(subParentRef);
121+
if (refElement?.parentElement && !parentElement) {
122+
setParentElement(refElement.parentElement);
114123
}
115-
}, [textRef, subParentRef, textElement, parentElement]);
124+
}, [subParentRef, parentElement]);
116125

117126
useEffect(() => {
118127
if (textElement && parentElement && !observer.current && !shouldRenderByMaxChars) {
@@ -222,25 +231,37 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
222231
);
223232
};
224233

234+
const TruncateWrapper = href ? 'a' : 'span';
225235
const truncateBody = (
226-
<span
227-
ref={subParentRef}
236+
<TruncateWrapper
237+
ref={!tooltipProps?.triggerRef ? (subParentRef as React.MutableRefObject<any>) : null}
238+
href={href}
228239
className={css(styles.truncate, shouldRenderByMaxChars && styles.modifiers.fixed, className)}
229-
{...(isTruncated && { tabIndex: 0 })}
240+
{...(isTruncated && !href && !tooltipProps?.triggerRef && { tabIndex: 0 })}
230241
{...props}
231242
>
232243
{!shouldRenderByMaxChars ? renderResizeObserverContent() : renderMaxDisplayContent()}
233-
</span>
244+
</TruncateWrapper>
234245
);
235246

236247
return (
237248
<>
238249
{isTruncated && (
239-
<Tooltip hidden={!isTruncated} position={tooltipPosition} content={content} triggerRef={subParentRef} />
250+
<Tooltip
251+
hidden={!isTruncated}
252+
position={tooltipPosition}
253+
content={content}
254+
triggerRef={subParentRef}
255+
{...tooltipProps}
256+
/>
240257
)}
241258
{truncateBody}
242259
</>
243260
);
244261
};
245262

263+
export const Truncate = forwardRef((props: TruncateProps, ref: React.Ref<HTMLAnchorElement | HTMLSpanElement>) => (
264+
<TruncateBase innerRef={ref} {...props} />
265+
));
266+
246267
Truncate.displayName = 'Truncate';

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
1919
disconnect: jest.fn()
2020
}));
2121

22+
test('Renders with span wrapper by default', () => {
23+
render(<Truncate content={''} data-testid="test-id" />);
24+
25+
expect(screen.getByTestId('test-id').tagName).toBe('SPAN');
26+
});
27+
28+
test('Renders with anchor wrapper when href prop is passed', () => {
29+
render(<Truncate content={'Link content'} href="#" />);
30+
31+
expect(screen.getByRole('link')).toHaveTextContent('Link content');
32+
});
33+
34+
test('Passes href to anchor when href prop is passed', () => {
35+
render(<Truncate content={'Link content'} href="#home" />);
36+
37+
expect(screen.getByRole('link')).toHaveAttribute('href', '#home');
38+
});
39+
2240
test(`renders with class ${styles.truncate}`, () => {
2341
render(<Truncate content={''} aria-label="test-id" />);
2442

@@ -145,6 +163,12 @@ test('renders tooltip content', () => {
145163
expect(input).toBeVisible();
146164
});
147165

166+
test('Renders with additional tooltip props spread', () => {
167+
render(<Truncate content={''} tooltipProps={{ distance: 32 }} />);
168+
169+
expect(screen.getByTestId('Tooltip-mock')).toHaveAttribute('distance', '32');
170+
});
171+
148172
test('renders with inherited element props spread to the component', () => {
149173
render(<Truncate content={'Test'} data-testid="test-id" aria-label="labelling-id" />);
150174

packages/react-core/src/components/Truncate/examples/Truncate.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,11 @@ Truncating based on a maximum amount of characters will truncate the content at
5252
```ts file="./TruncateMaxChars.tsx"
5353

5454
```
55+
56+
### With links
57+
58+
To truncate link text, you can pass the `href` property in.
59+
60+
```ts file="./TruncateLinks.tsx"
61+
62+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Truncate } from '@patternfly/react-core';
2+
3+
export const TruncateLinks: React.FunctionComponent = () => {
4+
const content = 'A very lengthy anchor text content to trigger truncation';
5+
return (
6+
<>
7+
<div>With default width-observing truncation:</div>
8+
<div className="truncate-example-resize">
9+
<Truncate href="#" content={content} />
10+
<Truncate position="start" href="#" content={content} />
11+
<Truncate position="middle" href="#" content={content} />
12+
</div>
13+
<br />
14+
<div>With max characters truncation:</div>
15+
<Truncate maxCharsDisplayed={15} href="#" content={content} />
16+
<br />
17+
<Truncate maxCharsDisplayed={15} position="start" href="#" content={content} />
18+
<br />
19+
<Truncate maxCharsDisplayed={15} position="middle" href="#" content={content} />
20+
</>
21+
);
22+
};

packages/react-core/src/helpers/util.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,3 +545,20 @@ export const getLanguageDirection = (targetElement: HTMLElement, defaultDirectio
545545

546546
return defaultDirection;
547547
};
548+
549+
/**
550+
* Gets a reference element based on a ref property, which can typically be 1 of several types.
551+
*
552+
* @param {HTMLElement | (() => HTMLElement) | React.RefObject<any>} refProp The ref property to get a reference element from.
553+
* @returns The reference element if one is found.
554+
*/
555+
export const getReferenceElement = (refProp: HTMLElement | (() => HTMLElement) | React.RefObject<any>) => {
556+
if (refProp instanceof HTMLElement) {
557+
return refProp;
558+
}
559+
if (typeof refProp === 'function') {
560+
return refProp();
561+
}
562+
563+
return refProp?.current;
564+
};

0 commit comments

Comments
 (0)