Skip to content

Commit 1d16abc

Browse files
committed
chore: address first batch of code review
1 parent 99cb640 commit 1d16abc

3 files changed

Lines changed: 72 additions & 116 deletions

File tree

packages/circuit-ui/components/CopyButton/CopyButton.spec.tsx

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515

1616
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1717

18-
import { act, axe, fireEvent, render, screen } from '../../util/test-utils.js';
18+
import {
19+
act,
20+
axe,
21+
fireEvent,
22+
render,
23+
screen,
24+
userEvent,
25+
} from '../../util/test-utils.js';
1926
import { ToastProvider } from '../ToastContext/index.js';
2027

2128
import { CopyButton } from './CopyButton.js';
@@ -24,7 +31,7 @@ const defaultProps = {
2431
label: 'API token',
2532
value: 'secret-token',
2633
copyLabel: 'Copy token',
27-
onCopyLabel: 'Copied to clipboard.',
34+
successLabel: 'Copied to clipboard.',
2835
};
2936

3037
const renderWithToastProvider = (ui: React.ReactNode) =>
@@ -53,7 +60,7 @@ describe('CopyButton', () => {
5360
});
5461

5562
it('should render text instead of value in the input variant when provided', () => {
56-
render(<CopyButton {...defaultProps} text="••••••••••••token" />);
63+
render(<CopyButton {...defaultProps} visibleValue="••••••••••••token" />);
5764

5865
expect(screen.getByDisplayValue('••••••••••••token')).toBeVisible();
5966
expect(
@@ -67,7 +74,7 @@ describe('CopyButton', () => {
6774
renderWithToastProvider(
6875
<CopyButton
6976
{...defaultProps}
70-
onCopyLabel="Token copied"
77+
successLabel="Token copied"
7178
onCopy={onCopy}
7279
/>,
7380
);
@@ -90,7 +97,7 @@ describe('CopyButton', () => {
9097
copyVariant="button"
9198
value="secret-token"
9299
copyLabel="Copy token"
93-
onCopyLabel="Copied to clipboard."
100+
successLabel="Copied to clipboard."
94101
/>,
95102
);
96103

@@ -108,54 +115,16 @@ describe('CopyButton', () => {
108115
it('should render the icon variant', () => {
109116
render(
110117
<CopyButton
111-
copyVariant="icon"
118+
copyVariant="icon-button"
112119
value="secret-token"
113120
copyLabel="Copy token"
114-
onCopyLabel="Copied to clipboard."
121+
successLabel="Copied to clipboard."
115122
/>,
116123
);
117124

118125
expect(screen.getByRole('button', { name: 'Copy token' })).toBeVisible();
119126
});
120127

121-
it('should fall back to execCommand when navigator.clipboard is unavailable', async () => {
122-
const execCommand = vi.fn();
123-
const selection = {
124-
addRange: vi.fn(),
125-
removeAllRanges: vi.fn(),
126-
};
127-
128-
Object.defineProperty(navigator, 'clipboard', {
129-
configurable: true,
130-
value: undefined,
131-
});
132-
Object.defineProperty(document, 'execCommand', {
133-
configurable: true,
134-
value: execCommand,
135-
});
136-
Object.defineProperty(window, 'getSelection', {
137-
configurable: true,
138-
value: vi.fn(() => selection),
139-
});
140-
141-
renderWithToastProvider(
142-
<CopyButton {...defaultProps} onCopyLabel="Token copied" />,
143-
);
144-
145-
fireEvent.click(
146-
screen.getByRole('button', { name: 'Copy token: API token' }),
147-
);
148-
149-
await act(async () => {
150-
await Promise.resolve();
151-
});
152-
153-
expect(execCommand).toHaveBeenCalledWith('copy');
154-
expect(selection.removeAllRanges).toHaveBeenCalledTimes(2);
155-
expect(selection.addRange).toHaveBeenCalledTimes(1);
156-
expect(await screen.findByText('Token copied')).toBeInTheDocument();
157-
});
158-
159128
it('should not announce success when clipboard write fails', async () => {
160129
const onCopy = vi.fn();
161130

@@ -181,20 +150,46 @@ describe('CopyButton', () => {
181150
});
182151

183152
it('should disable copying when the value is empty', () => {
184-
render(<CopyButton {...defaultProps} value="" text="N/A" />);
153+
render(<CopyButton {...defaultProps} value="" visibleValue="N/A" />);
185154

186155
expect(
187156
screen.getByRole('button', { name: 'Copy token: API token' }),
188157
).toHaveAttribute('aria-disabled', 'true');
189158
});
190159

191-
it('should have no accessibility violations', async () => {
192-
const { container } = renderWithToastProvider(
193-
<CopyButton {...defaultProps} />,
194-
);
160+
describe('as input', async () => {
161+
it('should have no accessibility violations', async () => {
162+
const { container } = renderWithToastProvider(
163+
<CopyButton {...defaultProps} />,
164+
);
165+
166+
const actual = await axe(container);
195167

196-
const actual = await axe(container);
168+
expect(actual).toHaveNoViolations();
169+
});
170+
});
197171

198-
expect(actual).toHaveNoViolations();
172+
describe('as button', async () => {
173+
it('should have no accessibility violations', async () => {
174+
const { container } = renderWithToastProvider(
175+
<CopyButton {...defaultProps} copyVariant="button" />,
176+
);
177+
178+
const actual = await axe(container);
179+
180+
expect(actual).toHaveNoViolations();
181+
});
182+
});
183+
184+
describe('as icon button', async () => {
185+
it('should have no accessibility violations', async () => {
186+
const { container } = renderWithToastProvider(
187+
<CopyButton {...defaultProps} copyVariant="icon-button" />,
188+
);
189+
190+
const actual = await axe(container);
191+
192+
expect(actual).toHaveNoViolations();
193+
});
199194
});
200195
});

packages/circuit-ui/components/CopyButton/CopyButton.stories.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ export default {
3030
),
3131
],
3232
argTypes: {
33-
onCopyLabel: { control: 'text' },
33+
successLabel: { control: 'text' },
3434
copyLabel: { control: 'text' },
3535
copyVariant: {
36-
options: ['input', 'button', 'icon'],
36+
options: ['input', 'button', 'icon-button'],
3737
control: { type: 'select' },
3838
},
3939
disabled: { control: 'boolean' },
@@ -54,7 +54,7 @@ const inputArgs = {
5454
label: 'API token',
5555
value: 'sk_live_1234567890',
5656
copyLabel: 'Copy token',
57-
onCopyLabel: 'Token copied',
57+
successLabel: 'Token copied',
5858
} satisfies CopyButtonProps;
5959

6060
export const Input = (args: CopyButtonProps) => <CopyButton {...args} />;
@@ -74,7 +74,7 @@ LongValue.args = {
7474
label: 'Webhook secret',
7575
value: 'whsec_4VbX8i2LwY7nQp3Rk5Tm9Uc1Fd6Hs0Za',
7676
copyLabel: 'Copy secret',
77-
onCopyLabel: 'Secret copied',
77+
successLabel: 'Secret copied',
7878
} satisfies CopyButtonProps;
7979

8080
export const FullButton = (args: CopyButtonProps) => <CopyButton {...args} />;
@@ -83,16 +83,16 @@ FullButton.args = {
8383
copyVariant: 'button',
8484
value: 'ref_1234567890',
8585
copyLabel: 'Copy reference',
86-
onCopyLabel: 'Reference copied',
86+
successLabel: 'Reference copied',
8787
} satisfies CopyButtonProps;
8888

8989
export const IconOnly = (args: CopyButtonProps) => <CopyButton {...args} />;
9090

9191
IconOnly.args = {
92-
copyVariant: 'icon',
92+
copyVariant: 'icon-button',
9393
value: 'ref_1234567890',
9494
copyLabel: 'Copy reference',
95-
onCopyLabel: 'Reference copied',
95+
successLabel: 'Reference copied',
9696
} satisfies CopyButtonProps;
9797

9898
export const Disabled = (args: CopyButtonProps) => <CopyButton {...args} />;
@@ -128,13 +128,13 @@ export const AllVariants = () => (
128128
copyVariant="button"
129129
value="ref_1234567890"
130130
copyLabel="Copy reference"
131-
onCopyLabel="Reference copied"
131+
successLabel="Reference copied"
132132
/>
133133
<CopyButton
134-
copyVariant="icon"
134+
copyVariant="icon-button"
135135
value="ref_1234567890"
136136
copyLabel="Copy reference"
137-
onCopyLabel="Reference copied"
137+
successLabel="Reference copied"
138138
/>
139139
</Stack>
140140
);

packages/circuit-ui/components/CopyButton/CopyButton.tsx

Lines changed: 15 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
'use client';
1717

1818
import { CopyPaste } from '@sumup-oss/icons';
19-
import { useId } from 'react';
2019

2120
import { Button, IconButton } from '../Button/index.js';
2221
import { Input, type InputProps } from '../Input/index.js';
@@ -40,7 +39,7 @@ type CommonCopyButtonProps = {
4039
/**
4140
* Test copy shown in as a notification after a successful copy action.
4241
*/
43-
onCopyLabel: string;
42+
successLabel: string;
4443
};
4544

4645
type InputCopyButtonProps = CommonCopyButtonProps &
@@ -57,7 +56,7 @@ type InputCopyButtonProps = CommonCopyButtonProps &
5756
/**
5857
* Optional text rendered inside the field instead of the copied value.
5958
*/
60-
text?: string;
59+
visibleValue?: string;
6160
};
6261

6362
type ButtonCopyButtonProps = CommonCopyButtonProps &
@@ -73,62 +72,29 @@ type IconCopyButtonProps = CommonCopyButtonProps &
7372
/**
7473
* The CopyButton variant.
7574
*/
76-
copyVariant: 'icon';
75+
copyVariant: 'icon-button';
7776
};
7877

7978
export type CopyButtonProps =
8079
| InputCopyButtonProps
8180
| ButtonCopyButtonProps
8281
| IconCopyButtonProps;
8382

84-
async function copyToClipboard(text: string): Promise<void> {
85-
if (navigator.clipboard?.writeText) {
86-
await navigator.clipboard.writeText(text);
87-
return;
88-
}
89-
90-
// Fallback behaviour for older browsers,
91-
// see: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#browser_compatibility
92-
if (typeof document === 'undefined') {
93-
return;
94-
}
95-
96-
const selection = window.getSelection();
97-
const range = document.createRange();
98-
const tempNode = document.createElement('span');
99-
tempNode.textContent = text;
100-
tempNode.style.position = 'fixed';
101-
tempNode.style.opacity = '0';
102-
document.body.append(tempNode);
103-
104-
range.selectNodeContents(tempNode);
105-
selection?.removeAllRanges();
106-
selection?.addRange(range);
107-
document.execCommand?.('copy');
108-
selection?.removeAllRanges();
109-
tempNode.remove();
110-
}
111-
11283
/**
11384
* The CopyButton component copies a provided value to the clipboard and
11485
* can render as a read-only input, a button, or an icon button.
11586
*/
11687
export function CopyButton(props: CopyButtonProps) {
117-
const generatedId = useId();
11888
const { setToast } = useNotificationToast();
119-
const { onCopyLabel, copyLabel, onCopy, value } = props;
89+
const { successLabel, copyLabel, onCopy, value } = props;
12090
const isCopyDisabled = Boolean(props.disabled || value.length === 0);
12191

12292
const handleCopy = async (event: ClickEvent) => {
123-
if (isCopyDisabled) {
124-
return;
125-
}
126-
12793
try {
128-
await copyToClipboard(value);
94+
// eslint-disable-next-line compat/compat
95+
await navigator.clipboard.writeText(value);
12996
setToast({
130-
body: onCopyLabel,
131-
iconLabel: '',
97+
body: successLabel,
13298
});
13399
onCopy?.(event);
134100
} catch {
@@ -139,7 +105,7 @@ export function CopyButton(props: CopyButtonProps) {
139105
if (props.copyVariant === 'button') {
140106
const {
141107
copyVariant,
142-
onCopyLabel: _onCopyLabel,
108+
successLabel: _successLabel,
143109
copyLabel: _copyLabel,
144110
onCopy: _onCopy,
145111
value: _value,
@@ -159,10 +125,10 @@ export function CopyButton(props: CopyButtonProps) {
159125
);
160126
}
161127

162-
if (props.copyVariant === 'icon') {
128+
if (props.copyVariant === 'icon-button') {
163129
const {
164130
copyVariant,
165-
onCopyLabel: _onCopyLabel,
131+
successLabel: _successLabel,
166132
copyLabel: _copyLabel,
167133
onCopy: _onCopy,
168134
value: _value,
@@ -184,38 +150,33 @@ export function CopyButton(props: CopyButtonProps) {
184150

185151
const {
186152
copyVariant,
187-
onCopyLabel: _onCopyLabel,
153+
successLabel: _successLabel,
188154
copyLabel: _copyLabel,
189-
id: customId,
190155
inputClassName,
191156
onCopy: _onCopy,
192-
text,
157+
visibleValue,
193158
value: _value,
194159
...inputProps
195160
} = props;
196-
const fieldId = customId || generatedId;
197-
const displayText = text ?? value;
161+
const displayText = visibleValue ?? value;
198162
const buttonLabel = `${copyLabel}: ${props.label}`;
199163

200164
return (
201165
<Input
202166
{...inputProps}
203-
id={fieldId}
204167
value={displayText}
205168
readOnly
206169
renderSuffix={(renderProps) => (
207170
<IconButton
208171
className={renderProps.className}
209172
type="button"
210173
size="s"
211-
variant="tertiary"
174+
variant="secondary"
212175
disabled={isCopyDisabled}
213176
onClick={handleCopy}
214-
aria-label={buttonLabel}
215-
aria-controls={fieldId}
216177
icon={CopyPaste}
217178
>
218-
{copyLabel}
179+
{buttonLabel}
219180
</IconButton>
220181
)}
221182
inputClassName={inputClassName}

0 commit comments

Comments
 (0)