Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

Commit

Permalink
Merge pull request #932 from AnnMarieW/clipboard
Browse files Browse the repository at this point in the history
Added Copy to Clipboard component
  • Loading branch information
alexcjohnson authored Apr 12, 2021
2 parents fe60f3c + 8c6f735 commit aa5ea32
Show file tree
Hide file tree
Showing 8 changed files with 2,624 additions and 1,202 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).


## UNRELEASED
### Added
- [#932](https://github.com/plotly/dash-core-components/pull/932). Adds a new copy to clipboard component.


## [1.16.0] - 2021-04-08
### Added
- [#863](https://github.com/plotly/dash-core-components/pull/863) Adds a new `Download` component. Along with this several utility functions are added to help construct the appropriate data format:
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# AUTO GENERATED FILE - DO NOT EDIT

export(dccChecklist)
export(dccClipboard)
export(dccConfirmDialog)
export(dccConfirmDialogProvider)
export(dccDatePickerRange)
Expand Down
3,543 changes: 2,346 additions & 1,197 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"maintainer": "Ryan Patrick Kyle <ryan@plotly.com>",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-regular-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"base64-js": "^1.3.1",
"color": "^3.1.0",
"fast-isnumeric": "^1.1.3",
Expand All @@ -50,7 +54,7 @@
"prop-types": "^15.6.0",
"ramda": "^0.26.1",
"rc-slider": "^9.1.0",
"react-addons-shallow-compare": "^15.6.0",
"react-addons-shallow-compare": "^15.6.3",
"react-dates": "^20.1.0",
"react-docgen": "^3.0.0",
"react-dropzone": "^4.1.2",
Expand All @@ -61,7 +65,7 @@
"uniqid": "^5.0.3"
},
"devDependencies": {
"@babel/cli": "^7.4.0",
"@babel/cli": "^7.13.0",
"@babel/core": "^7.4.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
Expand All @@ -76,11 +80,11 @@
"babel-loader": "^8.0.5",
"check-prop-types": "^1.1.2",
"component-playground": "^3.0.0",
"copyfiles": "^2.0.0",
"copyfiles": "^2.4.1",
"css-loader": "^1.0.1",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"es-check": "^5.0.0",
"es-check": "^5.2.3",
"eslint": "^5.8.0",
"eslint-config-prettier": "^3.0.1",
"eslint-plugin-import": "^2.14.0",
Expand Down
186 changes: 186 additions & 0 deletions src/components/Clipboard.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React, {Component} from 'react'; // eslint-disable-line no-unused-vars
import PropTypes from 'prop-types';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCopy, faCheckCircle} from '@fortawesome/free-regular-svg-icons';

const clipboardAPI = navigator.clipboard;

function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}

/**
* The Clipboard component copies text to the clipboard
*/

export default class Clipboard extends React.Component {
constructor(props) {
super(props);
this.copyToClipboard = this.copyToClipboard.bind(this);
this.copySuccess = this.copySuccess.bind(this);
this.getTargetText = this.getTargetText.bind(this);
this.loading = this.loading.bind(this);
this.stringifyId = this.stringifyId.bind(this);
this.state = {
copied: false,
};
}

// stringifies object ids used in pattern matching callbacks
stringifyId(id) {
if (typeof id !== 'object') {
return id;
}
const stringifyVal = v => (v && v.wild) || JSON.stringify(v);
const parts = Object.keys(id)
.sort()
.map(k => JSON.stringify(k) + ':' + stringifyVal(id[k]));
return '{' + parts.join(',') + '}';
}

async copySuccess(text) {
const showCopiedIcon = 1000;
await clipboardAPI.writeText(text);
this.setState({copied: true});
await wait(showCopiedIcon);
this.setState({copied: false});
}

getTargetText() {
// get the inner text. If none, use the content of the value param
const id = this.stringifyId(this.props.target_id);
const target = document.getElementById(id);
if (!target) {
throw new Error(
'Clipboard copy failed: no element found for target_id ' +
this.props.target_id
);
}
let text = target.innerText;
if (!text) {
text = target.value;
text = text === undefined ? null : text;
}
return text;
}

async loading() {
while (this.props.loading_state?.is_loading) {
await wait(100);
}
}

async copyToClipboard() {
this.props.setProps({
n_clicks: this.props.n_clicks + 1,
});

let text;
if (this.props.target_id) {
text = this.getTargetText();
} else {
await wait(100); // gives time for callback to start
await this.loading();
text = this.props.text;
}
if (text) {
this.copySuccess(text);
}
}

componentDidMount() {
if (!clipboardAPI) {
console.warn('Copy to clipboard not available with this browser'); // eslint-disable-line no-console
}
}

render() {
const {id, title, className, style, loading_state} = this.props;
const copyIcon = <FontAwesomeIcon icon={faCopy} />;
const copiedIcon = <FontAwesomeIcon icon={faCheckCircle} />;
const btnIcon = this.state.copied ? copiedIcon : copyIcon;

return clipboardAPI ? (
<div
id={id}
title={title}
style={style}
className={className}
onClick={this.copyToClipboard}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
>
<i> {btnIcon}</i>
</div>
) : null;
}
}

Clipboard.defaultProps = {
text: null,
target_id: null,
n_clicks: 0,
};

Clipboard.propTypes = {
/**
* The ID used to identify this component.
*/
id: PropTypes.string,

/**
* The id of target component containing text to copy to the clipboard.
* The inner text of the `children` prop will be copied to the clipboard. If none, then the text from the
* `value` prop will be copied.
*/
target_id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),

/**
* The text to be copied to the clipboard if the `target_id` is None.
*/
text: PropTypes.string,

/**
* The number of times copy button was clicked
*/
n_clicks: PropTypes.number,

/**
* The text shown as a tooltip when hovering over the copy icon.
*/
title: PropTypes.string,

/**
* The icon's styles
*/
style: PropTypes.object,

/**
* The class name of the icon element
*/
className: PropTypes.string,

/**
* Object that holds the loading state object coming from dash-renderer
*/
loading_state: PropTypes.shape({
/**
* Determines if the component is loading or not
*/
is_loading: PropTypes.bool,
/**
* Holds which property is loading
*/
prop_name: PropTypes.string,
/**
* Holds the name of the component that is loading
*/
component_name: PropTypes.string,
}),

/**
* Dash-assigned callback that gets fired when the value changes.
*/
setProps: PropTypes.func,
};
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Tabs from './components/Tabs.react';
import Tab from './components/Tab.react';
import Store from './components/Store.react';
import LogoutButton from './components/LogoutButton.react';
import Clipboard from './components/Clipboard.react';

import 'react-dates/lib/css/_datepicker.css';
import './components/css/react-dates@20.1.0-fix.css';
Expand Down Expand Up @@ -49,5 +50,6 @@ export {
Upload,
Store,
LogoutButton,
Download
Download,
Clipboard
};
17 changes: 17 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@ def dash_dcc(request, dash_thread_server, tmpdir):
pause=request.config.getoption("pause"),
) as dc:
yield dc


@pytest.fixture
def dash_dcc_headed(request, dash_thread_server, tmpdir):
with DashCoreComponentsComposite(
dash_thread_server,
browser=request.config.getoption("webdriver"),
remote=request.config.getoption("remote"),
remote_url=request.config.getoption("remote_url"),
headless=False,
options=request.config.hook.pytest_setup_options(),
download_path=tmpdir.mkdir("download").strpath,
percy_assets_root=request.config.getoption("percy_assets"),
percy_finalize=request.config.getoption("nopercyfinalize"),
pause=request.config.getoption("pause"),
) as dc:
yield dc
57 changes: 57 additions & 0 deletions tests/integration/clipboard/test_clipboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash.testing.wait as wait
import time


from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys


def test_clp001_clipboard_text(dash_dcc_headed):
copy_text = "Hello, Dash!"
app = dash.Dash(__name__, prevent_initial_callbacks=True)
app.layout = html.Div(
[
html.Div(copy_text, id="copy"),
dcc.Clipboard(id="copy_icon", target_id="copy"),
dcc.Textarea(id="paste"),
]
)
dash_dcc_headed.start_server(app)

dash_dcc_headed.find_element("#copy_icon").click()
# time.sleep(2)
dash_dcc_headed.find_element("#paste").click()
ActionChains(dash_dcc_headed.driver).key_down(Keys.CONTROL).send_keys("v").key_up(
Keys.CONTROL
).perform()

wait.until(
lambda: dash_dcc_headed.find_element("#paste").get_attribute("value")
== copy_text,
timeout=3,
)


def test_clp002_clipboard_text(dash_dcc_headed):
copy_text = "Copy this text to the clipboard"
app = dash.Dash(__name__, prevent_initial_callbacks=True)
app.layout = html.Div(
[dcc.Clipboard(id="copy_icon", text=copy_text), dcc.Textarea(id="paste")]
)
dash_dcc_headed.start_server(app)

dash_dcc_headed.find_element("#copy_icon").click()
time.sleep(1)
dash_dcc_headed.find_element("#paste").click()
ActionChains(dash_dcc_headed.driver).key_down(Keys.CONTROL).send_keys("v").key_up(
Keys.CONTROL
).perform()

wait.until(
lambda: dash_dcc_headed.find_element("#paste").get_attribute("value")
== copy_text,
timeout=3,
)

0 comments on commit aa5ea32

Please sign in to comment.