Skip to content
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

copy button that copies text to clipboard #1112

Merged
merged 11 commits into from
Aug 14, 2018
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## [`master`](https://github.com/elastic/eui/tree/master)

- Added `EuiCopy` ([#1112](https://github.com/elastic/eui/pull/1112))
- Added `disabled` to `EuiRadioGroup.options` ([#1111](https://github.com/elastic/eui/pull/1111))

**Bug fixes**
Expand Down
4 changes: 4 additions & 0 deletions src-docs/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ import { ComboBoxExample }
import { ContextMenuExample }
from './views/context_menu/context_menu_example';

import { CopyExample }
from './views/copy/copy_example';

import { DatePickerExample }
from './views/date_picker/date_picker_example';

Expand Down Expand Up @@ -377,6 +380,7 @@ const navigation = [{
name: 'Utilities',
items: [
AccessibilityExample,
CopyExample,
ResponsiveExample,
DelayHideExample,
ErrorBoundaryExample,
Expand Down
43 changes: 43 additions & 0 deletions src-docs/src/views/copy/copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { Component } from 'react';

import {
EuiCopy,
EuiButton,
EuiFieldText,
EuiSpacer,
} from '../../../../src/components/';

export default class extends Component {

state = {
copyText: 'I am the text that will be copied'
}

onChange = e => {
this.setState({
copyText: e.target.value,
});
};

render() {
return (
<div>
<EuiFieldText
placeholder="Enter text that will be copied to clipboard"
value={this.state.copyText}
onChange={this.onChange}
/>

<EuiSpacer size="m" />

<EuiCopy textToCopy={this.state.copyText}>
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about making this a render prop instead of adding a span with its own onClick?

<EuiCopy textToCopy={this.state.copyText}>
  {(copyFn) => (
      <EuiButton onClick={copyFn}>Click to copy input text</EuiButton>
  )}
</EuiCopy>

That keeps the utility component from affecting DOM and makes it more re-useable in other places.

Copy link
Contributor

Choose a reason for hiding this comment

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

++

{(copy) => (
<EuiButton onClick={copy}>
Click to copy input text
</EuiButton>
)}
</EuiCopy>
</div>
);
}
}
38 changes: 38 additions & 0 deletions src-docs/src/views/copy/copy_example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import { renderToHtml } from '../../services';

import {
GuideSectionTypes,
} from '../../components';

import {
EuiCode,
EuiCopy,
} from '../../../../src/components';

import Copy from './copy';
const copySource = require('!!raw-loader!./copy');
const copyHtml = renderToHtml(Copy);

export const CopyExample = {
title: 'Copy',
sections: [{
source: [{
type: GuideSectionTypes.JS,
code: copySource,
}, {
type: GuideSectionTypes.HTML,
code: copyHtml,
}],
text: (
<p>
The <EuiCode>EuiCopy</EuiCode> component is a utility for copying text to clipboard.
Wrap a function that returns a Component. The first argument will be a `copy` function.
</p>
),
components: { EuiCopy },
demo: <Copy />,
props: { EuiCopy },
}],
};
80 changes: 80 additions & 0 deletions src/components/copy/copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { copyToClipboard } from '../../services';
import { EuiToolTip } from '../tool_tip';

export class EuiCopy extends React.Component {

constructor(props) {
super(props);

this.state = {
tooltipText: this.props.beforeMessage
};
}

copy = () => {
const isCopied = copyToClipboard(this.props.textToCopy);
if (isCopied) {
this.setState({
tooltipText: this.props.afterMessage,
});
}
}

resetTooltipText = () => {
this.setState({
tooltipText: this.props.beforeMessage,
});
}

render() {
const {
children,
textToCopy, // eslint-disable-line no-unused-vars
beforeMessage, // eslint-disable-line no-unused-vars
afterMessage, // eslint-disable-line no-unused-vars
...rest
} = this.props;

return (
<EuiToolTip
Copy link
Contributor

Choose a reason for hiding this comment

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

May want consider passing down {...rest} to the tooltip so consumers can customize it even more and pass down something like 'data-test-subj'.

content={this.state.tooltipText}
onMouseOut={this.resetTooltipText}
{...rest}
>
{children(this.copy)}
</EuiToolTip>
);
}
}

EuiCopy.propTypes = {

/**
* Text that will be copied to clipboard when copy function is executed.
*/
textToCopy: PropTypes.string.isRequired,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add comments to each prop type to populate the props list table in the docs?

Also, you may want to consider add TS def's ;)


/**
* Tooltip message displayed before copy function is called.
*/
beforeMessage: PropTypes.string,

/**
* Tooltip message displayed after copy function is called that lets the user know that
* 'textToCopy' has been copied to the clipboard.
*/
afterMessage: PropTypes.string.isRequired,

/**
* Function that must return a Component. First argument is 'copy' function.
* Use your own logic to create the component that user's interactact with when triggering copy.
*/
children: PropTypes.func.isRequired,
};

EuiCopy.defaultProps = {
afterMessage: 'Copied',
};

3 changes: 3 additions & 0 deletions src/components/copy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {
EuiCopy,
} from './copy';
4 changes: 4 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export {
EuiContextMenuItem,
} from './context_menu';

export {
EuiCopy,
} from './copy';

export {
EuiDatePicker,
EuiDatePickerRange,
Expand Down
8 changes: 6 additions & 2 deletions src/components/tool_tip/tool_tip.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export class EuiToolTip extends Component {
this.hideToolTip();
}
}

if (this.props.onMouseOut) {
this.props.onMouseOut();
}
};

render() {
Expand All @@ -142,7 +146,7 @@ export class EuiToolTip extends Component {
);

let tooltip;
if (visible) {
if (visible && content) {
Copy link
Contributor

@cchaos cchaos Aug 14, 2018

Choose a reason for hiding this comment

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

Sorry, one last thing, but because tooltips accept title's as well can you change this to be if (visible && (content || title))

tooltip = (
<EuiPortal>
<EuiToolTipPopover
Expand Down Expand Up @@ -201,7 +205,7 @@ EuiToolTip.propTypes = {
/**
* The main content of your tooltip.
*/
content: PropTypes.node.isRequired,
content: PropTypes.node,

/**
* An optional title for your tooltip.
Expand Down
46 changes: 46 additions & 0 deletions src/services/copy_to_clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
function createHiddenTextElement(text) {
const textElement = document.createElement('span');
textElement.textContent = text;
textElement.style.all = 'unset';
// prevents scrolling to the end of the page
textElement.style.position = 'fixed';
textElement.style.top = 0;
textElement.style.clip = 'rect(0, 0, 0, 0)';
// used to preserve spaces and line breaks
textElement.style.whiteSpace = 'pre';
// do not inherit user-select (it may be `none`)
textElement.style.webkitUserSelect = 'text';
textElement.style.MozUserSelect = 'text';
textElement.style.msUserSelect = 'text';
textElement.style.userSelect = 'text';
return textElement;
}

export function copyToClipboard(text) {
let isCopied = true;
const range = document.createRange();
const selection = window.getSelection();
const elementToBeCopied = createHiddenTextElement(text);

document.body.appendChild(elementToBeCopied);
range.selectNode(elementToBeCopied);
selection.removeAllRanges();
selection.addRange(range);

if (!document.execCommand('copy')) {
isCopied = false;
console.warn('Unable to copy to clipboard.'); // eslint-disable-line no-console
}

if (selection) {
if (typeof selection.removeRange === 'function') {
selection.removeRange(range);
} else {
selection.removeAllRanges();
}
}

document.body.removeChild(elementToBeCopied);

return isCopied;
}
4 changes: 4 additions & 0 deletions src/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export {
DEFAULT_VISUALIZATION_COLOR,
} from './color';

export {
copyToClipboard
} from './copy_to_clipboard';

export {
formatAuto,
formatBoolean,
Expand Down