diff --git a/packages/block-editor/src/components/rich-text/content.scss b/packages/block-editor/src/components/rich-text/content.scss index 63810797a0256f..38a7a4feee1d7d 100644 --- a/packages/block-editor/src/components/rich-text/content.scss +++ b/packages/block-editor/src/components/rich-text/content.scss @@ -59,5 +59,5 @@ figcaption.block-editor-rich-text__editable [data-rich-text-placeholder]::before // A background, not a pseudo-element: generated content adds a caret stop in // Firefox. .is-selected .non-breaking-space { - background: radial-gradient(circle, $alert-red 2px, transparent 2.5px) center / 100% 100% no-repeat; + background: radial-gradient(circle, var(--wp-admin-theme-color) 2px, transparent 2.5px) center / 100% 100% no-repeat; } diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 2aa537c3218743..f59a86807b8e31 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -4,6 +4,7 @@ ### Bug Fixes +- Non-breaking space: Remove a non-breaking space instead of replacing it when HTML would collapse the space ([#85873](https://github.com/WordPress/gutenberg/pull/85873)). - Math: Let keyboard users move focus out of the LaTeX field when the formula has an error, instead of sending focus back to the field ([#86027](https://github.com/WordPress/gutenberg/pull/86027)). - Non-breaking space: Label the replace button "Replace non-breaking space" when it replaces only one ([#85777](https://github.com/WordPress/gutenberg/pull/85777)). diff --git a/packages/format-library/src/non-breaking-space/index.tsx b/packages/format-library/src/non-breaking-space/index.tsx index e8094cecd2d654..b898d1056fa11d 100644 --- a/packages/format-library/src/non-breaking-space/index.tsx +++ b/packages/format-library/src/non-breaking-space/index.tsx @@ -1,5 +1,5 @@ import { __ } from '@wordpress/i18n'; -import { applyFormat, insert, useAnchor } from '@wordpress/rich-text'; +import { applyFormat, insert, remove, useAnchor } from '@wordpress/rich-text'; import type { RichTextValue } from '@wordpress/rich-text'; import { RichTextShortcut, @@ -71,13 +71,29 @@ export const nonBreakingSpace = { const [ from, to ] = range; const { text } = value; - onChange( { + let newValue: RichTextValue = { ...value, text: text.slice( 0, from ) + text.slice( from, to ).replaceAll( NBSP, ' ' ) + text.slice( to ), - } ); + }; + + // HTML collapses a space next to another space or at the start or + // end of a line, so remove the non-breaking space there instead. + for ( let index = to - 1; index >= from; index-- ) { + const isCollapsed = [ + newValue.text[ index - 1 ], + newValue.text[ index + 1 ], + ].some( ( character ) => + [ undefined, ' ', '\n' ].includes( character ) + ); + if ( text[ index ] === NBSP && isCollapsed ) { + newValue = remove( newValue, index, index + 1 ); + } + } + + onChange( newValue ); onFocus(); } diff --git a/test/e2e/specs/editor/various/rich-text.spec.js b/test/e2e/specs/editor/various/rich-text.spec.js index a89ea29e0e722e..35f8fccbd84b43 100644 --- a/test/e2e/specs/editor/various/rich-text.spec.js +++ b/test/e2e/specs/editor/various/rich-text.spec.js @@ -1091,12 +1091,75 @@ test.describe( 'RichText (@firefox, @webkit)', () => { exact: true, } ) .click(); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'one two' }, + }, + ] ); + } ); + + test( 'should remove non-breaking spaces that would collapse when replaced', async ( { + page, + editor, + pageUtils, + } ) => { + const toolbar = page.getByRole( 'toolbar', { name: 'Block tools' } ); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'one \u00a0 two' }, + } ); + await editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .click(); + await pageUtils.pressKeys( 'End' ); + // After the non-breaking space. + await pageUtils.pressKeys( 'ArrowLeft', { times: 4 } ); + await editor.showBlockToolbar(); + await toolbar + .getByRole( 'button', { + name: 'Replace non-breaking space', + exact: true, + } ) + .click(); await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/paragraph', attributes: { content: 'one two' }, }, ] ); + + await page.keyboard.type( '!' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'one ! two' }, + }, + ] ); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: '\u00a0one\u00a0\u00a0two\u00a0' }, + } ); + await editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .last() + .click(); + await pageUtils.pressKeys( 'primary+a' ); + await editor.showBlockToolbar(); + await toolbar + .getByRole( 'button', { + name: 'Replace non-breaking spaces', + exact: true, + } ) + .click(); + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ + { attributes: { content: 'one ! two' } }, + { attributes: { content: 'one two' } }, + ] ); } ); test( 'should delete all content of a block', async ( {