Clipboard API dengan Claude Code: tombol copy, permission, fallback, dan test
Implementasi Clipboard API dengan Claude Code: copy, paste, permission, fallback, React, privasi, dan Playwright.
Clipboard API sering terlihat seperti fitur kecil. Sebuah tombol memanggil navigator.clipboard.writeText() lalu teks berhasil disalin. Di production, detailnya tidak sesederhana itu. Tombol yang sama bisa gagal di URL preview HTTP, di dalam iframe, setelah fetch yang terlalu lama, pada browser dengan permission berbeda, atau pada alur mobile Safari yang membutuhkan gesture pengguna yang masih segar.
Panduan ini menunjukkan cara memakai Claude Code untuk membuat UX copy dan paste yang siap dipakai di React dan TypeScript. Clipboard API adalah API browser untuk membaca atau menulis clipboard sistem operasi. Async Clipboard adalah versi berbasis Promise. Secure context berarti lingkungan yang dianggap aman oleh browser, seperti HTTPS atau localhost. Fallback adalah jalur cadangan ketika API modern tidak tersedia.
Bacaan terkait di ClaudeCodeLab: aksesibilitas dengan Claude Code, testing Playwright dengan Claude Code, dan validasi form dengan Claude Code. Referensi resmi: MDN Clipboard API, MDN writeText, W3C Clipboard API and events, Playwright BrowserContext, WebKit Async Clipboard API, dan Claude Code docs.
Mulai dari kriteria yang jelas
Jangan hanya meminta Claude Code membuat tombol copy. Tulis juga kondisi gagal yang perlu ditangani.
Goal: Implement Clipboard API copy and paste UX in React.
Scope: edit only src/lib/clipboard.ts, src/components/CopyButton.tsx, and matching tests.
Requirements:
- Use navigator.clipboard.writeText in secure contexts.
- Keep the write call inside a user click handler.
- Provide a textarea fallback for unsupported or HTTP pages.
- Never read clipboard on page load.
- Show accessible copied/error feedback.
- Add Playwright tests for copy success and paste normalization.
Do not stage, commit, or edit unrelated files.
Alur yang diinginkan seperti ini.
flowchart TD
A["Pengguna klik copy"] --> B{"Async Clipboard tersedia?"}
B -->|yes| C["writeText"]
B -->|no| D["textarea + execCommand fallback"]
C --> E{"Berhasil?"}
D --> E
E -->|yes| F["Umumkan copied dengan aria-live"]
E -->|no| G["Tampilkan panduan copy manual"]
H["Pengguna paste secara eksplisit"] --> I["readText atau onPaste"]
I --> J["Normalize, limit, validate"]
Aturan privasi paling penting: jangan membaca clipboard saat page load atau sebagai aksi tersembunyi. Clipboard bisa berisi password, alamat, URL internal, data pelanggan, atau source code. Read action sebaiknya hanya terjadi lewat tombol paste yang terlihat atau event normal onPaste.
Pisahkan logic copy
Buat utility yang tidak bergantung pada React. Dengan begitu fallback, error, dan test berada di satu tempat.
// src/lib/clipboard.ts
export type CopyResult =
| { ok: true; method: "async-clipboard" | "textarea-fallback" }
| { ok: false; method: "async-clipboard" | "textarea-fallback" | "unsupported"; error: string };
export async function copyText(text: string): Promise<CopyResult> {
if (!text) {
return { ok: false, method: "unsupported", error: "Copy text is empty." };
}
if (canUseAsyncClipboard()) {
try {
await navigator.clipboard.writeText(text);
return { ok: true, method: "async-clipboard" };
} catch (error) {
const fallback = fallbackCopyText(text);
if (fallback) return { ok: true, method: "textarea-fallback" };
return {
ok: false,
method: "async-clipboard",
error: error instanceof Error ? error.message : "Clipboard write was blocked.",
};
}
}
if (fallbackCopyText(text)) {
return { ok: true, method: "textarea-fallback" };
}
return {
ok: false,
method: "unsupported",
error: "Clipboard API is unavailable in this browser or context.",
};
}
function canUseAsyncClipboard(): boolean {
return (
typeof window !== "undefined" &&
window.isSecureContext &&
typeof navigator !== "undefined" &&
Boolean(navigator.clipboard?.writeText)
);
}
function fallbackCopyText(text: string): boolean {
if (typeof document === "undefined") return false;
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.top = "0";
textarea.style.left = "-9999px";
textarea.style.opacity = "0";
const selection = document.getSelection();
const selectedRange =
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
return document.execCommand("copy");
} catch {
return false;
} finally {
document.body.removeChild(textarea);
if (selection && selectedRange) {
selection.removeAllRanges();
selection.addRange(selectedRange);
}
}
}
document.execCommand("copy") adalah API lama, jadi jangan jadikan jalur utama. Namun ia masih berguna sebagai fallback terakhir untuk browser lama, WebView terbatas, atau preview HTTP. Fallback ini juga bisa gagal jika tidak dipicu oleh aksi pengguna.
Hook React dan tombol accessible
Hook berikut menyimpan status copying, copied, dan failed. Komponen memberi feedback melalui role="status" dan aria-live.
// src/components/CopyButton.tsx
import { useCallback, useEffect, useRef, useState } from "react";
import { copyText, type CopyResult } from "../lib/clipboard";
type ClipboardStatus = "idle" | "copying" | "copied" | "failed";
export function useClipboard(resetAfter = 2000) {
const [status, setStatus] = useState<ClipboardStatus>("idle");
const [message, setMessage] = useState("");
const timerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) window.clearTimeout(timerRef.current);
};
}, []);
const copy = useCallback(
async (text: string): Promise<CopyResult> => {
if (timerRef.current) window.clearTimeout(timerRef.current);
setStatus("copying");
setMessage("Copying...");
const result = await copyText(text);
if (result.ok) {
setStatus("copied");
setMessage("Copied to clipboard.");
} else {
setStatus("failed");
setMessage("Copy failed. Select the text and copy it manually.");
}
timerRef.current = window.setTimeout(() => {
setStatus("idle");
setMessage("");
}, resetAfter);
return result;
},
[resetAfter],
);
return { copy, status, message };
}
type CopyButtonProps = {
text: string;
label?: string;
copiedLabel?: string;
className?: string;
};
export function CopyButton({
text,
label = "Copy",
copiedLabel = "Copied",
className = "",
}: CopyButtonProps) {
const { copy, status, message } = useClipboard();
const isCopying = status === "copying";
return (
<div className="inline-flex items-center gap-2">
<button
type="button"
className={className}
onClick={() => void copy(text)}
disabled={isCopying}
aria-label={status === "copied" ? copiedLabel : label}
>
{status === "copied" ? copiedLabel : label}
</button>
<span role="status" aria-live="polite" className="sr-only">
{message}
</span>
</div>
);
}
Toast visual saja tidak cukup. Pengguna screen reader juga perlu tahu apakah aksi berhasil. Region status ini juga membuat assertion Playwright lebih stabil.
Copy code block tanpa layout shift
Use case paling umum adalah code block di dokumentasi atau artikel. Pada implementasi pertama ClaudeCodeLab, Masa melihat tombol berubah dari “Copy” menjadi “Copied”, lebarnya bertambah, lalu area kode ikut bergeser. Minimum width mencegah detail seperti itu.
// src/components/CodeBlockWithCopy.tsx
import { CopyButton } from "./CopyButton";
type CodeBlockWithCopyProps = {
code: string;
language?: string;
};
export function CodeBlockWithCopy({ code, language = "text" }: CodeBlockWithCopyProps) {
return (
<figure className="relative my-6 overflow-hidden rounded-md border border-slate-700 bg-slate-950">
<figcaption className="flex min-h-10 items-center justify-between border-b border-slate-800 px-3 text-xs text-slate-300">
<span>{language}</span>
<CopyButton
text={code}
label="Copy code"
copiedLabel="Copied"
className="min-w-24 rounded bg-slate-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-400 disabled:opacity-60"
/>
</figcaption>
<pre tabIndex={0} className="overflow-x-auto p-4 text-sm leading-6">
<code>{code}</code>
</pre>
</figure>
);
}
Tombol yang sama bisa menyalin command CLI, invite link, coupon, support case ID, atau prompt yang dihasilkan. Minta Claude Code membuat komponen reusable dan contoh penggunaan, bukan solusi yang terkunci pada satu code block.
Perlakukan paste sebagai input sensitif
Untuk form, utamakan event normal onPaste. Gunakan navigator.clipboard.readText() hanya di balik tombol paste yang jelas.
// src/components/PasteImportBox.tsx
import { useState } from "react";
export function normalizePastedText(input: string): string {
return input
.replace(/\r\n?/g, "\n")
.replace(/\u0000/g, "")
.slice(0, 10_000);
}
export function PasteImportBox() {
const [value, setValue] = useState("");
const [message, setMessage] = useState("");
async function pasteFromClipboard() {
if (!navigator.clipboard?.readText || !window.isSecureContext) {
setMessage("Use your browser paste shortcut instead.");
return;
}
try {
const text = await navigator.clipboard.readText();
setValue(normalizePastedText(text));
setMessage("Pasted from clipboard.");
} catch {
setMessage("Paste was blocked. Use Ctrl+V or Cmd+V in the text area.");
}
}
return (
<section aria-labelledby="paste-import-title">
<h2 id="paste-import-title">Import prompt</h2>
<button type="button" onClick={pasteFromClipboard}>
Paste from clipboard
</button>
<textarea
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
onPaste={(event) => {
const text = event.clipboardData.getData("text/plain");
if (!text) return;
event.preventDefault();
setValue(normalizePastedText(text));
setMessage("Pasted text was normalized.");
}}
aria-describedby="paste-import-help"
/>
<p id="paste-import-help" role="status" aria-live="polite">
{message}
</p>
</section>
);
}
Jangan percaya isi paste begitu saja. Batasi panjang, normalize line ending, hapus control character, dan validasi format yang diharapkan. Jika menerima HTML, sanitize sebelum render dan jangan kirim HTML dari clipboard langsung ke dangerouslySetInnerHTML.
Use case dan kegagalan umum
| Use case | Detail implementasi | Kegagalan umum |
|---|---|---|
| Copy kode di docs | Copy hanya string kode dan announce sukses | Label tombol menyebabkan layout shift |
| Copy order ID di admin | Copy hanya ID, bukan seluruh row | Nama atau alamat pelanggan ikut tersalin |
| Paste log ke support tool | Normalize, batasi ukuran, scan secrets | Token atau cookie tersimpan tanpa batas |
| Share invite link | Copy URL yang punya expiry dan tampilkan expiry | Link kedaluwarsa membuat tiket support |
Ini juga relevan untuk produk ClaudeCodeLab. Command di PDF gratis, langkah setup workshop, dan prompt diagnosis untuk konsultasi lebih berguna jika pembaca bisa copy teks yang tepat. Tetapi license key, email pembeli, dan data pelanggan tidak boleh digabung dalam satu tombol copy yang terlalu nyaman.
HTTP, iframe, dan mobile Safari
Async Clipboard modern membutuhkan secure context. HTTPS dan localhost biasanya aman; http://192.168.x.x bisa saja tidak memiliki navigator.clipboard. Coba fallback, lalu tampilkan panduan copy manual jika masih gagal.
Di dalam iframe, browser Chromium bisa membutuhkan Permissions Policy atau atribut allow.
<iframe
src="https://docs.example.com/embed"
allow="clipboard-read; clipboard-write"
title="Documentation preview"
></iframe>
Pada Safari dan iOS WebKit, jaga agar pemanggilan clipboard tetap dekat dengan click atau tap. Hindari menunggu fetch, timer, animasi, atau navigasi sebelum writeText. Siapkan value lebih dulu, copy langsung di handler, lalu update UI. Flow penting sebaiknya diuji di perangkat iOS asli.
Test Playwright untuk clipboard
Berikan permission ke origin yang sama dengan halaman yang dikunjungi. Dokumentasi Playwright menyebut support permission bisa berbeda menurut browser dan versi. Dalam praktik, Chromium bisa memverifikasi isi clipboard asli, sedangkan WebKit bisa memverifikasi feedback UI.
// tests/clipboard.spec.ts
import { expect, test } from "@playwright/test";
const baseURL = "http://127.0.0.1:4173";
test.describe("clipboard UX", () => {
test.beforeEach(async ({ context }) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"], {
origin: baseURL,
});
});
test("copies a code block", async ({ page }) => {
await page.goto(`${baseURL}/docs/install`);
await page.getByRole("button", { name: /copy code/i }).first().click();
await expect(page.getByRole("status")).toContainText(/copied/i);
await expect
.poll(() => page.evaluate(() => navigator.clipboard.readText()))
.toContain("npm");
});
test("normalizes pasted text", async ({ page }) => {
await page.goto(`${baseURL}/support/import`);
await page.evaluate(() => navigator.clipboard.writeText("line1\r\nline2\u0000"));
await page.getByRole("button", { name: /paste from clipboard/i }).click();
await expect(page.getByRole("textbox")).toHaveValue("line1\nline2");
});
});
Jika gagal di CI, periksa origin terlebih dahulu. http://localhost:4173 dan http://127.0.0.1:4173 berbeda. Setelah itu periksa apakah browser project mendukung permission tersebut dan apakah label tombol yang diterjemahkan merusak role query.
Checklist aksesibilitas
- Gunakan
buttonasli, bukandivyang diberi click handler. - Umumkan sukses dan gagal dengan
role="status"danaria-live="polite". - Pakai label spesifik: “Copy code”, “Copy invite link”, “Copy order ID”.
- Pastikan keyboard focus terlihat.
- Beri minimum width agar layout tidak bergeser.
- Saat gagal, jelaskan langkah berikutnya.
- Jangan memakai warna saja untuk menandai sukses.
- Baca clipboard hanya setelah aksi paste yang eksplisit.
Review terarah dengan Claude Code
Setelah implementasi, jangan minta review umum. Gunakan prompt yang menargetkan risiko.
Review only clipboard-related changes.
Check:
1. Clipboard read is never triggered on page load.
2. writeText is called from a user action.
3. HTTP or unsupported browser fallback is handled.
4. copied/error feedback is accessible.
5. pasted text is normalized and size-limited.
6. Playwright tests grant permissions for the correct origin.
Return findings with file and line references.
ClaudeCodeLab sering memakai Web API kecil seperti ini dalam training karena ia memaksa loop engineering yang lengkap: membaca spesifikasi, implementasi, desain fallback, test browser, review aksesibilitas, dan dokumentasi. Untuk template dan panduan praktik, lihat produk ClaudeCodeLab. Untuk adopsi tim dan review workflow, lihat training Claude Code.
Hasil saat dicoba
Pada UI copy code milik Masa, masalah pertama bersifat visual: setelah copy berhasil, tombol menjadi lebih lebar dan code block bergeser. Masalah kedua muncul pada preview mobile lewat HTTP, ketika jalur async clipboard tidak tersedia. Versi stabil memindahkan logic copy ke utility, menambahkan textarea fallback dan panduan copy manual, menetapkan lebar tombol, dan membuat Playwright memberi permission untuk origin yang tepat. Clipboard API memang kecil, tetapi baru siap production ketika permission, privasi, aksesibilitas, dan test dirancang bersama.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission safety ladder Claude Code: perluas akses tanpa kehilangan kontrol
Naik dari read-only ke edit terbatas, command bukti, dan cek deploy dengan kontrol yang jelas.
Claude Code Small PR Proof Pack: perubahan kecil yang mudah direview
Paket bukti untuk PR Claude Code: diff, check, URL publik, jalur CTA, dan rollback.
Review gate Claude Code sebelum commit: diff, test, URL publik, dan CTA
Cara memakai Claude Code sebelum commit: diff scope, build, URL publik, link Gumroad, CTA konsultasi, missing test, dan file tidak terkait.