diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 4e2b491f..62d053f1 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -52,7 +52,6 @@ "@popperjs/core": "^2.11.7", "@types/flat": "^5.0.2", "@types/react-transition-group": "^4.4.5", - "copy-to-clipboard": "^3.3.1", "deepmerge": "^4.3.1", "flat": "^5.0.2", "focus-trap": "^7.5.4", diff --git a/packages/react-components/src/hooks/index.ts b/packages/react-components/src/hooks/index.ts index f84ad124..5be5ca6a 100644 --- a/packages/react-components/src/hooks/index.ts +++ b/packages/react-components/src/hooks/index.ts @@ -1,7 +1,6 @@ export { useMediaQuery } from './use_media_query'; export { useMergedRef } from './use_merged_ref'; export { useClipboard } from './use_clipboard'; -export type { UseClipboardOptions } from './use_clipboard'; export { useControllableState } from './use_controllable'; export type { UseControllableStateProps } from './use_controllable'; export { useId } from './use_id'; diff --git a/packages/react-components/src/hooks/use_clipboard.mdx b/packages/react-components/src/hooks/use_clipboard.mdx index a1280efe..e86acaa9 100644 --- a/packages/react-components/src/hooks/use_clipboard.mdx +++ b/packages/react-components/src/hooks/use_clipboard.mdx @@ -13,17 +13,14 @@ React hook that provides copy to clipboard functionality. import { useClipboard } from '@peculiar/react-components'; export const Demo = () => { - const { - onCopy, - hasCopied, - } = useClipboard('Text to copy'); + const { copy, isCopied } = useClipboard(); return ( ); }; diff --git a/packages/react-components/src/hooks/use_clipboard.stories.tsx b/packages/react-components/src/hooks/use_clipboard.stories.tsx index 547029d1..8e725827 100644 --- a/packages/react-components/src/hooks/use_clipboard.stories.tsx +++ b/packages/react-components/src/hooks/use_clipboard.stories.tsx @@ -9,17 +9,14 @@ const meta = { export default meta; export const DemoExample = () => { - const { - onCopy, - hasCopied, - } = useClipboard('Text to copy'); + const { copy, isCopied } = useClipboard(); return ( ); }; diff --git a/packages/react-components/src/hooks/use_clipboard.ts b/packages/react-components/src/hooks/use_clipboard.ts index 51d18132..20c85cb6 100644 --- a/packages/react-components/src/hooks/use_clipboard.ts +++ b/packages/react-components/src/hooks/use_clipboard.ts @@ -1,59 +1,38 @@ import React from 'react'; -import copy from 'copy-to-clipboard'; - -export type UseClipboardOptions = { - /** - * timeout delay (in ms) to switch back to initial state once copied. - */ - timeout?: number; - /** - * Set the desired MIME type - */ - format?: string; -}; +import { copyToClipboard } from '../utils'; /** - * React hook to copy content to clipboard - * - * @param text the text or value to copy - * @param {Number} [optionsOrTimeout=1500] optionsOrTimeout - - * delay (in ms) to switch back to initial state once copied. - * @param {Object} optionsOrTimeout - * @param {string} optionsOrTimeout.format - set the desired MIME type - * @param {number} optionsOrTimeout.timeout - - * delay (in ms) to switch back to initial state once copied. + * React hook to copy content to clipboard. */ -export function useClipboard( - text: string, - optionsOrTimeout: number | UseClipboardOptions = {}, -) { - const [hasCopied, setHasCopied] = React.useState(false); - - const { timeout = 1500, ...copyOptions } = typeof optionsOrTimeout === 'number' - ? { timeout: optionsOrTimeout } - : optionsOrTimeout; - - const onCopy = React.useCallback(() => { - const didCopy = copy(text, copyOptions); - - setHasCopied(didCopy); - }, [text, copyOptions]); +export function useClipboard() { + const [isCopied, setIsCopied] = React.useState(false); + const timeout = React.useRef>(); + const mounted = React.useRef(false); React.useEffect(() => { - let timeoutId: number | null = null; - - if (hasCopied) { - timeoutId = window.setTimeout(() => { - setHasCopied(false); - }, timeout); - } + mounted.current = true; return () => { - if (timeoutId) { - window.clearTimeout(timeoutId); - } + mounted.current = false; }; - }, [timeout, hasCopied]); + }, []); + + const copy = async (text: string) => { + try { + setIsCopied(true); + clearTimeout(timeout.current); + + timeout.current = setTimeout(() => { + if (mounted) { + setIsCopied(false); + } + }, 1500); + + copyToClipboard(text); + } catch (error) { + // ignore error + } + }; - return { value: text, onCopy, hasCopied }; + return { copy, isCopied }; } diff --git a/packages/react-components/src/utils/copy_to_clipboard.ts b/packages/react-components/src/utils/copy_to_clipboard.ts new file mode 100644 index 00000000..84663230 --- /dev/null +++ b/packages/react-components/src/utils/copy_to_clipboard.ts @@ -0,0 +1,52 @@ +type SuccessCallback = () => void; + +const fallbackCopyToClipboard = (text: string, onSuccess?: SuccessCallback) => { + const textareaElement = document.createElement('textarea'); + + textareaElement.value = text; + document.body.appendChild(textareaElement); + + if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { + const range = document.createRange(); + + textareaElement.setAttribute('contentEditable', 'true'); + textareaElement.setAttribute('readOnly', 'true'); + + range.selectNodeContents(textareaElement); + + const selection = window.getSelection(); + + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + + textareaElement.setSelectionRange(0, text.length); + } else { + textareaElement.select(); + } + + try { + document.execCommand('copy'); + + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error('Fallback copy error:', error); + } + + document.body.removeChild(textareaElement); +}; + +export const copyToClipboard = (text: string, onSuccess?: SuccessCallback) => { + if (!navigator.clipboard) { + fallbackCopyToClipboard(text, onSuccess); + + return; + } + + navigator.clipboard.writeText(text) + .then(onSuccess) + .catch(() => fallbackCopyToClipboard(text, onSuccess)); +}; diff --git a/packages/react-components/src/utils/index.ts b/packages/react-components/src/utils/index.ts index e69de29b..58e3bb61 100644 --- a/packages/react-components/src/utils/index.ts +++ b/packages/react-components/src/utils/index.ts @@ -0,0 +1 @@ +export { copyToClipboard } from './copy_to_clipboard'; diff --git a/yarn.lock b/yarn.lock index 15d88a7e..c3000c29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4878,13 +4878,6 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -copy-to-clipboard@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" - integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== - dependencies: - toggle-selection "^1.0.6" - core-js-compat@^3.31.0, core-js-compat@^3.33.1: version "3.35.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.0.tgz#c149a3d1ab51e743bc1da61e39cb51f461a41873" @@ -10562,11 +10555,6 @@ tocbot@^4.20.1: resolved "https://registry.yarnpkg.com/tocbot/-/tocbot-4.25.0.tgz#bc38aea5ec8f076779bb39636f431b044129a237" integrity sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA== -toggle-selection@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" - integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"