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
@@ -0,0 +1,27 @@
import { FunctionComponent, HTMLProps, MouseEvent } from 'react';
import { OneOf } from '../../typeUtils';
import { PopoverPosition } from '../Popover';

export const ClipboardCopyVariant = {
inline: 'inline',
expansion: 'expansion'
};

export interface ClipboardCopyProps extends HTMLProps<HTMLDivElement> {
hoverTip?: string;
clickTip?: string;
'toggle-aria-label'?: string;
isReadOnly?: boolean;
variant?: OneOf<typeof ClipboardCopyVariant, keyof typeof ClipboardCopyVariant>;
position?: OneOf<typeof PopoverPosition, keyof typeof PopoverPosition>;
maxWidth?: string;
exitDelay?: number;
entryDelay?: number;
switchDelay?: number;
onCopy?: (event: MouseEvent, text?: string) => void;
onChange?: (text: string) => void;
}

declare const ClipboardCopy: FunctionComponent<ClipboardCopyProps>;

export default ClipboardCopy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/ClipboardCopy/clipboard-copy.css';
import { css } from '@patternfly/react-styles';
import { CopyIcon } from '@patternfly/react-icons';
import PropTypes from 'prop-types';
import { TextInput } from '../TextInput';
import { Tooltip, TooltipPosition } from '../Tooltip';
import GenerateId from '../../helpers/GenerateId/GenerateId';
import CopyButton from './CopyButton';
import ToggleButton from './ToggleButton';
import ExpandedContent from './ExpandedContent';

const clipboardCopyFunc = (event, text) => {
const clipboard = event.currentTarget.parentElement;
const el = document.createElement('input');
el.value = text;
clipboard.appendChild(el);
el.select();
document.execCommand('copy');
clipboard.removeChild(el);
};

export const ClipboardCopyVariant = {
inline: 'inline',
expansion: 'expansion'
};

class ClipboardCopy extends React.Component {
constructor(props) {
super(props);
this.timer = null;
this.state = {
text: this.props.children,
expanded: false,
copied: false
};
}

expandContent = () => {
this.setState(prevState => ({
expanded: !prevState.expanded
}));
};

updateText = text => {
this.setState({ text });
this.props.onChange(text);
};

render() {
const {
isReadOnly,
exitDelay,
maxWidth,
entryDelay,
switchDelay,
onCopy,
hoverTip,
clickTip,
'aria-label': ariaLabel,
'toggle-aria-label': toggleAriaLabel,
variant,
position,
classname,
onChange,
...props
} = this.props;
const textIdPrefix = 'text-input-';
const toggleIdPrefix = 'toggle-';
const contentIdPrefix = 'content-';
const copyButtonIdPrefix = 'copy-button-';
return (
<div
className={css(styles.clipboardCopy, this.state.expanded && styles.modifiers.expanded, classname)}
{...props}
>
<GenerateId prefix="">
{id => (
<React.Fragment>
<div className={css(styles.clipboardCopyGroup)}>
{variant === 'expansion' && (
<ToggleButton
isExpanded={this.state.expanded}
onClick={this.expandContent}
id={`${toggleIdPrefix}-${id}`}
textId={`${textIdPrefix}-${id}`}
contentId={`${contentIdPrefix}-${id}`}
aria-label={toggleAriaLabel}
/>
)}
<TextInput
isReadOnly={isReadOnly}
onChange={this.updateText}
value={this.state.text}
id={`text-input-${id}`}
aria-label={ariaLabel}
/>
<CopyButton
exitDelay={exitDelay}
entryDelay={entryDelay}
maxWidth={maxWidth}
position={position}
id={`copy-button-${id}`}
textId={`text-input-${id}`}
aria-label={hoverTip}
onClick={event => {
if (this.timer) {
clearTimeout(this.timer);
this.setState({ copied: false });
}
onCopy(event, this.state.text);
this.setState({ copied: true }, () => {
this.timer = setTimeout(() => {
this.setState({ copied: false });
this.timer = null;
}, switchDelay);
});
}}
>
{this.state.copied ? clickTip : hoverTip}
</CopyButton>
</div>
{this.state.expanded && (
<ExpandedContent id={`content-${id}`} onChange={this.updateText}>
{this.state.text}
</ExpandedContent>
)}
</React.Fragment>
)}
</GenerateId>
</div>
);
}
}

ClipboardCopy.propTypes = {
/** Additional classes added to the clipboard copy container. */
classname: PropTypes.string,
/** Tooltip message to display when hover the copy button */
hoverTip: PropTypes.string,
/** Tooltip message to display when clicking the copy button */
clickTip: PropTypes.string,
/** Custom flag to show that the input requires an associated id or aria-label. */
'aria-label': PropTypes.string,
/** Custom flag to show that the toggle button requires an associated id or aria-label. */
'toggle-aria-label': PropTypes.string,
/** Flag to show if the input is read only. */
isReadOnly: PropTypes.bool,
/** Adds Clipboard Copy variant styles. */
variant: PropTypes.oneOf(Object.keys(ClipboardCopyVariant)),
/** Copy button popover position. */
position: PropTypes.oneOf(Object.keys(TooltipPosition)),
/** Maximum width of the tooltip (default 150px). */
maxWidth: PropTypes.string,
/** Delay in ms before the tooltip disappears. */
exitDelay: PropTypes.number,
/** Delay in ms before the tooltip appears. */
entryDelay: PropTypes.number,
/** Delay in ms before the tooltip message switch to hover tip. */
switchDelay: PropTypes.number,
/** A function that is triggered on clicking the copy button. */
onCopy: PropTypes.func,
/** A function that is triggered on changing the text. */
onChange: PropTypes.func,
/** The text which is copied. */
children: PropTypes.node.isRequired,
/** Additional props are spread to the container <div>. */
'': PropTypes.any
};

ClipboardCopy.defaultProps = {
hoverTip: 'Copy to clipboard',
clickTip: 'Successfully copied to clipboard!',
isReadOnly: false,
variant: ClipboardCopyVariant.inline,
position: TooltipPosition.top,
maxWidth: '150px',
exitDelay: 1600,
entryDelay: 100,
switchDelay: 2000,
onCopy: clipboardCopyFunc,
onChange: () => {},
'aria-label': 'Copyable input',
'toggle-aria-label': 'Show content'
};

export default ClipboardCopy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---

title: 'ClipboardCopy'
cssPrefix: 'pf-c-copyclipboard'

---

## Clipboard Copy

```js
import React from 'react';
import { ClipboardCopy } from '@patternfly/react-core';
class SimpleClipboardCopy extends React.Component {
render() {
return <ClipboardCopy>This is editable</ClipboardCopy>;
}
}
```

## Read Only Clipboard Copy

```js
import React from 'react';
import { ClipboardCopy } from '@patternfly/react-core';
class SimpleClipboardCopy extends React.Component {
render() {
return <ClipboardCopy isReadOnly>This is editable</ClipboardCopy>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this say "not editable" since it's read only?

Copy link
Member Author

Choose a reason for hiding this comment

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

Dang! You're right!

}
}
```

## Expanded Clipboard Copy

```js
import React from 'react';
import { ClipboardCopy, ClipboardCopyVariant } from '@patternfly/react-core';
class SimpleClipboardCopy extends React.Component {
render() {
return (
<ClipboardCopy variant={ClipboardCopyVariant.expansion}>
Got a lot of text here, need to see all of it? Click that arrow on the left side and check out the resulting
expansion.
</ClipboardCopy>
);
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/ClipboardCopy/clipboard-copy.css';
import { css } from '@patternfly/react-styles';
import { CopyIcon } from '@patternfly/react-icons';
import PropTypes from 'prop-types';
import { Tooltip, TooltipPosition } from '../Tooltip';

const CopyButton = ({
className,
onClick,
exitDelay,
entryDelay,
maxWidth,
position,
children,
'aria-label': ariaLabel,
id,
textId,
...props
}) => (
<Tooltip
trigger={'mouseenter focus click'}
exitDelay={exitDelay}
entryDelay={entryDelay}
maxWidth={maxWidth}
position={position}
content={<div>{children}</div>}
>
<button
onClick={onClick}
className={css(styles.clipboardCopyGroupCopy, className)}
aria-label={ariaLabel}
id={id}
aria-labelledby={`${id} ${textId}`}
{...props}
>
<CopyIcon />
</button>
</Tooltip>
);

CopyButton.propTypes = {
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
id: PropTypes.string.isRequired,
textId: PropTypes.string.isRequired,
className: PropTypes.string,
exitDelay: PropTypes.number,
entryDelay: PropTypes.number,
maxWidth: PropTypes.string,
position: PropTypes.oneOf(Object.keys(TooltipPosition)),
'aria-label': PropTypes.string
};

CopyButton.defaultProps = {
className: '',
exitDelay: 100,
entryDelay: 100,
maxWidth: '100px',
position: 'top',
'aria-label': 'Copyable input'
};

export default CopyButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { shallow } from 'enzyme';
import CopyButton from './CopyButton';

const props = {
id: 'my-id',
textId: 'my-text-id',
className: 'fancy-copy-button',
onClick: jest.fn(),
exitDelay: 1000,
entryDelay: 2000,
maxWidth: '500px',
position: 'right',
'aria-label': 'click this button to copy text'
};

test('copy button render', () => {
const view = shallow(<CopyButton {...props}>Copy Me</CopyButton>);
expect(view).toMatchSnapshot();
});

test('copy button onClick', () => {
const onclick = jest.fn();
const view = shallow(<CopyButton {...props} onClick={onclick}>Copy to Clipboard</CopyButton>);
view.find('button').simulate('click');
expect(onclick).toBeCalled();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FunctionComponent, HTMLProps, ReactNode } from 'react';
import { Omit } from '../../typeUtils';

export interface ExpandedContentProps extends Omit<HTMLProps<HTMLDivElement>, 'onChange' | 'children'> {
children: ReactNode;
onChange?(value: string, event: FormEvent<HTMLInputElement>): void;
}

declare const ExpandedContent: FunctionComponent<ExpandedContentProps>;

export default ExpandedContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/ClipboardCopy/clipboard-copy.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';

const ExpandedContent = ({ className, children, onChange, ...props }) => (
<div className={css(styles.clipboardCopyExpandableContent, className)} {...props}>
<textarea
onChange={e => onChange(e.target.value, e)}
value={children}
style={{ resize: 'none', width: '100%', height: '100%', borderWidth: '0' }}
/>
</div>
);

ExpandedContent.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired
};

ExpandedContent.defaultProps = {
className: ''
};

export default ExpandedContent;
Loading