Tips & Tricks (Diperbarui: 2/6/2026)

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 dengan Claude Code: tombol copy, permission, fallback, dan test

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 caseDetail implementasiKegagalan umum
Copy kode di docsCopy hanya string kode dan announce suksesLabel tombol menyebabkan layout shift
Copy order ID di adminCopy hanya ID, bukan seluruh rowNama atau alamat pelanggan ikut tersalin
Paste log ke support toolNormalize, batasi ukuran, scan secretsToken atau cookie tersimpan tanpa batas
Share invite linkCopy URL yang punya expiry dan tampilkan expiryLink 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 button asli, bukan div yang diberi click handler.
  • Umumkan sukses dan gagal dengan role="status" dan aria-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.

#Claude Code #Clipboard API #React #Playwright #Accessibility
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.