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' },
+ },
+ ] );
+ } );
} );