CopyButton.tsx80 lines · main
1import { Check, Copy } from 'lucide-react'
2import { ComponentProps, forwardRef, useEffect, useState } from 'react'
3import { Button, cn, copyToClipboard } from 'ui'
4
5type CopyButtonBaseProps = {
6 iconOnly?: boolean
7 copyLabel?: string
8 copiedLabel?: string
9}
10
11type CopyButtonWithText = CopyButtonBaseProps & {
12 text: string
13 asyncText?: never
14}
15
16type CopyButtonWithAsyncText = CopyButtonBaseProps & {
17 text?: never
18 asyncText: () => Promise<string> | string
19}
20
21export type CopyButtonProps = (CopyButtonWithText | CopyButtonWithAsyncText) &
22 ComponentProps<typeof Button>
23
24const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
25 (
26 {
27 text,
28 asyncText,
29 iconOnly = false,
30 children,
31 onClick,
32 copyLabel = 'Copy',
33 copiedLabel = 'Copied',
34 type = 'primary',
35 icon,
36 className,
37 ...props
38 },
39 ref
40 ) => {
41 const [showCopied, setShowCopied] = useState(false)
42
43 useEffect(() => {
44 if (!showCopied) return
45 const timer = setTimeout(() => setShowCopied(false), 2000)
46 return () => clearTimeout(timer)
47 }, [showCopied])
48
49 return (
50 <Button
51 ref={ref}
52 onClick={(e) => {
53 const textToCopy = asyncText ? asyncText() : text
54 setShowCopied(true)
55 copyToClipboard(textToCopy)
56 onClick?.(e)
57 }}
58 {...props}
59 type={type}
60 className={cn({ 'px-1': iconOnly }, className)}
61 icon={
62 showCopied ? (
63 <Check
64 strokeWidth={2}
65 className={cn(type === 'primary' ? 'text-inherit' : 'text-brand')}
66 />
67 ) : (
68 (icon ?? <Copy />)
69 )
70 }
71 >
72 {!iconOnly && <>{children ?? (showCopied ? copiedLabel : copyLabel)}</>}
73 </Button>
74 )
75 }
76)
77
78CopyButton.displayName = 'CopyButton'
79
80export default CopyButton