diff --git a/README.md b/README.md index 4bef7df8..e6ea05c9 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ - [**Side-effects**](./docs/Side-effects.md) - [`useAsync`](./docs/useAsync.md) — resolves an `async` function. - [`useAsyncRetry`](./docs/useAsyncRetry.md) — `useAsync` with `retry()` method. + - [`useCopyToClipboard`](./docs/useCopyToClipboard.md) — copies text to clipboard. - [`useDebounce`](./docs/useDebounce.md) — debounces a function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usedebounce--demo) - [`useFavicon`](./docs/useFavicon.md) — sets favicon of the page. - [`useLocalStorage`](./docs/useLocalStorage.md) — manages a value in `localStorage`. diff --git a/docs/useCopyToClipboard.md b/docs/useCopyToClipboard.md new file mode 100644 index 00000000..cc56dae1 --- /dev/null +++ b/docs/useCopyToClipboard.md @@ -0,0 +1,35 @@ +# `useCopyToClipboard` + +Copy text to a user's clipboard. + + +## Usage + +Basic usage + +```jsx +const Demo = () => { + const [text, setText] = React.useState(''); + const [copied, copyToClipboard] = useCopyToClipboard(text); + + return ( +
+ setText(e.target.value)} /> + +
Copied: {copied ? 'Yes' : 'No'}
+
+ ) +} +``` + +## Reference + +```js +const [copied, copyToClipboard] = useCopyToClipboard(text); +const [copied, copyToClipboard] = useCopyToClipboard(text, writeText); +``` + +, where + +- `writeText` — function that receives a single string argument, which + it copies to user's clipboard. diff --git a/package.json b/package.json index ed73e001..6089b8d5 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "homepage": "https://github.com/streamich/react-use#readme", "dependencies": { + "copy-to-clipboard": "^3.1.0", "nano-css": "^5.1.0", "react-fast-compare": "^2.0.4", "react-wait": "^0.3.0", diff --git a/src/__stories__/useCopyToClipboard.story.tsx b/src/__stories__/useCopyToClipboard.story.tsx new file mode 100644 index 00000000..7fcd5a7f --- /dev/null +++ b/src/__stories__/useCopyToClipboard.story.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import ShowDocs from './util/ShowDocs'; +import {useCopyToClipboard} from '..'; + +const Demo = () => { + const [text, setText] = React.useState(''); + const [copied, copyToClipboard] = useCopyToClipboard(text, { + onCopy: txt => alert('success: ' + txt), + onError: err => alert(err), + }); + + return ( +
+ setText(e.target.value)} /> + +
Copied: {copied ? 'Yes' : 'No'}
+
+ +
+
+ ) +} + +storiesOf('Side-effects|useCopyToClipboard', module) + .add('Docs', () => ) + .add('Demo', () => ) diff --git a/src/index.ts b/src/index.ts index 272a20b7..a13d2d0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import useAsyncRetry from './useAsyncRetry'; import useAudio from './useAudio'; import useBattery from './useBattery'; import useBoolean from './useBoolean'; +import useCopyToClipboard from './useCopyToClipboard'; import useDrop from './useDrop'; import useDropArea from './useDropArea'; import useCounter from './useCounter'; @@ -73,6 +74,7 @@ export { useAudio, useBattery, useBoolean, + useCopyToClipboard, useDrop, useDropArea, useClickAway, diff --git a/src/useCopyToClipboard.ts b/src/useCopyToClipboard.ts new file mode 100644 index 00000000..244bcc1b --- /dev/null +++ b/src/useCopyToClipboard.ts @@ -0,0 +1,55 @@ +import {useState, useCallback, useRef} from 'react'; +import useUpdateEffect from './useUpdateEffect'; +import useRefMounted from './useRefMounted'; +const writeTextDefault = require('copy-to-clipboard'); + +export type WriteText = (text: string) => Promise; // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText +export interface UseCopyToClipboardOptions { + writeText?: WriteText; + onCopy?: (text: string) => void; + onError?: (error: any, text: string) => void; +} +export type UseCopyToClipboard = (text?: string, options?: UseCopyToClipboardOptions) => [boolean, () => void]; + +const useCopyToClipboard: UseCopyToClipboard = (text = '', options) => { + const {writeText = writeTextDefault, onCopy, onError} = (options || {}) as UseCopyToClipboardOptions; + + if (process.env.NODE_ENV !== 'production') { + if (typeof text !== 'string') { + console.warn('useCopyToClipboard hook expects first argument to be string.'); + } + } + + const mounted = useRefMounted(); + const latestText = useRef(text); + const [copied, setCopied] = useState(false); + const copyToClipboard = useCallback(async () => { + if (latestText.current !== text) { + if (process.env.NODE_ENV !== 'production') { + console.warn('Trying to copy stale text.'); + } + return; + } + + try { + await writeText(text); + if (!mounted.current) return; + setCopied(true); + onCopy && onCopy(text); + } catch (error) { + if (!mounted.current) return; + console.error(error); + setCopied(false); + onError && onError(error, text); + } + }, [text]); + + useUpdateEffect(() => { + setCopied(false); + latestText.current = text; + }, [text]); + + return [copied, copyToClipboard]; +} + +export default useCopyToClipboard; diff --git a/yarn.lock b/yarn.lock index 75bd4d62..8219b6d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3932,6 +3932,13 @@ copy-to-clipboard@^3.0.8: dependencies: toggle-selection "^1.0.3" +copy-to-clipboard@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.1.0.tgz#0a28141899e6bd217b9dc13fd1689b3b38820b44" + integrity sha512-+RNyDq266tv5aGhfRsL6lxgj8Y6sCvTrVJnFUVvuxuqkcSMaLISt1wd4JkdQSphbcLTIQ9kEpTULNnoCXAFdng== + dependencies: + toggle-selection "^1.0.6" + core-js-compat@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.0.0.tgz#cd9810b8000742535a4a43773866185e310bd4f7" @@ -11010,7 +11017,7 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -toggle-selection@^1.0.3: +toggle-selection@^1.0.3, 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=