Skip to content

Non-breaking space: Remove non-breaking spaces that would collapse when replaced - #85873

Merged
ellatrix merged 7 commits into
trunkfrom
claude/nbsp-replacement-handling-2dd19a
Nov 2, 2026
Merged

ellatrix merged 7 commits into
trunkfrom
claude/nbsp-replacement-handling-2dd19a

Conversation

@ellatrix

@ellatrix ellatrix commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

What?

Closes #85768

When a non-breaking space sits next to a space, or at the start or end of a line, the "Replace non-breaking spaces" button removes it instead of turning it into a space. Regular spaces are never removed. The dots marking non-breaking spaces are now the admin theme color (blue by default) instead of red.

Why?

Non-breaking spaces often sit next to regular spaces, for example word   word. Replacing it gave three regular spaces in a row. HTML shows them as one, so the replaced space added nothing, and the editor (which uses white-space: pre-wrap) showed an extra wide gap until reload.

The red dot also looked like an error, while a non-breaking space is usually fine.

How?

  1. Non-breaking spaces in the range become regular spaces, as before.
  2. Going from right to left, each of them that now has a space, a line break, or the edge of the text next to it is removed. This uses remove() from @wordpress/rich-text, so formats stay in place.
  3. Regular spaces that were already there are left alone. So word   word becomes word word, which HTML shows with one space.
  4. The dot uses var(--wp-admin-theme-color).

Which non-breaking spaces get replaced (the selection, or next to the caret) is unchanged.

Testing Instructions

Open the demo post in Playground

  1. Click into the paragraph. The non-breaking spaces show as blue dots.
  2. Put the caret right after the dot following "non-breaking".
  3. Hover the active button in the block toolbar: it reads "Replace non-breaking space". Click it. The dot is gone and only the two regular spaces that were there before are left.
  4. Select the whole paragraph text. The button now reads "Replace non-breaking spaces". Click it. The three dots after "when" are removed, and the regular spaces between them stay.
  5. Save and reload. The text looks the same.

Testing Instructions for Keyboard

  1. Do step 2 with the arrow keys.
  2. Press Alt+F10 (Option+F10 on Mac) to move to the block toolbar, go to "Replace non-breaking space" with the arrow keys, and press Enter.
  3. Focus goes back to the paragraph and the dot is gone.

Screenshots or screencast

Before After
Caret next to the space Red dots on the non-breaking spaces Blue dots on the non-breaking spaces
After clicking the button A wide gap between non-breaking and spaces Only the two regular spaces left between non-breaking and spaces

The new e2e test, "should remove non-breaking spaces that would collapse when replaced", fails on trunk with one two (three spaces) instead of one two (the two regular spaces). The test from #85777 now expects one two, since both non-breaking spaces there sit next to each other.

Use of AI Tools

Authored by Claude (Opus 5.5) in collaboration with @ellatrix.

🤖 Generated with Claude Code

ellatrix and others added 2 commits October 28, 2026 12:22
Replacing a non-breaking space next to a regular space left two spaces,
which the editor shows as a wide gap and the front end shows as one. Remove
the extra spaces instead, and show the dots in the admin theme color so they
don't look like an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added [Package] Format library /packages/format-library [Package] Block editor /packages/block-editor labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ellatrix <ellatrix@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: singhakanshu00 <akanshusingh@git.wordpress.org>
Co-authored-by: hanneslsm <hanneslsm@git.wordpress.org>
Co-authored-by: andreawetzel <awetz583@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: +61 B (0%)

Total Size: 8.57 MB

📦 View Changed
Filename Size Change
build/scripts/format-library/index.min.js 79.3 kB +72 B (+0.09%)
build/styles/block-editor/content-rtl.css 6 kB -2 B (-0.03%)
build/styles/block-editor/content-rtl.min.css 4.47 kB -3 B (-0.07%)
build/styles/block-editor/content.css 6 kB -3 B (-0.05%)
build/styles/block-editor/content.min.css 4.46 kB -3 B (-0.07%)

1afa342 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 832145f trunk % Change
timeToFirstByte 56.7 ms +3.09% -1.76% 54.95 ms +11.56% -3.91% 3.18%
largestContentfulPaint 96 ms +8.33% -4.17% 90 ms +6.67% -2.22% 6.67%
lcpMinusTtfb 36.4 ms +20.33% -5.91% 34.4 ms +13.66% -5.81% 5.81%
wpBeforeTemplate 29.81 ms +11.34% -3.69% 28.68 ms +15.55% -1.88% 3.94%
wpTemplate 22.53 ms +5.5% -3.51% 22.28 ms +6.28% -5.48% 1.12%
wpTotal 52.89 ms +2.72% -2.1% 50.97 ms +12.6% -3.49% 3.77%
wpMemoryUsage 7.70 MB +0% -0% 7.67 MB +0% -0% 0.43%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 832145f trunk % Change
timeToFirstByte 42.2 ms +5.81% -3.08% 41.9 ms +5.01% -3.1% 0.72%
largestContentfulPaint 90 ms +2.22% -2.22% 88 ms +4.55% -0% 2.27%
lcpMinusTtfb 47.6 ms +4.31% -1.47% 46.9 ms +1.92% -2.45% 1.49%
wpBeforeTemplate 26.1 ms +2.68% -0.46% 26.13 ms +2.56% -0.73% -0.11%
wpTemplate 12.7 ms +3.94% -5.35% 12.57 ms +6.21% -5.73% 1.03%
wpTotal 39.21 ms +6.66% -3.09% 38.6 ms +5.83% -1.97% 1.58%
wpMemoryUsage 6.28 MB +0% -0% 6.25 MB +0% -0% 0.54%
wpDbQueries 9 +0% -0% 9 +0% -0% 0%

media-processing

Metric 832145f trunk % Change
mediaProcessingJpeg 398.72 ms +1.23% -0.8% 401.65 ms +1.07% -0.91% -0.73%
mediaProcessingAvif 6131.4 ms +0.35% -0.27% 6136.39 ms +0.1% -0.34% -0.08%
mediaProcessingJpegToAvif 4228.73 ms +0.21% -0.36% 4216.48 ms +0.06% -0.53% 0.29%

media-upload

Metric 832145f trunk % Change
jpegUploadProcessing 1954.77 ms +0.82% -24.3% 1478.68 ms +3.2% -1.91% 32.2%
pngUploadProcessing 169.19 ms +9.65% -4.07% 191.74 ms +31.59% -6.2% -11.76%
largeJpegUploadProcessing 1935.02 ms +1.44% -24.24% 1459.6 ms +1.4% -0.49% 32.57%
multipleImageUploadProcessing 2051.73 ms +1.79% -0.47% 2034.3 ms +1.06% -0.68% 0.86%

post-editor

Metric 832145f trunk % Change
serverResponse 450.09 ms +2.68% -9% 456.8 ms +2.17% -8.06% -1.47%
firstPaint 366.66 ms +26.04% -18.64% 414.87 ms +21.23% -45.6% -11.62%
domContentLoaded 877.24 ms +2.32% -1.81% 881.96 ms +0.54% -0.65% -0.54%
loaded 878.51 ms +2.31% -1.8% 883.01 ms +0.56% -0.63% -0.51%
firstContentfulPaint 511.71 ms +1.87% -4.02% 509.62 ms +1.64% -2.96% 0.41%
firstBlock 2655.29 ms +1.74% -1.09% 2671.88 ms +1.03% -0.53% -0.62%
type 19.66 ms +3.41% -7.73% 19.04 ms +5.04% -7.83% 3.26%
typeWithoutInspector 18.67 ms +3.7% -7.93% 19.18 ms +8.34% -8.6% -2.66%
typeWithTopToolbar 26.16 ms +7% -8.6% 25.02 ms +5.76% -8.87% 4.56%
typeContainer 9.27 ms +2.91% -8.09% 8.93 ms +8.17% -5.04% 3.81%
focus 127.19 ms +4.5% -7.2% 122.61 ms +1.99% -2.01% 3.74%
firstFocus 265.87 ms +0% -0% 269.58 ms +0% -0% -1.38%
selectAll 380.61 ms +8.98% -2.04% 372.23 ms +1.24% -4.01% 2.25%
undo 31.47 ms +6.96% -2.64% 31.2 ms +7.31% -3.01% 0.87%
listViewOpen 81.25 ms +2.73% -6.01% 76.51 ms +10.64% -9.41% 6.2%
inserterOpen 48.28 ms +2.17% -5.16% 45.17 ms +2.19% -3.7% 6.89%
inserterHover 12.48 ms +8.09% -5.93% 12.72 ms +4.17% -11.79% -1.89%
inserterSearch 9.2 ms +8.37% -9.57% 9.96 ms +6.93% -2.71% -7.63%
loadPatterns 705.26 ms +4.35% -2.71% 726.29 ms +5.58% -3.74% -2.9%
wpTotal 440.39 ms +2.76% -9.33% 446.41 ms +2.37% -8.07% -1.35%
wpMemoryUsage 14.73 MB +0% -0% 14.72 MB +0% -0% 0.1%
wpDbQueries 55 +0% -1.82% 55 +0% -1.82% 0%

site-editor

Metric 832145f trunk % Change
serverResponse 342.81 ms +1.42% -5.59% 334.83 ms +2.64% -6.91% 2.38%
firstPaint 202.18 ms +15.12% -11.66% 223.13 ms +31.59% -8.76% -9.39%
domContentLoaded 706.98 ms +1.96% -0.68% 692.87 ms +2.81% -1.31% 2.04%
loaded 707.99 ms +1.96% -0.68% 694.17 ms +2.73% -1.33% 1.99%
firstContentfulPaint 403.61 ms +1.51% -3.69% 384.05 ms +7.97% -6.49% 5.09%
firstBlock 2830.93 ms +1.8% -0.56% 2782.1 ms +2.65% -1.56% 1.76%
type 14.64 ms +12.3% -8.47% 16.08 ms +3.17% -10.14% -8.96%
navigate 88.7 ms +17.22% -0.37% 89.41 ms +15.73% -3.98% -0.79%
loadPatterns 933.27 ms +19.76% -6.21% 1015.33 ms +10.82% -10.12% -8.08%
loadPages 1001.01 ms +2.93% -6.22% 1034.29 ms +0.47% -2.41% -3.22%
wpTotal 334.16 ms +1.39% -5.6% 326.32 ms +2.65% -7.25% 2.4%
wpMemoryUsage 12.77 MB +0% -0% 12.73 MB +0% -0% 0.27%
wpDbQueries 44 +2.27% -0% 44.5 +1.12% -1.12% -1.12%

1afa342 Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Undo restores the post selection when another synced entity is loaded (RTC WebSocket) in /test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 2

  Object {
    "attributeKey": "content",
    "blockIndex": 0,
    "content": "abcdef",
-   "endOffset": 3,
-   "offset": 3,
+   "endOffset": 6,
+   "offset": 6,
  }

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts:363:5
inserts a block in proper place after having clicked `Browse All` from inline inserter in /test/e2e/specs/editor/various/inserting-blocks.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Add block' })
    - locator resolved to <button type="button" aria-pressed="false" aria-haspopup="true" aria-expanded="false" aria-label="Add block" class="components-button block-editor-inserter__toggle is-next-40px-default-size has-icon">…</button>
  - attempting click action
    - waiting for element to be visible, enabled and stable
    - element is visible, enabled and stable
    - scrolling into view if needed
    - done scrolling
    - performing click action
    - <html lang="en-US" data-wpds-root-provider="true" data-wpds-corner-radius="subtle" class="wp-toolbar interface-interface-skeleton__html-container">…</html> intercepts pointer events
  - retrying click action
    - waiting for element to be visible, enabled and stable
  - element was detached from the DOM, retrying

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/inserting-blocks.spec.js:557:5
keeps the inline inserter open when the Block Library panel closes in /test/e2e/specs/editor/various/inserting-blocks.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Add block' })
    - locator resolved to <button type="button" aria-pressed="false" aria-haspopup="true" aria-expanded="false" aria-label="Add block" class="components-button block-editor-inserter__toggle is-next-40px-default-size has-icon">…</button>
  - attempting click action
    - waiting for element to be visible, enabled and stable
    - element is visible, enabled and stable
    - scrolling into view if needed
    - done scrolling
  - element was detached from the DOM, retrying

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/inserting-blocks.spec.js:614:59
should select by dragging into nested block in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 0

  Array [
    1,
-   2,
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:569:5

1afa342 Run

@ellatrix ellatrix added [Type] Bug An existing feature does not function as intended [Feature] Rich Text Related to the Rich Text component that allows developers to render a contenteditable labels Oct 28, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ellatrix ellatrix changed the title Non-breaking space: Remove spaces that would collapse when replaced Non-breaking space: Remove non-breaking spaces that would collapse when replaced Oct 28, 2026
@Mamaduka

Copy link
Copy Markdown
Member

Don't think this fully addresses #85768, or at least without a comment explaining why we're keeping "Replace non-breaking spaces" rather than using a dynamic label - #85777.

ellatrix and others added 2 commits October 28, 2026 19:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent-handling-2dd19a

# Conflicts:
#	packages/format-library/CHANGELOG.md
#	packages/format-library/src/non-breaking-space/index.tsx
#	test/e2e/specs/editor/various/rich-text.spec.js
@ellatrix
ellatrix marked this pull request as ready for review October 29, 2026 23:11
…ent-handling-2dd19a

# Conflicts:
#	test/e2e/specs/editor/various/rich-text.spec.js
…ent-handling-2dd19a

# Conflicts:
#	packages/format-library/CHANGELOG.md
@ellatrix
ellatrix merged commit 5fec808 into trunk Nov 2, 2026
84 of 85 checks passed
@ellatrix
ellatrix deleted the claude/nbsp-replacement-handling-2dd19a branch November 2, 2026 22:38
@github-actions github-actions Bot added this to the Gutenberg 24.4 milestone Nov 2, 2026
@Mamaduka Mamaduka added the Backport to WP 7.2 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

There was a conflict while trying to cherry-pick the commit to the wp/7.2 branch. Please resolve the conflict manually and create a PR to the wp/7.2 branch.

PRs to wp/7.2 are similar to PRs to trunk, but you should base your PR on the wp/7.2 branch instead of trunk.

# Checkout the wp/7.2 branch instead of trunk.
git checkout wp/7.2

# Create a new branch for your PR.
git checkout -b my-branch

# Cherry-pick the commit.
git cherry-pick 5fec8083534421d009afee57eda996ff87df58e9

# Check which files have conflicts.
git status

# Resolve the conflict...
# Add the resolved files to the staging area.
git status
git add .
git cherry-pick --continue

# Push the branch to the repository
git push origin my-branch

# Create a PR and set the base to the wp/7.2 branch.
# See https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request.

@Mamaduka

Mamaduka commented Nov 3, 2026

Copy link
Copy Markdown
Member

I'll handle this as part of the bulk backport. It should be faster than backporting each item individually.

Mamaduka pushed a commit that referenced this pull request Nov 3, 2026
@Mamaduka Mamaduka added Backported to WP Core Pull request that has been successfully merged into WP Core and removed Backport to WP 7.2 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta labels Nov 3, 2026
wporg-sync pushed a commit to WordPress/wordpress-develop that referenced this pull request Nov 3, 2026
This updates the pinned commit hash of the Gutenberg repository from `cc75a79186fd23e72eb015d594e82f40c6209eb5` to `c035c1a649ef68f5c1646694cf440df1d7ae6e62` (fixes cherry-picked to the `wp/7.2` branch).

A full list of changes included in this commit can be found on GitHub: WordPress/gutenberg@cc75a79...c035c1a

- Components: Use solid brand highlights for popup items (WordPress/gutenberg#84583) (#85848)
- Fix endless sideload loop when the browser can't decode a big image (WordPress/gutenberg#83530)
- Notes: Show reaction picker as a row of emoji (WordPress/gutenberg#85819) (#85892)
- Notes: Add thumbs up and plus reactions (WordPress/gutenberg#85565)
- Notes: Share thread focus recovery across actions (WordPress/gutenberg#85455)
- Core Fields: Prevent extra space between `/` and text in slug input (WordPress/gutenberg#85935)
- Notes: Add search and a status filter to the All Notes sidebar (WordPress/gutenberg#84060)
- Notes: Use the note permission check when deleting a reaction (WordPress/gutenberg#85883)
- [WP 7.2] Math block: Keyboard focus is trapped in the LaTeX field when the formula has an error (WordPress/gutenberg#86032)
- Core fields: Keep the status icon full size when its label wraps (WordPress/gutenberg#85828) (#86050)
- [WP 7.2] ToolsPanel: Keep the "Reset" label visible on the highlighted menu item (WordPress/gutenberg#86060)
- Interactivity Router: Ignore router regions and attachTo targets inside data-wp-html content (WordPress/gutenberg#84919) (#86059)
- [WP 7.2] wp-build: Derive CSS module class names from each stylesheet's CSS (WordPress/gutenberg#86062)
- Playlist: Allow track without an attachment ID to render on the frontend (WordPress/gutenberg#83880) (#86158)
- UI: Show only the day number in Calendar day cells (WordPress/gutenberg#86010) (#86183)
- Notes: Tolerate a null comment in the reaction cascade hooks (WordPress/gutenberg#85959)
- Rich Text: Remove stale format boundary when field loses selection (WordPress/gutenberg#86168)
- Notes: Only let a reaction's author delete it (WordPress/gutenberg#85415)
- Writing flow: Fix Arrow Up and Down getting stuck at the edge of the screen (WordPress/gutenberg#85408)
- Try: Run e2e tests in parallel, on one wp-env site per two cores (WordPress/gutenberg#85420)
- Raw handling: Keep shortcodes in text inline (WordPress/gutenberg#85795)
- Fix: Fixes non-breaking space replacement tooltip (WordPress/gutenberg#85777)
- Non-breaking space: Remove non-breaking spaces that would collapse when replaced (WordPress/gutenberg#85873)

Props mamaduka.
See #66340.

git-svn-id: https://develop.svn.wordpress.org/trunk@65225 602fd350-edb4-49c9-b593-d223f7449a82
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Nov 3, 2026
This updates the pinned commit hash of the Gutenberg repository from `cc75a79186fd23e72eb015d594e82f40c6209eb5` to `c035c1a649ef68f5c1646694cf440df1d7ae6e62` (fixes cherry-picked to the `wp/7.2` branch).

A full list of changes included in this commit can be found on GitHub: WordPress/gutenberg@cc75a79...c035c1a

- Components: Use solid brand highlights for popup items (WordPress/gutenberg#84583) (#85848)
- Fix endless sideload loop when the browser can't decode a big image (WordPress/gutenberg#83530)
- Notes: Show reaction picker as a row of emoji (WordPress/gutenberg#85819) (#85892)
- Notes: Add thumbs up and plus reactions (WordPress/gutenberg#85565)
- Notes: Share thread focus recovery across actions (WordPress/gutenberg#85455)
- Core Fields: Prevent extra space between `/` and text in slug input (WordPress/gutenberg#85935)
- Notes: Add search and a status filter to the All Notes sidebar (WordPress/gutenberg#84060)
- Notes: Use the note permission check when deleting a reaction (WordPress/gutenberg#85883)
- [WP 7.2] Math block: Keyboard focus is trapped in the LaTeX field when the formula has an error (WordPress/gutenberg#86032)
- Core fields: Keep the status icon full size when its label wraps (WordPress/gutenberg#85828) (#86050)
- [WP 7.2] ToolsPanel: Keep the "Reset" label visible on the highlighted menu item (WordPress/gutenberg#86060)
- Interactivity Router: Ignore router regions and attachTo targets inside data-wp-html content (WordPress/gutenberg#84919) (#86059)
- [WP 7.2] wp-build: Derive CSS module class names from each stylesheet's CSS (WordPress/gutenberg#86062)
- Playlist: Allow track without an attachment ID to render on the frontend (WordPress/gutenberg#83880) (#86158)
- UI: Show only the day number in Calendar day cells (WordPress/gutenberg#86010) (#86183)
- Notes: Tolerate a null comment in the reaction cascade hooks (WordPress/gutenberg#85959)
- Rich Text: Remove stale format boundary when field loses selection (WordPress/gutenberg#86168)
- Notes: Only let a reaction's author delete it (WordPress/gutenberg#85415)
- Writing flow: Fix Arrow Up and Down getting stuck at the edge of the screen (WordPress/gutenberg#85408)
- Try: Run e2e tests in parallel, on one wp-env site per two cores (WordPress/gutenberg#85420)
- Raw handling: Keep shortcodes in text inline (WordPress/gutenberg#85795)
- Fix: Fixes non-breaking space replacement tooltip (WordPress/gutenberg#85777)
- Non-breaking space: Remove non-breaking spaces that would collapse when replaced (WordPress/gutenberg#85873)

Props mamaduka.
See #66340.
Built from https://develop.svn.wordpress.org/trunk@65225


git-svn-id: http://core.svn.wordpress.org/trunk@64374 1a063a9b-81f0-0310-95a4-ce76da25c4cd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backported to WP Core Pull request that has been successfully merged into WP Core [Feature] Rich Text Related to the Rich Text component that allows developers to render a contenteditable [Package] Block editor /packages/block-editor [Package] Format library /packages/format-library [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace non-breaking spaces UI is confusing in 7.2-beta2

2 participants