Skip to content

Commit 0d8767a

Browse files
committed
fix!: unable to copy from safari in clipboard button
1 parent 60ddc72 commit 0d8767a

File tree

3 files changed

+86
-67
lines changed

3 files changed

+86
-67
lines changed

src/Common/ClipboardButton/ClipboardButton.tsx

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useState, useEffect, useCallback } from 'react'
18-
import Tippy from '@tippyjs/react'
17+
import { useState, useEffect, useRef } from 'react'
1918
import { copyToClipboard, noop, stopPropagation } from '../Helper'
19+
import Tooltip from '@Common/Tooltip/Tooltip'
2020
import ClipboardProps from './types'
2121
import { ReactComponent as ICCopy } from '../../Assets/Icon/ic-copy.svg'
2222
import { ReactComponent as Check } from '../../Assets/Icon/ic-check.svg'
@@ -25,73 +25,87 @@ import { ReactComponent as Check } from '../../Assets/Icon/ic-check.svg'
2525
* @param content - Content to be copied
2626
* @param copiedTippyText - Text to be shown in the tippy when the content is copied, default 'Copied!'
2727
* @param duration - Duration for which the tippy should be shown, default 1000
28-
* @param trigger - To trigger the copy action outside the button, if set to true the content will be copied, use case being triggering the copy action from outside the component
29-
* @param setTrigger - Callback function to set the trigger outside the button
28+
* @param copyToClipboardPromise - the promise returned by copyToClipboard util function
3029
* @param rootClassName - additional classes to add to button
3130
* @param iconSize - size of svg icon to be shown, default 16 (icon-dim-16)
3231
*/
3332
export default function ClipboardButton({
3433
content,
3534
copiedTippyText = 'Copied!',
3635
duration = 1000,
37-
trigger,
38-
setTrigger = noop,
36+
copyToClipboardPromise,
3937
rootClassName = '',
4038
iconSize = 16,
4139
}: ClipboardProps) {
4240
const [copied, setCopied] = useState<boolean>(false)
43-
const [enableTippy, setEnableTippy] = useState<boolean>(false)
41+
const setCopiedFalseTimeoutRef = useRef<ReturnType<typeof setTimeout>>(-1)
4442

45-
const handleTextCopied = () => {
43+
const handleTriggerCopy = () => {
4644
setCopied(true)
45+
46+
setCopiedFalseTimeoutRef.current = setTimeout(() => {
47+
setCopied(false)
48+
49+
setCopiedFalseTimeoutRef.current = -1
50+
}, duration)
4751
}
48-
const isTriggerUndefined = typeof trigger === 'undefined'
4952

50-
const handleEnableTippy = () => setEnableTippy(true)
51-
const handleDisableTippy = () => setEnableTippy(false)
52-
const handleCopyContent = useCallback(
53-
(e?) => {
54-
if (e) stopPropagation(e)
55-
copyToClipboard(content, handleTextCopied)
56-
},
57-
[content],
58-
)
59-
const iconClassName = `icon-dim-${iconSize} dc__no-shrink`
53+
const handleAwaitCopyToClipboardPromise = async (shouldRunCopy?: boolean) => {
54+
try {
55+
if (shouldRunCopy) {
56+
await copyToClipboard(content)
57+
} else {
58+
await copyToClipboardPromise
59+
}
6060

61-
useEffect(() => {
62-
if (!copied) return
61+
handleTriggerCopy()
62+
} catch {
63+
noop()
64+
}
65+
}
6366

64-
const timeout = setTimeout(() => {
65-
setCopied(false)
66-
setTrigger(false)
67-
}, duration)
67+
const handleCopyContent = async (e?: React.MouseEvent) => {
68+
if (e) {
69+
stopPropagation(e)
70+
}
71+
72+
await handleAwaitCopyToClipboardPromise(true)
73+
}
74+
75+
useEffect(() => {
76+
if (!copyToClipboardPromise) {
77+
return
78+
}
6879

69-
return () => clearTimeout(timeout)
70-
}, [copied, duration, setTrigger])
80+
handleAwaitCopyToClipboardPromise().catch(noop)
81+
}, [copyToClipboardPromise])
7182

7283
useEffect(() => {
73-
if (!isTriggerUndefined && trigger) {
74-
setCopied(true)
75-
handleCopyContent()
84+
return () => {
85+
if (setCopiedFalseTimeoutRef.current > -1) {
86+
clearTimeout(setCopiedFalseTimeoutRef.current)
87+
}
7688
}
77-
}, [trigger, handleCopyContent])
89+
}, [])
90+
91+
const iconClassName = `icon-dim-${iconSize} dc__no-shrink`
92+
7893
return (
79-
<Tippy
80-
className="default-tt"
81-
content={copied ? copiedTippyText : 'Copy'}
82-
placement="bottom"
83-
visible={copied || enableTippy}
84-
arrow={false}
94+
<Tooltip
95+
content={'Copy'}
96+
alwaysShowTippyOnHover={!copied}
8597
>
8698
<button
8799
type="button"
88100
className={`dc__outline-none-imp p-0 flex dc__transparent--unstyled dc__no-border ${rootClassName}`}
89-
onMouseEnter={handleEnableTippy}
90-
onMouseLeave={handleDisableTippy}
91-
onClick={isTriggerUndefined ? handleCopyContent : noop}
101+
onClick={handleCopyContent}
92102
>
93-
{copied ? <Check className={iconClassName} /> : <ICCopy className={iconClassName} />}
103+
<Tooltip content={copiedTippyText} alwaysShowTippyOnHover visible={copied}>
104+
<div className="flex">
105+
{copied ? <Check className={iconClassName} /> : <ICCopy className={iconClassName} />}
106+
</div>
107+
</Tooltip>
94108
</button>
95-
</Tippy>
109+
</Tooltip>
96110
)
97111
}

src/Common/ClipboardButton/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ export default interface ClipboardProps {
1818
content: string
1919
copiedTippyText?: string
2020
duration?: number
21-
trigger?: boolean
22-
setTrigger?: React.Dispatch<React.SetStateAction<boolean>>
21+
copyToClipboardPromise?: Promise<void>
2322
rootClassName?: string
2423
iconSize?: number
2524
}

src/Common/Helper.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -346,43 +346,49 @@ export function cleanKubeManifest(manifestJsonString: string): string {
346346
return manifestJsonString
347347
}
348348
}
349-
const unsecureCopyToClipboard = (str, callback = noop) => {
349+
const unsecureCopyToClipboard = (str: string) => {
350350
const listener = function (ev) {
351351
ev.preventDefault()
352352
ev.clipboardData.setData('text/plain', str)
353353
}
354354
document.addEventListener('copy', listener)
355355
document.execCommand('copy')
356356
document.removeEventListener('copy', listener)
357-
callback()
358357
}
359358

360359
/**
361-
* It will copy the passed content to clipboard and invoke the callback function, in case of error it will show the toast message.
362-
* On HTTP system clipboard is not supported, so it will use the unsecureCopyToClipboard function
360+
* This is a promise<void> that will resolve if str is successfully copied
361+
* On HTTP (other than localhost) system clipboard is not supported, so it will use the unsecureCopyToClipboard function
363362
* @param str
364-
* @param callback
365363
*/
366-
export function copyToClipboard(str, callback = noop) {
367-
if (!str) {
368-
return
369-
}
364+
export function copyToClipboard(str: string): Promise<void> {
365+
return new Promise<void>((resolve, reject) => {
366+
if (!str) {
367+
resolve()
370368

371-
if (window.isSecureContext && navigator.clipboard) {
372-
navigator.clipboard
373-
.writeText(str)
374-
.then(() => {
375-
callback()
376-
})
377-
.catch(() => {
378-
ToastManager.showToast({
379-
variant: ToastVariantType.error,
380-
description: 'Failed to copy to clipboard',
369+
return
370+
}
371+
372+
if (window.isSecureContext && navigator.clipboard) {
373+
navigator.clipboard
374+
.writeText(str)
375+
.then(() => {
376+
resolve()
381377
})
382-
})
383-
} else {
384-
unsecureCopyToClipboard(str, callback)
385-
}
378+
.catch(() => {
379+
ToastManager.showToast({
380+
variant: ToastVariantType.error,
381+
description: 'Failed to copy to clipboard',
382+
})
383+
384+
reject()
385+
})
386+
} else {
387+
unsecureCopyToClipboard(str)
388+
389+
resolve()
390+
}
391+
})
386392
}
387393

388394
export function useAsync<T>(

0 commit comments

Comments
 (0)