diff --git a/library/spec/pivotal-ui-react/copy-to-clipboard/clipboard-helper_spec.js b/library/spec/pivotal-ui-react/copy-to-clipboard/clipboard-helper_spec.js
new file mode 100644
index 000000000..1b291db42
--- /dev/null
+++ b/library/spec/pivotal-ui-react/copy-to-clipboard/clipboard-helper_spec.js
@@ -0,0 +1,51 @@
+require('../spec_helper');
+
+describe('ClipboardHelper', () => {
+ let subject;
+
+ beforeEach(() => {
+ subject = require('../../../src/pivotal-ui-react/copy-to-clipboard/clipboard-helper');
+ });
+
+ describe('copy', () => {
+ const element = 'mock element';
+ let window, document, range, selection;
+ beforeEach(() => {
+ range = jasmine.createSpyObj('range', ['selectNode']);
+ selection = jasmine.createSpyObj('selection', ['removeAllRanges', 'addRange']);
+ window = jasmine.createSpyObj('window', ['getSelection']);
+ window.getSelection.and.returnValue(selection);
+ document = jasmine.createSpyObj('document', ['createRange', 'execCommand']);
+ document.createRange.and.returnValue(range);
+ });
+
+ it('does some useful things', () => {
+ subject.copy(window, document, element);
+ expect(selection.removeAllRanges).toHaveBeenCalled();
+ expect(selection.addRange).toHaveBeenCalledWith(range);
+ expect(range.selectNode).toHaveBeenCalledWith(element);
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ expect(selection.removeAllRanges.calls.count()).toBe(2);
+ });
+ });
+
+ describe('select', () => {
+ const element = 'mock element';
+ let window, document, range, selection;
+ beforeEach(() => {
+ range = jasmine.createSpyObj('range', ['selectNode']);
+ selection = jasmine.createSpyObj('selection', ['removeAllRanges', 'addRange']);
+ window = jasmine.createSpyObj('window', ['getSelection']);
+ window.getSelection.and.returnValue(selection);
+ document = jasmine.createSpyObj('document', ['createRange']);
+ document.createRange.and.returnValue(range);
+ });
+
+ it('does some useful things', () => {
+ subject.select(window, document, element);
+ expect(selection.removeAllRanges).toHaveBeenCalled();
+ expect(selection.addRange).toHaveBeenCalledWith(range);
+ expect(range.selectNode).toHaveBeenCalledWith(element);
+ });
+ });
+});
\ No newline at end of file
diff --git a/library/spec/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard_spec.js b/library/spec/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard_spec.js
new file mode 100644
index 000000000..a3df3bf79
--- /dev/null
+++ b/library/spec/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard_spec.js
@@ -0,0 +1,78 @@
+require('../spec_helper');
+
+const {itPropagatesAttributes} = require('../support/shared_examples');
+
+describe('CopyToClipboard', () => {
+ const text = 'some copy text';
+ let onClick, CopyToClipboard, CopyToClipboardButton;
+
+ beforeEach(() => {
+ CopyToClipboard = require('../../../src/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard').CopyToClipboard;
+ CopyToClipboardButton = require('../../../src/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard').CopyToClipboardButton;
+ onClick = jasmine.createSpy('onClick');
+ spyOn(document, 'execCommand');
+ });
+
+ describe('CopyToClipboard (basic)', () => {
+ beforeEach(() => {
+ ReactDOM.render(, root);
+ });
+
+ it('renders the text', () => {
+ expect('.sr-only').toHaveText(text);
+ });
+
+ itPropagatesAttributes('.copy-to-clipboard', {className: 'test-class', id: 'test-id', style: {opacity: '0.5'}});
+
+ describe('when clicking on copy to clipboard', () => {
+ beforeEach(() => {
+ $('.copy-to-clipboard').simulate('click');
+ });
+
+ it('copies the text to the clipboard', () => {
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ });
+
+ it('calls the provided callback', () => {
+ expect(onClick).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('CopyToClipboardButton', () => {
+ let Tooltip;
+
+ beforeEach(() => {
+ ReactDOM.render(, root);
+ });
+
+ itPropagatesAttributes('.copy-to-clipboard-button', {className: 'test-class', id: 'test-id', style: {opacity: '0.5'}});
+
+
+ describe('clicking on the button', () => {
+ beforeEach(() => {
+ Tooltip = require('pui-react-tooltip').Tooltip;
+ spyOn(Tooltip.prototype, 'render').and.callThrough();
+ $('.copy-to-clipboard-image').simulate('click');
+ });
+
+ it('renders a tooltip that says "Copied"', () => {
+ expect('.pui-tooltip').toContainText('Copied');
+ });
+
+ it('hides tooltip after 3 seconds', () => {
+ expect('.pui-tooltip:visible').toExist();
+ jasmine.clock().tick(3500);
+ expect('.pui-tooltip:visible').not.toExist();
+ });
+
+ it('copies the text to the clipboard', () => {
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ });
+
+ it('calls the provided callback', () => {
+ expect(onClick).toHaveBeenCalled();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/library/src/pivotal-ui-react/copy-to-clipboard/clipboard-helper.js b/library/src/pivotal-ui-react/copy-to-clipboard/clipboard-helper.js
new file mode 100644
index 000000000..264cc9dee
--- /dev/null
+++ b/library/src/pivotal-ui-react/copy-to-clipboard/clipboard-helper.js
@@ -0,0 +1,20 @@
+const ClipboardHelper = {
+ select(window, document, element) {
+ window.getSelection().removeAllRanges();
+ const range = document.createRange();
+ range.selectNode(element);
+ window.getSelection().addRange(range);
+ },
+
+ copy(window, document, element) {
+ ClipboardHelper.select(window, document, element);
+ try {
+ document.execCommand('copy');
+ } catch (e) {
+ } finally {
+ window.getSelection().removeAllRanges();
+ }
+ }
+};
+
+module.exports = ClipboardHelper;
\ No newline at end of file
diff --git a/library/src/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard.js b/library/src/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard.js
new file mode 100644
index 000000000..555122b5d
--- /dev/null
+++ b/library/src/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard.js
@@ -0,0 +1,85 @@
+const {copy} = require('./clipboard-helper');
+const {mergeProps} = require('pui-react-helpers');
+const {OverlayTrigger} = require('pui-react-overlay-trigger');
+const React = require('react');
+const {Tooltip} = require('pui-react-tooltip');
+require('pui-css-copy-to-clipboard');
+
+const types = React.PropTypes;
+
+function click({props, text}, e) {
+ copy(window, document, text);
+ const {onClick} = props;
+ if (onClick) onClick(e);
+}
+
+function CopyToClipboard(props) {
+ const {children, text, onClick, ...others} = props;
+ const obj = {props, text: null};
+
+ const anchorProps = mergeProps(others, {
+ className: 'copy-to-clipboard',
+ onClick: click.bind(undefined, obj),
+ role: 'button'
+ });
+
+ return (
+
+ obj.text = ref}>{text}
+ {children}
+
+ );
+}
+
+CopyToClipboard.propTypes = {
+ text: types.string.isRequired,
+ onClick: types.func
+};
+
+class CopyToClipboardButton extends React.Component {
+ static propTypes = {
+ text: types.string,
+ onClick: types.func
+ };
+
+ static defaultProps = {
+ onClick() {
+ }
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ this.state = {display: false};
+ }
+
+ click = (e) => {
+ if (!this.state.display) this.setState({display: true}, () => {
+ this.setState({display: false});
+ });
+ this.props.onClick(e)
+ };
+
+ render() {
+ const {onClick, ...props} = this.props;
+ const {display} = this.state;
+
+ const copyProps = mergeProps(props, {
+ className: 'copy-to-clipboard-button',
+ onClick: this.click
+ });
+
+ return (
+
+ Copied} {...{display}}>
+
+
+
+ );
+ }
+}
+
+module.exports = {CopyToClipboard, CopyToClipboardButton};
\ No newline at end of file
diff --git a/library/src/pivotal-ui-react/copy-to-clipboard/package.json b/library/src/pivotal-ui-react/copy-to-clipboard/package.json
new file mode 100644
index 000000000..8370b744e
--- /dev/null
+++ b/library/src/pivotal-ui-react/copy-to-clipboard/package.json
@@ -0,0 +1,9 @@
+{
+ "version": "5.3.0",
+ "description": "React copy component",
+ "homepage": "http://styleguide.pivotal.io/",
+ "dependencies": {
+ "pui-react-overlay-trigger": "^5.3.0",
+ "pui-react-tooltip": "^5.3.0"
+ }
+}
\ No newline at end of file
diff --git a/library/src/pivotal-ui/components/copy-to-clipboard/README.md b/library/src/pivotal-ui/components/copy-to-clipboard/README.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/library/src/pivotal-ui/components/copy-to-clipboard/copy-to-clipboard.scss b/library/src/pivotal-ui/components/copy-to-clipboard/copy-to-clipboard.scss
new file mode 100644
index 000000000..a95672b54
--- /dev/null
+++ b/library/src/pivotal-ui/components/copy-to-clipboard/copy-to-clipboard.scss
@@ -0,0 +1,16 @@
+@import "../pui-variables";
+
+.copy-to-clipboard-image {
+ path {
+ fill: $gray-3;
+ }
+}
+
+.clipboard-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid $gray-2;
+ height: $input-height-base;
+ width: $input-height-base;
+}
\ No newline at end of file
diff --git a/library/src/pivotal-ui/components/copy-to-clipboard/index.js b/library/src/pivotal-ui/components/copy-to-clipboard/index.js
new file mode 100644
index 000000000..e02ab0ade
--- /dev/null
+++ b/library/src/pivotal-ui/components/copy-to-clipboard/index.js
@@ -0,0 +1,4 @@
+try {
+ require('./copy-to-clipboard.css');
+} catch(e) {
+}
diff --git a/library/src/pivotal-ui/components/copy-to-clipboard/package.json b/library/src/pivotal-ui/components/copy-to-clipboard/package.json
new file mode 100644
index 000000000..c7f844b4e
--- /dev/null
+++ b/library/src/pivotal-ui/components/copy-to-clipboard/package.json
@@ -0,0 +1,5 @@
+{
+ "homepage": "http://styleguide.pivotal.io/",
+ "dependencies": {},
+ "version": "5.3.0"
+}
\ No newline at end of file
diff --git a/styleguide/docs/react/copy-to-clipboard.js b/styleguide/docs/react/copy-to-clipboard.js
new file mode 100644
index 000000000..1e6a5c5d5
--- /dev/null
+++ b/styleguide/docs/react/copy-to-clipboard.js
@@ -0,0 +1,35 @@
+/*doc
+ ---
+ title: Copy To Clipboard
+ name: copy_to_clipboard_react
+ categories:
+ - react_components_copy-to-clipboard
+ - react_all
+ ---
+
+
+
+ npm install pui-react-copy-to-clipboard --save
+
+
+ Require the subcomponents:
+
+ ```
+var CopyToClipboard = require('pui-react-copy-to-clipboard').CopyToClipboard;
+var CopyToClipboardButton = require('pui-react-copy-to-clipboard').CopyToClipboardButton;
+ ```
+
+
+ ```react_example_table
+
+
+
+ ```
+
+ The CopyToClipboard Components require the following property:
+
+ Property | Type | Description
+ ------------- | --------------| --------------------------------------------------------------------------
+ `text` | String | Text that is copied when the user clicks
+
+ */
diff --git a/styleguide/package.json b/styleguide/package.json
index f40d89e03..db64f2ecc 100644
--- a/styleguide/package.json
+++ b/styleguide/package.json
@@ -26,6 +26,7 @@
"pui-css-buttons": "file:../library/dist/css/buttons",
"pui-css-code": "file:../library/dist/css/code",
"pui-css-collapse": "file:../library/dist/css/collapse",
+ "pui-css-copy-to-clipboard": "file:../library/dist/css/copy-to-clipboard",
"pui-css-colors": "file:../library/dist/css/colors",
"pui-css-dividers": "file:../library/dist/css/dividers",
"pui-css-dropdowns": "file:../library/dist/css/dropdowns",
@@ -62,6 +63,7 @@
"pui-react-checkbox": "file:../library/dist/react/checkbox",
"pui-react-collapse": "file:../library/dist/react/collapse",
"pui-react-collapsible": "file:../library/dist/react/collapsible",
+ "pui-react-copy-to-clipboard": "file:../library/dist/react/copy-to-clipboard",
"pui-react-dividers": "file:../library/dist/react/dividers",
"pui-react-draggable-list": "file:../library/dist/react/draggable-list",
"pui-react-dropdowns": "file:../library/dist/react/dropdowns",
diff --git a/styleguide/src/pivotal-ui-components.js b/styleguide/src/pivotal-ui-components.js
index d15481e0e..67b825514 100644
--- a/styleguide/src/pivotal-ui-components.js
+++ b/styleguide/src/pivotal-ui-components.js
@@ -18,6 +18,7 @@ assignToGlobal([
require('pui-react-buttons'),
require('pui-react-checkbox'),
require('pui-react-collapse'),
+ require('pui-react-copy-to-clipboard'),
require('pui-react-dividers'),
require('pui-react-draggable-list'),
require('pui-react-lists'),