Skip to content

Commit

Permalink
feat(CopyToClipboard): Add CopyToClipboard and CopyToClipboardButton
Browse files Browse the repository at this point in the history
components

[finishes #104647528]

Signed-off-by: Weyman Fung <wfung@pivotal.io>
  • Loading branch information
charleshansen authored and weymanf committed Sep 21, 2016
1 parent 50149a5 commit 175855a
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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(<CopyToClipboard {...{text, onClick, className: 'test-class', id: 'test-id', style: {opacity: '0.5'}}}/>, 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(<CopyToClipboardButton {...{text, onClick, className: 'test-class', id: 'test-id', style: {opacity: '0.5'}}}/>, 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();
});
});
});
});
20 changes: 20 additions & 0 deletions library/src/pivotal-ui-react/copy-to-clipboard/clipboard-helper.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 (
<a {...anchorProps}>
<span className="sr-only" ref={ref => obj.text = ref}>{text}</span>
{children}
</a>
);
}

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 (
<CopyToClipboard {...copyProps}>
<OverlayTrigger trigger="manual" delayHide={3000} placement="top" overlay={<Tooltip id="copy-to-clipboard">Copied</Tooltip>} {...{display}}>
<div className="clipboard-button">
<svg className="copy-to-clipboard-image" width="20" height="20" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/>
</svg>
</div>
</OverlayTrigger>
</CopyToClipboard>
);
}
}

module.exports = {CopyToClipboard, CopyToClipboardButton};
9 changes: 9 additions & 0 deletions library/src/pivotal-ui-react/copy-to-clipboard/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions library/src/pivotal-ui/components/copy-to-clipboard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
try {
require('./copy-to-clipboard.css');
} catch(e) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"homepage": "http://styleguide.pivotal.io/",
"dependencies": {},
"version": "5.3.0"
}
35 changes: 35 additions & 0 deletions styleguide/docs/react/copy-to-clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*doc
---
title: Copy To Clipboard
name: copy_to_clipboard_react
categories:
- react_components_copy-to-clipboard
- react_all
---
<code class="pam">
<i class="fa fa-download" alt="Install the Component"></i>
npm install pui-react-copy-to-clipboard --save
</code>
Require the subcomponents:
```
var CopyToClipboard = require('pui-react-copy-to-clipboard').CopyToClipboard;
var CopyToClipboardButton = require('pui-react-copy-to-clipboard').CopyToClipboardButton;
```
```react_example_table
<CopyToClipboard text="I got copied by a button"><button>Click Me To Copy</button></CopyToClipboard>
<CopyToClipboardButton text="I got copied by a good looking button"/>
```
The CopyToClipboard Components require the following property:
Property | Type | Description
------------- | --------------| --------------------------------------------------------------------------
`text` | String | Text that is copied when the user clicks
*/
2 changes: 2 additions & 0 deletions styleguide/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions styleguide/src/pivotal-ui-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down

0 comments on commit 175855a

Please sign in to comment.