Skip to content

CopyButton: clipboard write fails in Safari when url prop requires async fetch #3378

@djchrisssssss

Description

@djchrisssssss

Description

The Copy to clipboard button in the Export section of a record's landing page does not work in Safari. Clicking it produces:

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.

This was reported downstream in zenodo/zenodo-rdm#1266.

Root Cause

In CopyButton.js, the SimpleCopyButton.handleClick method is async and calls await this.fetchUrl(url) before navigator.clipboard.writeText(). Safari requires clipboard writes to occur synchronously within the user-gesture call stack. The await fetch() suspends execution, so the subsequent writeText() is no longer in the user gesture's context, and Safari rejects it with NotAllowedError.

// Current broken code (for Safari)
handleClick = async () => {
    const { url, text, onCopy } = this.props;
    let textToCopy = text;
    if (url) {
      textToCopy = await this.fetchUrl(url);  // breaks user-gesture chain
    }
    await navigator.clipboard.writeText(textToCopy);  // Safari rejects
    onCopy(text);
};

DOI/Citation copy buttons work fine because they pass text directly (no fetch needed). Only the Export copy button is affected because it uses the url prop, triggering the async fetch.

Introduced In

PR #3106 (merged July 16, 2025) replaced react-copy-to-clipboard with direct navigator.clipboard.writeText() calls. The react-copy-to-clipboard library handled this case correctly; the replacement did not account for Safari's stricter clipboard security model.

Proposed Fix

Use the ClipboardItem API, which accepts a Promise as its value. This keeps navigator.clipboard.write() synchronous within the user gesture while resolving the fetch data asynchronously inside the Clipboard API's internal handling:

handleClick = () => {
    const { url, text, onCopy } = this.props;
    if (url) {
      const textPromise = fetch(url).then((r) => r.text());
      if (typeof ClipboardItem !== "undefined") {
        // Safari & Chrome: ClipboardItem accepts a Promise
        const item = new ClipboardItem({
          "text/plain": textPromise.then(
            (t) => new Blob([t], { type: "text/plain" })
          ),
        });
        navigator.clipboard.write([item]).then(() => onCopy(text));
      } else {
        // Firefox fallback: ClipboardItem may not support Promise values
        textPromise.then((t) =>
          navigator.clipboard.writeText(t).then(() => onCopy(text))
        );
      }
    } else {
      navigator.clipboard.writeText(text).then(() => onCopy(text));
    }
};

Reference: How to use Clipboard API in Safari

Affected File

invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/components/CopyButton.js

Browser Compatibility

Browser ClipboardItem with Promise async/await + writeText after fetch
Safari 13.1+ ❌ (user gesture lost)
Chrome 76+
Firefox 127+ ⚠️ (needs fallback)

I'll submit a PR with the fix shortly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions