Skip to content

Commit

Permalink
copy button that copies text to clipboard (#1112)
Browse files Browse the repository at this point in the history
* copy button that copies text to clipboard

* change log

* move to component under utilities

* update change log

* make children be a function

* make copy messages be props

* add comments on props, pass rest, more details in usage example

* do not provide default beforeMessage

* besure to check for title in tooltip
  • Loading branch information
nreese authored Aug 14, 2018
1 parent 8bfffc5 commit 8391daf
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 2 deletions.
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) => (
<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
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,

/**
* 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 || 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

0 comments on commit 8391daf

Please sign in to comment.