diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js index a7145363a..2bacc3779 100644 --- a/frontend/src/Components/Form/FormInputButton.js +++ b/frontend/src/Components/Form/FormInputButton.js @@ -42,7 +42,9 @@ function FormInputButton(props) { FormInputButton.propTypes = { className: PropTypes.string.isRequired, isLastButton: PropTypes.bool.isRequired, - canSpin: PropTypes.bool.isRequired + canSpin: PropTypes.bool.isRequired, + children: PropTypes.element, + id: PropTypes.string }; FormInputButton.defaultProps = { diff --git a/frontend/src/Components/Link/ClipboardButton.js b/frontend/src/Components/Link/ClipboardButton.js deleted file mode 100644 index 55843f05f..000000000 --- a/frontend/src/Components/Link/ClipboardButton.js +++ /dev/null @@ -1,139 +0,0 @@ -import Clipboard from 'clipboard'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FormInputButton from 'Components/Form/FormInputButton'; -import Icon from 'Components/Icon'; -import { icons, kinds } from 'Helpers/Props'; -import getUniqueElememtId from 'Utilities/getUniqueElementId'; -import styles from './ClipboardButton.css'; - -class ClipboardButton extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._id = getUniqueElememtId(); - this._successTimeout = null; - this._testResultTimeout = null; - - this.state = { - showSuccess: false, - showError: false - }; - } - - componentDidMount() { - this._clipboard = new Clipboard(`#${this._id}`, { - text: () => this.props.value, - container: document.getElementById(this._id) - }); - - this._clipboard.on('success', this.onSuccess); - } - - componentDidUpdate() { - const { - showSuccess, - showError - } = this.state; - - if (showSuccess || showError) { - this._testResultTimeout = setTimeout(this.resetState, 3000); - } - } - - componentWillUnmount() { - if (this._clipboard) { - this._clipboard.destroy(); - } - - if (this._testResultTimeout) { - clearTimeout(this._testResultTimeout); - } - } - - // - // Control - - resetState = () => { - this.setState({ - showSuccess: false, - showError: false - }); - }; - - // - // Listeners - - onSuccess = () => { - this.setState({ - showSuccess: true - }); - }; - - onError = () => { - this.setState({ - showError: true - }); - }; - - // - // Render - - render() { - const { - value, - className, - ...otherProps - } = this.props; - - const { - showSuccess, - showError - } = this.state; - - const showStateIcon = showSuccess || showError; - const iconName = showError ? icons.DANGER : icons.CHECK; - const iconKind = showError ? kinds.DANGER : kinds.SUCCESS; - - return ( - - - { - showSuccess && - - - - } - - { - - - - } - - - ); - } -} - -ClipboardButton.propTypes = { - className: PropTypes.string.isRequired, - value: PropTypes.string.isRequired -}; - -ClipboardButton.defaultProps = { - className: styles.button -}; - -export default ClipboardButton; diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx new file mode 100644 index 000000000..09095ae74 --- /dev/null +++ b/frontend/src/Components/Link/ClipboardButton.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import FormInputButton from 'Components/Form/FormInputButton'; +import Icon from 'Components/Icon'; +import { icons, kinds } from 'Helpers/Props'; +import { ButtonProps } from './Button'; +import styles from './ClipboardButton.css'; + +export interface ClipboardButtonProps extends Omit { + value: string; +} + +export type ClipboardState = 'success' | 'error' | null; + +export default function ClipboardButton({ + id, + value, + className = styles.button, + ...otherProps +}: ClipboardButtonProps) { + const [state, setState] = useState(null); + + useEffect(() => { + if (!state) { + return; + } + + const timeoutId = setTimeout(() => { + setState(null); + }, 3000); + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [state]); + + const handleClick = useCallback(async () => { + try { + await navigator.clipboard.writeText(value); + setState('success'); + } catch (_) { + setState('error'); + } + }, [value]); + + return ( + + + {state ? ( + + + + ) : null} + + + + + + + ); +} diff --git a/frontend/src/Utilities/getUniqueElementId.ts b/frontend/src/Utilities/getUniqueElementId.ts index dae5150b7..1b380851d 100644 --- a/frontend/src/Utilities/getUniqueElementId.ts +++ b/frontend/src/Utilities/getUniqueElementId.ts @@ -1,7 +1,9 @@ let i = 0; -// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) - +/** + * @deprecated Use React's useId() instead + * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + */ export default function getUniqueElementId() { return `id-${i++}`; } diff --git a/package.json b/package.json index 4d62f7d2a..e5e0bbb8f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "classnames": "2.3.2", - "clipboard": "2.0.11", "connected-react-router": "6.9.3", "element-class": "0.2.2", "filesize": "10.0.7", diff --git a/yarn.lock b/yarn.lock index 2fd9a7e55..f46048a70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2477,15 +2477,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clipboard@2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2880,11 +2871,6 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -3811,13 +3797,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6271,11 +6250,6 @@ section-iterator@^2.0.0: resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" integrity sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ== -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -6756,11 +6730,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"