Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
const shouldTruncate = variant === ClipboardCopyVariant.inlineCompact && truncation;
const inlineCompactContent = shouldTruncate ? (
<Truncate
refToGetParent={this.clipboardRef}
tooltipProps={{ triggerRef: this.clipboardRef }}
content={copyableText}
{...(typeof truncation === 'object' && truncation)}
/>
Expand All @@ -223,6 +223,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
className
)}
ref={this.clipboardRef}
{...(shouldTruncate && { tabIndex: 0 })}
{...divProps}
{...getOUIAProps(ClipboardCopy.displayName, ouiaId, ouiaSafe)}
>
Expand Down
59 changes: 40 additions & 19 deletions packages/react-core/src/components/Truncate/Truncate.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import { Fragment, useEffect, useRef, useState, forwardRef } from 'react';
import styles from '@patternfly/react-styles/css/components/Truncate/truncate';
import { css } from '@patternfly/react-styles';
import { Tooltip, TooltipPosition } from '../Tooltip';
import { Tooltip, TooltipPosition, TooltipProps } from '../Tooltip';
import { getReferenceElement } from '../../helpers';
import { getResizeObserver } from '../../helpers/resizeObserver';

export enum TruncatePosition {
Expand All @@ -17,11 +18,15 @@ const truncateStyles = {

const minWidthCharacters: number = 12;

export interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
export interface TruncateProps extends Omit<React.HTMLProps<HTMLSpanElement | HTMLAnchorElement>, 'ref'> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could omitting ref be problematic if people were trying to use it in this component before? Do we need to make sure to forward a ref or something here?

/** Class to add to outer span */
className?: string;
/** Text to truncate */
content: string;
/** An HREF to turn the truncate wrapper into an anchor element. For more custom control, use the
* tooltipProps with a triggerRef property passed in.
*/
href?: string;
/** The number of characters displayed in the second half of a middle truncation. This will be overridden by
* the maxCharsDisplayed prop.
*/
Expand Down Expand Up @@ -52,24 +57,24 @@ export interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
| 'left-end'
| 'right-start'
| 'right-end';
/** @hide The element whose parent to reference when calculating whether truncation should occur. This must be an ancestor
* 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
* and is subject to change.
*/
refToGetParent?: React.RefObject<any>;
/** Additional props to pass to the tooltip. */
tooltipProps?: Omit<TooltipProps, 'content'>;
/** @hide Forwarded ref */
innerRef?: React.Ref<any>;
}

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

export const Truncate: React.FunctionComponent<TruncateProps> = ({
const TruncateBase: React.FunctionComponent<TruncateProps> = ({
className,
href,
position = 'end',
tooltipPosition = 'top',
tooltipProps,
trailingNumChars = 7,
maxCharsDisplayed,
omissionContent = '\u2026',
content,
refToGetParent,
...props
}: TruncateProps) => {
const [isTruncated, setIsTruncated] = useState(true);
Expand All @@ -78,7 +83,8 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
const [shouldRenderByMaxChars, setShouldRenderByMaxChars] = useState(maxCharsDisplayed > 0);

const textRef = useRef<HTMLElement>(null);
const subParentRef = useRef<HTMLDivElement>(null);
const defaultSubParentRef = useRef<any>(null);
const subParentRef = tooltipProps?.triggerRef || defaultSubParentRef;
const observer = useRef(null);

if (maxCharsDisplayed <= 0) {
Expand Down Expand Up @@ -108,11 +114,14 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
if (textRef && textRef.current && !textElement) {
setTextElement(textRef.current);
}
}, [textRef, textElement]);

if ((refToGetParent?.current || (subParentRef?.current && subParentRef.current.parentElement)) && !parentElement) {
setParentElement(refToGetParent?.current.parentElement || subParentRef?.current.parentElement);
useEffect(() => {
const refElement = getReferenceElement(subParentRef);
if (refElement?.parentElement && !parentElement) {
setParentElement(refElement.parentElement);
}
}, [textRef, subParentRef, textElement, parentElement]);
}, [subParentRef, parentElement]);

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

const TruncateWrapper = href ? 'a' : 'span';
const truncateBody = (
<span
ref={subParentRef}
<TruncateWrapper
ref={!tooltipProps?.triggerRef ? (subParentRef as React.MutableRefObject<any>) : null}
href={href}
className={css(styles.truncate, shouldRenderByMaxChars && styles.modifiers.fixed, className)}
{...(isTruncated && { tabIndex: 0 })}
{...(isTruncated && !href && !tooltipProps?.triggerRef && { tabIndex: 0 })}
{...props}
>
{!shouldRenderByMaxChars ? renderResizeObserverContent() : renderMaxDisplayContent()}
</span>
</TruncateWrapper>
);

return (
<>
{isTruncated && (
<Tooltip hidden={!isTruncated} position={tooltipPosition} content={content} triggerRef={subParentRef} />
<Tooltip
hidden={!isTruncated}
position={tooltipPosition}
content={content}
triggerRef={subParentRef}
{...tooltipProps}
/>
)}
{truncateBody}
</>
);
};

export const Truncate = forwardRef((props: TruncateProps, ref: React.Ref<HTMLAnchorElement | HTMLSpanElement>) => (
<TruncateBase innerRef={ref} {...props} />
));

Truncate.displayName = 'Truncate';
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn()
}));

test('Renders with span wrapper by default', () => {
render(<Truncate content={''} data-testid="test-id" />);

expect(screen.getByTestId('test-id').tagName).toBe('SPAN');
});

test('Renders with anchor wrapper when href prop is passed', () => {
render(<Truncate content={'Link content'} href="#" />);

expect(screen.getByRole('link')).toHaveTextContent('Link content');
});

test('Passes href to anchor when href prop is passed', () => {
render(<Truncate content={'Link content'} href="#home" />);

expect(screen.getByRole('link')).toHaveAttribute('href', '#home');
});

test(`renders with class ${styles.truncate}`, () => {
render(<Truncate content={''} aria-label="test-id" />);

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

test('Renders with additional tooltip props spread', () => {
render(<Truncate content={''} tooltipProps={{ distance: 32 }} />);

expect(screen.getByTestId('Tooltip-mock')).toHaveAttribute('distance', '32');
});

test('renders with inherited element props spread to the component', () => {
render(<Truncate content={'Test'} data-testid="test-id" aria-label="labelling-id" />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ Truncating based on a maximum amount of characters will truncate the content at
```ts file="./TruncateMaxChars.tsx"

```

### With links

To truncate link text, you can pass the `href` property in.

```ts file="./TruncateLinks.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Truncate } from '@patternfly/react-core';

export const TruncateLinks: React.FunctionComponent = () => {
const content = 'A very lengthy anchor text content to trigger truncation';
return (
<>
<div>With default width-observing truncation:</div>
<div className="truncate-example-resize">
<Truncate href="#" content={content} />
<Truncate position="start" href="#" content={content} />
<Truncate position="middle" href="#" content={content} />
</div>
<br />
<div>With max characters truncation:</div>
<Truncate maxCharsDisplayed={15} href="#" content={content} />
<br />
<Truncate maxCharsDisplayed={15} position="start" href="#" content={content} />
<br />
<Truncate maxCharsDisplayed={15} position="middle" href="#" content={content} />
</>
);
};
17 changes: 17 additions & 0 deletions packages/react-core/src/helpers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,3 +545,20 @@ export const getLanguageDirection = (targetElement: HTMLElement, defaultDirectio

return defaultDirection;
};

/**
* Gets a reference element based on a ref property, which can typically be 1 of several types.
*
* @param {HTMLElement | (() => HTMLElement) | React.RefObject<any>} refProp The ref property to get a reference element from.
* @returns The reference element if one is found.
*/
export const getReferenceElement = (refProp: HTMLElement | (() => HTMLElement) | React.RefObject<any>) => {
if (refProp instanceof HTMLElement) {
return refProp;
}
if (typeof refProp === 'function') {
return refProp();
}

return refProp?.current;
};
Loading