-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCopyButton.tsx
More file actions
120 lines (111 loc) · 3.37 KB
/
Copy pathCopyButton.tsx
File metadata and controls
120 lines (111 loc) · 3.37 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
import React from 'react'
export interface CopyButtonRenderState {
copied: boolean
copy: () => Promise<void>
error: Error | null
}
export interface CopyButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onCopy' | 'onError'> {
/** Text to copy to the clipboard. Either `value` or `getValue` is required. */
value?: string
/** Lazily resolves the value when the button is pressed (useful for large or async sources). */
getValue?: () => string | Promise<string>
/** How long the copied state stays true (ms). */
resetMs?: number
/** Called after a successful copy. */
onCopy?: (value: string) => void
/** Called when the clipboard write fails. */
onError?: (error: Error) => void
/** Render-prop children. Receives copied state and the copy handler. */
children?: React.ReactNode | ((state: CopyButtonRenderState) => React.ReactNode)
}
const writeToClipboard = async (value: string) => {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value)
return
}
if (typeof document === 'undefined') {
throw new Error('Clipboard API is not available in this environment')
}
const ta = document.createElement('textarea')
ta.value = value
ta.setAttribute('readonly', '')
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
const ok = document.execCommand('copy')
document.body.removeChild(ta)
if (!ok) {
throw new Error('Failed to copy')
}
}
export const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
(
{
value,
getValue,
resetMs = 2000,
onCopy,
onError,
onClick,
children,
type = 'button',
'aria-label': ariaLabel,
...rest
},
ref
) => {
const [copied, setCopied] = React.useState(false)
const [error, setError] = React.useState<Error | null>(null)
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
React.useEffect(
() => () => {
if (timer.current) {
clearTimeout(timer.current)
}
},
[]
)
const copy = React.useCallback(async () => {
try {
const v = getValue ? await getValue() : (value ?? '')
await writeToClipboard(v)
setError(null)
setCopied(true)
onCopy?.(v)
if (timer.current) {
clearTimeout(timer.current)
}
timer.current = setTimeout(() => setCopied(false), resetMs)
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e))
setError(err)
setCopied(false)
onError?.(err)
}
}, [value, getValue, resetMs, onCopy, onError])
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(e)
if (e.defaultPrevented) {
return
}
void copy()
}
const renderState: CopyButtonRenderState = { copied, copy, error }
const label = ariaLabel ?? (copied ? 'Copied' : 'Copy to clipboard')
return (
<button
ref={ref}
type={type}
data-state={copied ? 'copied' : 'idle'}
aria-label={label}
onClick={handleClick}
{...rest}
>
{typeof children === 'function' ? children(renderState) : (children ?? (copied ? 'Copied' : 'Copy'))}
</button>
)
}
)
CopyButton.displayName = 'CopyButton'