Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 2e4bc25

Browse files
authored
chore: add new hook useCopyToClipboard (#3109)
* Chore: add new hook `useCopyToClipboard` * Chore: replace util with hook * Chore: remove util * CHore: update delay * Chore: update format * Chore: update test * Chore: fix nits * Chore: fix nits
1 parent 568b952 commit 2e4bc25

6 files changed

Lines changed: 114 additions & 100 deletions

File tree

src/components/ArticleComponents/Codebox/index.tsx

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
22
import { highlight, languages } from 'prismjs';
33
import { sanitize } from 'isomorphic-dompurify';
44
import classnames from 'classnames';
5-
import { copyTextToClipboard } from '../../../util/copyTextToClipboard';
5+
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
66
import styles from './index.module.scss';
77

88
interface Props {
@@ -26,8 +26,8 @@ const replaceLabelLanguages = (language: string) =>
2626
.toUpperCase();
2727

2828
const Codebox = ({ children: { props } }: Props): JSX.Element => {
29-
const [copied, setCopied] = useState(false);
3029
const [parsedCode, setParsedCode] = useState('');
30+
const [copied, copyText] = useCopyToClipboard();
3131

3232
// eslint-disable-next-line react/prop-types
3333
const className = props.className || 'text';
@@ -43,23 +43,9 @@ const Codebox = ({ children: { props } }: Props): JSX.Element => {
4343

4444
const handleCopyCode = async (event: React.MouseEvent<HTMLButtonElement>) => {
4545
event.preventDefault();
46-
setCopied(await copyTextToClipboard(stringCode));
46+
copyText(stringCode);
4747
};
4848

49-
useEffect((): (() => void) => {
50-
let timer: ReturnType<typeof setTimeout>;
51-
52-
if (copied) {
53-
timer = setTimeout(() => setCopied(false), 3000);
54-
}
55-
56-
return () => {
57-
if (timer) {
58-
clearTimeout(timer);
59-
}
60-
};
61-
}, [copied]);
62-
6349
useEffect(() => {
6450
const parsedLangauge = replaceLanguages(language);
6551

src/components/CommonComponents/ShellBox/index.tsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { copyTextToClipboard } from '../../../util/copyTextToClipboard';
1+
import React from 'react';
2+
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
33
import styles from './index.module.scss';
44

55
interface Props {
@@ -10,27 +10,13 @@ const ShellBox = ({
1010
children,
1111
textToCopy,
1212
}: React.PropsWithChildren<Props>): JSX.Element => {
13-
const [copied, setCopied] = useState(false);
13+
const [copied, copyText] = useCopyToClipboard();
1414

1515
const handleCopyCode = async (event: React.MouseEvent<HTMLButtonElement>) => {
1616
event.preventDefault();
17-
setCopied(await copyTextToClipboard(textToCopy));
17+
copyText(textToCopy);
1818
};
1919

20-
useEffect((): (() => void) => {
21-
let timer: ReturnType<typeof setTimeout>;
22-
23-
if (copied) {
24-
timer = setTimeout(() => setCopied(false), 3000);
25-
}
26-
27-
return () => {
28-
if (timer) {
29-
clearTimeout(timer);
30-
}
31-
};
32-
}, [copied]);
33-
3420
return (
3521
<pre className={styles.shellBox}>
3622
<div className={styles.top}>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
3+
import { useCopyToClipboard } from '../useCopyToClipboard';
4+
5+
describe('useCopyToClipboard', () => {
6+
const HookRenderer = ({ text }: { text: string }): JSX.Element => {
7+
const [copied, copyText] = useCopyToClipboard();
8+
9+
return (
10+
<button onClick={() => copyText(text)} type="button">
11+
{copied ? 'copied' : 'copy'}
12+
</button>
13+
);
14+
};
15+
16+
it('should have `copy` text when failed', async () => {
17+
const navigatorClipboardWriteTextSpy = jest
18+
.fn()
19+
.mockImplementation(() => Promise.reject());
20+
21+
Object.defineProperty(window.navigator, 'clipboard', {
22+
writable: true,
23+
value: {
24+
writeText: navigatorClipboardWriteTextSpy,
25+
},
26+
});
27+
28+
render(<HookRenderer text="test copy" />);
29+
const button = screen.getByRole('button');
30+
fireEvent.click(button);
31+
expect(button).toHaveTextContent('copy');
32+
});
33+
34+
it('should change to `copied` when copy succeeded', async () => {
35+
jest.useFakeTimers();
36+
const navigatorClipboardWriteTextSpy = jest
37+
.fn()
38+
.mockImplementation(() => Promise.resolve());
39+
40+
Object.defineProperty(window.navigator, 'clipboard', {
41+
writable: true,
42+
value: {
43+
writeText: navigatorClipboardWriteTextSpy,
44+
},
45+
});
46+
47+
render(<HookRenderer text="test copy" />);
48+
const button = screen.getByRole('button');
49+
fireEvent.click(button);
50+
await waitFor(() => {
51+
expect(button).toHaveTextContent('copied');
52+
});
53+
jest.advanceTimersByTime(3000);
54+
await waitFor(() => {
55+
expect(button).toHaveTextContent('copy');
56+
});
57+
});
58+
59+
it('should call clipboard API with `test` once', () => {
60+
const navigatorClipboardWriteTextSpy = jest
61+
.fn()
62+
.mockImplementation(() => Promise.resolve());
63+
64+
Object.defineProperty(window.navigator, 'clipboard', {
65+
writable: true,
66+
value: {
67+
writeText: navigatorClipboardWriteTextSpy,
68+
},
69+
});
70+
71+
render(<HookRenderer text="test" />);
72+
const button = screen.getByRole('button');
73+
fireEvent.click(button);
74+
expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1);
75+
expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test');
76+
});
77+
});

src/hooks/useCopyToClipboard.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useEffect, useState } from 'react';
2+
3+
const copyToClipboard = (value: string) => {
4+
if (typeof window === 'undefined') {
5+
return Promise.resolve(false);
6+
}
7+
8+
return navigator.clipboard
9+
.writeText(value)
10+
.then(() => true)
11+
.catch(() => false);
12+
};
13+
14+
export const useCopyToClipboard = (): [boolean, (text: string) => void] => {
15+
const [copied, setCopied] = useState(false);
16+
17+
const copyText = (text: string) => copyToClipboard(text).then(setCopied);
18+
19+
useEffect(() => {
20+
if (!copied) {
21+
return undefined;
22+
}
23+
24+
const timerId = setTimeout(() => setCopied(false), 3000);
25+
26+
return () => clearTimeout(timerId);
27+
}, [copied]);
28+
29+
return [copied, copyText];
30+
};

src/util/__tests__/copyTextToClipboard.test.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/util/copyTextToClipboard.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)