FIX: Fall back to clipboard.writeText if ClipboardItem not supported (#16419)

Firefox does not support window.ClipboardItem yet (it is behind
a flag (dom.events.asyncClipboard.clipboardItem) as at version 87.)
so we need to fall back to the normal non-async clipboard copy, that
works in every browser except Safari.

This commit also tests the clipboardCopyAsync function by stubbing out
the clipboard on the window.navigator.

This fixes an issue in the discourse-chat plugin, where the
"Quote in Topic" button errored in Firefox.
This commit is contained in:
Martin Brennan 2022-04-11 13:00:45 +10:00 committed by GitHub
parent f26d07c1ad
commit cecdef83a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 82 additions and 11 deletions

View File

@ -313,7 +313,7 @@ export function isAppleDevice() {
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
// This will apply hack on all iDevices
let caps = helperContext().capabilities;
return caps.isIOS && !navigator.userAgent.match(/Trident/g);
return caps.isIOS && !window.navigator.userAgent.match(/Trident/g);
}
let iPadDetected = undefined;
@ -321,8 +321,8 @@ let iPadDetected = undefined;
export function isiPad() {
if (iPadDetected === undefined) {
iPadDetected =
navigator.userAgent.match(/iPad/g) &&
!navigator.userAgent.match(/Trident/g);
window.navigator.userAgent.match(/iPad/g) &&
!window.navigator.userAgent.match(/Trident/g);
}
return iPadDetected;
}
@ -512,8 +512,8 @@ export function translateModKey(string) {
export function clipboardCopy(text) {
// Use the Async Clipboard API when available.
// Requires a secure browsing context (i.e. HTTPS)
if (navigator.clipboard) {
return navigator.clipboard.writeText(text).catch(function (err) {
if (window.navigator.clipboard) {
return window.navigator.clipboard.writeText(text).catch(function (err) {
throw err !== undefined
? err
: new DOMException("The request is not allowed", "NotAllowedError");
@ -532,12 +532,29 @@ export function clipboardCopy(text) {
//
// Note that the promise passed in should return a Blob with type of
// text/plain.
export function clipboardCopyAsync(promise) {
export function clipboardCopyAsync(functionReturningPromise) {
// Use the Async Clipboard API when available.
// Requires a secure browsing context (i.e. HTTPS)
if (navigator.clipboard) {
return navigator.clipboard
.write([new window.ClipboardItem({ "text/plain": promise() })])
if (window.navigator.clipboard) {
// Firefox does not support window.ClipboardItem yet (it is behind
// a flag (dom.events.asyncClipboard.clipboardItem) as at version 87.)
// so we need to fall back to the normal non-async clipboard copy, that
// works in every browser except Safari.
//
// TODO: (martin) Look at this on 2022-07-01 to see if support has
// changed.
if (!window.ClipboardItem) {
return functionReturningPromise().then((textBlob) => {
return textBlob.text().then((text) => {
return clipboardCopy(text);
});
});
}
return window.navigator.clipboard
.write([
new window.ClipboardItem({ "text/plain": functionReturningPromise() }),
])
.catch(function (err) {
throw err !== undefined
? err
@ -546,7 +563,7 @@ export function clipboardCopyAsync(promise) {
}
// ...Otherwise, use document.execCommand() fallback
return promise().then((textBlob) => {
return functionReturningPromise().then((textBlob) => {
textBlob.text().then((text) => {
return clipboardCopyFallback(text);
});

View File

@ -1,7 +1,9 @@
import { Promise } from "rsvp";
import {
avatarImg,
avatarUrl,
caretRowCol,
clipboardCopyAsync,
defaultHomepage,
emailValid,
escapeExpression,
@ -15,9 +17,13 @@ import {
slugify,
toAsciiPrintable,
} from "discourse/lib/utilities";
import sinon from "sinon";
import { test } from "qunit";
import Handlebars from "handlebars";
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import {
chromeTest,
discourseModule,
} from "discourse/tests/helpers/qunit-helpers";
discourseModule("Unit | Utilities", function () {
test("escapeExpression", function (assert) {
@ -283,3 +289,51 @@ discourseModule("Unit | Utilities", function () {
});
});
});
discourseModule("Unit | Utilities | clipboard", function (hooks) {
let mockClipboard;
hooks.beforeEach(function () {
mockClipboard = {
writeText: sinon.stub().resolves(true),
write: sinon.stub().resolves(true),
};
sinon.stub(window.navigator, "clipboard").get(() => mockClipboard);
});
function getPromiseFunction() {
return () =>
new Promise((resolve) => {
resolve(
new Blob(["some text to copy"], {
type: "text/plain",
})
);
});
}
test("clipboardCopyAsync - browser does not support window.ClipboardItem", async function (assert) {
// without this check the stubbing will fail on Firefox
if (window.ClipboardItem) {
sinon.stub(window, "ClipboardItem").value(null);
}
await clipboardCopyAsync(getPromiseFunction());
assert.strictEqual(
mockClipboard.writeText.calledWith("some text to copy"),
true,
"it writes to the clipboard using writeText instead of write"
);
});
chromeTest(
"clipboardCopyAsync - browser does support window.ClipboardItem",
async function (assert) {
await clipboardCopyAsync(getPromiseFunction());
assert.strictEqual(
mockClipboard.write.called,
true,
"it writes to the clipboard using write"
);
}
);
});