How to use Clipboard API in Safari async

Safari support the Clipboard API, but only when directly triggered by a user interaction. You can't write to the Clipboard in an async context. Or can you?

How to use Clipboard API in Safari async
Photo by Ashim D’Silva / Unsplash

Updated: see Copy to Clipboard async using Clipboard API for a solution that works across Chrome, Safari and Firefox.

Some strings attached

Safari supports the Clipboard API for some time now, but it locks it down to only work when triggered by a direct user interaction. That sounds like a sensible precaution to keep malicious sites from messing with your clipboard contents:
You can't use the Clipboard API detached from user interaction in Safari. You can't even use it async in a Promise that was triggered by a user interaction.

I ran into an issue, where I tried to fetch something and copy the response into the clipboard. This works fine in Chrome and Firefox:

fetch(someUrl)
  .then(response => response.text())
  .then(text => navigator.clipboard.writeText(text))

But it does not work in Safari unfortunately, because the security measures kick in when we are trying to use the Clipboard API in an async context. The error messages is as eloquent as useless: Unhandled Promise Rejection: NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

Make it work even with async functions

But not all hope is lost! After some digging I found something promising on Apple's developer forums. You can wrap the Promise in a way that Safari (and Chrome) would accept.

The solution is to not call the Clipboard API in an async context, but rather turn things around and give the Clipboard API the async Promise. This way the Clipboard API is called from a synchronous context directly triggered by user interaction. And the Clipboard API will deal with resolving the Promise internally.
This involves creating a ClipboardItem and adding this to the clipboard instead of adding plain text to the clipboard.

const text = new ClipboardItem({
  "text/plain": fetch(this.sourceUrlValue)
    .then(response => response.text())
    .then(text => new Blob([text], { type: "text/plain" }))
})
navigator.clipboard.write([text])

Caveat: Not working in Firefox

Well, at least not out of the box. When testing my new solution, that worked so nicely with Safari and Chrome, on Firefox I was presented with yet another error message: ReferenceError: ClipboardItem is not defined.

The ClipboardItem class I used to wrap the Promise is supported by Firefox, but is behind the dom.events.asyncClipboard.clipboardItem preference, which is turned off by default.
Back to square one, I guess. I'm going to try fixing this for Firefox next, stay tuned.

Update: see Copy to Clipboard async using Clipboard API for a solution that works across Chrome, Safari and Firefox.