forked from Creditra/Creditra-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCopyToClipboard.tsx
More file actions
136 lines (124 loc) · 4.39 KB
/
Copy pathCopyToClipboard.tsx
File metadata and controls
136 lines (124 loc) · 4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { useEffect, useRef, useState } from 'react';
import type { MouseEvent, ReactNode } from 'react';
import { Check, Copy } from 'lucide-react';
import { copyTextToClipboard } from '../utils/clipboard';
import './CopyToClipboard.css';
export const COPY_FEEDBACK_DURATION_MS = 2000;
interface CopyToClipboardProps {
/** The raw string written to the clipboard on click. */
value: string;
/**
* Optional override for the visible value. Defaults to `value`. Use
* this when the displayed form is a truncated address but the copied
* form should be the full string.
*/
displayValue?: ReactNode;
/**
* Required descriptive `aria-label` for the copy button, e.g.
* `Copy connected wallet address` or
* `Copy transaction hash for TX-001`. Must be specific so the user
* knows which value is being copied when several `CopyToClipboard`
* instances live on the same screen.
*/
ariaLabel: string;
/** Label shown in the idle state. Defaults to `Copy`. */
copyLabel?: string;
/** Label shown for `COPY_FEEDBACK_DURATION_MS` after success. Defaults to `Copied`. */
copiedLabel?: string;
/**
* Visual treatment: `inline` flows with surrounding text; `surface`
* styles the value as a chip on a contrasting background.
*/
variant?: 'inline' | 'surface';
/** Class name appended to the root container. */
className?: string;
/** Class name applied to the displayed value element. */
valueClassName?: string;
/** Class name applied to the copy button. */
buttonClassName?: string;
/**
* When true, the click handler calls `event.stopPropagation()`. Use
* inside a clickable parent (e.g. a list row that navigates) so the
* copy click doesn't also trigger row navigation.
*/
stopPropagation?: boolean;
}
/**
* Standardised "copy to clipboard" affordance for wallet addresses and
* transaction hashes.
*
* The button flips its label from `Copy` to `Copied` for
* `COPY_FEEDBACK_DURATION_MS` (2000 ms) on success, then returns to the
* idle state. The label change is announced to screen readers via a
* polite live region so AT users get the same feedback as sighted users.
*
* Always renders a real `<button>` (not a `<div role="button">`) so
* keyboard activation, focus styling, and disabled states are inherited
* for free.
*
* See `docs/ACCESSIBILITY.md` section 5 for the canonical contract.
*/
export function CopyToClipboard({
value,
displayValue,
ariaLabel,
copyLabel = 'Copy',
copiedLabel = 'Copied',
variant = 'inline',
className = '',
valueClassName = '',
buttonClassName = '',
stopPropagation = false,
}: CopyToClipboardProps) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<number | null>(null);
useEffect(() => () => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
}, []);
const handleCopy = async (event: MouseEvent<HTMLButtonElement>) => {
if (stopPropagation) {
event.stopPropagation();
}
await copyTextToClipboard(value);
setCopied(true);
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setCopied(false);
timeoutRef.current = null;
}, COPY_FEEDBACK_DURATION_MS);
};
const containerClassName = [
'copy-affordance',
variant === 'surface' ? 'copy-affordance--surface' : '',
className,
].filter(Boolean).join(' ');
const resolvedValueClassName = ['copy-affordance__value', valueClassName].filter(Boolean).join(' ');
const resolvedButtonClassName = ['copy-affordance__button', buttonClassName].filter(Boolean).join(' ');
const announcement = copied ? `${copiedLabel}: ${ariaLabel}` : '';
return (
<div className={containerClassName}>
{displayValue ? <span className={resolvedValueClassName}>{displayValue}</span> : null}
<button
type="button"
className={resolvedButtonClassName}
aria-label={ariaLabel}
data-copied={copied}
onClick={handleCopy}
>
<span className="copy-affordance__button-label">{copied ? copiedLabel : copyLabel}</span>
{copied ? (
<Check className="copy-affordance__icon" aria-hidden="true" />
) : (
<Copy className="copy-affordance__icon" aria-hidden="true" />
)}
</button>
<span className="sr-only" role="status" aria-live="polite">
{announcement}
</span>
</div>
);
}