chore(ui): remove react-copy-to-clipboard and add custom hook for copy text (#10534)

* chore(ui): remove react-copy-to-clipboard and add custom hook for copy text

* test: add unit test

* test: update test imports

* test: improve the copy to clipboard button component test
This commit is contained in:
Sachin Chaurasiya 2023-03-13 17:31:03 +05:30 committed by GitHub
parent d03b06daf6
commit 60d5285059
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 247 additions and 74 deletions

View File

@ -70,7 +70,6 @@
"react-awesome-query-builder": "5.1.2",
"react-codemirror2": "^7.2.1",
"react-context-mutex": "^2.0.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.2",
"react-dom": "^16.14.0",
@ -159,7 +158,6 @@
"@types/node": "^15.6.1",
"@types/pako": "^2.0.0",
"@types/react": "^17.0.8",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-lazylog": "^4.5.1",
"@types/react-router-dom": "^5.1.7",

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="448" height="512" fill="none"><path fill="#37352F" d="M336 0H144c-33.856 0-64 31.056-64 64l-18.496.432C27.664 64.432 0 95.056 0 128v320c0 32.945 30.144 64 64 64h240c33.856 0 64-31.055 64-64h16c33.856 0 64-31.055 64-63.999V128.384L336 0Zm-32 480.001H64c-16.8 0-32-15.695-32-32.001V128c0-16.304 13.68-31.472 30.48-31.472L80 96v288.001C80 416.945 110.144 448 144 448h192c0 16.305-15.2 32.001-32 32.001Zm112.001-96C416.001 400.304 400.8 416 384 416H144c-16.8 0-32-15.696-32-31.999v-320c0-16.304 15.2-32 32-32h160c-.256 36.848 0 64.4 0 64.4 0 33.248 29.92 63.6 64 63.6h48.001v224ZM368 128c-17.039 0-32-30.96-32-47.568V32.464L416.001 128H368Zm-32 96.289H192c-8.832 0-16 7.152-16 15.983 0 8.832 7.168 15.984 16 15.984h144c8.832 0 16-7.152 16-15.984 0-8.831-7.168-15.983-16-15.983Zm0 79.919H192c-8.832 0-16 7.152-16 15.983 0 8.833 7.168 15.985 16 15.985h144c8.832 0 16-7.152 16-15.985 0-8.831-7.168-15.983-16-15.983Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="none"><path fill="#37352F" d="M336 0H144c-33.856 0-64 31.056-64 64l-18.496.432C27.664 64.432 0 95.056 0 128v320c0 32.945 30.144 64 64 64h240c33.856 0 64-31.055 64-64h16c33.856 0 64-31.055 64-63.999V128.384L336 0Zm-32 480.001H64c-16.8 0-32-15.695-32-32.001V128c0-16.304 13.68-31.472 30.48-31.472L80 96v288.001C80 416.945 110.144 448 144 448h192c0 16.305-15.2 32.001-32 32.001Zm112.001-96C416.001 400.304 400.8 416 384 416H144c-16.8 0-32-15.696-32-31.999v-320c0-16.304 15.2-32 32-32h160c-.256 36.848 0 64.4 0 64.4 0 33.248 29.92 63.6 64 63.6h48.001v224ZM368 128c-17.039 0-32-30.96-32-47.568V32.464L416.001 128H368Zm-32 96.289H192c-8.832 0-16 7.152-16 15.983 0 8.832 7.168 15.984 16 15.984h144c8.832 0 16-7.152 16-15.984 0-8.831-7.168-15.983-16-15.983Zm0 79.919H192c-8.832 0-16 7.152-16 15.983 0 8.833 7.168 15.985 16 15.985h144c8.832 0 16-7.152 16-15.985 0-8.831-7.168-15.983-16-15.983Z"/></svg>

Before

Width:  |  Height:  |  Size: 964 B

After

Width:  |  Height:  |  Size: 961 B

View File

@ -11,23 +11,83 @@
* limitations under the License.
*/
import { getByTestId, render } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import CopyToClipboardButton from './CopyToClipboardButton';
const mockProps = {
copyText: 'mock-copy',
const clipboardWriteTextMock = jest.fn();
const clipboardMock = {
writeText: clipboardWriteTextMock,
};
const value = 'Test Value';
const callBack = jest.fn();
Object.defineProperty(window.navigator, 'clipboard', {
value: clipboardMock,
writable: true,
});
describe('Test CopyToClipboardButton Component', () => {
it('Should render all child elements', () => {
const { container } = render(<CopyToClipboardButton {...mockProps} />, {
wrapper: MemoryRouter,
it('Should render all child elements', async () => {
render(<CopyToClipboardButton copyText={value} />);
expect(screen.getByTestId('copy-icon')).toBeInTheDocument();
expect(screen.getByTestId('copy-secret')).toBeInTheDocument();
});
it('Should calls onCopy callback when clicked', async () => {
render(<CopyToClipboardButton copyText={value} onCopy={callBack} />);
await act(async () => {
fireEvent.click(screen.getByTestId('copy-secret'));
});
const copyIcon = getByTestId(container, 'copy-icon');
expect(callBack).toHaveBeenCalled();
});
expect(copyIcon).toBeInTheDocument();
it('Should show success message and hide after timeout', async () => {
jest.useFakeTimers();
await act(async () => {
render(<CopyToClipboardButton copyText={value} />);
});
await act(async () => {
fireEvent.click(screen.getByTestId('copy-secret'));
});
expect(screen.getByTestId('copy-success')).toBeInTheDocument();
jest.advanceTimersByTime(1500);
// success message should not be in the dom after timeout
expect(screen.queryByTestId('copy-success')).not.toBeInTheDocument();
});
it('Should have copied text in clipboard', async () => {
render(<CopyToClipboardButton copyText={value} />);
await act(async () => {
fireEvent.click(screen.getByTestId('copy-secret'));
});
// clipboard should have the copied text
expect(clipboardWriteTextMock).toHaveBeenCalledWith(value);
});
it('Should handles error when cannot access clipboard API', async () => {
Object.defineProperty(window.navigator, 'clipboard', {
value: undefined,
writable: true,
});
render(<CopyToClipboardButton copyText={value} />);
await act(async () => {
fireEvent.click(screen.getByTestId('copy-secret'));
});
// not show the success message if clipboard API has error
expect(screen.queryByTestId('copy-success')).not.toBeInTheDocument();
});
});

View File

@ -11,13 +11,11 @@
* limitations under the License.
*/
import { Popover, PopoverProps } from 'antd';
import React, { FunctionComponent, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { Button, Popover, PopoverProps } from 'antd';
import { ReactComponent as CopyIcon } from 'assets/svg/icon-copy.svg';
import { useClipboard } from 'hooks/useClipBoard';
import React, { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { Button } from '../Button/Button';
interface Props {
copyText: string;
@ -33,44 +31,33 @@ export const CopyToClipboardButton: FunctionComponent<Props> = ({
onCopy,
}: Props) => {
const { t } = useTranslation();
const [copied, setCopied] = useState<boolean>(false);
const handleCopying = () => {
setCopied(true);
onCopy?.();
setTimeout(() => {
setCopied(false);
}, copyTimer);
};
const { hasCopied, onCopyToClipBoard } = useClipboard(
copyText,
copyTimer,
onCopy
);
return (
<CopyToClipboard text={copyText} onCopy={handleCopying}>
<Popover
destroyTooltipOnHide
content={
<span
className="tw-text-grey-body tw-text-xs tw-font-medium tw-italic"
data-testid="copy-success">
{t('message.copied-to-clipboard')}
</span>
}
open={hasCopied}
placement={position}
trigger="click">
<Button
className="tw-h-8 tw-ml-4 tw-relative"
data-testid="copy-secret"
size="custom"
theme="default"
variant="text">
<Popover
content={
<span
className="tw-text-grey-body tw-text-xs tw-font-medium tw-italic"
data-testid="copy-success">
{t('message.copied-to-clipboard')}
</span>
}
open={copied}
placement={position}
trigger="click">
<SVGIcons
alt="Copy"
data-testid="copy-icon"
icon={Icons.COPY}
width="16px"
/>
</Popover>
</Button>
</CopyToClipboard>
icon={<CopyIcon data-testid="copy-icon" width="16" />}
type="text"
onClick={onCopyToClipBoard}
/>
</Popover>
);
};

View File

@ -0,0 +1,83 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useClipboard } from './useClipBoard';
const clipboardWriteTextMock = jest.fn();
const clipboardMock = {
writeText: clipboardWriteTextMock,
};
Object.defineProperty(window.navigator, 'clipboard', {
value: clipboardMock,
writable: true,
});
const value = 'Test Value';
const callBack = jest.fn();
const timeout = 1000;
describe('useClipboard hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('Should copy to clipboard', async () => {
clipboardWriteTextMock.mockResolvedValue(value);
const { result } = renderHook(() => useClipboard(value, timeout, callBack));
await act(async () => {
result.current.onCopyToClipBoard();
});
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
expect(result.current.hasCopied).toBe(true);
expect(callBack).toHaveBeenCalled();
});
it('Should handle error while copying to clipboard', async () => {
clipboardWriteTextMock.mockRejectedValue('Error');
const { result } = renderHook(() => useClipboard(value));
await act(async () => {
result.current.onCopyToClipBoard();
});
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
expect(result.current.hasCopied).toBe(false);
});
it('Should reset hasCopied after the timeout', async () => {
clipboardWriteTextMock.mockResolvedValue(value);
jest.useFakeTimers();
const { result, rerender } = renderHook(
({ value, timeout, callBack }) => useClipboard(value, timeout, callBack),
{ initialProps: { value, timeout, callBack } }
);
await act(async () => {
result.current.onCopyToClipBoard();
});
expect(result.current.hasCopied).toBe(true);
jest.advanceTimersByTime(timeout);
rerender({ value, timeout, callBack });
expect(result.current.hasCopied).toBe(false);
});
});

View File

@ -0,0 +1,67 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { toNumber } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
/**
* React hook to copy text to clipboard
* @param value the text to copy
* @param timeout delay (in ms) to switch back to initial state once copied.
* @param callBack execute when content is copied to clipboard
*/
export const useClipboard = (
value: string,
timeout = 1500,
callBack?: () => void
) => {
// local state
const [hasCopied, setHasCopied] = useState(false);
const [valueState, setValueState] = useState(value);
// handlers
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(valueState);
setHasCopied(true);
callBack && callBack();
} catch (error) {
setHasCopied(false);
}
}, [valueState]);
// side effects
useEffect(() => setValueState(value), [value]);
useEffect(() => {
let timeoutId: number | null = null;
if (hasCopied) {
timeoutId = toNumber(
setTimeout(() => {
setHasCopied(false);
}, timeout)
);
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [timeout, hasCopied]);
return {
onCopyToClipBoard: handleCopy,
hasCopied,
};
};

View File

@ -3701,13 +3701,6 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-copy-to-clipboard@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#c29690b472a54edff35916f0d1c6c797ad0fd34b"
integrity sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==
dependencies:
"@types/react" "*"
"@types/react-dom@*", "@types/react-dom@>=16.9.0":
version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
@ -5762,13 +5755,6 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-to-clipboard@^3:
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"
copy-to-clipboard@^3.2.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
@ -11399,7 +11385,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -12077,14 +12063,6 @@ react-context-mutex@^2.0.0:
resolved "https://registry.yarnpkg.com/react-context-mutex/-/react-context-mutex-2.0.0.tgz#4060bc8fbced6d2680ab093139c727201d402b88"
integrity sha512-Qss0YfJAKQ/+CYdVjHB3w7wRLdeetSrp2IMEEouXEpoTdohaCXIKZtFyNMMKDytLVWqHiiXg0RDA2kj/DQbeMQ==
react-copy-to-clipboard@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#42ec519b03eb9413b118af92d1780c403a5f19bf"
integrity sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==
dependencies:
copy-to-clipboard "^3"
prop-types "^15.5.8"
react-dnd-html5-backend@14.0.2:
version "14.0.2"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c"