Consolidate copy to clipboard (#36353)

* Consolidate copy to clipboard

* Fix

* Fix some extra clipboard events.

* Fix the tests. Fix a small issue with the copy button.

* Fix

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
Joshen Lim
2025-06-13 19:08:08 +08:00
committed by GitHub
parent 4e1a0390da
commit 27d9b44526
30 changed files with 136 additions and 178 deletions

View File

@@ -4,7 +4,7 @@ import { Check, Copy } from 'lucide-react'
import { type MouseEvent, useCallback, useEffect, useState } from 'react'
import { type ThemedToken } from 'shiki'
import { type NodeHover } from 'twoslash'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { cn, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export function AnnotatedSpan({
token,
@@ -89,13 +89,10 @@ export function CodeCopyButton({ className, content }: { className?: string; con
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(content)
copyToClipboard(content, () => {
setCopied(true)
setTimeout(() => setCopied(false), 1000)
} catch (error) {
console.error('Failed to copy text: ', error)
}
})
}
return (

View File

@@ -5,8 +5,9 @@ import { Item, ItemParams, Menu } from 'react-contexify'
import type { SupaRow } from 'components/grid/types'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { copyToClipboard } from 'ui'
import { ROW_CONTEXT_MENU_ID } from '.'
import { copyToClipboard, formatClipboardValue } from '../../utils/common'
import { formatClipboardValue } from '../../utils/common'
export type RowContextMenuProps = {
rows: SupaRow[]

View File

@@ -5,12 +5,3 @@ export function formatClipboardValue(value: any) {
}
return value
}
export const copyToClipboard = (str: string, callback = () => {}) => {
const focused = window.document.hasFocus()
if (focused) {
window.navigator?.clipboard?.writeText(str).then(callback)
} else {
console.warn('Unable to copy to clipboard')
}
}

View File

@@ -3,8 +3,7 @@ import { useMemo } from 'react'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { copyToClipboard } from 'lib/helpers'
import { Badge } from 'ui'
import { Badge, copyToClipboard } from 'ui'
import type { ICommand } from 'ui-patterns/CommandMenu'
import {
PageType,

View File

@@ -2,8 +2,7 @@ import { Link } from 'lucide-react'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { copyToClipboard } from 'lib/helpers'
import { Badge } from 'ui'
import { Badge, copyToClipboard } from 'ui'
import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
import { orderCommandSectionsByPriority } from './ordering'

View File

@@ -1,10 +1,9 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check, Webhook } from 'lucide-react'
import { Badge, Input } from 'ui'
import { Badge, Input, copyToClipboard } from 'ui'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { copyToClipboard } from 'lib/helpers'
import { Hook } from './hooks.constants'
interface HookCardProps {

View File

@@ -5,7 +5,6 @@ import { Column, useRowSelection } from 'react-data-grid'
import { User } from 'data/auth/users-infinite-query'
import { BASE_PATH } from 'lib/constants'
import { copyToClipboard } from 'lib/helpers'
import {
Checkbox_Shadcn_,
cn,
@@ -14,6 +13,7 @@ import {
ContextMenuItem_Shadcn_,
ContextMenuSeparator_Shadcn_,
ContextMenuTrigger_Shadcn_,
copyToClipboard,
} from 'ui'
import { PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation'
import { ColumnConfiguration, USERS_TABLE_COLUMNS } from './Users.constants'

View File

@@ -1,13 +1,13 @@
import { Check, ChevronRight, Copy } from 'lucide-react'
import { useState } from 'react'
import { copyToClipboard } from 'lib/helpers'
import {
Button,
cn,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
copyToClipboard,
Separator,
} from 'ui'

View File

@@ -1,6 +1,7 @@
import { Check, Clipboard } from 'lucide-react'
import { forwardRef, useState } from 'react'
import { cn } from 'ui'
import { cn, copyToClipboard } from 'ui'
const CommandRender = forwardRef<HTMLDivElement, { commands: any[]; className?: string }>(
({ commands, className }, ref) => {
@@ -35,10 +36,8 @@ const Command = ({ item }: any) => {
onClick={() => {
function onCopy(value: any) {
setIsCopied(true)
navigator.clipboard.writeText(value).then()
setTimeout(function () {
setIsCopied(false)
}, 3000)
copyToClipboard(value)
setTimeout(() => setIsCopied(false), 3000)
}
onCopy(item.command)
}}

View File

@@ -8,7 +8,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import Table from 'components/to-be-cleaned/Table'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import type { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
interface EdgeFunctionsListItemProps {
function: EdgeFunctionsResponse
@@ -55,10 +55,8 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
onClick={(event: any) => {
function onCopy(value: any) {
setIsCopied(true)
navigator.clipboard.writeText(value).then()
setTimeout(function () {
setIsCopied(false)
}, 3000)
copyToClipboard(value)
setTimeout(() => setIsCopied(false), 3000)
}
event.stopPropagation()
onCopy(endpoint)

View File

@@ -1,8 +1,7 @@
import { useParams } from 'common'
import { Button, Input } from 'ui'
import { Button, Input, copyToClipboard } from 'ui'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { copyToClipboard } from 'lib/helpers'
import { Copy } from 'lucide-react'
import { useEffect, useState } from 'react'
import ContentSnippet from '../ContentSnippet'

View File

@@ -3,13 +3,13 @@ import { useState } from 'react'
import DataGrid, { CalculatedColumn } from 'react-data-grid'
import { handleCopyCell } from 'components/grid/SupabaseGrid.utils'
import { copyToClipboard } from 'lib/helpers'
import {
cn,
ContextMenu_Shadcn_,
ContextMenuContent_Shadcn_,
ContextMenuItem_Shadcn_,
ContextMenuTrigger_Shadcn_,
copyToClipboard,
} from 'ui'
import { CellDetailPanel } from './CellDetailPanel'

View File

@@ -11,10 +11,16 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { copyToClipboard } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { ResponseError } from 'types'
import { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup, cn } from 'ui'
import {
Button,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
cn,
copyToClipboard,
} from 'ui'
import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'

View File

@@ -7,10 +7,16 @@ import DatePicker from 'react-datepicker'
import { Label } from '@ui/components/shadcn/ui/label'
import { RadioGroup, RadioGroupItem } from '@ui/components/shadcn/ui/radio-group'
import TimeSplitInput from 'components/ui/DatePicker/TimeSplitInput'
import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui'
import {
Button,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
cn,
copyToClipboard,
} from 'ui'
import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from './Logs.constants'
import type { DatetimeHelper } from './Logs.types'
import { copyToClipboard } from 'lib/helpers'
export type DatePickerValue = {
to: string

View File

@@ -1,17 +1,17 @@
import dayjs from 'dayjs'
import { BookOpen, Check, ChevronDown, Clipboard, ExternalLink, X } from 'lucide-react'
import Link from 'next/link'
import { ReactNode, useState } from 'react'
import { IS_PLATFORM } from 'common'
import Table from 'components/to-be-cleaned/Table'
import dayjs from 'dayjs'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useFlag } from 'hooks/ui/useFlag'
import { copyToClipboard } from 'lib/helpers'
import { BookOpen, Check, ChevronDown, Clipboard, ExternalLink, X } from 'lucide-react'
import { logConstants } from 'shared-data'
import {
Badge,
Button,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,

View File

@@ -27,11 +27,11 @@ import { isTableLike } from 'data/table-editor/table-editor-types'
import { fetchAllTableRows } from 'data/table-rows/table-rows-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { formatSql } from 'lib/formatSql'
import { copyToClipboard } from 'lib/helpers'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import {
cn,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,

View File

@@ -1,8 +1,8 @@
import { toast } from 'sonner'
import { downloadBucketObject } from 'data/storage/bucket-object-download-mutation'
import { StorageObject } from 'data/storage/bucket-objects-list-mutation'
import { copyToClipboard } from 'lib/helpers'
import { toast } from 'sonner'
import { SONNER_DEFAULT_DURATION } from 'ui'
import { SONNER_DEFAULT_DURATION, copyToClipboard } from 'ui'
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES } from '../Storage.constants'
import { StorageItem, StorageItemMetadata, StorageItemWithColumn } from '../Storage.types'

View File

@@ -3,8 +3,8 @@ import { toast } from 'sonner'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { copyToClipboard } from 'lib/helpers'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import { copyToClipboard } from 'ui'
import { URL_EXPIRY_DURATION } from '../Storage.constants'
import { fetchFileUrl } from './useFetchFileUrlQuery'

View File

@@ -1,8 +1,7 @@
import { Check, Clipboard } from 'lucide-react'
import React, { forwardRef, useEffect, useState } from 'react'
import { ComponentProps, forwardRef, useEffect, useState } from 'react'
import { copyToClipboard } from 'lib/helpers'
import { Button, cn } from 'ui'
import { Button, cn, copyToClipboard } from 'ui'
type CopyButtonBaseProps = {
iconOnly?: boolean
@@ -22,7 +21,7 @@ type CopyButtonWithAsyncText = CopyButtonBaseProps & {
}
export type CopyButtonProps = (CopyButtonWithText | CopyButtonWithAsyncText) &
React.ComponentProps<typeof Button>
ComponentProps<typeof Button>
const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
(
@@ -52,7 +51,7 @@ const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
onClick={async (e) => {
const textToCopy = asyncText ? await asyncText() : text
setShowCopied(true)
await copyToClipboard(textToCopy!)
copyToClipboard(textToCopy)
onClick?.(e)
}}
{...props}

View File

@@ -5,9 +5,9 @@ import Papa from 'papaparse'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { copyToClipboard } from 'lib/helpers'
import {
Button,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,

View File

@@ -4,9 +4,16 @@ import { useState } from 'react'
import { LOGS_EXPLORER_DOCS_URL } from 'components/interfaces/Settings/Logs/Logs.constants'
import Table from 'components/to-be-cleaned/Table'
import { copyToClipboard } from 'lib/helpers'
import { logConstants } from 'shared-data'
import { Button, SidePanel, Tabs, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import {
Button,
SidePanel,
Tabs,
Tooltip,
TooltipContent,
TooltipTrigger,
copyToClipboard,
} from 'ui'
import { DocsButton } from '../DocsButton'
export interface LogsExplorerHeaderProps {

View File

@@ -1,29 +1,30 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
tryParseJson,
minifyJSON,
prettifyJSON,
removeJSONTrailingComma,
timeout,
getURL,
makeRandomString,
pluckObjectFields,
tryParseInt,
propsAreEqual,
formatBytes,
snakeToCamel,
copyToClipboard,
detectBrowser,
detectOS,
pluralize,
isValidHttpUrl,
removeCommentsFromSql,
getSemanticVersion,
formatBytes,
formatCurrency,
getDatabaseMajorVersion,
getDistanceLatLonKM,
formatCurrency,
getSemanticVersion,
getURL,
isValidHttpUrl,
makeRandomString,
minifyJSON,
pluckObjectFields,
pluralize,
prettifyJSON,
propsAreEqual,
removeCommentsFromSql,
removeJSONTrailingComma,
snakeToCamel,
timeout,
tryParseInt,
tryParseJson,
} from './helpers'
import { copyToClipboard } from 'ui'
describe('tryParseJson', () => {
it('should return the parsed JSON', () => {
const result = tryParseJson('{"test": "test"}')
@@ -170,6 +171,9 @@ describe('copyToClipboard', () => {
},
})
// CopyToClipboard uses setTimeout to call the callback
vi.useFakeTimers()
// If ClipboardItem is used
vi.stubGlobal('ClipboardItem', function (items: any) {
return items
@@ -181,10 +185,12 @@ describe('copyToClipboard', () => {
afterEach(() => {
vi.unstubAllGlobals()
vi.useRealTimers()
})
it('uses clipboard.write if available', async () => {
await copyToClipboard('hello')
vi.runAllTimers()
expect(writeMock).toHaveBeenCalled()
})

View File

@@ -1,6 +1,3 @@
import { noop } from 'lodash'
import { toast } from 'sonner'
export { default as passwordStrength } from './password-strength'
export { default as uuidv4 } from './uuid'
@@ -146,42 +143,6 @@ export const snakeToCamel = (str: string) =>
group.toUpperCase().replace('-', '').replace('_', '')
)
/**
* Copy text content (string or Promise<string>) into Clipboard. Safari doesn't support write text into clipboard async,
* so if you need to load text content async before coping, please use Promise<string> for the 1st arg.
*
* IF YOU NEED TO CHANGE THIS FUNCTION, PLEASE TEST IT IN SAFARI with a promised string. Expiring URL to a file in a
* private bucket will do.
*
* Copied code from https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/
*/
export const copyToClipboard = async (str: string | Promise<string>, callback = noop) => {
const focused = window.document.hasFocus()
if (focused) {
if (typeof ClipboardItem && navigator.clipboard?.write) {
// NOTE: Safari locks down the clipboard API to only work when triggered
// by a direct user interaction. You can't use it async in a promise.
// But! You can wrap the promise in a ClipboardItem, and give that to
// the clipboard API.
// Found this on https://developer.apple.com/forums/thread/691873
const text = new ClipboardItem({
'text/plain': Promise.resolve(str).then((text) => new Blob([text], { type: 'text/plain' })),
})
navigator.clipboard.write([text]).then(callback)
} else {
// NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
// but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
// Good news is that other than Safari, Firefox does not care about
// Clipboard API being used async in a Promise.
Promise.resolve(str)
.then((text) => navigator.clipboard?.writeText(text))
.then(callback)
}
} else {
toast.error('Unable to copy to clipboard')
}
}
export const detectBrowser = () => {
if (!navigator) return undefined

View File

@@ -3,6 +3,7 @@ import Link from 'next/link'
import { useState } from 'react'
import SVG from 'react-inlinesvg'
import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
import Success from 'components/interfaces/Support/Success'
import { SupportFormV2 } from 'components/interfaces/Support/SupportFormV2'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
@@ -15,8 +16,7 @@ import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import { toast } from 'sonner'
import { NextPageWithLayout } from 'types'
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
import { Button, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
const SupportPage: NextPageWithLayout = () => {
const [sentCategory, setSentCategory] = useState<string>()
@@ -118,9 +118,7 @@ const SupportPage: NextPageWithLayout = () => {
type="text"
text="support@supabase.com"
iconOnly
onClick={() => {
toast.success('Copied to clipboard')
}}
onClick={() => toast.success('Copied to clipboard')}
/>
</p>
<p>
@@ -139,7 +137,7 @@ const SupportPage: NextPageWithLayout = () => {
<li key={project.id} className="cursor-default">
<button
onClick={() => {
navigator.clipboard.writeText(project.ref)
copyToClipboard(project.ref)
toast.success('Copied to clipboard')
}}
className="w-full group py-1.5 px-2 gap-x-1 text-foreground hover:bg-muted grid grid-cols-2 text-left rounded-sm"

View File

@@ -9,6 +9,7 @@ import { Check, Copy } from 'lucide-react'
import {
Button,
cn,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -26,7 +27,7 @@ export async function copyToClipboardWithMeta(
value: string
// event?: Event
) {
navigator.clipboard.writeText(value)
copyToClipboard(value)
if (event) {
// trackEvent(event)
}

View File

@@ -1,10 +1,17 @@
import React, { Fragment, MouseEvent, ReactNode, useRef, useState } from 'react'
import { CheckIcon, ClipboardIcon } from '@heroicons/react/outline'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Fragment, MouseEvent, ReactNode, useRef, useState } from 'react'
import { useClickAway, useKey } from 'react-use'
import { CheckIcon, ClipboardIcon } from '@heroicons/react/outline'
import { cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
import {
cn,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
@@ -62,7 +69,7 @@ const RightClickBrandLogo = () => {
* Copy to clipboard logo SVG
*/
const handleCopyToClipboard = (menuItem: MenuItemProps) => {
navigator.clipboard.writeText(menuItem.clipboard ?? '').then(() => {
copyToClipboard(menuItem.clipboard ?? '', () => {
setCopied(true)
setTimeout(() => {
setCopied(false)

View File

@@ -6,7 +6,7 @@ import React, {
forwardRef,
useState,
} from 'react'
import { Button, Input_Shadcn_, cn } from 'ui'
import { Button, Input_Shadcn_, cn, copyToClipboard } from 'ui'
import styleHandler from 'ui/src/lib/theme/styleHandler'
import InputIconContainer from '../form/Layout/InputIconContainer'
@@ -47,20 +47,14 @@ const Input = forwardRef<
const __styles = styleHandler('input')
function _onCopy(value: any) {
navigator.clipboard.writeText(value)?.then(
function () {
/* clipboard successfully set */
setCopyLabel('Copied')
setTimeout(function () {
setCopyLabel('Copy')
}, 3000)
onCopy?.()
},
function () {
/* clipboard write failed */
setCopyLabel('Failed to copy')
}
)
copyToClipboard(value, () => {
/* clipboard successfully set */
setCopyLabel('Copied')
setTimeout(function () {
setCopyLabel('Copy')
}, 3000)
onCopy?.()
})
}
function onReveal() {

View File

@@ -1,7 +1,6 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui/src/components/shadcn/ui/tooltip'
import { cn } from 'ui/src/lib/utils'
import { cn, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
@@ -103,12 +102,12 @@ export const TimestampInfo = ({
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
navigator.clipboard.writeText(value)
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1000)
copyToClipboard(value, () => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1000)
})
}}
className={cn(
'relative cursor-pointer flex gap-y-2 gap-x-0.5 hover:bg-surface-100 px-2 py-1 group',

View File

@@ -8,6 +8,7 @@ import InputErrorIcon from '../../lib/Layout/InputErrorIcon'
import InputIconContainer from '../../lib/Layout/InputIconContainer'
import { HIDDEN_PLACEHOLDER } from '../../lib/constants'
import styleHandler from '../../lib/theme/styleHandler'
import { copyToClipboard } from '../../lib/utils'
import { cn } from '../../lib/utils/cn'
import { Button } from '../Button'
import { useFormContext } from '../Form/FormContext'
@@ -115,20 +116,13 @@ function Input({
// }, [errors, touched])
function _onCopy(value: any) {
navigator.clipboard.writeText(value)?.then(
function () {
/* clipboard successfully set */
setCopyLabel('Copied')
setTimeout(function () {
setCopyLabel('Copy')
}, 3000)
onCopy?.()
},
function () {
/* clipboard write failed */
setCopyLabel('Failed to copy')
}
)
copyToClipboard(value, () => {
setCopyLabel('Copied')
setTimeout(() => {
setCopyLabel('Copy')
}, 3000)
onCopy?.()
})
}
function onReveal() {
@@ -256,20 +250,14 @@ function TextArea({
const [copyLabel, setCopyLabel] = useState('Copy')
function _onCopy(value: any) {
navigator.clipboard.writeText(value).then(
function () {
/* clipboard successfully set */
setCopyLabel('Copied')
setTimeout(function () {
setCopyLabel('Copy')
}, 3000)
onCopy?.()
},
function () {
/* clipboard write failed */
setCopyLabel('Failed to copy')
}
)
copyToClipboard(value, () => {
/* clipboard successfully set */
setCopyLabel('Copied')
setTimeout(() => {
setCopyLabel('Copy')
}, 3000)
onCopy?.()
})
}
const { formContextOnChange, values, errors, handleBlur, touched, fieldLevelValidation } =

View File

@@ -22,7 +22,11 @@ export const copyToClipboard = async (str: string | Promise<string>, callback =
const text = new ClipboardItem({
'text/plain': Promise.resolve(str).then((text) => new Blob([text], { type: 'text/plain' })),
})
navigator.clipboard.write([text]).then(callback)
// [Joshen] Adding a timeout based on this comment here about a workaround
// https://stackoverflow.com/questions/62327358/javascript-clipboard-api-safari-ios-notallowederror-message
setTimeout(() => {
navigator.clipboard.write([text]).then(callback)
}, 0)
} else {
// NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
// but those are behind `dom.events.asyncClipboard.clipboardItem` preference.