-
Notifications
You must be signed in to change notification settings - Fork 377
feat(clipboardcopy): add clipboardcopy to PF4 #1538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
187 changes: 187 additions & 0 deletions
187
packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
boaz0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** 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; | ||
47 changes: 47 additions & 0 deletions
47
packages/patternfly-4/react-core/src/components/ClipboardCopy/ClipboardCopy.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this say "not editable" since it's read only?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
64 changes: 64 additions & 0 deletions
64
packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
27 changes: 27 additions & 0 deletions
27
packages/patternfly-4/react-core/src/components/ClipboardCopy/CopyButton.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); |
11 changes: 11 additions & 0 deletions
11
packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
26 changes: 26 additions & 0 deletions
26
packages/patternfly-4/react-core/src/components/ClipboardCopy/ExpandedContent.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.