Repository: plotly/dash
File: components/dash-core-components/src/components/Clipboard.react.js (JavaScript)

Code before:
     */
    html_content: PropTypes.string,

    /**
     * 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,
    /**
     * The icon's styles while copying
     */
    copied_style: PropTypes.object,

    /**
     * The class  name of the icon element while copying
     */
    copied_className: PropTypes.string,

    /**
     * Dash-assigned callback that gets fired when the value changes.
     */
    setProps: PropTypes.func,
};


Review comment (performance):
Hmm, this seems like we're adding 2 or 3 ways of solving this problem at once. If we are allowing custom `children` as above, is there really any need to have these 2 style props? Users could apply their own styling to the child components and achieve the same effect with less complexity on our end.
(also, there might be good reasons here, I'm just not seeing it immediately!)

Code after:
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';

import LoadingElement from '../utils/LoadingElement';

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 {
    static contextType = window.dash_component_api.DashContext;

    constructor(props) {
        super(props);
        this.copyToClipboard = this.copyToClipboard.bind(this);
        this.onClickHandler = this.onClickHandler.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,
        };
    }

    onClickHandler() {
        this.props.setProps({n_clicks: this.props.n_clicks + 1});
    }

    componentDidUpdate(prevProps) {
        // If the clicks has not changed, do nothing
        if (
            !this.props.n_clicks ||
            this.props.n_clicks === prevProps.n_clicks
        ) {
            return;
        }
        // If the clicks has changed, copy to clipboard
        this.copyToClipboard();
    }

    // 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(content, htmlContent) {
        const showCopiedIcon = 1000;
        if (htmlContent) {
            const blobHtml = new Blob([htmlContent], {type: 'text/html'});
            const blobText = new Blob([content ?? htmlContent], {
                type: 'text/plain',
            });
            const data = [
                new ClipboardItem({
                    ['text/plain']: blobText,
                    ['text/html']: blobHtml,
                }),
            ];
            await navigator.clipboard.write(data);
        } else {
            await clipboardAPI.writeText(content);
        }
        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 content = target.innerText;
        if (!content) {
            content = target.value;
            content = content === undefined ? null : content;
        }
        return content;
    }

    async loading() {
        while (this.context.isLoading()) {
            await wait(100);
        }
    }

    async copyToClipboard() {
        let content;
        let htmlContent;
        if (this.props.target_id) {
            content = this.getTargetText();
        } else {
            await wait(100); // gives time for callback to start
            await this.loading();
            content = this.props.content;
            htmlContent = this.props.html_content;
        }
        if (content || htmlContent) {
            this.copySuccess(content, htmlContent);
        }
    }

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

    render() {
        const {id, title, className, style, children, copied_children} =
            this.props;

        const isCopied = this.state.copied;

        const button_children = isCopied
            ? copied_children ?? <FontAwesomeIcon icon={faCheckCircle} />
            : children ?? <FontAwesomeIcon icon={faCopy} />;

        return clipboardAPI ? (
            <LoadingElement
                id={id}
                title={title}
                style={style}
                className={className}
                onClick={this.onClickHandler}
            >
                {button_children}
            </LoadingElement>
        ) : null;
    }
}

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

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

    /**
     * Children rendered inside the Clipboard button before copying. By default, a copy icon.
     */
    children: PropTypes.node,

    /**
     * Children rendered inside the Clipboard button after the value has been copied. By default, a check icon.
     */
    copied_children: PropTypes.node,

    /**
     * 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.
     */
    content: PropTypes.string,

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

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

    /**
     * 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,

    /**
     * Dash-assigned callback that gets fired when the value changes.
     */
    setProps: PropTypes.func,
};