Skip to content

Commit

Permalink
Make CopyToClipboard work for text with newlines [#154782444] (#555)
Browse files Browse the repository at this point in the history
  • Loading branch information
reidmit authored and pivotal committed Feb 15, 2018
1 parent 1a210bd commit 5474094
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 149 deletions.
72 changes: 34 additions & 38 deletions spec/pivotal-ui-react/copy-to-clipboard/clipboard-helper_spec.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,48 @@
import '../spec_helper' ;
import ClipboardHelper from '../../../src/react/copy-to-clipboard/clipboard-helper';
import {copy} from '../../../src/react/copy-to-clipboard/clipboard-helper';

describe('ClipboardHelper', () => {
let subject;

beforeEach(() => {
subject = ClipboardHelper;
});
let document, copyText, textarea;

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);
document = jasmine.createSpyObj('document', [
'execCommand',
'createElement'
]);

document.body = jasmine.createSpyObj('body', [
'appendChild',
'removeChild'
]);

textarea = jasmine.createSpyObj('textarea', ['select']);
document.createElement.and.returnValue(textarea);

copyText = 'Text to be copied';
copy(document, copyText);
});

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);
it('creates a textarea and appends it to the body', () => {
expect(document.createElement).toHaveBeenCalledWith('textarea');
expect(document.body.appendChild).toHaveBeenCalledWith(textarea);
});
});

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('sets the correct value and className on the textarea', () => {
expect(textarea.value).toBe(copyText);
expect(textarea.className).toBe('sr-only');
});

it('selects the textarea content', () => {
expect(textarea.select).toHaveBeenCalled();
});

it('executes a copy command', () => {
expect(document.execCommand).toHaveBeenCalledWith('copy');
});

it('does some useful things', () => {
subject.select(window, document, element);
expect(selection.removeAllRanges).toHaveBeenCalled();
expect(selection.addRange).toHaveBeenCalledWith(range);
expect(range.selectNode).toHaveBeenCalledWith(element);
it('removes the textarea from the body', () => {
expect(document.body.removeChild).toHaveBeenCalledWith(textarea);
});
});
});
});
131 changes: 51 additions & 80 deletions spec/pivotal-ui-react/copy-to-clipboard/copy-to-clipboard_spec.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,75 @@
import '../spec_helper';

import {CopyToClipboard} from '../../../src/react/copy-to-clipboard';
import ClipboardHelper from '../../../src/react/copy-to-clipboard/clipboard-helper';

describe('CopyToClipboard', () => {
const text = 'some copy text';
let onClick, getWindow, window, document, range, selection, subject;
let text, onClick, subject;

beforeEach(() => {
text = 'some copy text';
onClick = jasmine.createSpy('onClick');

range = jasmine.createSpyObj('range', ['selectNode']);
selection = jasmine.createSpyObj('selection', ['removeAllRanges', 'addRange']);
window = jasmine.createSpyObj('window', ['getSelection']);
getWindow = jasmine.createSpy('getWindow').and.returnValue(window);
document = jasmine.createSpyObj('document', ['createRange', 'execCommand']);
spyOn(ClipboardHelper, 'copy');

document.createRange.and.returnValue(range);
window.document = document;
window.getSelection.and.returnValue(selection);
subject = ReactDOM.render(<CopyToClipboard {...{
text,
onClick
}}/>, root);
});

describe('CopyToClipboard (basic)', () => {
const renderComponent = props => ReactDOM.render(<CopyToClipboard {...props}/>, root);

it('renders the text', () => {
subject = renderComponent({text, onClick, className: 'test-class', id: 'test-id', style: {opacity: '0.5'}});
expect('.sr-only').toHaveText(text);
});
it('renders an anchor', () => {
expect('a.pui-copy-to-clipboard').toExist();
expect('a.pui-copy-to-clipboard').toHaveAttr('role', 'button');
});

it('propagates attributes', () => {
subject = renderComponent({text, onClick, className: 'test-class', id: 'test-id', style: {opacity: '0.5'}});
it('renders a hidden tooltip with default text', () => {
expect('.tooltip-container').toHaveClass('tooltip-container-hidden');
expect('.tooltip-content').toHaveText('Copied');
});

expect('.copy-to-clipboard').toHaveClass('test-class');
expect('.copy-to-clipboard').toHaveAttr('id', 'test-id');
expect('.copy-to-clipboard').toHaveCss({opacity: '0.5'});
describe('when given tooltip text', () => {
beforeEach(() => {
subject::setProps({tooltip: 'Copied successfully!'});
});

describe('clicking on the element', () => {
it('renders a tooltip that says "Copied in"', () => {
subject = renderComponent({
getWindow,
text,
onClick,
className: 'test-class',
id: 'test-id',
style: {opacity: '0.5'},
tooltip: 'Copied in'
});

$('.copy-to-clipboard .tooltip').simulate('click');

expect('.tooltip-container').toHaveClass('tooltip-container-visible');
expect('.tooltip-content').toHaveText('Copied in');
});

it('hides tooltip after 1 seconds', () => {
subject = renderComponent({
getWindow,
text,
onClick,
className: 'test-class',
id: 'test-id',
style: {opacity: '0.5'}
});

$('.copy-to-clipboard').simulate('click');
jasmine.clock().tick(2000);
it('uses the custom tooltip text', () => {
expect('.tooltip-content').toHaveText('Copied successfully!');
});
});

expect('.tooltip-container').not.toHaveClass('tooltip-container-visible');
});
describe('when given additional props', () => {
beforeEach(() => {
subject::setProps({className: 'test-class', id: 'test-id', style: {opacity: '0.5'}});
});

it('copies the text to the clipboard', () => {
subject = renderComponent({
getWindow,
text,
onClick,
className: 'test-class',
id: 'test-id',
style: {opacity: '0.5'}
});
it('passes the props to the anchor', () => {
expect('a.pui-copy-to-clipboard').toHaveClass('test-class');
expect('a.pui-copy-to-clipboard').toHaveAttr('id', 'test-id');
expect('a.pui-copy-to-clipboard').toHaveCss({opacity: '0.5'});
});
});

$('.copy-to-clipboard').simulate('click');
describe('clicking on the element', () => {
beforeEach(() => {
$('.pui-copy-to-clipboard .tooltip').click();
$('.pui-copy-to-clipboard').click();
});

expect(document.execCommand).toHaveBeenCalledWith('copy');
});
it('makes tooltip visible', () => {
expect('.tooltip-container').toHaveClass('tooltip-container-visible');
});

it('calls the provided callback', () => {
subject = renderComponent({
getWindow,
text,
onClick,
className: 'test-class',
id: 'test-id',
style: {opacity: '0.5'}
});
it('hides tooltip after 1 seconds', () => {
jasmine.clock().tick(2000);
expect('.tooltip-container').not.toHaveClass('tooltip-container-visible');
});

$('.copy-to-clipboard').simulate('click');
it('copies the text to the clipboard', () => {
expect(ClipboardHelper.copy).toHaveBeenCalledWith(document, text);
});

expect(onClick).toHaveBeenCalled();
});
it('calls the provided callback', () => {
expect(onClick).toHaveBeenCalled();
});
});
});
});
18 changes: 8 additions & 10 deletions src/react/copy-to-clipboard/clipboard-helper.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
const select = (window, document, element) => {
window.getSelection().removeAllRanges();
const range = document.createRange();
range.selectNode(element);
window.getSelection().addRange(range);
};
const copy = (document, text) => {
const textarea = document.createElement('textarea');
textarea.className = 'sr-only';
textarea.value = text;
document.body.appendChild(textarea);

const copy = (window, document, element) => {
select(window, document, element);
try {
textarea.select();
document.execCommand('copy');
} catch (e) {
} finally {
window.getSelection().removeAllRanges();
document.body.removeChild(textarea);
}
};

export default {select, copy};
export default {copy};
33 changes: 12 additions & 21 deletions src/react/copy-to-clipboard/copy-to-clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,33 @@ export class CopyToClipboard extends React.PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
onClick: PropTypes.func,
getWindow: PropTypes.func,
tooltip: PropTypes.string
};

static defaultProps = {
getWindow: () => window,
};

componentDidMount() {
require('../../css/copy-to-clipboard');
}

click = ({props, text}, e) => {
const window = this.props.getWindow();
copy(window, window.document, text);
const {onClick} = props;
click = ({onClick, text}, e) => {
copy(document, text);
if (onClick) onClick(e);
};

render() {
const {children, text, onClick, getWindow, tooltip = "Copied", ...others} = this.props;
const obj = {props: this.props, text: null};
const {children, text, onClick, tooltip = 'Copied', ...others} = this.props;

const anchorProps = mergeProps(others, {
className: 'copy-to-clipboard',
onClick: this.click.bind(undefined, obj),
className: 'copy-to-clipboard pui-copy-to-clipboard',
onClick: this.click.bind(null, this.props),
role: 'button'
});

return (
<a {...anchorProps}>
<TooltipTrigger {...{tooltip, trigger: "click"}}>
<span className="sr-only" ref={ref => obj.text = ref}>{text}</span>
{children}
</TooltipTrigger>
</a>
);
<a {...anchorProps}>
<TooltipTrigger {...{tooltip, trigger: 'click'}}>
{children}
</TooltipTrigger>
</a>
);
}
}
}

0 comments on commit 5474094

Please sign in to comment.