Skip to content

Commit 1ad9423

Browse files
fix: Make CopyToClipboard popup optimistic (#3855)
Co-authored-by: Amr Ahmed Taher Mohamed <[email protected]>
1 parent e25122e commit 1ad9423

File tree

2 files changed

+83
-6
lines changed

2 files changed

+83
-6
lines changed

src/copy-to-clipboard/__tests__/copy-to-clipboard.test.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@ const defaultProps = {
1616

1717
describe('CopyToClipboard', () => {
1818
const originalNavigatorClipboard = global.navigator.clipboard;
19+
const originalNavigatorPermissions = global.navigator.permissions;
1920

2021
beforeEach(() => {
2122
Object.assign(global.navigator, {
2223
clipboard: {
2324
writeText: (text: string) =>
2425
new Promise<void>((resolve, reject) => (text.includes('error') ? reject() : resolve())),
2526
},
27+
permissions: {
28+
query: jest.fn().mockResolvedValue({ state: 'granted' }),
29+
},
2630
});
2731
});
2832

2933
afterEach(() => {
30-
Object.assign(global.navigator, { clipboard: originalNavigatorClipboard });
34+
Object.assign(global.navigator, {
35+
clipboard: originalNavigatorClipboard,
36+
permissions: originalNavigatorPermissions,
37+
});
3138
});
3239

3340
test('renders a normal button with button text and aria-label and no text to copy', () => {
@@ -220,4 +227,60 @@ describe('CopyToClipboard', () => {
220227
});
221228
});
222229
});
230+
231+
describe('permissions API behavior', () => {
232+
test('shows error state when clipboard-write permission is denied', async () => {
233+
Object.assign(global.navigator, {
234+
permissions: {
235+
query: jest.fn().mockResolvedValue({ state: 'denied' }),
236+
},
237+
});
238+
239+
const { container } = render(<CopyToClipboard {...defaultProps} />);
240+
const wrapper = createWrapper(container).findCopyToClipboard()!;
241+
242+
wrapper.findCopyButton().click();
243+
await waitFor(() =>
244+
expect(wrapper.findStatusText()!.getElement().textContent).toBe('Failed to copy to clipboard')
245+
);
246+
});
247+
248+
test('shows success state when clipboard-write permission is granted', async () => {
249+
Object.assign(global.navigator, {
250+
permissions: {
251+
query: jest.fn().mockResolvedValue({ state: 'granted' }),
252+
},
253+
});
254+
255+
const { container } = render(<CopyToClipboard {...defaultProps} />);
256+
const wrapper = createWrapper(container).findCopyToClipboard()!;
257+
258+
wrapper.findCopyButton().click();
259+
await waitFor(() => expect(wrapper.findStatusText()!.getElement().textContent).toBe('Copied to clipboard'));
260+
});
261+
262+
test('defaults to success state when permissions API is not available', async () => {
263+
Object.assign(global.navigator, { permissions: undefined });
264+
265+
const { container } = render(<CopyToClipboard {...defaultProps} />);
266+
const wrapper = createWrapper(container).findCopyToClipboard()!;
267+
268+
wrapper.findCopyButton().click();
269+
await waitFor(() => expect(wrapper.findStatusText()!.getElement().textContent).toBe('Copied to clipboard'));
270+
});
271+
272+
test('defaults to success state when permissions query fails', async () => {
273+
Object.assign(global.navigator, {
274+
permissions: {
275+
query: jest.fn().mockRejectedValue(new Error('Permission query failed')),
276+
},
277+
});
278+
279+
const { container } = render(<CopyToClipboard {...defaultProps} />);
280+
const wrapper = createWrapper(container).findCopyToClipboard()!;
281+
282+
wrapper.findCopyButton().click();
283+
await waitFor(() => expect(wrapper.findStatusText()!.getElement().textContent).toBe('Copied to clipboard'));
284+
});
285+
});
223286
});

src/copy-to-clipboard/internal.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import React, { useState } from 'react';
3+
import React, { useEffect, useState } from 'react';
44
import clsx from 'clsx';
55

66
import InternalButton from '../button/internal';
@@ -29,8 +29,24 @@ export default function InternalCopyToClipboard({
2929
__internalRootRef,
3030
...restProps
3131
}: InternalCopyToClipboardProps) {
32-
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
33-
const [statusText, setStatusText] = useState('');
32+
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('success');
33+
const [statusText, setStatusText] = useState(copySuccessText);
34+
35+
useEffect(() => {
36+
if (navigator.permissions) {
37+
navigator.permissions
38+
.query({ name: 'clipboard-write' as PermissionName })
39+
.then(result => {
40+
if (result.state === 'denied') {
41+
setStatus('error');
42+
setStatusText(copyErrorText);
43+
}
44+
})
45+
.catch(() => {
46+
// Permissions API not supported or failed.
47+
});
48+
}
49+
}, [copyErrorText]);
3450

3551
const baseProps = getBaseProps(restProps);
3652
const onClick = () => {
@@ -41,8 +57,6 @@ export default function InternalCopyToClipboard({
4157
return;
4258
}
4359

44-
setStatus('pending');
45-
setStatusText('');
4660
navigator.clipboard
4761
.writeText(textToCopy)
4862
.then(() => {

0 commit comments

Comments
 (0)