Skip to content
File

Blob: src/shared/browser/use-copy-feedback.ts

typescript74 lines
1import { useState, useCallback, useRef, useEffect } from "react";
2 
3export async function copyTextToClipboard(text: string): Promise<void> {
4 if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
5 try {
6 await navigator.clipboard.writeText(text);
7 return;
8 } catch {
9 // Fall through to the selection-based copy path.
10 }
11 }
12 if (fallbackCopyText(text)) return;
13 throw new Error("Clipboard unavailable");
14}
15 
16function fallbackCopyText(text: string): boolean {
17 if (typeof document === "undefined" || !document.body) return false;
18 
19 const textarea = document.createElement("textarea");
20 textarea.value = text;
21 textarea.setAttribute("readonly", "");
22 textarea.style.position = "fixed";
23 textarea.style.top = "0";
24 textarea.style.left = "-9999px";
25 
26 const selection = document.getSelection();
27 const ranges: Range[] = [];
28 if (selection) {
29 for (let index = 0; index < selection.rangeCount; index += 1) {
30 ranges.push(selection.getRangeAt(index).cloneRange());
31 }
32 }
33 
34 document.body.appendChild(textarea);
35 textarea.select();
36 
37 try {
38 return document.execCommand("copy");
39 } catch {
40 return false;
41 } finally {
42 textarea.remove();
43 if (selection) {
44 selection.removeAllRanges();
45 for (const range of ranges) {
46 selection.addRange(range);
47 }
48 }
49 }
50}
51 
52export function useCopyFeedback<T = string>(timeout = 2000) {
53 const [copiedId, setCopiedId] = useState<T | null>(null);
54 const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
55 
56 useEffect(() => {
57 return () => clearTimeout(timerRef.current);
58 }, []);
59 
60 const copy = useCallback(
61 (id: T, text: string) => {
62 void copyTextToClipboard(text).catch(() => {
63 // Keep the static surface usable when clipboard is unavailable.
64 });
65 clearTimeout(timerRef.current);
66 setCopiedId(id);
67 timerRef.current = setTimeout(() => setCopiedId(null), timeout);
68 },
69 [timeout],
70 );
71 
72 return { copiedId, copy };
73}