Skip to content

Commit 6064be0

Browse files
maximebonhommeMaxime Bonhomme
andauthored
feat: address can be copied to clipboard (#105)
* 86-address-component-copiable: add chakra-ui packages as we will need to use a couple of icons * 86-address-component-copiable: Update Address component to accept copiable boolean prop - make sure we copy input value to clipboard when clicking on input - display icons at far right of the input when copiable is set to true as well as a feedback check mark when copied * 86-address-component-copiable: Add new story for the Address component with copiable set to true - add tests to make sure input is copiable * create changeset - minor bump * merge main into feat/address-component-copiable * add comment as to why we are assigning navigator to global object * we only show the copy-to-clipbaord success state for 2 seconds before going back to default state * address input should be readOnly for now as we are not using onChange handler * we should copy the props.value instead of the input.value as it could be shortened and it makes more sense to copy the FULL address * Address input should display pointer cursor when copiable Co-authored-by: Maxime Bonhomme <[email protected]>
1 parent f5924ad commit 6064be0

File tree

6 files changed

+124
-7
lines changed

6 files changed

+124
-7
lines changed

.changeset/chilly-jokes-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web3-ui/components': minor
3+
---
4+
5+
Address component now accept copiable prop to allow users to copy the address value into their clipboard

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"types": "dist/web3-ui-components.cjs.d.ts",
3030
"dependencies": {
3131
"@babel/runtime": "^7.16.3",
32+
"@chakra-ui/icons": "^1.1.1",
3233
"@chakra-ui/react": "^1.7.2",
3334
"@emotion/core": "^11.0.0",
3435
"@emotion/react": "^11",

packages/components/src/components/Address/Address.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const AddressUsingProvider = (props: AddressProps) => {
2727
return (
2828
<>
2929
<Address
30+
copiable
3031
value={connected ? connection.ens || connection.userAddress || '' : 'Not connected'}
3132
shortened={props.shortened}
3233
/>
@@ -46,3 +47,5 @@ export const WithWalletShortened = () => (
4647
<AddressUsingProvider shortened />
4748
</Provider>
4849
);
50+
51+
export const CanBeCopied = () => <Address value='0x00000000000000' copiable />;

packages/components/src/components/Address/Address.test.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
import React from 'react';
2-
import { render } from '@testing-library/react';
2+
import { jest } from '@jest/globals';
3+
import { render, fireEvent, waitFor } from '@testing-library/react';
34

45
import { Address } from './Address';
56

7+
/**
8+
* We need to mock the Clipboard API by creating a global navigator object.
9+
*
10+
* We're assigning an empty function to `writeText`
11+
* as we're only testing if it has been called with specific argument.
12+
*/
13+
Object.assign(navigator, {
14+
clipboard: {
15+
writeText: () => {},
16+
},
17+
});
18+
619
describe('Address', () => {
720
it('renders without throwing', () => {
8-
const { container } = render(<Address value='taylorswift.eth' shortened={false} />);
21+
const { container } = render(<Address value='taylorswift.eth' />);
22+
expect(container).toBeInTheDocument();
23+
});
24+
});
25+
26+
describe('Address copiable prop true', () => {
27+
it('renders without throwing', () => {
28+
const { container } = render(<Address copiable value='taylorswift.eth' />);
929
expect(container).toBeInTheDocument();
1030
});
31+
32+
it('renders with icon', () => {
33+
const { container } = render(<Address copiable value='taylorswift.eth' />);
34+
const svg = container.querySelector('svg') as SVGElement;
35+
36+
expect(svg).toBeInTheDocument();
37+
});
38+
39+
it('uses writeText from Clipboard API', async () => {
40+
const { container } = render(<Address copiable value='taylorswift.eth' />);
41+
const input = container.querySelector('input') as HTMLElement;
42+
43+
jest.spyOn(navigator.clipboard, 'writeText');
44+
45+
fireEvent.click(input);
46+
47+
await waitFor(() => {
48+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('taylorswift.eth');
49+
});
50+
});
1151

1252
it('checks the length of the address when shortened', () => {
1353
const { container } = render(<Address value='0x00000000000000' shortened />);

packages/components/src/components/Address/Address.tsx

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
1-
import { Input } from '@chakra-ui/react';
2-
import React, { FC } from 'react';
1+
import {
2+
FormControl,
3+
FormErrorMessage,
4+
Input,
5+
InputGroup,
6+
InputRightElement,
7+
} from '@chakra-ui/react';
8+
import { CopyIcon, CheckIcon } from '@chakra-ui/icons';
9+
import React, { useState, useEffect } from 'react';
310

411
export interface AddressProps {
512
/**
613
* The address to display
714
*/
815
value: string;
16+
/**
17+
* Whether the address can be copied or not
18+
*/
19+
copiable?: boolean;
920
/**
1021
* Shorten the address if it does not resolve to an ENS name
1122
*/
1223
shortened?: boolean;
1324
}
1425

26+
interface EventTarget {
27+
value: string;
28+
}
29+
30+
interface SyntheticEvent {
31+
currentTarget: EventTarget;
32+
}
33+
1534
/**
1635
* A component to display an address
1736
*/
18-
export const Address: FC<AddressProps> = ({ value, shortened = false }) => {
37+
export const Address: React.FC<AddressProps> = ({ value, copiable = false, shortened = false }) => {
38+
const [error, setError] = useState<null | string>(null);
39+
const [copied, setCopied] = useState<boolean>(false);
40+
let feedbackTimeOut: ReturnType<typeof setTimeout>;
1941
let displayAddress: string;
2042

2143
if (shortened) {
@@ -32,5 +54,43 @@ export const Address: FC<AddressProps> = ({ value, shortened = false }) => {
3254
displayAddress = value;
3355
}
3456

35-
return <Input value={displayAddress} />;
57+
const handleClick = async (event: SyntheticEvent): Promise<void> => {
58+
if (copiable && value) {
59+
try {
60+
await navigator.clipboard.writeText(value);
61+
setError(null);
62+
setCopied(true);
63+
64+
feedbackTimeOut = setTimeout(() => {
65+
setCopied(false);
66+
}, 2000);
67+
} catch (error) {
68+
setError(error as string);
69+
}
70+
}
71+
};
72+
73+
useEffect(() => {
74+
return () => clearTimeout(feedbackTimeOut);
75+
}, []);
76+
77+
return (
78+
<FormControl isInvalid={!!error}>
79+
<InputGroup>
80+
{copiable && (
81+
<InputRightElement
82+
pointerEvents='none'
83+
children={copied ? <CheckIcon color='green.500' /> : <CopyIcon color='gray.300' />}
84+
/>
85+
)}
86+
<Input
87+
onClick={handleClick}
88+
value={displayAddress}
89+
cursor={copiable ? 'pointer' : 'initial'}
90+
readOnly
91+
/>
92+
</InputGroup>
93+
<FormErrorMessage>{error}</FormErrorMessage>
94+
</FormControl>
95+
);
3696
};

yarn.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,14 @@
13331333
dependencies:
13341334
"@chakra-ui/utils" "1.9.1"
13351335

1336+
"@chakra-ui/icons@^1.1.1":
1337+
version "1.1.1"
1338+
resolved "https://registry.yarnpkg.com/@chakra-ui/icons/-/icons-1.1.1.tgz#e4b191fd38be999c4434ff2b1fb69a5eaf3cf226"
1339+
integrity sha512-/+u6euCOFw6J1DZW7NcVFtib4mygJBoqRdsKiU1Z3uiTC+zQTBzcCt54NQ+kK8rhuNzJ+odahnt/AbjBJgZ+5A==
1340+
dependencies:
1341+
"@chakra-ui/icon" "1.2.1"
1342+
"@types/react" "^17.0.15"
1343+
13361344
"@chakra-ui/[email protected]":
13371345
version "1.1.1"
13381346
resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-1.1.1.tgz#39ecb77155e9e1fbbc68e825eb46405808805a8c"
@@ -4369,7 +4377,7 @@
43694377
dependencies:
43704378
"@types/react" "*"
43714379

4372-
"@types/react@*", "@types/react@^17.0.36":
4380+
"@types/react@*", "@types/react@^17.0.15", "@types/react@^17.0.36":
43734381
version "17.0.37"
43744382
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959"
43754383
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==

0 commit comments

Comments
 (0)