diff --git a/packages/rich-text/src/component/use-input-and-selection.js b/packages/rich-text/src/component/use-input-and-selection.js index 870bfe170635c..b06f82adb6d51 100644 --- a/packages/rich-text/src/component/use-input-and-selection.js +++ b/packages/rich-text/src/component/use-input-and-selection.js @@ -9,6 +9,7 @@ import { useRefEffect } from '@wordpress/compose'; */ import { getActiveFormats } from '../get-active-formats'; import { updateFormats } from '../update-formats'; +import { isCollapsed } from '../is-collapsed'; /** * All inserting input types that would insert HTML into the DOM. @@ -96,15 +97,59 @@ export function useInputAndSelection( props ) { return; } - const currentValue = createRecord(); - const { start, activeFormats: oldActiveFormats = [] } = + const newValue = createRecord(); + const { start: oldStart, activeFormats: oldActiveFormats = [] } = record.current; + const SP = ' '; + const NBSP = '\u00a0'; + const isSpOrNbsp = ( char ) => char === SP || char === NBSP; + + // When inserting multiple spaces, alternate between a normal space + // and a non-breaking space. This is to prevent browsers from + // collapsing multiple spaces into one. + if ( + isCollapsed( newValue ) && + oldStart !== newValue.start && + ( oldStart < newValue.start + ? // Check if the inserted character is a space. + newValue.text.slice( oldStart, newValue.start ) === SP + : // Check if the deleted character is a space or + // non-breaking space (in both cases we need to fix the + // alternating pattern). + isSpOrNbsp( + record.current.text.slice( + newValue.start, + oldStart + ) + ) ) + ) { + const text = newValue.text; + let startOffset = newValue.start; + let endOffset = newValue.start; + + // We need to make sure the alternating pattern is maintained, + // so replace all spaces before and after the selection. + while ( isSpOrNbsp( text[ startOffset - 1 ] ) ) { + startOffset--; + } + while ( isSpOrNbsp( text[ endOffset ] ) ) { + endOffset++; + } + + newValue.text = + text.slice( 0, startOffset ) + + Array.from( { length: endOffset - startOffset } ) + .map( ( _, index ) => ( index % 2 === 0 ? SP : NBSP ) ) + .join( '' ) + + text.slice( endOffset ); + } + // Update the formats between the last and new caret position. const change = updateFormats( { - value: currentValue, - start, - end: currentValue.start, + value: newValue, + start: oldStart, + end: newValue.start, formats: oldActiveFormats, } ); diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js index 7e654ca12790f..1ccd421987f33 100644 --- a/test/e2e/specs/editor/blocks/links.spec.js +++ b/test/e2e/specs/editor/blocks/links.spec.js @@ -969,9 +969,7 @@ test.describe( 'Links', () => { name: 'core/paragraph', attributes: { content: - 'Text with leading and trailing' + - textToSelect + - '', + 'Text with leading and trailing         spaces     ', }, }, ] ); diff --git a/test/e2e/specs/editor/various/rich-text.spec.js b/test/e2e/specs/editor/various/rich-text.spec.js index 2969a33d25485..fd26071b2d2c5 100644 --- a/test/e2e/specs/editor/various/rich-text.spec.js +++ b/test/e2e/specs/editor/various/rich-text.spec.js @@ -853,4 +853,64 @@ test.describe( 'RichText', () => { }, ] ); } ); + + test( 'should pad multiple spaces', async ( { page, editor } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'a b' ); + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a  b' }, + }, + ] ); + } ); + + test( 'should pad starting from space', async ( { page, editor } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'a b' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( ' ' ); + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a  b' }, + }, + ] ); + } ); + + test( 'should restore alternating padding on backspace', async ( { + page, + editor, + } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'a b' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Backspace' ); + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a   b' }, + }, + ] ); + } ); + + test( 'should restore alternating padding on delete', async ( { + page, + editor, + } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'a b' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Delete' ); + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a   b' }, + }, + ] ); + } ); } );