diff --git a/articles/flow/browser-apis/clipboard-api.adoc b/articles/flow/browser-apis/clipboard-api.adoc
index 94e53aa4d1..bd0d0cbff6 100644
--- a/articles/flow/browser-apis/clipboard-api.adoc
+++ b/articles/flow/browser-apis/clipboard-api.adoc
@@ -1,8 +1,8 @@
---
title: Clipboard
page-title: How to use the Clipboard API in Vaadin
-description: Using the Clipboard API to copy text and HTML to the user's clipboard from the server.
-meta-description: Learn how to copy text and HTML to the user's clipboard in Vaadin applications using the server-side Clipboard API.
+description: Using the Clipboard API to copy text, HTML, and images to the user's clipboard, read clipboard content, and handle paste events from the server.
+meta-description: Learn how to copy text, HTML, and images to the clipboard, read clipboard content, and handle paste events in Vaadin with the Clipboard API.
order: 10
---
@@ -10,7 +10,7 @@ order: 10
= [since:com.vaadin:vaadin@V25.2]#Clipboard#
:toc:
-The [classname]`Clipboard` API lets server-side Java code copy text and HTML to the user's clipboard through the https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API[browser Clipboard API]. Actions are bound to a clickable component and run during the DOM event that fires the underlying click, so the browser sees a fresh user gesture and allows the write.
+The [classname]`Clipboard` API lets server-side Java code copy text, HTML, and images to the user's clipboard through the https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API[browser Clipboard API]. Actions are bound to a clickable component and run during the DOM event that fires the underlying click, so the browser sees a fresh user gesture and allows the write. The same click binding can also <<#reading-the-clipboard,read the clipboard>>, and separate listeners react to browser paste events -- including <<#handling-pasted-files,pasted files>> -- without any click binding.
[NOTE]
The Clipboard API requires a secure context (HTTPS), except on `localhost` during development. Browsers also require the [code]`write` call to happen inside a short-lived user gesture (click, key press, …) -- writes triggered outside of a gesture, for example from a background thread or a timer, will fail with a [code]`NotAllowedError`.
@@ -72,16 +72,39 @@ Clipboard.onClick(copy).writeHtml("Hello, world!");
Most targets fall back to a plain-text representation when only [code]`text/html` is present, but the fallback is browser-dependent. To control both representations explicitly, use the multi-format form below.
+== Copying Images
+
+Use [methodname]`writeImage()` to copy a PNG version of an [code]`` element to the clipboard:
+
+[source,java]
+----
+Image preview = new Image("images/chart.png", "Chart preview");
+Clipboard.onClick(copy).writeImage(preview);
+----
+
+The source can be a same-origin image, a [code]`data:` URL, or any cross-origin image served with matching CORS headers and [code]`crossorigin="anonymous"` on the [code]``. If the image is served from the server, pass a [classname]`DownloadHandler` instead:
+
+[source,java]
+----
+Clipboard.onClick(copy).writeImage(
+ DownloadHandler.forClassResource(MainView.class, "chart.png"));
+----
+
+To include an image together with text or HTML, use [methodname]`ClipboardContent.image()` in the multi-format form.
+
+
== Multi-Format Content
-[classname]`ClipboardContent` packs several MIME types into a single clipboard item, so the paste target can pick the representation it understands. Set the [code]`text/plain` and [code]`text/html` slots independently; at least one must be set.
+[classname]`ClipboardContent` packs several MIME types into a single clipboard item, so the paste target can pick the representation it understands. Set the [code]`text/plain`, [code]`text/html`, and [code]`image/png` slots independently; at least one must be set.
[source,java]
----
+Image preview = new Image("images/chart.png", "Chart preview");
Button copy = new Button("Copy");
Clipboard.onClick(copy).write(ClipboardContent.create()
.text("Hello, world!")
- .html("Hello, world!"));
+ .html("Hello, world!")
+ .image(preview));
----
The [methodname]`text()` setter also accepts a component, with the same client-side read semantics as [methodname]`writeText(Component)`:
@@ -97,6 +120,45 @@ Clipboard.onClick(copy).write(ClipboardContent.create()
[NOTE]
The HTML slot only accepts a literal string -- there is no component overload. If the HTML depends on a field value, build it on the server before binding, or use the plain-text component overload alongside a static HTML literal.
+[NOTE]
+The image slot only accepts a component whose root element is an [code]``. If the image source is not already on the page, use [methodname]`writeImage(DownloadHandler)` to serve it through a hidden image element.
+
+
+[#reading-the-clipboard]
+== Reading the Clipboard
+
+The same [classname]`ClipboardBinding` that writes can also read. The [methodname]`read*` methods read the clipboard via [code]`navigator.clipboard.read()` when the bound click fires and deliver the result to a server-side callback. [methodname]`readText()` delivers the [code]`text/plain` content:
+
+[source,java]
+----
+TextField address = new TextField("Address");
+Button paste = new Button("Paste");
+Clipboard.onClick(paste).readText(
+ text -> address.setValue(text != null ? text : ""),
+ error -> Notification.show("Couldn't read: " + error.message()));
+----
+
+Both consumers are required. The payload consumer receives [code]`null` when the clipboard is empty or has no representation of the requested type. [methodname]`readHtml()` works the same way for the [code]`text/html` representation.
+
+To receive both textual representations at once, use [methodname]`read()`. It delivers a [classname]`ClipboardPayload` record whose [methodname]`text()` and [methodname]`html()` accessors are each [code]`null` when the corresponding MIME type is not present on the clipboard item:
+
+[source,java]
+----
+Clipboard.onClick(paste).read(
+ payload -> {
+ if (payload == null) {
+ Notification.show("Clipboard is empty");
+ } else if (payload.html() != null) {
+ renderHtml(payload.html());
+ } else {
+ renderText(payload.text());
+ }
+ },
+ error -> Notification.show("Couldn't read: " + error.message()));
+----
+
+[NOTE]
+Reading is more restricted than writing: in addition to the user gesture, the browser requires the user to grant the [code]`clipboard-read` permission, typically through a prompt on the first read. If the permission is denied, the read is rejected with a [code]`NotAllowedError`, which is delivered to the [code]`onError` consumer -- see <<#handling-errors,Handling Errors>>.
[#observing-the-outcome]
@@ -123,6 +185,7 @@ Clipboard.onClick(copy).writeText(token,
----
+[#handling-errors]
== Handling Errors
The [methodname]`onError` consumer receives a [classname]`PromiseAction.Error` record with the browser's rejection details:
@@ -157,3 +220,109 @@ The Clipboard API requires every write to happen inside a fresh user gesture. A
- Writes cannot be initiated from a background thread, a scheduled task, or a server push. The gesture must originate in the browser.
- Each click is one gesture. Chaining multiple [methodname]`write*` calls on the same binding attaches multiple actions to the same click, but each one needs to succeed against the browser's gesture budget.
- If the application performs a long server round-trip before the write call, the browser may consider the gesture stale and reject the write.
+
+Reads bound to a click are subject to the same requirement -- the click satisfies the gesture, but the [code]`clipboard-read` permission is still checked separately.
+
+
+[#handling-paste-events]
+== Handling Paste Events
+
+[methodname]`Clipboard.onPaste(component, listener)` registers a server-side listener for the browser's native [code]`paste` event. Unlike the write and read APIs, it needs no click binding -- the listener is attached directly to the component's element and is invoked on the UI thread once per paste gesture targeting the component, or any of its descendants, since [code]`paste` bubbles. The listener receives a [classname]`PasteEvent` carrying the [code]`text/plain` and [code]`text/html` representations of the pasted content:
+
+[source,java]
+----
+Div pasteTarget = new Div();
+pasteTarget.getElement().setAttribute("tabindex", "0");
+add(pasteTarget);
+
+Clipboard.onPaste(pasteTarget, event -> {
+ if (event.hasHtml()) {
+ renderHtml(event.getHtml());
+ } else if (event.hasText()) {
+ renderText(event.getText());
+ }
+});
+----
+
+The call returns a [classname]`Registration`; call [methodname]`remove()` on it to detach the listener. The component does not need to be attached at registration time -- the DOM listener is applied when the element is attached to a UI.
+
+[classname]`PasteEvent` exposes the paste through these methods:
+
+`getText()`, `getHtml()`:: The [code]`text/plain` and [code]`text/html` content, or [code]`null` when the paste did not include that representation. The browser returns an empty string both for an absent MIME type and for a pasted empty string; [classname]`PasteEvent` collapses both into [code]`null`, so [methodname]`hasText()` and [methodname]`hasHtml()` are sufficient checks.
+`getTargetElement()`:: The closest Flow-tracked [classname]`Element` ancestor of the paste's DOM target, resolved against the live DOM in the browser. For a paste inside a Vaadin web component this is typically the component's host element. Use [methodname]`Element.getComponent()` to look up the enclosing component. Returns [code]`null` in the rare case that no Flow-tracked element encloses the target.
+`getSource()`:: The component the listener was registered on.
+
+A few browser caveats apply:
+
+- The browser only fires [code]`paste` when the target element is focused at the moment the user invokes paste. Non-editable elements such as a plain [classname]`Div` must be made focusable, typically via [code]`tabindex="0"`.
+- On editable targets ([code]``, [code]`