diff --git a/packages/lexical-clipboard/src/clipboard.ts b/packages/lexical-clipboard/src/clipboard.ts index e33f1d9b27b..47aacb4b92b 100644 --- a/packages/lexical-clipboard/src/clipboard.ts +++ b/packages/lexical-clipboard/src/clipboard.ts @@ -148,7 +148,12 @@ export function $insertDataTransferForRichText( } const htmlString = dataTransfer.getData('text/html'); - if (htmlString) { + const plainString = dataTransfer.getData('text/plain'); + + // Skip HTML handling if it matches the plain text representation. + // This avoids unnecessary processing for plain text strings created by + // iOS Safari autocorrect, which incorrectly includes a `text/html` type. + if (htmlString && plainString !== htmlString) { try { const parser = new DOMParser(); const dom = parser.parseFromString( @@ -165,8 +170,7 @@ export function $insertDataTransferForRichText( // Multi-line plain text in rich text mode pasted as separate paragraphs // instead of single paragraph with linebreaks. // Webkit-specific: Supports read 'text/uri-list' in clipboard. - const text = - dataTransfer.getData('text/plain') || dataTransfer.getData('text/uri-list'); + const text = plainString || dataTransfer.getData('text/uri-list'); if (text != null) { if ($isRangeSelection(selection)) { const parts = text.split(/(\r?\n|\t)/); diff --git a/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts b/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts index b146548383c..0a6cd197873 100644 --- a/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts +++ b/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts @@ -7,6 +7,7 @@ */ import {$insertDataTransferForRichText} from '@lexical/clipboard'; +import {$patchStyleText} from '@lexical/selection'; import { $createParagraphNode, $getRoot, @@ -110,6 +111,35 @@ describe('HTMLCopyAndPaste tests', () => { expect(testEnv.innerHTML).toBe(testCase.expectedHTML); }); }); + + test('iOS fix: Word predictions should be handled as plain text to maintain selection formatting', async () => { + const {editor} = testEnv; + + const dataTransfer = new DataTransferMock(); + + // we simulate choosing an iOS Safari `autocorrect` or `word prediction` + // which pastes the word into the editor with both the `text/plain` and `text/html` data types + dataTransfer.setData('text/plain', 'Prediction'); + dataTransfer.setData('text/html', 'Prediction'); + + // to compensate, the clipboard content will only be inserted as HTML if the `text/html` content differs from the `text/plain` content + await editor.update(() => { + const selection = $getSelection(); + invariant( + $isRangeSelection(selection), + 'isRangeSelection(selection)', + ); + $patchStyleText(selection, { + 'background-color': 'rgb(255,170,45)', + }); + $insertDataTransferForRichText(dataTransfer, selection, editor); + }); + + // the editor's selection formatting is maintained because the text has been inserted as plain text + expect(testEnv.innerHTML).toBe( + '
Prediction
', + ); + }); }, { namespace: 'test',