Skip to content

Commit a7d513a

Browse files
authored
feat: show copy success (#26)
1 parent 5fb3139 commit a7d513a

File tree

2 files changed

+75
-18
lines changed

2 files changed

+75
-18
lines changed

src/components/DataKeyPair.tsx

+32-18
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {
55
Edit as EditIcon
66
} from '@mui/icons-material'
77
import { Box, styled } from '@mui/material'
8-
import copy from 'copy-to-clipboard'
98
import type React from 'react'
109
import { useCallback, useMemo, useState } from 'react'
1110

1211
import { useTextColor } from '../hooks/useColor'
12+
import { useClipboard } from '../hooks/useCopyToClipboard'
1313
import { useInspect } from '../hooks/useInspect'
1414
import { useJsonViewerStore } from '../stores/JsonViewerStore'
1515
import { useTypeComponents } from '../stores/typeRegistry'
@@ -22,7 +22,7 @@ export type DataKeyPairProps = {
2222
path: (string | number)[]
2323
}
2424

25-
const IconBox = styled(props => <Box {...props} component='span'/>)`
25+
const IconBox = styled(props => <Box {...props} component='span' />)`
2626
cursor: pointer;
2727
padding-left: 0.7rem;
2828
` as typeof Box
@@ -56,6 +56,8 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
5656
setInspect,
5757
value
5858
}), [inspect, path, setInspect, value])
59+
const { copy, copied } = useClipboard()
60+
5961
const actionIcons = useMemo(() => {
6062
if (editing) {
6163
return (
@@ -101,11 +103,23 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
101103
event.preventDefault()
102104
}}
103105
>
104-
<ContentCopyIcon
105-
sx={{
106-
fontSize: '.8rem'
107-
}}
108-
/>
106+
{
107+
copied
108+
? (
109+
<CheckIcon
110+
sx={{
111+
fontSize: '.8rem'
112+
}}
113+
/>
114+
)
115+
: (
116+
<ContentCopyIcon
117+
sx={{
118+
fontSize: '.8rem'
119+
}}
120+
/>
121+
)
122+
}
109123
</IconBox>
110124
{/* todo: support edit object */}
111125
{Editor &&
@@ -126,15 +140,15 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
126140
}
127141
</>
128142
)
129-
}, [Editor, editing, onChange, path, tempValue, value])
143+
}, [Editor, copied, copy, editing, onChange, path, tempValue, value])
130144

131145
const expandable = PreComponent && PostComponent
132146
const KeyRenderer = useJsonViewerStore(store => store.keyRenderer)
133147
return (
134148
<Box className='data-key-pair'
135-
onMouseEnter={
136-
useCallback(() => setHover(path), [setHover, path])
137-
}
149+
onMouseEnter={
150+
useCallback(() => setHover(path), [setHover, path])
151+
}
138152
>
139153
<DataBox
140154
component='span'
@@ -156,11 +170,11 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
156170
>
157171
{
158172
KeyRenderer.when(downstreamProps)
159-
? <KeyRenderer {...downstreamProps}/>
173+
? <KeyRenderer {...downstreamProps} />
160174
: !props.nested && (
161175
isNumberKey
162176
? <Box component='span'
163-
style={{ color: numberKeyColor }}>{displayKey}</Box>
177+
style={{ color: numberKeyColor }}>{displayKey}</Box>
164178
: <>&quot;{displayKey}&quot;</>
165179
)
166180
}
@@ -169,18 +183,18 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
169183
<DataBox sx={{ mx: 0.5 }}>:</DataBox>
170184
)
171185
}
172-
{PreComponent && <PreComponent {...downstreamProps}/>}
186+
{PreComponent && <PreComponent {...downstreamProps} />}
173187
{(isHover && expandable && inspect) && actionIcons}
174188
</DataBox>
175189
{
176190
editing
177-
? (Editor && <Editor value={tempValue} setValue={setTempValue}/>)
191+
? (Editor && <Editor value={tempValue} setValue={setTempValue} />)
178192
: (Component)
179-
? <Component {...downstreamProps}/>
193+
? <Component {...downstreamProps} />
180194
: <Box component='span'
181-
className='data-value-fallback'>{`fallback: ${value}`}</Box>
195+
className='data-value-fallback'>{`fallback: ${value}`}</Box>
182196
}
183-
{PostComponent && <PostComponent {...downstreamProps}/>}
197+
{PostComponent && <PostComponent {...downstreamProps} />}
184198
{(isHover && expandable && !inspect) && actionIcons}
185199
{(isHover && !expandable) && actionIcons}
186200
</Box>

src/hooks/useCopyToClipboard.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import copyToClipboard from 'copy-to-clipboard'
2+
import { useCallback, useRef, useState } from 'react'
3+
4+
/**
5+
* useClipboard hook accepts one argument options in which copied status timeout duration is defined (defaults to 2000). Hook returns object with properties:
6+
* - copy – function to copy value to clipboard
7+
* - copied – value that indicates that copy handler was called less than options.timeout ms ago
8+
* - reset – function to clear timeout and reset copied to false
9+
*/
10+
export function useClipboard ({ timeout = 2000 } = {}) {
11+
const [copied, setCopied] = useState(false)
12+
const copyTimeout = useRef<number | null>(null)
13+
14+
const handleCopyResult = useCallback((value: boolean) => {
15+
if (copyTimeout.current) {
16+
clearTimeout(copyTimeout.current)
17+
}
18+
copyTimeout.current = setTimeout(() => setCopied(false), timeout)
19+
setCopied(value)
20+
}, [timeout])
21+
22+
const copy = useCallback((valueToCopy: string) => {
23+
if ('clipboard' in navigator) {
24+
navigator.clipboard
25+
.writeText(valueToCopy)
26+
.then(() => handleCopyResult(true))
27+
// When navigator.clipboard throws an error, fallback to copy-to-clipboard package
28+
.catch(() => copyToClipboard(valueToCopy))
29+
} else {
30+
// fallback to copy-to-clipboard when navigator.clipboard is not available
31+
copyToClipboard(valueToCopy)
32+
}
33+
}, [handleCopyResult])
34+
35+
const reset = useCallback(() => {
36+
setCopied(false)
37+
if (copyTimeout.current) {
38+
clearTimeout(copyTimeout.current)
39+
}
40+
}, [])
41+
42+
return { copy, reset, copied }
43+
}

0 commit comments

Comments
 (0)