Skip to content

Commit dd628ff

Browse files
authored
Merge pull request #258 from acelaya-forks/feature/copy-to-clipboard
Replace dependency on react-copy-to-clipboard with custom logic
2 parents 88dfcb3 + ec34b6d commit dd628ff

14 files changed

+101
-87
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1010

1111
### Changed
1212
* [#249](https://github.com/shlinkio/shlink-web-component/issues/249) Replace `react-datepicker` with native `input[type="date"]` and `input[type="datetime-local"]` elements.
13+
* [#257](https://github.com/shlinkio/shlink-web-component/issues/257) Remove dependency on `react-copy-to-clipboard`.
1314

1415
### Deprecated
1516
* *Nothing*

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,6 @@ export function App() {
172172

173173
Since this is a complex component, this project provides a convenient way to do some manual tests.
174174

175-
Simply run `npm run dev` and a local vite server will be started on port `3002`.
175+
Simply run `npm run dev` or `docker compose up`, and a local vite server will be started on port `3002`.
176176

177-
Just visit http://localhost:3002 to see your changes in real time.
177+
Then visit http://localhost:3002 to see your changes in real time.

docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ services:
44
shlink_web_component:
55
container_name: shlink_web_component
66
image: node:20.5-alpine
7+
command: /bin/sh -c "cd /shlink-web-component && npm run dev"
78
volumes:
89
- ./:/shlink-web-component
910
ports:

package-lock.json

-36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
"lint:css:fix": "npm run lint:css -- --fix",
3636
"lint:js:fix": "npm run lint:js -- --fix",
3737
"types": "tsc",
38-
"dev": "vite serve --host=0.0.0.0 --port 3002",
39-
"dev:sub-route": "vite serve --host=0.0.0.0 --port 3003 --base=\"/sub/route\""
38+
"dev": "vite serve --host 0.0.0.0 --port 3002",
39+
"dev:sub-route": "vite serve --host 0.0.0.0 --port 3003 --base=\"/sub/route\""
4040
},
4141
"peerDependencies": {
4242
"@fortawesome/fontawesome-svg-core": "^6.4.2",
@@ -68,7 +68,6 @@
6868
"date-fns": "^3.3.1",
6969
"event-source-polyfill": "^1.0.31",
7070
"leaflet": "^1.9.4",
71-
"react-copy-to-clipboard": "^5.1.0",
7271
"react-external-link": "^2.2.0",
7372
"react-leaflet": "^4.2.1",
7473
"react-swipeable": "^7.0.1",
@@ -85,7 +84,6 @@
8584
"@total-typescript/ts-reset": "^0.5.1",
8685
"@types/leaflet": "^1.9.8",
8786
"@types/react": "^18.2.55",
88-
"@types/react-copy-to-clipboard": "^5.0.7",
8987
"@types/react-dom": "^18.2.19",
9088
"@vitejs/plugin-react": "^4.2.1",
9189
"@vitest/coverage-v8": "^1.2.2",

src/short-urls/helpers/CreateShortUrlResult.tsx

+9-11
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
44
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
55
import { Result } from '@shlinkio/shlink-frontend-kit';
66
import { useEffect } from 'react';
7-
import CopyToClipboard from 'react-copy-to-clipboard';
87
import { Tooltip } from 'reactstrap';
98
import { ShlinkApiError } from '../../common/ShlinkApiError';
109
import type { FCWithDeps } from '../../container/utils';
1110
import { componentFactory, useDependencies } from '../../container/utils';
11+
import { copyToClipboard } from '../../utils/helpers/clipboard';
1212
import type { ShortUrlCreation } from '../reducers/shortUrlCreation';
1313
import './CreateShortUrlResult.scss';
1414

@@ -67,16 +67,14 @@ const CreateShortUrlResult: FCWithDeps<CreateShortUrlResultProps, CreateShortUrl
6767
)}
6868
<span><b>Great!</b> The short URL is <b>{shortUrl}</b></span>
6969

70-
<CopyToClipboard text={shortUrl} onCopy={toggleShowCopyTooltip}>
71-
<button
72-
className="btn btn-light btn-sm create-short-url-result__copy-btn"
73-
id="copyBtn"
74-
type="button"
75-
>
76-
<FontAwesomeIcon icon={copyIcon} /> Copy
77-
</button>
78-
</CopyToClipboard>
79-
70+
<button
71+
className="btn btn-light btn-sm create-short-url-result__copy-btn"
72+
id="copyBtn"
73+
type="button"
74+
onClick={() => copyToClipboard({ text: shortUrl, onCopy: toggleShowCopyTooltip })}
75+
>
76+
<FontAwesomeIcon icon={copyIcon} /> Copy <span className="sr-only">{shortUrl} to clipboard</span>
77+
</button>
8078
<Tooltip placement="left" isOpen={showCopyTooltip} target="copyBtn">
8179
Copied!
8280
</Tooltip>

src/short-urls/helpers/ShortUrlsRow.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ const ShortUrlsRow: FCWithDeps<ShortUrlsRowProps, ShortUrlsRowDeps> = ({ shortUr
5353
<ExternalLink href={shortUrl.shortUrl} />
5454
</span>
5555
<CopyToClipboardIcon text={shortUrl.shortUrl} onCopy={setCopiedToClipboard} />
56-
<span className="badge bg-warning text-black short-urls-row__copy-hint" hidden={!copiedToClipboard}>
56+
<span
57+
role="status"
58+
className="badge bg-warning text-black short-urls-row__copy-hint"
59+
hidden={!copiedToClipboard}
60+
>
5761
Copied short URL!
5862
</span>
5963
</span>

src/utils/components/CopyToClipboardIcon.scss

-4
This file was deleted.
+16-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import { faClone as copyIcon } from '@fortawesome/free-regular-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import type { FC } from 'react';
4-
import CopyToClipboard from 'react-copy-to-clipboard';
5-
import './CopyToClipboardIcon.scss';
4+
import type { CopyToClipboardOptions } from '../helpers/clipboard';
5+
import { copyToClipboard as defaultCopyToClipboard } from '../helpers/clipboard';
6+
import { UnstyledButton } from './UnstyledButton';
67

7-
interface CopyToClipboardIconProps {
8-
text: string;
9-
onCopy?: (text: string, result: boolean) => void;
10-
}
8+
type CopyToClipboardIconProps = CopyToClipboardOptions & {
9+
copyToClipboard?: typeof defaultCopyToClipboard;
10+
};
1111

12-
export const CopyToClipboardIcon: FC<CopyToClipboardIconProps> = ({ text, onCopy }) => (
13-
<CopyToClipboard text={text} onCopy={onCopy}>
14-
<FontAwesomeIcon icon={copyIcon} className="ms-2 copy-to-clipboard-icon" />
15-
</CopyToClipboard>
12+
export const CopyToClipboardIcon: FC<CopyToClipboardIconProps> = (
13+
{ text, onCopy, copyToClipboard = defaultCopyToClipboard },
14+
) => (
15+
<UnstyledButton
16+
className="ms-2 p-0"
17+
aria-label={`Copy ${text} to clipboard`}
18+
onClick={() => copyToClipboard({ text, onCopy })}
19+
>
20+
<FontAwesomeIcon icon={copyIcon} className="fs-5" />
21+
</UnstyledButton>
1622
);

src/utils/helpers/clipboard.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type CopyToClipboardOptions = {
2+
text: string;
3+
onCopy?: (text: string, copied: boolean) => void;
4+
};
5+
6+
export const copyToClipboard = ({ text, onCopy }: CopyToClipboardOptions, navigator_: Navigator = navigator) =>
7+
navigator_.clipboard?.writeText(text)
8+
.then(() => onCopy?.(text, true))
9+
.catch(() => onCopy?.(text, false));

test/short-urls/helpers/QrCodeModal.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ describe('<QrCodeModal />', () => {
6868

6969
it('shows expected components based on server version', () => {
7070
setUp();
71-
const dropdowns = screen.getAllByRole('button');
7271

73-
expect(dropdowns).toHaveLength(2 + 2); // Add two because of the close and download buttons
72+
// Add three because of the close, download and copy-to-clipboard buttons
73+
expect(screen.getAllByRole('button')).toHaveLength(2 + 3);
7474
});
7575

7676
it('saves the QR code image when clicking the Download button', async () => {

test/utils/components/CopyToClipboardIcon.test.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { checkAccessibility } from '../../__helpers__/accessibility';
33
import { renderWithEvents } from '../../__helpers__/setUpTest';
44

55
describe('<CopyToClipboardIcon />', () => {
6+
const copyToClipboard = vi.fn();
67
const onCopy = vi.fn();
7-
const setUp = (text = 'foo') => renderWithEvents(<CopyToClipboardIcon text={text} onCopy={onCopy} />);
8+
const setUp = (text = 'foo') => renderWithEvents(
9+
<CopyToClipboardIcon text={text} onCopy={onCopy} copyToClipboard={copyToClipboard} />,
10+
);
811

912
it('passes a11y checks', () => checkAccessibility(setUp()));
1013

@@ -20,8 +23,8 @@ describe('<CopyToClipboardIcon />', () => {
2023
])('copies content to clipboard when clicked', async (text) => {
2124
const { user, container } = setUp(text);
2225

23-
expect(onCopy).not.toHaveBeenCalled();
26+
expect(copyToClipboard).not.toHaveBeenCalled();
2427
container.firstElementChild && await user.click(container.firstElementChild);
25-
expect(onCopy).toHaveBeenCalledWith(text, false);
28+
expect(copyToClipboard).toHaveBeenCalledWith({ text, onCopy });
2629
});
2730
});

test/utils/components/__snapshots__/CopyToClipboardIcon.test.tsx.snap

+21-14
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,27 @@
22

33
exports[`<CopyToClipboardIcon /> > wraps expected components 1`] = `
44
<div>
5-
<svg
6-
aria-hidden="true"
7-
class="svg-inline--fa fa-clone ms-2 copy-to-clipboard-icon"
8-
data-icon="clone"
9-
data-prefix="far"
10-
focusable="false"
11-
role="img"
12-
viewBox="0 0 512 512"
13-
xmlns="http://www.w3.org/2000/svg"
5+
<button
6+
aria-label="Copy foo to clipboard"
7+
class="border-0 ms-2 p-0"
8+
style="background-color: inherit; font-weight: inherit;"
9+
type="button"
1410
>
15-
<path
16-
d="M64 464H288c8.8 0 16-7.2 16-16V384h48v64c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h64v48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16zM224 304H448c8.8 0 16-7.2 16-16V64c0-8.8-7.2-16-16-16H224c-8.8 0-16 7.2-16 16V288c0 8.8 7.2 16 16 16zm-64-16V64c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V288c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64z"
17-
fill="currentColor"
18-
/>
19-
</svg>
11+
<svg
12+
aria-hidden="true"
13+
class="svg-inline--fa fa-clone fs-5"
14+
data-icon="clone"
15+
data-prefix="far"
16+
focusable="false"
17+
role="img"
18+
viewBox="0 0 512 512"
19+
xmlns="http://www.w3.org/2000/svg"
20+
>
21+
<path
22+
d="M64 464H288c8.8 0 16-7.2 16-16V384h48v64c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h64v48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16zM224 304H448c8.8 0 16-7.2 16-16V64c0-8.8-7.2-16-16-16H224c-8.8 0-16 7.2-16 16V288c0 8.8 7.2 16 16 16zm-64-16V64c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V288c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64z"
23+
fill="currentColor"
24+
/>
25+
</svg>
26+
</button>
2027
</div>
2128
`;

test/utils/helpers/clipboard.test.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { fromPartial } from '@total-typescript/shoehorn';
2+
import { copyToClipboard } from '../../../src/utils/helpers/clipboard';
3+
4+
describe('clipboard', () => {
5+
const writeText = vi.fn().mockResolvedValue(undefined);
6+
const navigator = fromPartial<Navigator>({
7+
clipboard: { writeText },
8+
});
9+
const text = 'foo';
10+
const onCopy = vi.fn();
11+
12+
it('does nothing when clipboard is not defined', async () => {
13+
await copyToClipboard({ text, onCopy }, fromPartial({}));
14+
expect(onCopy).not.toHaveBeenCalled();
15+
});
16+
17+
it('invokes callback with true when copying succeeds', async () => {
18+
await copyToClipboard({ text, onCopy }, navigator);
19+
expect(onCopy).toHaveBeenCalledWith(text, true);
20+
});
21+
22+
it('invokes callback with false when copying fails', async () => {
23+
writeText.mockRejectedValue(undefined);
24+
await copyToClipboard({ text, onCopy }, navigator);
25+
expect(onCopy).toHaveBeenCalledWith(text, false);
26+
});
27+
});

0 commit comments

Comments
 (0)