diff --git a/src/lib/clipboard.ts b/src/lib/clipboard.ts index 91a4340..0006cc0 100644 --- a/src/lib/clipboard.ts +++ b/src/lib/clipboard.ts @@ -1,41 +1,62 @@ -// useClipboard — navigator.clipboard.writeText with an execCommand fallback. -// Lifted from the host (apps/web/composables/useClipboard.ts, now a re-export -// shim). SSR/touch-safe: reports unsupported instead of throwing; copyText -// resolves false on any failure so callers can fall back to manual selection. +const focusTrapContainerSelector = '[role="dialog"], [role="alertdialog"]' + +function resolveFallbackContainer(documentRef: Document): HTMLElement { + const activeElement = documentRef.activeElement + if (activeElement instanceof HTMLElement) { + return activeElement.closest(focusTrapContainerSelector) ?? documentRef.body + } + return documentRef.body +} + +function copyWithExecCommand(documentRef: Document, text: string): boolean { + if (typeof documentRef.execCommand !== 'function') return false + + const previousFocus = documentRef.activeElement instanceof HTMLElement + ? documentRef.activeElement + : null + const textArea = documentRef.createElement('textarea') + textArea.value = text + textArea.readOnly = true + textArea.tabIndex = -1 + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + textArea.style.top = '0' + resolveFallbackContainer(documentRef).appendChild(textArea) + + try { + textArea.focus() + textArea.select() + return documentRef.execCommand('copy') + } catch { + return false + } finally { + textArea.remove() + previousFocus?.focus() + } +} + +// SSR/touch-safe clipboard access. The legacy fallback stays inside an active +// dialog so focus traps cannot steal its temporary textarea selection. export function useClipboard() { const hasNavigatorClipboard = typeof navigator !== 'undefined' && !!navigator.clipboard?.writeText const hasExecCommandFallback = typeof document !== 'undefined' && typeof document.execCommand === 'function' const isSupported = hasNavigatorClipboard || hasExecCommandFallback async function copyText(text: string): Promise { - if (!isSupported) { - return false - } + if (!isSupported) return false - try { - if (hasNavigatorClipboard) { + if (hasNavigatorClipboard && typeof navigator !== 'undefined') { + try { await navigator.clipboard.writeText(text) return true } - - if (!hasExecCommandFallback || typeof document === 'undefined') { + catch { return false } - - const textArea = document.createElement('textarea') - textArea.value = text - textArea.style.position = 'fixed' - textArea.style.left = '-9999px' - textArea.style.top = '0' - document.body.appendChild(textArea) - textArea.focus() - textArea.select() - const success = document.execCommand('copy') - document.body.removeChild(textArea) - return success - } catch { - return false } + + if (!hasExecCommandFallback || typeof document === 'undefined') return false + return copyWithExecCommand(document, text) } return {