Skip to content

fix(components): use ClipboardItem API for Safari-compatible clipboard writes#3379

Open
djchrisssssss wants to merge 2 commits into
inveniosoftware:masterfrom
djchrisssssss:fix/safari-clipboard-copy
Open

fix(components): use ClipboardItem API for Safari-compatible clipboard writes#3379
djchrisssssss wants to merge 2 commits into
inveniosoftware:masterfrom
djchrisssssss:fix/safari-clipboard-copy

Conversation

@djchrisssssss

Copy link
Copy Markdown

Summary

  • Fix CopyButton export copy failing in Safari with NotAllowedError
  • Replace async/await clipboard pattern with synchronous ClipboardItem + Promise API
  • Add fallback to writeText() for browsers without ClipboardItem support

Problem

PR #3106 replaced react-copy-to-clipboard with direct navigator.clipboard.writeText() calls. When the url prop is provided (Export section), await fetch(url) suspends execution and breaks the user-gesture call stack. Safari's Clipboard API requires clipboard.write() to be called synchronously within the user gesture context, so it rejects the write with:

NotAllowedError: The request is not allowed by the user agent or the platform
in the current context, possibly because the user denied permission.

DOI/Citation copy buttons are unaffected because they pass text directly (no fetch needed).

Solution

Use the ClipboardItem API which accepts a Promise as its value. This keeps navigator.clipboard.write() synchronous within the user gesture, while the Clipboard API internally resolves the fetch Promise:

const item = new ClipboardItem({
  "text/plain": fetch(url)
    .then((r) => r.text())
    .then((t) => new Blob([t], { type: "text/plain" })),
});
navigator.clipboard.write([item]);

A fallback using writeText() is included for browsers where ClipboardItem is not available (e.g., older Firefox).

Reference: How to use Clipboard API in Safari

Browser Compatibility

Browser ClipboardItem with Promise Previous async/await pattern
Safari 13.1+
Chrome 76+
Firefox 127+ ✅ (fallback provided)

Test Plan

  • Verify Export "Copy to clipboard" works in Safari
  • Verify Export "Copy to clipboard" still works in Chrome
  • Verify Export "Copy to clipboard" still works in Firefox
  • Verify DOI copy button still works in all browsers
  • Verify Citation copy button still works in all browsers

Closes #3378
Refs zenodo/zenodo-rdm#1266

🤖 Generated with Claude Code

…d writes

Safari requires clipboard writes to occur synchronously within the
user-gesture call stack. The previous async/await pattern with fetch()
broke this chain, causing NotAllowedError in Safari when copying export
content that requires fetching from a URL.

Replace async handleClick with synchronous ClipboardItem + Promise
pattern. ClipboardItem accepts a Promise value, keeping the
clipboard.write() call in the user-gesture context while resolving
data internally. Includes a fallback to writeText() for browsers
where ClipboardItem is not available.

Closes inveniosoftware#3378
Refs zenodo/zenodo-rdm#1266

@Samk13 Samk13 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, the Safari issue and the use of ClipboardItem look correct to me.

One suggestion: you can simplify the control flow slightly by resolving the source upfront, which removes the duplication between the two clipboard paths:

handleClick = () => {
  const { url, text, onCopy } = this.props;

  // Resolve what to copy (URL fetch or direct text)
  const dataPromise = url
    ? fetch(url).then((r) => r.text())
    : Promise.resolve(text);

  if (typeof ClipboardItem !== "undefined") {
    // Safari-safe: keep clipboard.write() synchronous
    const item = new ClipboardItem({
      "text/plain": dataPromise.then(
        (t) => new Blob([t], { type: "text/plain" })
      ),
    });

    navigator.clipboard.write([item]).then(() => onCopy(text));
  } else {
    // Fallback for browsers without ClipboardItem
    dataPromise.then((t) =>
      navigator.clipboard.writeText(t).then(() => onCopy(text))
    );
  }
};

WDYT?

Address review feedback from @Samk13: unify the url-fetch and
direct-text paths into a single dataPromise, removing the outer
if/else duplication. Also fix prettier formatting for the
ClipboardItem callback.

@djchrisssssss djchrisssssss left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Samk13! Great suggestion — adopted the unified dataPromise approach in the follow-up commit. Changes:

  1. Unified dataPromise: Merged the url fetch and direct text paths into a single Promise, removing the outer if/else duplication — exactly as you suggested.
  2. Removed dead fetchUrl() method: It was no longer referenced after the initial commit.
  3. Prettier fix: Collapsed the ClipboardItem callback to satisfy the project's prettier config.
  4. Linter pass: Ran run-js-linter.sh — CopyButton.js has zero errors/warnings.

Note: I used a follow-up commit (rather than amend) so the incremental diff is easier to review. Happy to squash before merge if preferred.

@Samk13

Samk13 commented Mar 24, 2026

Copy link
Copy Markdown
Member

@djchrisssssss Could you confirm this contribution aligns with the InvenioRDM copyright policy regarding ownership and attribution?
If it was developed in an employment context, please make sure the appropriate copyright holder is correctly reflected in the commit metadata and contribution record.

@djchrisssssss djchrisssssss force-pushed the fix/safari-clipboard-copy branch from 8bc43ef to 3e3a1bf Compare March 24, 2026 15:10
@djchrisssssss

Copy link
Copy Markdown
Author

Yes — this contribution was developed by me personally, outside any employment context, and I have the right to license it under the repository's MIT license. I've also updated the commit metadata accordingly.

@djchrisssssss

Copy link
Copy Markdown
Author

Hi @Samk13, just checking in — is there anything else needed from my side for this PR? I've adopted your dataPromise suggestion, removed the dead fetchUrl() method, and confirmed the linter passes cleanly. Happy to make further changes if needed.

@djchrisssssss

Copy link
Copy Markdown
Author

Hi @kpsherva, would you be able to take a look at this PR when you get a chance? It's a small fix for a Safari clipboard bug introduced in #3106 — the CopyButton export copy fails because async/await with fetch() breaks Safari's user-gesture requirement for clipboard writes.

The fix uses ClipboardItem with a Promise to keep clipboard.write() synchronous within the user gesture. @Samk13 already reviewed and suggested a simplification which has been applied. Linter passes cleanly. Thanks!

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

This PR was automatically marked as stale.

@github-actions github-actions Bot added the stale No activity for more than 60 days. label Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale No activity for more than 60 days.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CopyButton: clipboard write fails in Safari when url prop requires async fetch Export - Copy to clipboard doesn't work in Safari

2 participants