Skip to content

fix(useClipboard): prevents fail in Safari for async operation - #5369

Merged
9romise merged 8 commits into
vueuse:mainfrom
MatteoGabriele:fix/async-clipboard-text-in-safari
Apr 24, 2026
Merged

9romise merged 8 commits into
vueuse:mainfrom
MatteoGabriele:fix/async-clipboard-text-in-safari

Conversation

@MatteoGabriele

@MatteoGabriele MatteoGabriele commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Description

resolves #5368

Additional context

I've ensured compatibility with the current implementation to prevent breaking changes. The copy method still accepts an optional string, continuing to default from the source. Additionally, it now can also accept a function that returns a Promise resolving to a string. Since this operation can now be asynchronous, I've added a new copyPending boolean to enable showing a pending state when necessary.

This is my first time contributing to Vueuse, so let me know if I'm missing anything. I'm not too familiar with the codebase.

This could be a first draft. We can improve readability as well, if needed.
We could also decide to create a completely separate composable.

Thanks again for taking the time to read this ❤️

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. enhancement New feature or request labels Apr 17, 2026
@MatteoGabriele MatteoGabriele changed the title Fix/async clipboard text in safari fix(useClipboard): fails in Safari for async operation Apr 17, 2026
@MatteoGabriele MatteoGabriele changed the title fix(useClipboard): fails in Safari for async operation fix(useClipboard): prevents fail in Safari for async operation Apr 17, 2026
Comment thread packages/core/useClipboard/index.ts Outdated
}

return {
copyPending,

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.

this should be readonly:

Suggested change
copyPending,
copyPending: shallowReadonly(copyPending),

Comment thread packages/core/useClipboard/index.ts
Comment on lines +124 to +138
function createClipboardItem(value: ClipboardValue): ClipboardItem {
if (typeof value === 'string') {
text.value = value
return new ClipboardItem({ 'text/plain': value })
}
else {
return new ClipboardItem({
'text/plain': value().then((resolvedText = '') => {
text.value = resolvedText
return new Blob([resolvedText], { type: 'text/plain' })
}),
})
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To simplify this, we can wrap the string in a Promise, which reduces the amount of code. If it's a string, it becomes a Promise. If it's already a Promise, we just use it.

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.

not sure if we should do this. The check is cheaper than a promise

@OrbisK
OrbisK requested a review from 43081j April 18, 2026 11:23
expect(text.value).toBe('')
expect(copied.value).toBe(false)

await copy(async () => 'async text')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

instead of an immediately resolved promise, can you pass one you manually resolve()? then assert that copyPending changes as expected

@OrbisK OrbisK 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.

This looks overall good to me.

One thing we have to discuss might be how we want to handle multiple pending async copies.

should we abort? I think .write does not support AbortController afaik.

Comment thread packages/core/useClipboard/index.ts Outdated
}
catch {
useLegacy = true
copyPending.value = false

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.

lets move this to finally.

@MatteoGabriele

Copy link
Copy Markdown
Contributor Author

@OrbisK Yeah, there's no support for abort, but we could use the pending state to avoid triggering copy again until the previous operation is done.

@OrbisK

OrbisK commented Apr 22, 2026

Copy link
Copy Markdown
Member

@OrbisK Yeah, there's no support for abort, but we could use the pending state to avoid triggering copy again until the previous operation is done.

I think we should not defer/dedupe it. I think copy should just do what it does. But I think we need to make sure that pending is true as long as all copies are not finished.

copy(simpleAsynCopy) // sets pending to true - takes 10 seconds
copy("sync value") // sets pending to true and immediate to false, but async is still pending

copyPending.value // should be true, but might be false

@43081j

43081j commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

just like your regular clipboard, new copy actions should discard the old ones i think.

const asyncValue = somehowGetAsyncValue("foo");
const syncValue = "bar";
const asyncValue2 = somehowGetAsyncValue("baz");

copy(asyncValue); // pending
copy(syncValue); // set clipboard to "bar", abandon the promise from before
copy(asyncValue2); // pending
// eventually, set clipboard to "baz" because the last promise resolved

@MatteoGabriele

Copy link
Copy Markdown
Contributor Author

There may be a UX issue:

  • Copying async text "foo"
  • Copying sync text "bar"
    If the user clicks "foo" first, then "bar", it might copy "foo" instead of "bar" due to the delay.

@MatteoGabriele

Copy link
Copy Markdown
Contributor Author

Love that we used a classic foobar action without knowing, ahahahahha
I see what you mean, though. The useClipboard instance should always get the last clicked "copy" value, regardless of which method. I'll see what I can do.

@43081j

43081j commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

i checked, the clipboard API writes the last resolved value.

so lets just do the same here and not implement our own behaviours.

this:

const asyncValue = somehowGetAsyncValue("foo");
const syncValue = "bar";
const asyncValue2 = somehowGetAsyncValue("baz");

copy(asyncValue); // pending
copy(syncValue); // set clipboard to "bar"
// asyncValue resolved, set clipboard to "foo"
copy(asyncValue2); // pending
// eventually, set clipboard to "baz" because the last promise resolved

@OrbisK

OrbisK commented Apr 22, 2026

Copy link
Copy Markdown
Member

i checked, the clipboard API writes the last resolved value.

so lets just do the same here and not implement our own behaviours.

this:

const asyncValue = somehowGetAsyncValue("foo");
const syncValue = "bar";
const asyncValue2 = somehowGetAsyncValue("baz");

copy(asyncValue); // pending
copy(syncValue); // set clipboard to "bar"
// asyncValue resolved, set clipboard to "foo"
copy(asyncValue2); // pending
// eventually, set clipboard to "baz" because the last promise resolved

Nice! We still have to implement the "dedupe" for the legacy api. It currently await the resolve, so it will use the latest resolved value. We should handle this and we are good to go.

@MatteoGabriele

Copy link
Copy Markdown
Contributor Author

@OrbisK I think this last change should do the trick

43081j
43081j previously approved these changes Apr 23, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Apr 23, 2026
@pkg-pr-new

pkg-pr-new Bot commented Apr 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vueuse/components

pnpm add https://pkg.pr.new/@vueuse/components@5369
npm i https://pkg.pr.new/@vueuse/components@5369
yarn add https://pkg.pr.new/@vueuse/components@5369.tgz

@vueuse/core

pnpm add https://pkg.pr.new/@vueuse/core@5369
npm i https://pkg.pr.new/@vueuse/core@5369
yarn add https://pkg.pr.new/@vueuse/core@5369.tgz

@vueuse/electron

pnpm add https://pkg.pr.new/@vueuse/electron@5369
npm i https://pkg.pr.new/@vueuse/electron@5369
yarn add https://pkg.pr.new/@vueuse/electron@5369.tgz

@vueuse/firebase

pnpm add https://pkg.pr.new/@vueuse/firebase@5369
npm i https://pkg.pr.new/@vueuse/firebase@5369
yarn add https://pkg.pr.new/@vueuse/firebase@5369.tgz

@vueuse/integrations

pnpm add https://pkg.pr.new/@vueuse/integrations@5369
npm i https://pkg.pr.new/@vueuse/integrations@5369
yarn add https://pkg.pr.new/@vueuse/integrations@5369.tgz

@vueuse/math

pnpm add https://pkg.pr.new/@vueuse/math@5369
npm i https://pkg.pr.new/@vueuse/math@5369
yarn add https://pkg.pr.new/@vueuse/math@5369.tgz

@vueuse/metadata

pnpm add https://pkg.pr.new/@vueuse/metadata@5369
npm i https://pkg.pr.new/@vueuse/metadata@5369
yarn add https://pkg.pr.new/@vueuse/metadata@5369.tgz

@vueuse/nuxt

pnpm add https://pkg.pr.new/@vueuse/nuxt@5369
npm i https://pkg.pr.new/@vueuse/nuxt@5369
yarn add https://pkg.pr.new/@vueuse/nuxt@5369.tgz

@vueuse/router

pnpm add https://pkg.pr.new/@vueuse/router@5369
npm i https://pkg.pr.new/@vueuse/router@5369
yarn add https://pkg.pr.new/@vueuse/router@5369.tgz

@vueuse/rxjs

pnpm add https://pkg.pr.new/@vueuse/rxjs@5369
npm i https://pkg.pr.new/@vueuse/rxjs@5369
yarn add https://pkg.pr.new/@vueuse/rxjs@5369.tgz

@vueuse/shared

pnpm add https://pkg.pr.new/@vueuse/shared@5369
npm i https://pkg.pr.new/@vueuse/shared@5369
yarn add https://pkg.pr.new/@vueuse/shared@5369.tgz

@vueuse/skills

pnpm add https://pkg.pr.new/@vueuse/skills@5369
npm i https://pkg.pr.new/@vueuse/skills@5369
yarn add https://pkg.pr.new/@vueuse/skills@5369.tgz

commit: 121bb26

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.71429% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.07%. Comparing base (c541332) to head (121bb26).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
packages/core/useClipboard/index.ts 60.71% 10 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5369      +/-   ##
==========================================
- Coverage   65.10%   65.07%   -0.03%     
==========================================
  Files         346      346              
  Lines        8141     8163      +22     
  Branches     2508     2514       +6     
==========================================
+ Hits         5300     5312      +12     
- Misses       2313     2322       +9     
- Partials      528      529       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@OrbisK OrbisK 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.

Nice! Looks great! ❤️

@OrbisK
OrbisK requested a review from 9romise April 23, 2026 10:16
@9romise
9romise added this pull request to the merge queue Apr 24, 2026
Merged via the queue into vueuse:main with commit 5ec568d Apr 24, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG | useClipboard | fails in Safari for async operation

4 participants