Skip to content

Commit 078cb10

Browse files
authored
fix: add clipboard fallback for copy actions (#66)
1 parent 195314d commit 078cb10

4 files changed

Lines changed: 56 additions & 4 deletions

File tree

‎components/copy-input.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
55
import { RiFileCopyLine } from "@remixicon/react"
66
import { Button } from "@/components/ui/button"
77
import { Input } from "@/components/ui/input"
8+
import { copyToClipboard } from "@/lib/clipboard"
89
import { useMessage } from "@/lib/feedback/message"
910
import { cn } from "@/lib/utils"
1011

@@ -29,8 +30,7 @@ export function CopyInput({
2930

3031
const handleCopy = async () => {
3132
try {
32-
if (!value) throw new Error("No value to copy")
33-
await navigator.clipboard.writeText(value)
33+
await copyToClipboard(value)
3434
message.success(t("Copy Success"))
3535
} catch {
3636
message.error(t("Copy Failed"))

‎components/object/path-links.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as React from "react"
44
import { useTranslation } from "react-i18next"
55
import { Button } from "@/components/ui/button"
6+
import { copyToClipboard } from "@/lib/clipboard"
67
import { RiFileCopyLine, RiCheckLine } from "@remixicon/react"
78

89
interface Segment {
@@ -21,7 +22,7 @@ function useClipboard(value: string, copiedDuring = 3000) {
2122

2223
const copy = React.useCallback(async () => {
2324
try {
24-
await navigator.clipboard.writeText(value)
25+
await copyToClipboard(value)
2526
setCopied(true)
2627
setTimeout(() => setCopied(false), copiedDuring)
2728
} catch {

‎components/object/versions.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { DataTable } from "@/components/data-table/data-table"
99
import { useDataTable } from "@/hooks/use-data-table"
1010
import { useObject } from "@/hooks/use-object"
1111
import { useMessage } from "@/lib/feedback/message"
12+
import { copyToClipboard } from "@/lib/clipboard"
1213
import { exportFile } from "@/lib/export-file"
1314
import { getContentType } from "@/lib/mime-types"
1415
import { formatBytes } from "@/lib/functions"
@@ -72,7 +73,7 @@ export function ObjectVersions({
7273
async (versionId: string) => {
7374
if (!versionId) return
7475
try {
75-
await navigator.clipboard.writeText(versionId)
76+
await copyToClipboard(versionId)
7677
message.success(t("Copy Success"))
7778
} catch {
7879
message.error(t("Copy Failed"))

‎lib/clipboard.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export async function copyToClipboard(value: string): Promise<void> {
2+
if (!value) throw new Error("No value to copy")
3+
4+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
5+
try {
6+
await navigator.clipboard.writeText(value)
7+
return
8+
} catch (error) {
9+
// Fallback to legacy copy only when Clipboard API is denied or unsupported in current context
10+
const fallback = legacyCopyToClipboard(value)
11+
if (fallback) return
12+
if (error instanceof Error) throw error
13+
throw new Error("Failed to copy text")
14+
}
15+
}
16+
17+
const fallback = legacyCopyToClipboard(value)
18+
if (fallback) return
19+
20+
throw new Error("Failed to copy text")
21+
}
22+
23+
function legacyCopyToClipboard(value: string): boolean {
24+
if (typeof document === "undefined" || !document?.execCommand) return false
25+
26+
const textarea = document.createElement("textarea")
27+
textarea.value = value
28+
textarea.setAttribute("readonly", "")
29+
textarea.style.position = "fixed"
30+
textarea.style.top = "0"
31+
textarea.style.left = "0"
32+
textarea.style.opacity = "0"
33+
textarea.style.pointerEvents = "none"
34+
textarea.style.zIndex = "-1"
35+
36+
document.body.appendChild(textarea)
37+
textarea.focus()
38+
textarea.select()
39+
40+
let copied = false
41+
try {
42+
copied = document.execCommand("copy")
43+
} catch {
44+
copied = false
45+
} finally {
46+
document.body.removeChild(textarea)
47+
}
48+
49+
return copied
50+
}

0 commit comments

Comments
 (0)