Skip to content

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

Merged
mshabarov merged 11 commits into
mainfrom
feature/clipboard-image
Jun 4, 2026
Merged

feat: Add image/png to the Clipboard write API#24470
mshabarov merged 11 commits into
mainfrom
feature/clipboard-image

Conversation

@Artur-

@Artur- Artur- commented May 29, 2026

Copy link
Copy Markdown
Member

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

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Test Results

 1 434 files  ± 0   1 434 suites  ±0   1h 21m 39s ⏱️ + 1m 3s
10 092 tests +11  10 024 ✅ +11  68 💤 ±0  0 ❌ ±0 
10 564 runs  +11  10 495 ✅ +11  69 💤 ±0  0 ❌ ±0 

Results for commit a4b0cd4. ± Comparison against base commit 6e7a4ec.

♻️ This comment has been updated with latest results.

Comment thread flow-client/src/main/frontend/Clipboard.ts Outdated
mshabarov and others added 4 commits June 3, 2026 19:01
- 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>
@mshabarov

Copy link
Copy Markdown
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.

@mshabarov mshabarov enabled auto-merge June 4, 2026 11:10
@sonarqubecloud

sonarqubecloud Bot commented Jun 4, 2026

Copy link
Copy Markdown

@mshabarov mshabarov added this pull request to the merge queue Jun 4, 2026
Merged via the queue into main with commit b0ce12d Jun 4, 2026
34 checks passed
@mshabarov mshabarov deleted the feature/clipboard-image branch June 4, 2026 11:38
@github-project-automation github-project-automation Bot moved this from 🔎Iteration reviews to Done in Vaadin Flow | Hilla | Kits ongoing work Jun 4, 2026
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
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

Pull request created: #5633

Generated by Documentation Bot · gpt54 5.6M

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

3 participants