Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"tslib": "^2.8.1"
},
"devDependencies": {
"@patternfly/patternfly": "6.2.0-prerelease.20",
"@patternfly/patternfly": "6.2.0-prerelease.21",
"case-anything": "^3.1.2",
"css": "^3.0.0",
"fs-extra": "^11.3.0"
Expand Down
47 changes: 35 additions & 12 deletions packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Component, Fragment } from 'react';
import { Component, Fragment, createRef } from 'react';
import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy';
import { css } from '@patternfly/react-styles';
import { PickOptional } from '../../helpers/typeUtils';
import { TooltipPosition } from '../Tooltip';
import { TextInput } from '../TextInput';
import { Truncate, TruncateProps } from '../Truncate';
import { GenerateId } from '../../helpers/GenerateId/GenerateId';
import { ClipboardCopyButton } from './ClipboardCopyButton';
import { ClipboardCopyToggle } from './ClipboardCopyToggle';
Expand Down Expand Up @@ -92,6 +93,8 @@ export interface ClipboardCopyProps extends Omit<React.HTMLProps<HTMLDivElement>
children: string | string[];
/** Additional actions for inline clipboard copy. Should be wrapped with ClipboardCopyAction. */
additionalActions?: React.ReactNode;
/** Enables and customizes truncation for an inline-compact ClipboardCopy. */
truncation?: boolean | Omit<TruncateProps, 'content'>;
/** Value to overwrite the randomly generated data-ouia-component-id.*/
ouiaId?: number | string;
/** 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. */
Expand All @@ -101,6 +104,7 @@ export interface ClipboardCopyProps extends Omit<React.HTMLProps<HTMLDivElement>
class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
static displayName = 'ClipboardCopy';
timer = null as number;
private clipboardRef: React.RefObject<any>;
constructor(props: ClipboardCopyProps) {
super(props);
const text = Array.isArray(this.props.children) ? this.props.children.join(' ') : (this.props.children as string);
Expand All @@ -110,6 +114,8 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
copied: false,
textWhenExpanded: text
};

this.clipboardRef = createRef();
}

static defaultProps: PickOptional<ClipboardCopyProps> = {
Expand All @@ -128,6 +134,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
textAriaLabel: 'Copyable input',
toggleAriaLabel: 'Show content',
additionalActions: null,
truncation: false,
ouiaSafe: true
};

Expand Down Expand Up @@ -184,37 +191,53 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
position,
className,
additionalActions,
truncation,
ouiaId,
ouiaSafe,
...divProps
} = this.props;
const textIdPrefix = 'text-input-';
const toggleIdPrefix = 'toggle-';
const contentIdPrefix = 'content-';

const copyableText = this.state.text;
const shouldTruncate = variant === ClipboardCopyVariant.inlineCompact && truncation;
const inlineCompactContent = shouldTruncate ? (
<Truncate
refToGetParent={this.clipboardRef}
content={copyableText}
{...(typeof truncation === 'object' && truncation)}
/>
) : (
copyableText
);

return (
<div
className={css(
styles.clipboardCopy,
variant === 'inline-compact' && styles.modifiers.inline,
variant === ClipboardCopyVariant.inlineCompact && styles.modifiers.inline,
isBlock && styles.modifiers.block,
this.state.expanded && styles.modifiers.expanded,
shouldTruncate && styles.modifiers.truncate,
className
)}
ref={this.clipboardRef}
{...divProps}
{...getOUIAProps(ClipboardCopy.displayName, ouiaId, ouiaSafe)}
>
{variant === 'inline-compact' && (
{variant === ClipboardCopyVariant.inlineCompact && (
<GenerateId prefix="">
{(id) => (
<Fragment>
{!isCode && (
<span className={css(styles.clipboardCopyText)} id={`${textIdPrefix}${id}`}>
{this.state.text}
{inlineCompactContent}
</span>
)}
{isCode && (
<code className={css(styles.clipboardCopyText, styles.modifiers.code)} id={`${textIdPrefix}${id}`}>
{this.state.text}
{inlineCompactContent}
</code>
)}
<span className={css(styles.clipboardCopyActions)}>
Expand All @@ -229,7 +252,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
textId={`text-input-${id}`}
aria-label={hoverTip}
onClick={(event: any) => {
onCopy(event, this.state.text);
onCopy(event, copyableText);
this.setState({ copied: true });
}}
onTooltipHidden={() => this.setState({ copied: false })}
Expand All @@ -244,20 +267,20 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
)}
</GenerateId>
)}
{variant !== 'inline-compact' && (
{variant !== ClipboardCopyVariant.inlineCompact && (
<GenerateId prefix="">
{(id) => (
<Fragment>
<div className={css(styles.clipboardCopyGroup)}>
{variant === 'expansion' && (
{variant === ClipboardCopyVariant.expansion && (
<ClipboardCopyToggle
isExpanded={this.state.expanded}
onClick={(_event) => {
this.expandContent(_event);
if (this.state.expanded) {
this.setState({ text: this.state.textWhenExpanded });
} else {
this.setState({ textWhenExpanded: this.state.text });
this.setState({ textWhenExpanded: copyableText });
}
}}
id={`${toggleIdPrefix}${id}`}
Expand All @@ -269,7 +292,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
<TextInput
readOnlyVariant={isReadOnly || this.state.expanded ? 'default' : undefined}
onChange={this.updateText}
value={this.state.expanded ? this.state.textWhenExpanded : this.state.text}
value={this.state.expanded ? this.state.textWhenExpanded : copyableText}
id={`text-input-${id}`}
aria-label={textAriaLabel}
{...(isCode && { dir: 'ltr' })}
Expand All @@ -283,7 +306,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
textId={`text-input-${id}`}
aria-label={hoverTip}
onClick={(event: any) => {
onCopy(event, this.state.expanded ? this.state.textWhenExpanded : this.state.text);
onCopy(event, this.state.expanded ? this.state.textWhenExpanded : copyableText);
this.setState({ copied: true });
}}
onTooltipHidden={() => this.setState({ copied: false })}
Expand All @@ -298,7 +321,7 @@ class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
id={`content-${id}`}
onChange={this.updateTextWhenExpanded}
>
{this.state.text}
{copyableText}
</ClipboardCopyExpanded>
)}
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { screen, render } from '@testing-library/react';
import { ClipboardCopy } from '../ClipboardCopy';
import { ClipboardCopy, ClipboardCopyVariant } from '../ClipboardCopy';
import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy';
import truncateStyles from '@patternfly/react-styles/css/components/Truncate/truncate';
import userEvent from '@testing-library/user-event';

jest.mock('../../../helpers/GenerateId/GenerateId');
Expand Down Expand Up @@ -104,6 +105,52 @@ test(`Renders with class ${styles.modifiers.block} when isBlock is passed`, () =
expect(screen.getByTestId(testId)).toHaveClass(styles.modifiers.block);
});

test(`Does not render with class ${styles.modifiers.truncate} by default`, () => {
render(<ClipboardCopy data-testid={testId}>{children}</ClipboardCopy>);

expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.truncate);
});

test(`Does not render with class ${styles.modifiers.truncate} for expansion variant`, () => {
render(
<ClipboardCopy variant={ClipboardCopyVariant.expansion} data-testid={testId}>
{children}
</ClipboardCopy>
);

expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.truncate);
});

test(`Does not render with class ${styles.modifiers.truncate} for inlinecompact variant and truncation is false`, () => {
render(
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} data-testid={testId}>
{children}
</ClipboardCopy>
);

expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.truncate);
});

test(`Renders with class ${styles.modifiers.truncate} when truncation is true and variant is inline-compact`, () => {
render(
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} truncation data-testid={testId}>
{children}
</ClipboardCopy>
);

expect(screen.getByTestId(testId)).toHaveClass(styles.modifiers.truncate);
});

test(`Renders with class ${styles.modifiers.truncate} when truncation is an object and variant is inlinecompact`, () => {
render(
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} truncation={{}} data-testid={testId}>
{children}
</ClipboardCopy>
);

expect(screen.getByTestId(testId)).toHaveClass(styles.modifiers.truncate);
});

test('Spreads additional props to container div', () => {
render(
<ClipboardCopy data-testid={testId} role="group">
Expand Down Expand Up @@ -350,6 +397,40 @@ test('Can take array of strings as children', async () => {
expect(onCopyMock).toHaveBeenCalledWith(expect.any(Object), children);
});

describe('ClipboardCopy with truncation', () => {
test('Does not render with truncate wrapper by default', () => {
render(<ClipboardCopy data-testid={testId}>{children}</ClipboardCopy>);

expect(screen.queryByTestId(testId)?.querySelector(`.${truncateStyles.truncate}`)).not.toBeInTheDocument();
});

test('Does not render with truncate wrapper when variant="inline-compact" and truncation is false', () => {
render(<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact}>{children}</ClipboardCopy>);

expect(screen.getByText(children).parentElement).not.toHaveClass(truncateStyles.truncate);
});

test('Renders with truncate wrapper when variant="inline-compact" and truncation is true', () => {
render(
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} truncation>
{children}
</ClipboardCopy>
);

expect(screen.getByText(children).parentElement).toHaveClass(truncateStyles.truncate);
});

test('Renders with truncate wrapper when variant="inline-compact" and truncation is prop object', () => {
render(
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} truncation={{ position: 'start' }}>
{children}
</ClipboardCopy>
);

expect(screen.getByText(children, { exact: false }).parentElement).toHaveClass(truncateStyles.truncate);
});
});

test('Matches snapshot', () => {
const { asFragment } = render(
<ClipboardCopy id="snapshot" ouiaId="snapshot">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ exports[`Matches snapshot 1`] = `
<input
aria-invalid="false"
aria-label="Copyable input"
data-ouia-component-id="OUIA-Generated-TextInputBase-27"
data-ouia-component-id="OUIA-Generated-TextInputBase-30"
data-ouia-component-type="PF6/TextInput"
data-ouia-safe="true"
id="text-input-generated-id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,10 @@ import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon';

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

### Inline compact with truncation

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.

```ts file="./ClipboardCopyInlineCompactTruncation.tsx"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ClipboardCopy } from '@patternfly/react-core';
export const ClipboardCopyInlineCompactTruncation: React.FunctionComponent = () => (
<>
<ClipboardCopy truncation hoverTip="Copy" clickTip="Copied" variant="inline-compact">
This lengthy, copyable content will be truncated with default settings when the truncation prop is simply set to
true. This is useful for quickly applying truncation without needing to worry about any other properties to set.
</ClipboardCopy>
<br />
<br />
<ClipboardCopy truncation={{ position: 'start' }} hoverTip="Copy" clickTip="Copied" variant="inline-compact">
This lengthy, copyable content will be truncated with customized settings when the truncation prop is passed an
object containing Truncate props. This is useful for finetuning truncation for your particular use-case.
</ClipboardCopy>
</>
);
15 changes: 12 additions & 3 deletions packages/react-core/src/components/Truncate/Truncate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const truncateStyles = {

const minWidthCharacters: number = 12;

interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
export interface TruncateProps extends React.HTMLProps<HTMLSpanElement> {
/** Class to add to outer span */
className?: string;
/** Text to truncate */
Expand All @@ -42,6 +42,11 @@ 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>;
}

const sliceContent = (str: string, slice: number) => [str.slice(0, str.length - slice), str.slice(-slice)];
Expand All @@ -52,6 +57,7 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
tooltipPosition = 'top',
trailingNumChars = 7,
content,
refToGetParent,
...props
}: TruncateProps) => {
const [isTruncated, setIsTruncated] = useState(true);
Expand Down Expand Up @@ -85,8 +91,11 @@ export const Truncate: React.FunctionComponent<TruncateProps> = ({
setTextElement(textRef.current);
}

if (subParentRef && subParentRef.current.parentElement.parentElement && !parentElement) {
setParentElement(subParentRef.current.parentElement.parentElement);
if (
(refToGetParent?.current || (subParentRef?.current && subParentRef.current.parentElement.parentElement)) &&
!parentElement
) {
setParentElement(refToGetParent?.current.parentElement || subParentRef?.current.parentElement.parentElement);
}
}, [textRef, subParentRef, textElement, parentElement]);

Expand Down
2 changes: 1 addition & 1 deletion packages/react-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test:a11y": "patternfly-a11y --config patternfly-a11y.config"
},
"dependencies": {
"@patternfly/patternfly": "6.2.0-prerelease.20",
"@patternfly/patternfly": "6.2.0-prerelease.21",
"@patternfly/react-charts": "workspace:^",
"@patternfly/react-code-editor": "workspace:^",
"@patternfly/react-core": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@patternfly/patternfly": "6.2.0-prerelease.20",
"@patternfly/patternfly": "6.2.0-prerelease.21",
"fs-extra": "^11.3.0",
"tslib": "^2.8.1"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react-styles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"clean": "rimraf dist css"
},
"devDependencies": {
"@patternfly/patternfly": "6.2.0-prerelease.20",
"@patternfly/patternfly": "6.2.0-prerelease.21",
"change-case": "^5.4.4",
"fs-extra": "^11.3.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react-tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"clean": "rimraf dist"
},
"devDependencies": {
"@patternfly/patternfly": "6.2.0-prerelease.20",
"@patternfly/patternfly": "6.2.0-prerelease.21",
"css": "^3.0.0",
"fs-extra": "^11.3.0"
}
Expand Down
Loading