From 26c3de7ff27d48d373e3e2b6ebd45d25128a091b Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Fri, 8 Mar 2019 02:57:19 +0200 Subject: [PATCH] feat(clipboardcopy): add clipboardcopy to PF4 Signed-off-by: Boaz Shuster --- .../ClipboardCopy/ClipboardCopy.d.ts | 27 +++ .../components/ClipboardCopy/ClipboardCopy.js | 187 ++++++++++++++++++ .../components/ClipboardCopy/ClipboardCopy.md | 47 +++++ .../components/ClipboardCopy/CopyButton.js | 64 ++++++ .../ClipboardCopy/CopyButton.test.js | 27 +++ .../ClipboardCopy/ExpandedContent.d.ts | 11 ++ .../ClipboardCopy/ExpandedContent.js | 26 +++ .../ClipboardCopy/ExpandedContent.test.js | 13 ++ .../ClipboardCopy/ToggleButton.d.ts | 15 ++ .../components/ClipboardCopy/ToggleButton.js | 35 ++++ .../ClipboardCopy/ToggleButton.test.js | 32 +++ .../__snapshots__/CopyButton.test.js.snap | 34 ++++ .../ExpandedContent.test.js.snap | 21 ++ .../__snapshots__/ToggleButton.test.js.snap | 21 ++ .../src/components/ClipboardCopy/index.d.ts | 1 + .../src/components/ClipboardCopy/index.js | 1 + .../react-core/src/components/index.ts | 2 +- 17 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.d.ts create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.md create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.test.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.d.ts create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.test.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ToggleButton.d.ts create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ToggleButton.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/ToggleButton.test.js create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/__snapshots__/CopyButton.test.js.snap create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/__snapshots__/ExpandedContent.test.js.snap create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/__snapshots__/ToggleButton.test.js.snap create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/index.d.ts create mode 100644 packages/patternfly-4/react-core/src/components/ClipboardCopy/index.js 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 }) => ( +
+