Bug 222262

Summary: Javascript Clipboard API write() does not work after await
Product: WebKit Reporter: Felipe Ruiz <fruiz>
Component: UI EventsAssignee: Nobody <webkit-unassigned>
Status: NEW ---    
Severity: Major CC: ap, bfulgham, christian, eoconnor, jer.noble, mjs, rniwa, tomac, webkit-bug-importer, wenson_hsieh
Priority: P2 Keywords: InRadar
Version: Safari 14   
Hardware: All   
OS: All   
See Also: https://bugs.webkit.org/show_bug.cgi?id=225559

Description Felipe Ruiz 2021-02-22 01:59:50 PST
I'm using javascript Clipboard API to copy an image to the clipboard. It works in Chrome and Edge but not in Safari in spite of official documentation of Safari says that it's supported.

Check the documentation: https://webkit.org/blog/10855/

In this example (not my real code), write() throws an error:

document.getElementById("copy").addEventListener("click", async function() {
    const response = await fetch('https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png');
    const blob = await response.blob();

    navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
      .then(function () { console.log('copied'); })
      .catch(function (error) { console.log(error); });
});

The given error is:

NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
Comment 1 Ryosuke Niwa 2021-02-22 14:11:28 PST
(In reply to Felipe Ruiz from comment #0)
> I'm using javascript Clipboard API to copy an image to the clipboard. It
> works in Chrome and Edge but not in Safari in spite of official
> documentation of Safari says that it's supported.

The issue is not so much async Clipboard API and the difference in the way user activation is tracked between Blink and WebKit.

> In this example (not my real code), write() throws an error:
> 
> document.getElementById("copy").addEventListener("click", async function() {
>     const response = await
> fetch('https://upload.wikimedia.org/wikipedia/commons/4/47/
> PNG_transparency_demonstration_1.png');
>     const blob = await response.blob();
> 
>     navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
>       .then(function () { console.log('copied'); })
>       .catch(function (error) { console.log(error); });
> });

You need to initiate the write inside click event handler as in:

document.getElementById("copy-html").addEventListener("click", event => {
    navigator.clipboard.write([
        new ClipboardItem({
            "text/png": async () => {
                const response = await fetch('https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png');
                return await response.blob();
            }
        }),
    ]);
});
Comment 2 Felipe Ruiz 2021-02-23 06:46:08 PST
(In reply to Ryosuke Niwa from comment #1)
> (In reply to Felipe Ruiz from comment #0)
> > I'm using javascript Clipboard API to copy an image to the clipboard. It
> > works in Chrome and Edge but not in Safari in spite of official
> > documentation of Safari says that it's supported.
> 
> The issue is not so much async Clipboard API and the difference in the way
> user activation is tracked between Blink and WebKit.
> 
> > In this example (not my real code), write() throws an error:
> > 
> > document.getElementById("copy").addEventListener("click", async function() {
> >     const response = await
> > fetch('https://upload.wikimedia.org/wikipedia/commons/4/47/
> > PNG_transparency_demonstration_1.png');
> >     const blob = await response.blob();
> > 
> >     navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
> >       .then(function () { console.log('copied'); })
> >       .catch(function (error) { console.log(error); });
> > });
> 
> You need to initiate the write inside click event handler as in:
> 
> document.getElementById("copy-html").addEventListener("click", event => {
>     navigator.clipboard.write([
>         new ClipboardItem({
>             "text/png": async () => {
>                 const response = await
> fetch('https://upload.wikimedia.org/wikipedia/commons/4/47/
> PNG_transparency_demonstration_1.png');
>                 return await response.blob();
>             }
>         }),
>     ]);
> });


Hi Ryosuke,

first of all, thanks for your reply. Unfortunately, I have tested your solution and I get the same error message.
Comment 3 Wenson Hsieh 2021-02-23 08:18:24 PST
> > You need to initiate the write inside click event handler as in:
> > 
> > document.getElementById("copy-html").addEventListener("click", event => {
> >     navigator.clipboard.write([
> >         new ClipboardItem({
> >             "text/png": async () => {
> >                 const response = await
> > fetch('https://upload.wikimedia.org/wikipedia/commons/4/47/
> > PNG_transparency_demonstration_1.png');
> >                 return await response.blob();
> >             }
> >         }),
> >     ]);
> > });
> 
> 
> Hi Ryosuke,
> 
> first of all, thanks for your reply. Unfortunately, I have tested your
> solution and I get the same error message.

Hi Felipe,

I adapted the above code as a test case here: https://whsieh.github.io/examples/dice, and it seems to work in Safari on macOS with a couple of adjustments:

- Changing the "text/png" above to "image/png".
- Making the corresponding value for the "image/png" type a Promise instead of an async function.

Please make sure that you're also testing in a secure context (https), where the clipboard API is available.

Thanks!
Wenson
Comment 4 Radar WebKit Bug Importer 2021-03-01 02:00:15 PST
<rdar://problem/74860840>
Comment 5 Thomas Steiner 2021-05-17 04:41:57 PDT
The different implementations unfortunately at the moment force developers to write code like this:

```js
copyButton.addEventListener('click', async () => {
  try {
    // Safari treats user activation differently:
    // https://bugs.webkit.org/show_bug.cgi?id=222262.
    navigator.clipboard.write([
      new ClipboardItem({
        'text/plain': new Promise(async (resolve) => {
          const svg = svgOutput.innerHTML;
          resolve(new Blob([svg], { type: 'text/plain' }));
        }),
      }),
    ]);
  } catch {
    // Chromium
    const svg = svgOutput.innerHTML;    
    const blob = new Blob([svg], { type: 'text/plain' });
    navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  }
});
```

Chromium is working on adding support for delayed generation of clipboard items (see https://crbug.com/1014310).

At the same time it would be great if WebKit didn't expire the user activation.
Comment 6 Alexey Proskuryakov 2022-09-05 12:54:31 PDT
*** Bug 244812 has been marked as a duplicate of this bug. ***