Clipboard API avec Claude Code: boutons copier, permissions, fallback et tests
Implémentez Clipboard API avec Claude Code: copier, coller, permissions, fallback, React et Playwright.
La Clipboard API semble triviale: un bouton appelle navigator.clipboard.writeText() et le texte est copié. En production, les choses se compliquent vite. Le code peut échouer sur une URL de prévisualisation en HTTP, dans un iframe, après un fetch trop long, avec des permissions différentes selon le navigateur, ou dans Safari mobile si l’appel n’est plus rattaché à un geste utilisateur récent.
Ce guide montre comment demander à Claude Code une implémentation utilisable en React et TypeScript. Clipboard API désigne l’API du navigateur qui lit ou écrit dans le presse-papiers du système. Async Clipboard est la version basée sur les Promises. Un secure context est un environnement considéré comme sûr par le navigateur, par exemple HTTPS ou localhost. Un fallback est le chemin de secours utilisé quand l’API moderne n’est pas disponible.
À lire aussi sur ClaudeCodeLab: améliorer l’accessibilité avec Claude Code, tester avec Playwright et Claude Code et valider les formulaires avec Claude Code. Gardez les références officielles à portée de main: MDN Clipboard API, MDN writeText, W3C Clipboard API and events, Playwright BrowserContext, WebKit Async Clipboard API et Claude Code docs.
Commencer par les critères d’acceptation
Ne demandez pas simplement à Claude Code de créer un bouton de copie. Donnez-lui directement les contraintes de navigateur, de sécurité et de test.
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.
Le flux cible est le suivant.
flowchart TD
A["L'utilisateur clique sur copier"] --> B{"Async Clipboard disponible ?"}
B -->|yes| C["writeText"]
B -->|no| D["textarea + execCommand fallback"]
C --> E{"Réussite ?"}
D --> E
E -->|yes| F["Annoncer la copie avec aria-live"]
E -->|no| G["Afficher une aide de copie manuelle"]
H["L'utilisateur colle explicitement"] --> I["readText ou onPaste"]
I --> J["Normaliser, limiter, valider"]
La règle de confidentialité est simple: ne lisez jamais le presse-papiers au chargement de la page ou en arrière-plan. Il peut contenir un mot de passe, une adresse, une URL interne, des données client ou du code privé. La lecture doit venir d’un bouton visible de collage ou d’un événement normal onPaste.
Isoler la logique de copie
Créez d’abord une fonction utilitaire indépendante de React. Les fallbacks, erreurs et tests restent ainsi au même endroit.
// 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") est obsolète. Il ne doit pas devenir la voie principale, mais reste utile comme dernier recours dans d’anciens navigateurs, des webviews limitées ou des pages de test en HTTP. Il peut lui aussi échouer si l’appel n’est pas déclenché par une action utilisateur.
Hook React et bouton accessible
Le hook gère les états “copie en cours”, “copié” et “échec”. Le composant annonce le résultat avec role="status" et 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>
);
}
Un toast visuel ne suffit pas. Les utilisateurs de lecteurs d’écran doivent aussi recevoir le retour d’état. Cette zone de statut donne en plus une cible stable aux tests Playwright.
Copier les blocs de code sans décalage
Le cas d’usage le plus fréquent est le bloc de code dans une documentation. Dans une première version de ClaudeCodeLab, Masa a vu le bouton s’élargir après le passage de “Copy” à “Copied”, ce qui décalait la zone de code. Une largeur minimale règle ce problème.
// 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>
);
}
Le même bouton peut copier une commande CLI, un lien d’invitation, un coupon, un identifiant de ticket ou un prompt généré. Demandez à Claude Code un composant réutilisable, puis des exemples d’utilisation.
Le collage est une entrée sensible
Dans un formulaire, préférez l’événement onPaste du navigateur. Réservez navigator.clipboard.readText() à un bouton de collage clairement visible.
// 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>
);
}
Ne faites pas confiance au texte collé. Limitez sa taille, normalisez les retours à la ligne, supprimez les caractères de contrôle et validez le format attendu. Si vous acceptez du HTML, nettoyez-le avant rendu et ne l’envoyez jamais directement dans dangerouslySetInnerHTML.
Cas d’usage et pièges concrets
| Cas d’usage | Point d’implémentation | Échec fréquent |
|---|---|---|
| Copier du code dans une doc | Copier uniquement la chaîne de code et annoncer le succès | Le libellé du bouton provoque un décalage |
| Copier un ID de commande | Copier l’ID, pas toute la ligne | Nom ou adresse client inclus par erreur |
| Coller des logs support | Limiter, normaliser et détecter les secrets | Tokens ou cookies stockés sans limite |
| Partager un lien d’invitation | Copier une URL à durée limitée et afficher l’expiration | Des liens expirés créent du support inutile |
Ce point compte aussi pour ClaudeCodeLab. Les commandes d’un PDF gratuit, les étapes d’un atelier ou un modèle de diagnostic pour une mission de conseil doivent être copiés exactement. En revanche, clés de licence, e-mails d’acheteurs et données clients ne doivent pas être regroupés dans un bouton de copie trop pratique.
HTTP, iframe et Safari mobile
L’Async Clipboard moderne exige un secure context. HTTPS et localhost fonctionnent généralement, mais une URL http://192.168.x.x peut ne pas exposer navigator.clipboard. Essayez le fallback, puis affichez une consigne de copie manuelle si nécessaire.
Dans un iframe, les navigateurs Chromium peuvent demander une Permissions Policy ou l’attribut allow.
<iframe
src="https://docs.example.com/embed"
allow="clipboard-read; clipboard-write"
title="Documentation preview"
></iframe>
Avec Safari et WebKit sur iOS, gardez l’appel au presse-papiers très proche du clic ou du tap. Évitez d’attendre un fetch, un timer, une animation ou une navigation avant writeText. Préparez la valeur avant le clic, copiez immédiatement dans le handler, puis mettez à jour l’interface. Les parcours critiques doivent être testés sur de vrais appareils iOS.
Tests Playwright pour la Clipboard API
Accordez les permissions au même origin que celui visité. Playwright précise que le support des permissions varie selon les navigateurs et versions. En pratique, Chromium peut vérifier le contenu réel du presse-papiers, tandis que WebKit peut vérifier le retour visuel.
// 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");
});
});
En CI, vérifiez d’abord l’origin. http://localhost:4173 et http://127.0.0.1:4173 ne sont pas identiques. Vérifiez ensuite le support des permissions par projet de navigateur et les libellés traduits utilisés dans vos queries par rôle.
Checklist accessibilité
- Utiliser un vrai
button, pas undivcliquable. - Annoncer succès et échec avec
role="status"etaria-live="polite". - Employer des libellés précis: “Copy code”, “Copy invite link”, “Copy order ID”.
- Garder le focus clavier visible.
- Définir une largeur minimale pour éviter les décalages.
- En cas d’échec, indiquer l’action suivante.
- Ne pas communiquer le succès uniquement par la couleur.
- Lire le presse-papiers seulement après une action explicite de collage.
Faire relire les risques par Claude Code
Après l’implémentation, utilisez une demande de revue ciblée.
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 utilise ce type de petite API Web en formation parce qu’elle force une boucle complète: lecture de spécification, implémentation, fallback, tests navigateur, revue accessibilité et documentation. Pour les modèles et guides, consultez les produits ClaudeCodeLab. Pour l’adoption en équipe, consultez la formation Claude Code.
Résultat après essai
Dans l’UI de copie de code de Masa, le premier problème était visuel: le bouton s’élargissait après la copie et décalait le bloc. Le second est apparu sur une prévisualisation mobile servie en HTTP, où l’Async Clipboard n’était pas disponible. La version stable a extrait la copie dans une fonction utilitaire, ajouté le fallback textarea, affiché une aide de copie manuelle, fixé la largeur du bouton et donné à Playwright les permissions pour l’origin exact. Clipboard API est une petite fonctionnalité, mais elle n’est prête pour la production que si permissions, confidentialité, accessibilité et tests sont pensés ensemble.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.