Skip to content

feat: Add image/png to the Clipboard write API (#24470) (CP: 25.2)#24551

Merged
Artur- merged 1 commit into
25.2from
cherry-pick-24470-to-25.2-1780996578525
Jun 9, 2026
Merged

feat: Add image/png to the Clipboard write API (#24470) (CP: 25.2)#24551
Artur- merged 1 commit into
25.2from
cherry-pick-24470-to-25.2-1780996578525

Conversation

@vaadin-bot

Copy link
Copy Markdown
Collaborator

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

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

## 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: 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>
@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

@github-actions github-actions Bot added the +0.1.0 label Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Test Results

 1 437 files  ± 0   1 437 suites  ±0   1h 23m 9s ⏱️ -59s
10 100 tests +11  10 032 ✅ +11  68 💤 ±0  0 ❌ ±0 
10 572 runs  +11  10 503 ✅ +11  69 💤 ±0  0 ❌ ±0 

Results for commit 09277a7. ± Comparison against base commit 096c2bf.

@Artur- Artur- enabled auto-merge (squash) June 9, 2026 09:32
@Artur- Artur- merged commit b513b5e into 25.2 Jun 9, 2026
33 checks passed
@Artur- Artur- deleted the cherry-pick-24470-to-25.2-1780996578525 branch June 9, 2026 09:33
@vaadin-bot

Copy link
Copy Markdown
Collaborator Author

This ticket/PR has been released with Vaadin 25.2.0-beta2.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants