navigator.clipboard.writeText fails in Safari

I'm porting an extension that works in Chrome and Firefox.

The MDN page here: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#clipboard_availability claims that this interface is supported in Safari.

But when I call it it throws an exception with no helpful information in the error object.

In Chrome and Firefox no special permissions are required to use clipboard write (at least on active pages). Is this permission needed in Safari?

I am calling it from a popup menu on an active page (where I have a content script and permissions).

Has anyone used navigator.clipboard.writeText in a Safari Web Extension? If so, did you have to request the permission in the manifest?

Update: I have tried adding the "clipboardWrite" permission to the manifest and it still fails in Safari [Version 15.0 (16612.1.29.41.4, 16612)]

Update2: I found this article which talks about the subject but doesn't give details on how to make it actually work: https://portswigger.net/daily-swig/new-safari-clipboard-api-includes-additional-browser-security-privacy-mechanisms

Update3: I found more info here: https://webkit.org/blog/10247/new-webkit-features-in-safari-13-1/ It says that the API "must be called within user gesture event handlers like pointerdown or pointerup, and only works for content served in a secure context (e.g. https://)"

That does not sound promising. My whole extension is for building citations which are copied to the clipboard. They a built procedurally when the user selects a menu item on the popup menu. I'm thinking I will have to scrap the port to Safari :(

It sounds like it may be possible using hacky solutions of creating a dummy element on a page and using execCommand('copy') which sounds horrible and not something that I want to pollute my extension with.

If anyone has any ideas on how to make it work please respond.

Replies

After further investigation it appears that navigator.clipboard.writeText will work in some code called from an extensions popup menu item.

By a process of elimination it appears that what causes it to fail is if the function called by clicking the menu item calls any code that has been loaded from a module.

So if all the code is in .js files loaded by popup.html it works. But in my case I load some code from an .mjs file which generates the citation. I structure it this way so that I can use a node.js test suite for my unit tests. Since my extension code is very modular and consists of many .mjs files it would be a big job to restructure it so all of the code could be included in popup.html as .js files. Structuring it this way also improves the performance of bringing up the popup menu - it doesn't need to load all of the code that will be used by all of the menu items.

It is possible that it is not just the fact that I'm calling code from a module. It could be something specific that I am doing in that module. I haven't gone through the process of creating a dummy module that does very little to confirm.

So for the Safari version I have a fallback. If the call to navigator.clipboard.writeText throws an exception I then change my popup to display the generated text with a button "Save to clipboard". Then when the user presses that button navigator.clipboard.writeText works.

Hey there! I have been facing a similar issue in my job, but I figured out a working solution (I had to dig a LOT to find it).

The main issue is that Apple's Clipboard API expects a Promise when writing something to the clipboard. writeText takes just a string. I wish writeText worked for me since it would make the component logic a lot cleaner, but I managed to get to get this logic working using write

Here is my fix:

const clipboardItem = new ClipboardItem({
    'text/plain': someAsyncMethod().then((result) => {

    /**
     * We have to return an empty string to the clipboard if something bad happens, otherwise the
     * return type for the ClipBoardItem is incorrect.
     */
    if (!result) {
        return new Promise(async (resolve) => {
            resolve(new Blob[``]())
        })
    }

    const copyText = `some string`
        return new Promise(async (resolve) => {
            resolve(new Blob([copyText]))
        })
    }),
})
// Now, we can write to the clipboard in Safari
navigator.clipboard.write([clipboardItem])

I adapted this code a bit from the actual business logic since it wouldn't make sense without context. But basically, to get get the Clipboard API working, you need to:

  1. Create a new ClipboardItem instance
  2. The ClipboardItem can have "regular" logic in the method body (do whatever you need to here)
  3. IMPORTANT PART: the method body for the new ClipboardItem should return a new Promise that contains resolve(new Blob([<DATA_TO_COPY>])