diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.d.ts b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.d.ts new file mode 100644 index 00000000000..945b8c778cd --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.d.ts @@ -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 { + hoverTip?: string; + clickTip?: string; + 'toggle-aria-label'?: string; + isReadOnly?: boolean; + variant?: OneOf; + position?: OneOf; + maxWidth?: string; + exitDelay?: number; + entryDelay?: number; + switchDelay?: number; + onCopy?: (event: MouseEvent, text?: string) => void; + onChange?: (text: string) => void; +} + +declare const ClipboardCopy: FunctionComponent; + +export default ClipboardCopy; diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.js b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.js new file mode 100644 index 00000000000..9dbaba9d4c5 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.js @@ -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 ( +
+ + {id => ( + +
+ {variant === 'expansion' && ( + + )} + + { + 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} + +
+ {this.state.expanded && ( + + {this.state.text} + + )} +
+ )} +
+
+ ); + } +} + +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
. */ + '': 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; diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.md b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.md new file mode 100644 index 00000000000..713f343519d --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.md @@ -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 This is editable; + } +} +``` + +## Read Only Clipboard Copy + +```js +import React from 'react'; +import { ClipboardCopy } from '@patternfly/react-core'; +class SimpleClipboardCopy extends React.Component { + render() { + return This is editable; + } +} +``` + +## Expanded Clipboard Copy + +```js +import React from 'react'; +import { ClipboardCopy, ClipboardCopyVariant } from '@patternfly/react-core'; +class SimpleClipboardCopy extends React.Component { + render() { + return ( + + 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. + + ); + } +} +``` diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.js b/packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.js new file mode 100644 index 00000000000..44a8a36b5c6 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.js @@ -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 +}) => ( + {children}
} + > + + +); + +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; diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.test.js b/packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.test.js new file mode 100644 index 00000000000..0557df0269d --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.test.js @@ -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(Copy Me); + expect(view).toMatchSnapshot(); +}); + +test('copy button onClick', () => { + const onclick = jest.fn(); + const view = shallow(Copy to Clipboard); + view.find('button').simulate('click'); + expect(onclick).toBeCalled(); +}); diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.d.ts b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.d.ts new file mode 100644 index 00000000000..0383fc3ef51 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.d.ts @@ -0,0 +1,11 @@ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { Omit } from '../../typeUtils'; + +export interface ExpandedContentProps extends Omit, 'onChange' | 'children'> { + children: ReactNode; + onChange?(value: string, event: FormEvent): void; +} + +declare const ExpandedContent: FunctionComponent; + +export default ExpandedContent; diff --git a/packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.js b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.js new file mode 100644 index 00000000000..ce7ed169f36 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.js @@ -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 }) => ( +
+