Skip to content

Commit 40ab996

Browse files
author
Eric Olkowski
committed
feat(ClipboardCopy): added truncation for inlinecompact variant
1 parent e8dc759 commit 40ab996

File tree

6 files changed

+111
-16
lines changed

6 files changed

+111
-16
lines changed

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { css } from '@patternfly/react-styles';
44
import { PickOptional } from '../../helpers/typeUtils';
55
import { TooltipPosition } from '../Tooltip';
66
import { TextInput } from '../TextInput';
7+
import { Truncate, TruncateProps } from '../Truncate';
78
import { GenerateId } from '../../helpers/GenerateId/GenerateId';
89
import { ClipboardCopyButton } from './ClipboardCopyButton';
910
import { ClipboardCopyToggle } from './ClipboardCopyToggle';
@@ -92,6 +93,8 @@ export interface ClipboardCopyProps extends Omit<React.HTMLProps<HTMLDivElement>
9293
children: string | string[];
9394
/** Additional actions for inline clipboard copy. Should be wrapped with ClipboardCopyAction. */
9495
additionalActions?: React.ReactNode;
96+
/** Enables and customizes truncation for an inline-compact ClipboardCopy. */
97+
truncation?: boolean | Omit<TruncateProps, 'content'>;
9598
/** Value to overwrite the randomly generated data-ouia-component-id.*/
9699
ouiaId?: number | string;
97100
/** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */
@@ -101,6 +104,7 @@ export interface ClipboardCopyProps extends Omit<React.HTMLProps<HTMLDivElement>
101104
class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopyState> {
102105
static displayName = 'ClipboardCopy';
103106
timer = null as number;
107+
private clipboardRef: React.RefObject<any>;
104108
constructor(props: ClipboardCopyProps) {
105109
super(props);
106110
const text = Array.isArray(this.props.children) ? this.props.children.join(' ') : (this.props.children as string);
@@ -110,6 +114,8 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
110114
copied: false,
111115
textWhenExpanded: text
112116
};
117+
118+
this.clipboardRef = React.createRef();
113119
}
114120

115121
static defaultProps: PickOptional<ClipboardCopyProps> = {
@@ -128,6 +134,7 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
128134
textAriaLabel: 'Copyable input',
129135
toggleAriaLabel: 'Show content',
130136
additionalActions: null,
137+
truncation: false,
131138
ouiaSafe: true
132139
};
133140

@@ -184,37 +191,59 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
184191
position,
185192
className,
186193
additionalActions,
194+
truncation,
187195
ouiaId,
188196
ouiaSafe,
189197
...divProps
190198
} = this.props;
191199
const textIdPrefix = 'text-input-';
192200
const toggleIdPrefix = 'toggle-';
193201
const contentIdPrefix = 'content-';
202+
203+
const copyableText = this.state.text;
204+
const shouldTruncate = variant === ClipboardCopyVariant.inlineCompact && truncation;
205+
194206
return (
195207
<div
196208
className={css(
197209
styles.clipboardCopy,
198-
variant === 'inline-compact' && styles.modifiers.inline,
210+
variant === ClipboardCopyVariant.inlineCompact && styles.modifiers.inline,
199211
isBlock && styles.modifiers.block,
200212
this.state.expanded && styles.modifiers.expanded,
201213
className
202214
)}
215+
ref={this.clipboardRef}
203216
{...divProps}
204217
{...getOUIAProps(ClipboardCopy.displayName, ouiaId, ouiaSafe)}
205218
>
206-
{variant === 'inline-compact' && (
219+
{variant === ClipboardCopyVariant.inlineCompact && (
207220
<GenerateId prefix="">
208221
{(id) => (
209222
<React.Fragment>
210223
{!isCode && (
211224
<span className={css(styles.clipboardCopyText)} id={`${textIdPrefix}${id}`}>
212-
{this.state.text}
225+
{shouldTruncate ? (
226+
<Truncate
227+
refToGetParent={this.clipboardRef}
228+
content={copyableText}
229+
{...(typeof truncation === 'object' && truncation)}
230+
/>
231+
) : (
232+
copyableText
233+
)}
213234
</span>
214235
)}
215236
{isCode && (
216237
<code className={css(styles.clipboardCopyText, styles.modifiers.code)} id={`${textIdPrefix}${id}`}>
217-
{this.state.text}
238+
{shouldTruncate ? (
239+
<Truncate
240+
refToGetParent={this.clipboardRef}
241+
content={copyableText}
242+
{...(typeof truncation === 'object' && truncation)}
243+
/>
244+
) : (
245+
copyableText
246+
)}
218247
</code>
219248
)}
220249
<span className={css(styles.clipboardCopyActions)}>
@@ -229,7 +258,7 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
229258
textId={`text-input-${id}`}
230259
aria-label={hoverTip}
231260
onClick={(event: any) => {
232-
onCopy(event, this.state.text);
261+
onCopy(event, copyableText);
233262
this.setState({ copied: true });
234263
}}
235264
onTooltipHidden={() => this.setState({ copied: false })}
@@ -244,20 +273,20 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
244273
)}
245274
</GenerateId>
246275
)}
247-
{variant !== 'inline-compact' && (
276+
{variant !== ClipboardCopyVariant.inlineCompact && (
248277
<GenerateId prefix="">
249278
{(id) => (
250279
<React.Fragment>
251280
<div className={css(styles.clipboardCopyGroup)}>
252-
{variant === 'expansion' && (
281+
{variant === ClipboardCopyVariant.expansion && (
253282
<ClipboardCopyToggle
254283
isExpanded={this.state.expanded}
255284
onClick={(_event) => {
256285
this.expandContent(_event);
257286
if (this.state.expanded) {
258287
this.setState({ text: this.state.textWhenExpanded });
259288
} else {
260-
this.setState({ textWhenExpanded: this.state.text });
289+
this.setState({ textWhenExpanded: copyableText });
261290
}
262291
}}
263292
id={`${toggleIdPrefix}${id}`}
@@ -269,7 +298,7 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
269298
<TextInput
270299
readOnlyVariant={isReadOnly || this.state.expanded ? 'default' : undefined}
271300
onChange={this.updateText}
272-
value={this.state.expanded ? this.state.textWhenExpanded : this.state.text}
301+
value={this.state.expanded ? this.state.textWhenExpanded : copyableText}
273302
id={`text-input-${id}`}
274303
aria-label={textAriaLabel}
275304
{...(isCode && { dir: 'ltr' })}
@@ -283,7 +312,7 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
283312
textId={`text-input-${id}`}
284313
aria-label={hoverTip}
285314
onClick={(event: any) => {
286-
onCopy(event, this.state.expanded ? this.state.textWhenExpanded : this.state.text);
315+
onCopy(event, this.state.expanded ? this.state.textWhenExpanded : copyableText);
287316
this.setState({ copied: true });
288317
}}
289318
onTooltipHidden={() => this.setState({ copied: false })}
@@ -298,7 +327,7 @@ class ClipboardCopy extends React.Component<ClipboardCopyProps, ClipboardCopySta
298327
id={`content-${id}`}
299328
onChange={this.updateTextWhenExpanded}
300329
>
301-
{this.state.text}
330+
{copyableText}
302331
</ClipboardCopyExpanded>
303332
)}
304333
</React.Fragment>

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react';
22
import { screen, render } from '@testing-library/react';
3-
import { ClipboardCopy } from '../ClipboardCopy';
3+
import { ClipboardCopy, ClipboardCopyVariant } from '../ClipboardCopy';
44
import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy';
5+
import truncateStyles from '@patternfly/react-styles/css/components/Truncate/Truncate';
56
import userEvent from '@testing-library/user-event';
67

78
jest.mock('../../../helpers/GenerateId/GenerateId');
@@ -351,6 +352,40 @@ test('Can take array of strings as children', async () => {
351352
expect(onCopyMock).toHaveBeenCalledWith(expect.any(Object), children);
352353
});
353354

355+
describe('ClipboardCopy with truncation', () => {
356+
test('Does not render with truncate wrapper by default', () => {
357+
render(<ClipboardCopy data-testid={testId}>{children}</ClipboardCopy>);
358+
359+
expect(screen.queryByTestId(testId)?.querySelector(`.${truncateStyles.truncate}`)).not.toBeInTheDocument();
360+
});
361+
362+
test('Does not render with truncate wrapper when variant="inline-compact" and truncation is false', () => {
363+
render(<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact}>{children}</ClipboardCopy>);
364+
365+
expect(screen.getByText(children).parentElement).not.toHaveClass(truncateStyles.truncate);
366+
});
367+
368+
test('Renders with truncate wrapper when variant="inline-compact" and truncation is true', () => {
369+
render(
370+
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} truncation>
371+
{children}
372+
</ClipboardCopy>
373+
);
374+
375+
expect(screen.getByText(children).parentElement).toHaveClass(truncateStyles.truncate);
376+
});
377+
378+
test('Renders with truncate wrapper when variant="inline-compact" and truncation is prop object', () => {
379+
render(
380+
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} truncation={{ position: 'start' }}>
381+
{children}
382+
</ClipboardCopy>
383+
);
384+
385+
expect(screen.getByText(children, { exact: false }).parentElement).toHaveClass(truncateStyles.truncate);
386+
});
387+
});
388+
354389
test('Matches snapshot', () => {
355390
const { asFragment } = render(
356391
<ClipboardCopy id="snapshot" ouiaId="snapshot">

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ exports[`Matches snapshot 1`] = `
1818
<input
1919
aria-invalid="false"
2020
aria-label="Copyable input"
21-
data-ouia-component-id="OUIA-Generated-TextInputBase-27"
21+
data-ouia-component-id="OUIA-Generated-TextInputBase-28"
2222
data-ouia-component-type="PF6/TextInput"
2323
data-ouia-safe="true"
2424
id="text-input-generated-id"

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,10 @@ import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon';
6464

6565
```ts file="./ClipboardCopyInlineCompactInSentence.tsx"
6666
```
67+
68+
### With truncation
69+
70+
You can control the truncation for an `inline-compact` variant by passing the `truncation` property. The following example shows the different ways to use the property: passing a boolean will apply default truncation, while passing an object of `TruncateProps` offers more fine-tuned control over the truncation behavior.
71+
72+
```ts file="./ClipboardCopyTruncation.tsx"
73+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { ClipboardCopy } from '@patternfly/react-core';
3+
export const ClipboardCopyTruncation: React.FunctionComponent = () => (
4+
<>
5+
<ClipboardCopy truncation hoverTip="Copy" clickTip="Copied" variant="inline-compact">
6+
This lengthy, copyable content will be truncated with default settings when the truncation prop is simply set to
7+
true. This is useful for quickly applying truncation without needing to worry about any other properties to set.
8+
</ClipboardCopy>
9+
<br />
10+
<br />
11+
<ClipboardCopy truncation={{ position: 'start' }} hoverTip="Copy" clickTip="Copied" variant="inline-compact">
12+
This lengthy, copyable content will be truncated with customized settings when the truncation prop is passed an
13+
object containing Truncate props. This is useful for finetuning truncation for your particular use-case.
14+
</ClipboardCopy>
15+
</>
16+
);

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const truncateStyles = {
1717

1818
const minWidthCharacters: number = 12;
1919

20-
interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
20+
export interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
2121
/** Class to add to outer span */
2222
className?: string;
2323
/** Text to truncate */
@@ -42,6 +42,10 @@ interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
4242
| 'left-end'
4343
| 'right-start'
4444
| 'right-end';
45+
/** The element whose parent to reference when calculating whether truncation should occur. This must be an ancestor
46+
* of the ClipboardCopy, and must have a valid width value.
47+
*/
48+
refToGetParent?: React.RefObject<any>;
4549
}
4650

4751
const sliceContent = (str: string, slice: number) => [str.slice(0, str.length - slice), str.slice(-slice)];
@@ -52,6 +56,7 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
5256
tooltipPosition = 'top',
5357
trailingNumChars = 7,
5458
content,
59+
refToGetParent,
5560
...props
5661
}: TruncateProps) => {
5762
const [isTruncated, setIsTruncated] = React.useState(true);
@@ -85,8 +90,11 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
8590
setTextElement(textRef.current);
8691
}
8792

88-
if (subParentRef && subParentRef.current.parentElement.parentElement && !parentElement) {
89-
setParentElement(subParentRef.current.parentElement.parentElement);
93+
if (
94+
refToGetParent?.current ||
95+
(subParentRef && subParentRef.current.parentElement.parentElement && !parentElement)
96+
) {
97+
setParentElement(refToGetParent.current.parentElement || subParentRef.current.parentElement.parentElement);
9098
}
9199
}, [textRef, subParentRef, textElement, parentElement]);
92100

0 commit comments

Comments
 (0)