From 175855a67da8bca68970b26631ed1d1588a188ac Mon Sep 17 00:00:00 2001 From: Charles Hansen Date: Wed, 21 Sep 2016 15:53:32 -0700 Subject: [PATCH] feat(CopyToClipboard): Add CopyToClipboard and CopyToClipboardButton components [finishes #104647528] Signed-off-by: Weyman Fung --- .../clipboard-helper_spec.js | 51 +++++++++++ .../copy-to-clipboard_spec.js | 78 +++++++++++++++++ .../copy-to-clipboard/clipboard-helper.js | 20 +++++ .../copy-to-clipboard/copy-to-clipboard.js | 85 +++++++++++++++++++ .../copy-to-clipboard/package.json | 9 ++ .../components/copy-to-clipboard/README.md | 0 .../copy-to-clipboard/copy-to-clipboard.scss | 16 ++++ .../components/copy-to-clipboard/index.js | 4 + .../components/copy-to-clipboard/package.json | 5 ++ styleguide/docs/react/copy-to-clipboard.js | 35 ++++++++ styleguide/package.json | 2 + styleguide/src/pivotal-ui-components.js | 1 + 12 files changed, 306 insertions(+) create mode 100644 library/spec/pivotal-ui-react/copy-to-clipboard/clipboard-helper_spec.js create mode 100644 library/spec/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard_spec.js create mode 100644 library/src/pivotal-ui-react/copy-to-clipboard/clipboard-helper.js create mode 100644 library/src/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard.js create mode 100644 library/src/pivotal-ui-react/copy-to-clipboard/package.json create mode 100644 library/src/pivotal-ui/components/copy-to-clipboard/README.md create mode 100644 library/src/pivotal-ui/components/copy-to-clipboard/copy-to-clipboard.scss create mode 100644 library/src/pivotal-ui/components/copy-to-clipboard/index.js create mode 100644 library/src/pivotal-ui/components/copy-to-clipboard/package.json create mode 100644 styleguide/docs/react/copy-to-clipboard.js 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'),