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))

## [`3.5.1`](https://github.com/elastic/eui/tree/v3.5.1)
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
41 changes: 41 additions & 0 deletions src-docs/src/views/copy/copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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.

++

<EuiButton>
Click to copy input text
</EuiButton>
</EuiCopy>
</div>
);
}
}
37 changes: 37 additions & 0 deletions src-docs/src/views/copy/copy_example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 simplified utility for copying text to clipboard.
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 give more description on how it's used?

</p>
),
components: { EuiCopy },
demo: <Copy />,
props: { EuiCopy },
}],
};
55 changes: 55 additions & 0 deletions src/components/copy/copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { copyToClipboard } from '../../services';
import { EuiToolTip } from '../tool_tip';

const UNCOPIED_MSG = 'Copy';
const COPIED_MSG = 'Copied';
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 make these messages customizable via props?


export class EuiCopy extends React.Component {

constructor(props) {
super(props);

this.state = {
tooltipText: UNCOPIED_MSG
};
}

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

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

render() {
const {
children,
} = 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}
>
<span onClick={this.copySnippet}>
{children}
</span>
</EuiToolTip>
);
}
}

EuiCopy.propTypes = {
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 ;)

};

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
4 changes: 4 additions & 0 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 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