Skip to content

feat: Add image/png to the Clipboard write API#24470

Open
Artur- wants to merge 7 commits into
mainfrom
feature/clipboard-image
Open

feat: Add image/png to the Clipboard write API#24470
Artur- wants to merge 7 commits into
mainfrom
feature/clipboard-image

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 29, 2026

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

// 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

// 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

// 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

// 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."

Artur- added 6 commits May 28, 2026 17:09
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
@Artur- Artur- changed the title feat: add Clipboard image copy feat: Add image/png to the Clipboard write API May 29, 2026
@Artur- Artur- marked this pull request as ready for review May 29, 2026 08:37
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 29, 2026

Test Results

 1 430 files  ± 0   1 430 suites  ±0   1h 24m 51s ⏱️ +7s
10 063 tests +11   9 995 ✅ +11  68 💤 ±0  0 ❌ ±0 
10 535 runs  +11  10 466 ✅ +11  69 💤 ±0  0 ❌ ±0 

Results for commit 89c02ce. ± Comparison against base commit b238ec1.

♻️ This comment has been updated with latest results.

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: 🔎Iteration reviews

Development

Successfully merging this pull request may close these issues.

2 participants