From 978d1236d38798a579bd41c9be9981129a82328c Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Sat, 18 Jul 2026 16:06:13 -0500 Subject: [PATCH] Fix ios share sheet paste --- package.json | 2 +- src/js/components/rich-text-input.js | 16 ++++++-- .../specs/components/rich-text-input.test.js | 40 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 511f40f0..0e4800ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.7", + "version": "0.18.8", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/components/rich-text-input.js b/src/js/components/rich-text-input.js index 5712283f..ad33c2dd 100644 --- a/src/js/components/rich-text-input.js +++ b/src/js/components/rich-text-input.js @@ -12,6 +12,13 @@ const FACET_TYPES = new Set([ "app.bsky.richtext.facet#tag", ]); +function parseUriList(uriList) { + return uriList + .split(/\r?\n/) + .filter((line) => line.length > 0 && !line.startsWith("#")) + .join("\n"); +} + function facetsOverlap(a, b) { return ( a.index.byteStart < b.index.byteEnd && a.index.byteEnd > b.index.byteStart @@ -443,9 +450,12 @@ export class RichTextInput extends Component { @paste=${(e) => { e.preventDefault(); // https://stackoverflow.com/a/58980415 - const text = (e.clipboardData || window.clipboardData).getData( - "text/plain", - ); + const clipboard = e.clipboardData || window.clipboardData; + // iOS share-sheet links arrive as text/uri-list with no + // text/plain representation + const text = + clipboard.getData("text/plain") || + parseUriList(clipboard.getData("text/uri-list")); // execCommand fires the input event synchronously, so // handleInput runs while this flag is set this.isPasting = true; diff --git a/tests/unit/specs/components/rich-text-input.test.js b/tests/unit/specs/components/rich-text-input.test.js index 3384f823..aaa44c16 100644 --- a/tests/unit/specs/components/rich-text-input.test.js +++ b/tests/unit/specs/components/rich-text-input.test.js @@ -204,6 +204,46 @@ describe("rich-text-input", () => { assert.deepEqual(details[0].inputType, "insertFromPaste"); }); + it("falls back to text/uri-list when text/plain is empty on paste", () => { + const element = document.createElement("rich-text-input"); + document.body.appendChild(element); + const input = element.querySelector(".rich-text-input"); + + const details = []; + element.addEventListener("input", (event) => { + details.push(event.detail); + }); + + const originalExecCommand = document.execCommand; + document.execCommand = (name, _ui, value) => { + input.textContent = input.textContent + value; + input.dispatchEvent(new window.InputEvent("input")); + return true; + }; + try { + const pasteEvent = new window.Event("paste", { + bubbles: true, + cancelable: true, + }); + pasteEvent.clipboardData = { + getData: (type) => + type === "text/uri-list" + ? "# copied from share sheet\r\nhttps://example.com/a\r\nhttps://example.com/b" + : "", + }; + input.dispatchEvent(pasteEvent); + } finally { + document.execCommand = originalExecCommand; + } + + assert.deepEqual(details.length, 1); + assert.deepEqual( + details[0].text, + "https://example.com/a\nhttps://example.com/b", + ); + assert.deepEqual(details[0].inputType, "insertFromPaste"); + }); + it("passes the native inputType through in the input detail", () => { const element = document.createElement("rich-text-input"); document.body.appendChild(element); -- 2.51.2