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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## [`master`](https://github.com/elastic/eui/tree/master)

No public interface changes since `3.5.1`.
- Added `EuiButtonCopy` ([#1112](https://github.com/elastic/eui/pull/1112))

## [`3.5.1`](https://github.com/elastic/eui/tree/v3.5.1)

Expand Down
13 changes: 13 additions & 0 deletions src-docs/src/views/button/button_copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

import {
EuiButtonCopy,
} from '../../../../src/components/';

export default () => (
<div>
<EuiButtonCopy textToCopy="foobar">
Copy
</EuiButtonCopy>
</div>
);
19 changes: 19 additions & 0 deletions src-docs/src/views/button/button_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ import ButtonGroup from './button_group';
const buttonGroupSource = require('!!raw-loader!./button_group');
const buttonGroupHtml = renderToHtml(ButtonGroup);

import ButtonCopy from './button_copy';
const buttonCopySource = require('!!raw-loader!./button_copy');
const buttonCopyHtml = renderToHtml(ButtonCopy);

export const ButtonExample = {
title: 'Button',
sections: [{
Expand Down Expand Up @@ -243,5 +247,20 @@ export const ButtonExample = {
</p>
),
demo: <ButtonGhost />,
}, {
title: 'ButtonCopy',
source: [{
type: GuideSectionTypes.JS,
code: buttonCopySource,
}, {
type: GuideSectionTypes.HTML,
code: buttonCopyHtml,
}],
text: (
<p>
Button for copying text to clipboard
</p>
),
demo: <ButtonCopy />,
}],
};
62 changes: 62 additions & 0 deletions src/components/button/button_copy/button_copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import { copyToClipboard } from '../../../services';
import { EuiButton } from '../button';
import { EuiToolTip } from '../../tool_tip';

const UNCOPIED_MSG = 'Copy to clipboard';
const COPIED_MSG = 'Copied';

export class EuiButtonCopy 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,
textToCopy, // eslint-disable-line no-unused-vars
onClick, // eslint-disable-line no-unused-vars
...rest
} = this.props;

return (
<EuiToolTip
content={this.state.tooltipText}
onMouseOut={this.resetTooltipText}
>
<EuiButton
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we may want to pull this out into a more generic service so that they can pass any type of child node (any button or link) that gets wrapped in this functionality. As it stands, you can't use EuiButtonEmpty or EuiButtonIcon.

I would also like to look into creating a "disappearing" tooltip to use here. Where there is no tooltip on hover, but then when you click the trigger element, the tooltip only stays up for a short period of time. I'll look into this in another branch.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe use a toast instead, which will auto dismiss itself and is good for this action (as well as for accessibility because it'll announce itself). The button will likely have the action in it so the tooltip on hover is likely unnecessary. Agree it should be more generic and accept any child element, not just a regular button.

Copy link
Contributor

Choose a reason for hiding this comment

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

My concern with a toast, is that it's not very tied to the button itself and can be easily overlooked. For small actions like this, maybe we should consider a new type of tooltip that reads out like a toast, but whose proximity is much closer to the triggering element?

Copy link
Contributor

Choose a reason for hiding this comment

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

There are some issues with the tooltip rendering

is a known bug, #996 ; coincidentally this exact use case

onClick={this.copySnippet}
{...rest}
>
{children}
</EuiButton>
</EuiToolTip>
);
}
}

EuiButtonCopy.propTypes = {
textToCopy: PropTypes.string.isRequired,
};

1 change: 1 addition & 0 deletions src/components/button/button_copy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EuiButtonCopy } from './button_copy';
4 changes: 4 additions & 0 deletions src/components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export {
export {
EuiButtonGroup,
} from './button_group';

export {
EuiButtonCopy,
} from './button_copy';
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
EuiButtonIcon,
EuiButtonToggle,
EuiButtonGroup,
EuiButtonCopy,
} from './button';

export {
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