feat: Add image/png to the Clipboard write API#24470
Merged
Merged
Conversation
Adds writeImage(Component) — fire-and-forget and observed flavours — on
ClipboardBinding, plus image(Component) on ClipboardContent for the
multi-format path. The source can be any rasterisable image
({@code image/png}, {@code image/jpeg}, {@code image/svg+xml}, ...) with
intrinsic dimensions; cross-origin sources need
{@code crossorigin="anonymous"} on the {@code <img>} plus matching CORS
headers, otherwise the canvas is tainted and the write fails.
Internals:
- ImageBlobInput extends Action.Input<Object>; its toJs yields the
source <img> element verbatim.
- WriteToClipboardAction gains a third (image) slot; the rendered
helper call is writePayload($0(event), $1(event), $2(event)).
- The TS helper writeClipboardPayload takes an HTMLImageElement third
argument and re-encodes it via imageToPngBlob (canvas + toBlob).
The resulting Promise<Blob> is fed directly to ClipboardItem so the
navigator.clipboard.write call stays synchronous inside the user
gesture — Safari otherwise loses activation on the first await.
Tests cover the image/png slot and the multi-format case with all three
slots together.
…ardAction Restores the original 2-arg (text, html) and 4-arg observed constructors that main had, and adds matching 1-arg (image) and 3-arg observed image constructors. The 3-arg / 5-arg multi-format constructors stay as the underlying implementation that the dedicated overloads delegate into via this(...). Callers in the typical text-only, html-only, or image-only shapes no longer need to pass null placeholders for the unused slots. ClipboardBinding's write* methods, SignalInput/LiteralInput Javadoc snippets, SignalInputTest and TriggerWriteToClipboardView are all adjusted to use the dedicated overloads.
ClipboardContent is now a passive data holder with public slot getters; WriteToClipboardAction gains constructors that accept a ClipboardContent and read its slots. The text+html and image dedicated constructors delegate into private 3-arg / 5-arg primitives, so the previously public multi-format Action.Input constructors disappear from the API surface. ClipboardBinding.write(content[, callbacks]) now constructs the action as new WriteToClipboardAction(content[, …]). The two existing test cases that called the input-based 3-arg constructor directly now go through ClipboardContent.
Adds writeImage(DownloadHandler) and its observed counterpart on ClipboardBinding so server-defined image bytes can be copied to the clipboard without the caller having to add a hidden Image to the page themselves. The overload appends a display:none <img> child to the trigger host, bound to the handler via the same setAttribute path Image.setSrc(DownloadHandler) uses, then routes it through ImageBlobInput. ImageBlobInput gains an Element-accepting constructor so the binding can hand it the freshly built <img> element without wrapping it in a Component. The browser begins fetching the image as soon as the binding is set up, so the bytes are typically decoded before the user clicks. If the click races the load, ImageBlobInput's canvas converter falls back to the <img>'s load event before drawing.
Adds three IT scenarios to TriggerWriteToClipboardView/IT:
- writeImage(Image) with an in-DOM data-URL <img>
- write(ClipboardContent.text + image) packing both into one
ClipboardItem
- writeImage(DownloadHandler) with a server-served PNG generated
at view-class load via ImageIO
The recording shim in the IT awaits Promise<Blob> entries from the
ClipboardItem and normalises them to {type, size}, so the assertions
can inspect the resulting image/png blob without dealing with binary
content.
Restructures TriggerWriteToClipboardView into headed sections, each
with a one-line description of what should land in the clipboard so a
manual tester can paste into an external app and verify. Adds:
- visible 32x32 image sources (was 4x4; too small to see)
- a distinct blue image for writeImage(DownloadHandler), so a
pasted result makes it obvious which button was used
- a "Copy text + html + image" button exercising all three slots in
one ClipboardItem, with a matching IT case
mshabarov
requested changes
Jun 3, 2026
- writeImage observed overloads now take a SerializableRunnable for the success callback instead of an always-null String consumer, matching the Fullscreen API convention for value-less promise outcomes. - Drop the silent DownloadHandler.inline() mutation in attachHiddenImg; writeImage(DownloadHandler) now assumes a downloadable image handler. - Make ClipboardContent.getTextInput/getHtmlInput/getImageInput package-private again; ClipboardBinding extracts the inputs and passes them to the now-public multi-input WriteToClipboardAction constructors, removing the ClipboardContent dependency from the internal action. - Document IllegalArgumentException on the image accessors that reject a non-<img> source. - Fix a silent hang in Clipboard.ts: a broken/empty <img> is already complete with naturalWidth 0, so its load/error events never fire again; settle the promise immediately instead of waiting forever. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ature/clipboard-image
mshabarov
approved these changes
Jun 4, 2026
Contributor
|
Need to test the docs bot workflow so pushed this forward by myself. If something doesn't seem correct let me know we'll fix later. |
|
vaadin-bot
added a commit
to vaadin/docs
that referenced
this pull request
Jun 4, 2026
Source PR: feat: Add image/png to the Clipboard write API Categories: NEW_FEATURE, API_CHANGE, BEHAVIOR_CHANGE
|
Pull request created: #5633
|
Artur-
added a commit
that referenced
this pull request
Jun 9, 2026
…24551) This PR cherry-picks changes from the original PR #24470 to branch 25.2. --- #### Original PR description > ## Summary > > - Lets a Vaadin app copy an image to the system clipboard as `image/png` when a click (or any trigger) fires, alongside the existing `text/plain` and `text/html` slots that `ClipboardBinding` already supports. > - Two image sources are supported: an `<img>`-rooted component already on the page, or a `DownloadHandler` that serves bytes from the server. > - Supports image-only writes and multi-format writes that pack text, HTML, and an image into a single `ClipboardItem`. > > ## Details > > - **Safari activation, two places to watch.** The TS helper hands the canvas-produced `Promise<Blob>` directly to `ClipboardItem` without awaiting it, so `navigator.clipboard.write` is called synchronously inside the user gesture. The `DownloadHandler` flavour also preloads the bytes via a hidden `<img>` attached at bind time, on purpose: fetching at click time would push the promise resolution past Safari's transient activation window and the write would abort silently. > - **`ClipboardContent` getters are public on the surface, returning internal `Action.Input<?>` types.** This is the price for `WriteToClipboardAction(ClipboardContent)` working across package boundaries; `Action.Input` is already documented "for internal use only" so the leak is contained. > - **`ImageBlobInput` rejects non-`<img>` sources at bind time** rather than letting the canvas converter produce an opaque error in the browser later. > - **Tests:** unit coverage on the new slot, the multi-format case, and the no-op stand-in for unset slots; IT coverage end-to-end via a 32×32 red PNG generated at view load (data-URL `<img>` plus the multi-format path) and a distinct blue PNG served by the `DownloadHandler` case. The IT view is sectioned and labelled so it doubles as a manual smoke-test page. > > ## API Changes: feature/clipboard-image vs origin/main > > 4 classes affected, 1 class added, 5 methods added, 4 constructors added. > > ### com.vaadin.flow.component.clipboard.ClipboardBinding > > ```java > // Added > public void writeImage(Component source) // copy component's root <img> as image/png > public void writeImage(Component source, SerializableConsumer<@nullable String> onCopied, SerializableConsumer<PromiseAction.Error> onError) > public void writeImage(DownloadHandler handler) // serve image bytes via a hidden <img> bound to the handler > public void writeImage(DownloadHandler handler, SerializableConsumer<@nullable String> onCopied, SerializableConsumer<PromiseAction.Error> onError) > ``` > > ### com.vaadin.flow.component.clipboard.ClipboardContent > > ```java > // Added > public ClipboardContent image(Component source) // set the image/png slot to a component's root <img> > public Action.@nullable Input<String> getTextInput() // visibility raised from package-private > public Action.@nullable Input<String> getHtmlInput() // visibility raised from package-private > public Action.@nullable Input<?> getImageInput() // new slot accessor > ``` > > ### com.vaadin.flow.component.trigger.internal.ImageBlobInput > > ```java > // Added > public class ImageBlobInput extends Action.Input<Object> // internal; renders the source <img> for the clipboard write helper > public ImageBlobInput(Component source) // throws IllegalArgumentException if source's root element is not <img> > public ImageBlobInput(Element source) // throws IllegalArgumentException if source is not <img> > protected JsFunction toJs(Trigger trigger) > ``` > > ### com.vaadin.flow.component.trigger.internal.WriteToClipboardAction > > ```java > // Added > public WriteToClipboardAction(Action.Input<?> imageInput) // dedicated image fire-and-forget > public WriteToClipboardAction(Action.Input<?> imageInput, SerializableConsumer<@nullable String> onCopied, SerializableConsumer<PromiseAction.Error> onError) // dedicated image observed > public WriteToClipboardAction(ClipboardContent content) // multi-format fire-and-forget > public WriteToClipboardAction(ClipboardContent content, SerializableConsumer<@nullable String> onCopied, SerializableConsumer<PromiseAction.Error> onError) // multi-format observed > ``` > > Note: The existing `Action.Input`-based constructors `(text, html)` and `(text, html, onCopied, onError)` keep their main signatures. The 3-arg and 5-arg multi-format input constructors are `private` and not part of the public surface. `WriteToClipboardAction` and `ImageBlobInput` live in `com.vaadin.flow.component.trigger.internal`, documented "For internal use only. May be renamed or removed in a future release." > Co-authored-by: Artur Signell <artur@vaadin.com> Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Co-authored-by: mikhail <mikhail@vaadin.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
image/pngwhen a click (or any trigger) fires, alongside the existingtext/plainandtext/htmlslots thatClipboardBindingalready supports.<img>-rooted component already on the page, or aDownloadHandlerthat serves bytes from the server.ClipboardItem.Details
Promise<Blob>directly toClipboardItemwithout awaiting it, sonavigator.clipboard.writeis called synchronously inside the user gesture. TheDownloadHandlerflavour also preloads the bytes via a hidden<img>attached at bind time, on purpose: fetching at click time would push the promise resolution past Safari's transient activation window and the write would abort silently.ClipboardContentgetters are public on the surface, returning internalAction.Input<?>types. This is the price forWriteToClipboardAction(ClipboardContent)working across package boundaries;Action.Inputis already documented "for internal use only" so the leak is contained.ImageBlobInputrejects non-<img>sources at bind time rather than letting the canvas converter produce an opaque error in the browser later.<img>plus the multi-format path) and a distinct blue PNG served by theDownloadHandlercase. The IT view is sectioned and labelled so it doubles as a manual smoke-test page.API Changes: feature/clipboard-image vs origin/main
4 classes affected, 1 class added, 5 methods added, 4 constructors added.
com.vaadin.flow.component.clipboard.ClipboardBinding
com.vaadin.flow.component.clipboard.ClipboardContent
com.vaadin.flow.component.trigger.internal.ImageBlobInput
com.vaadin.flow.component.trigger.internal.WriteToClipboardAction
Note: The existing
Action.Input-based constructors(text, html)and(text, html, onCopied, onError)keep their main signatures. The 3-arg and 5-arg multi-format input constructors areprivateand not part of the public surface.WriteToClipboardActionandImageBlobInputlive incom.vaadin.flow.component.trigger.internal, documented "For internal use only. May be renamed or removed in a future release."