Skip to content

Commit a4f784c

Browse files
authored
Merge pull request #405 from devtron-labs/fix/copy-button
fix: unable to copy in safari whenever sending trigger in ClipboardButton
2 parents cf6db00 + 64d47e7 commit a4f784c

File tree

9 files changed

+94
-77
lines changed

9 files changed

+94
-77
lines changed

.eslintignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ src/Common/BreadCrumb/BreadcrumbStore.tsx
1010
src/Common/CIPipeline.Types.ts
1111
src/Common/ChartVersionAndTypeSelector.tsx
1212
src/Common/Checkbox.tsx
13-
src/Common/ClipboardButton/ClipboardButton.tsx
1413
src/Common/ClipboardButton/__tests__/ClipboardButton.test.tsx
1514
src/Common/CodeEditor/CodeEditor.tsx
1615
src/Common/Common.service.ts

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.0.7",
3+
"version": "1.0.8",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/ClipboardButton/ClipboardButton.tsx

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
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'
18+
import Tooltip from '@Common/Tooltip/Tooltip'
1919
import { copyToClipboard, noop, stopPropagation } from '../Helper'
2020
import ClipboardProps from './types'
2121
import { ReactComponent as ICCopy } from '../../Assets/Icon/ic-copy.svg'
@@ -25,73 +25,86 @@ 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
*/
33-
export default function ClipboardButton({
32+
export const ClipboardButton = ({
3433
content,
3534
copiedTippyText = 'Copied!',
3635
duration = 1000,
37-
trigger,
38-
setTrigger = noop,
36+
copyToClipboardPromise,
3937
rootClassName = '',
4038
iconSize = 16,
41-
}: ClipboardProps) {
39+
}: 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+
}
6871

69-
return () => clearTimeout(timeout)
70-
}, [copied, duration, setTrigger])
72+
await handleAwaitCopyToClipboardPromise(true)
73+
}
7174

7275
useEffect(() => {
73-
if (!isTriggerUndefined && trigger) {
74-
setCopied(true)
75-
handleCopyContent()
76+
if (!copyToClipboardPromise) {
77+
return
7678
}
77-
}, [trigger, handleCopyContent])
79+
80+
handleAwaitCopyToClipboardPromise().catch(noop)
81+
}, [copyToClipboardPromise])
82+
83+
useEffect(
84+
() => () => {
85+
if (setCopiedFalseTimeoutRef.current > -1) {
86+
clearTimeout(setCopiedFalseTimeoutRef.current)
87+
}
88+
},
89+
[],
90+
)
91+
92+
const iconClassName = `icon-dim-${iconSize} dc__no-shrink`
93+
7894
return (
79-
<Tippy
80-
className="default-tt"
81-
content={copied ? copiedTippyText : 'Copy'}
82-
placement="bottom"
83-
visible={copied || enableTippy}
84-
arrow={false}
85-
>
95+
<Tooltip content="Copy" alwaysShowTippyOnHover={!copied}>
96+
{/* TODO: semantically buttons should not be nested; fix later */}
8697
<button
8798
type="button"
8899
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}
100+
onClick={handleCopyContent}
92101
>
93-
{copied ? <Check className={iconClassName} /> : <ICCopy className={iconClassName} />}
102+
<Tooltip content={copiedTippyText} alwaysShowTippyOnHover visible={copied}>
103+
<div className="flex">
104+
{copied ? <Check className={iconClassName} /> : <ICCopy className={iconClassName} />}
105+
</div>
106+
</Tooltip>
94107
</button>
95-
</Tippy>
108+
</Tooltip>
96109
)
97110
}

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/CodeEditor/CodeEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { YAMLStringify, cleanKubeManifest, useJsonYaml } from '../Helper'
3131
import { useWindowSize } from '../Hooks'
3232
import Select from '../Select/Select'
3333
import RadioGroup from '../RadioGroup/RadioGroup'
34-
import ClipboardButton from '../ClipboardButton/ClipboardButton'
34+
import { ClipboardButton } from '../ClipboardButton/ClipboardButton'
3535
import { Progressing } from '../Progressing'
3636
import {
3737
CodeEditorComposition,

src/Common/Helper.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -353,43 +353,49 @@ export function cleanKubeManifest(manifestJsonString: string): string {
353353
return manifestJsonString
354354
}
355355
}
356-
const unsecureCopyToClipboard = (str, callback = noop) => {
356+
const unsecureCopyToClipboard = (str: string) => {
357357
const listener = function (ev) {
358358
ev.preventDefault()
359359
ev.clipboardData.setData('text/plain', str)
360360
}
361361
document.addEventListener('copy', listener)
362362
document.execCommand('copy')
363363
document.removeEventListener('copy', listener)
364-
callback()
365364
}
366365

367366
/**
368-
* It will copy the passed content to clipboard and invoke the callback function, in case of error it will show the toast message.
369-
* On HTTP system clipboard is not supported, so it will use the unsecureCopyToClipboard function
367+
* This is a promise<void> that will resolve if str is successfully copied
368+
* On HTTP (other than localhost) system clipboard is not supported, so it will use the unsecureCopyToClipboard function
370369
* @param str
371-
* @param callback
372370
*/
373-
export function copyToClipboard(str, callback = noop) {
374-
if (!str) {
375-
return
376-
}
371+
export function copyToClipboard(str: string): Promise<void> {
372+
return new Promise<void>((resolve, reject) => {
373+
if (!str) {
374+
resolve()
377375

378-
if (window.isSecureContext && navigator.clipboard) {
379-
navigator.clipboard
380-
.writeText(str)
381-
.then(() => {
382-
callback()
383-
})
384-
.catch(() => {
385-
ToastManager.showToast({
386-
variant: ToastVariantType.error,
387-
description: 'Failed to copy to clipboard',
376+
return
377+
}
378+
379+
if (window.isSecureContext && navigator.clipboard) {
380+
navigator.clipboard
381+
.writeText(str)
382+
.then(() => {
383+
resolve()
388384
})
389-
})
390-
} else {
391-
unsecureCopyToClipboard(str, callback)
392-
}
385+
.catch(() => {
386+
ToastManager.showToast({
387+
variant: ToastVariantType.error,
388+
description: 'Failed to copy to clipboard',
389+
})
390+
391+
reject()
392+
})
393+
} else {
394+
unsecureCopyToClipboard(str)
395+
396+
resolve()
397+
}
398+
})
393399
}
394400

395401
export function useAsync<T>(

src/Common/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export { default as DebouncedSearch } from './DebouncedSearch/DebouncedSearch'
5555
export { default as Grid } from './Grid/Grid'
5656
// export { default as CodeEditor } from './CodeEditor/CodeEditor'
5757
export { default as Select } from './Select/Select'
58-
export { default as ClipboardButton } from './ClipboardButton/ClipboardButton'
58+
export { ClipboardButton } from './ClipboardButton/ClipboardButton'
5959
export * from './Hooks'
6060
export * from './RJSF'
6161
export * from './DevtronProgressing'

src/Shared/Components/GitCommitInfoGeneric/GitCommitInfoGeneric.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
/* eslint-disable eqeqeq */
1818
import moment from 'moment'
1919
import Tippy from '@tippyjs/react'
20-
import ClipboardButton from '@Common/ClipboardButton/ClipboardButton'
20+
import { ClipboardButton } from '@Common/ClipboardButton/ClipboardButton'
2121
import { ReactComponent as Circle } from '@Icons/ic-circle.svg'
2222
import { ReactComponent as Commit } from '@Icons/ic-commit.svg'
2323
import { ReactComponent as PersonIcon } from '@Icons/ic-person.svg'

0 commit comments

Comments
 (0)