diff --git a/src/lib/utils/clipboard.ts b/src/lib/utils/clipboard.ts index 5e4ea16..0e97c2e 100644 --- a/src/lib/utils/clipboard.ts +++ b/src/lib/utils/clipboard.ts @@ -12,20 +12,32 @@ export async function copyToClipboard(text: string): Promise { } } - // Fallback: hidden textarea + execCommand for HTTP contexts + // Fallback: hidden textarea + execCommand for HTTP (insecure) contexts, where + // navigator.clipboard is unavailable. The textarea must live INSIDE the current + // focus-trap (e.g. a shadcn/bits-ui Dialog): appended to document.body it sits + // outside the trap, which yanks focus back to the dialog before execCommand runs, + // so the copy silently fails and pastes stale clipboard content (#1488). Mount it + // in the open dialog when there is one, else document.body. try { + const openDialog = document.querySelector('[role="dialog"]'); + const host = openDialog ?? document.body; + const active = document.activeElement as HTMLElement | null; + const textarea = document.createElement('textarea'); textarea.value = text; + textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; textarea.style.top = '-9999px'; textarea.style.opacity = '0'; - document.body.appendChild(textarea); + host.appendChild(textarea); textarea.focus(); textarea.select(); textarea.setSelectionRange(0, textarea.value.length); const ok = document.execCommand('copy'); - document.body.removeChild(textarea); + host.removeChild(textarea); + // Restore focus to whatever the user had (keeps the dialog's focus-trap happy). + active?.focus?.(); if (ok) return true; } catch { /* fall through */