diff --git a/packages/outline-playground/__tests__/e2e/TextEntry-test.js b/packages/outline-playground/__tests__/e2e/TextEntry-test.js index a05da3e236c..3462a9f2c34 100644 --- a/packages/outline-playground/__tests__/e2e/TextEntry-test.js +++ b/packages/outline-playground/__tests__/e2e/TextEntry-test.js @@ -68,6 +68,32 @@ describe('TextEntry', () => { }); }); + it(`Can type 'Hello Outline' in the editor and replace it with an empty space`, async () => { + const {page} = e2e; + + const targetText = 'Hello Outline'; + await page.focus('div.editor'); + await page.keyboard.type(targetText); + + // Select all the text + await keyDownCtrlOrMeta(page); + await page.keyboard.press('a'); + await keyUpCtrlOrMeta(page); + + await page.keyboard.type(' '); + + await assertHTML( + page, + '
', + ); + await assertSelection(page, { + anchorPath: [0, 0, 0], + anchorOffset: 1, + focusPath: [0, 0, 0], + focusOffset: 1, + }); + }); + it('Paragraphed text entry and selection', async () => { const {isRichText, page} = e2e; diff --git a/packages/outline-react/src/shared/EventHandlers.js b/packages/outline-react/src/shared/EventHandlers.js index dfd97f0b893..ca76b41fbca 100644 --- a/packages/outline-react/src/shared/EventHandlers.js +++ b/packages/outline-react/src/shared/EventHandlers.js @@ -664,6 +664,24 @@ function canRemoveText( ); } +function shouldPreventDefaultAndInsertText( + selection: Selection, + text: string, +): boolean { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = anchor.getNode(); + + return ( + anchor.key !== focus.key || + !isTextNode(anchorNode) || + anchorNode.getFormat() !== selection.textFormat || + shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode, true) || + text.length > 1 || + (text === ' ' && anchor.offset !== focus.offset) + ); +} + export function onBeforeInputForPlainText( event: InputEvent, editor: OutlineEditor, @@ -714,20 +732,12 @@ export function onBeforeInputForPlainText( const text = event.dataTransfer.getData('text/plain'); event.preventDefault(); insertRichText(selection, text); - } else if (data != null) { - const anchorKey = anchor.key; - const focusKey = focus.key; - - if ( - anchorKey !== focusKey || - !isTextNode(anchorNode) || - anchorNode.getFormat() !== selection.textFormat || - shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode, true) || - data.length > 1 - ) { - event.preventDefault(); - insertText(selection, data); - } + } else if ( + data != null && + shouldPreventDefaultAndInsertText(selection, data) + ) { + event.preventDefault(); + insertText(selection, data); } return; } @@ -856,20 +866,12 @@ export function onBeforeInputForRichText( const text = event.dataTransfer.getData('text/plain'); event.preventDefault(); insertRichText(selection, text); - } else if (data != null) { - const anchorKey = anchor.key; - const focusKey = focus.key; - - if ( - anchorKey !== focusKey || - !isTextNode(anchorNode) || - anchorNode.getFormat() !== selection.textFormat || - shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode, true) || - data.length > 1 - ) { - event.preventDefault(); - insertText(selection, data); - } + } else if ( + data != null && + shouldPreventDefaultAndInsertText(selection, data) + ) { + event.preventDefault(); + insertText(selection, data); } return; } @@ -984,13 +986,13 @@ export function onInput(event: InputEvent, editor: OutlineEditor) { if (!CAN_USE_BEFORE_INPUT) { const selection = view.getSelection(); const data = event.data; - if (selection !== null && data != null) { - const anchor = selection.anchor; - const focus = selection.focus; - if (anchor.type === 'block' || focus.type === 'block') { - insertText(selection, data); - return; - } + if ( + data != null && + selection !== null && + shouldPreventDefaultAndInsertText(selection, data) + ) { + insertText(selection, data); + return; } } updateSelectedTextFromDOM(editor, view);