Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/copy-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
import { RiFileCopyLine } from "@remixicon/react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { copyToClipboard } from "@/lib/clipboard"
import { useMessage } from "@/lib/feedback/message"
import { cn } from "@/lib/utils"

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

const handleCopy = async () => {
try {
if (!value) throw new Error("No value to copy")
await navigator.clipboard.writeText(value)
await copyToClipboard(value)
message.success(t("Copy Success"))
} catch {
message.error(t("Copy Failed"))
Expand Down
3 changes: 2 additions & 1 deletion components/object/path-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as React from "react"
import { useTranslation } from "react-i18next"
import { Button } from "@/components/ui/button"
import { copyToClipboard } from "@/lib/clipboard"
import { RiFileCopyLine, RiCheckLine } from "@remixicon/react"

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

const copy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(value)
await copyToClipboard(value)
setCopied(true)
setTimeout(() => setCopied(false), copiedDuring)
} catch {
Expand Down
3 changes: 2 additions & 1 deletion components/object/versions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DataTable } from "@/components/data-table/data-table"
import { useDataTable } from "@/hooks/use-data-table"
import { useObject } from "@/hooks/use-object"
import { useMessage } from "@/lib/feedback/message"
import { copyToClipboard } from "@/lib/clipboard"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { formatBytes } from "@/lib/functions"
Expand Down Expand Up @@ -72,7 +73,7 @@ export function ObjectVersions({
async (versionId: string) => {
if (!versionId) return
try {
await navigator.clipboard.writeText(versionId)
await copyToClipboard(versionId)
message.success(t("Copy Success"))
} catch {
message.error(t("Copy Failed"))
Expand Down
50 changes: 50 additions & 0 deletions lib/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export async function copyToClipboard(value: string): Promise<void> {
if (!value) throw new Error("No value to copy")

if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value)
return
} catch (error) {
// Fallback to legacy copy only when Clipboard API is denied or unsupported in current context
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the legacy path is used "only when Clipboard API is denied or unsupported", but the code falls back on any navigator.clipboard.writeText error. Either narrow the fallback condition (e.g., specific error names) or adjust the comment to match actual behavior.

Suggested change
// Fallback to legacy copy only when Clipboard API is denied or unsupported in current context
// Fallback to legacy copy on any Clipboard API error (e.g., denied or unsupported in current context)

Copilot uses AI. Check for mistakes.
const fallback = legacyCopyToClipboard(value)
if (fallback) return
if (error instanceof Error) throw error
throw new Error("Failed to copy text")
}
}

const fallback = legacyCopyToClipboard(value)
if (fallback) return

throw new Error("Failed to copy text")
}

function legacyCopyToClipboard(value: string): boolean {
if (typeof document === "undefined" || !document?.execCommand) return false

const textarea = document.createElement("textarea")
textarea.value = value
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.top = "0"
textarea.style.left = "0"
textarea.style.opacity = "0"
textarea.style.pointerEvents = "none"
textarea.style.zIndex = "-1"

document.body.appendChild(textarea)
textarea.focus()
textarea.select()

let copied = false
try {
copied = document.execCommand("copy")
} catch {
copied = false
} finally {
document.body.removeChild(textarea)
}
Comment on lines +36 to +47
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

legacyCopyToClipboard assumes document.body exists and will throw if it’s null (e.g., called very early or in unusual document states). Consider guarding for document.body before appendChild/removeChild and returning false if it’s not available.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +47
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The legacy fallback moves focus to a hidden <textarea> (textarea.focus()), which can disrupt keyboard users and screen readers, and focus is not restored afterward. Save the previously focused element and restore focus after copying/removal (or otherwise ensure focus remains on the triggering control).

Copilot uses AI. Check for mistakes.

return copied
}
Loading