Skip to content

Fix: Delete key on empty heading transforms following paragraph into heading - #78779

Open
coderGtm wants to merge 9 commits into
WordPress:trunkfrom
coderGtm:fix/heading-block-delete-flow
Open

Fix: Delete key on empty heading transforms following paragraph into heading#78779
coderGtm wants to merge 9 commits into
WordPress:trunkfrom
coderGtm:fix/heading-block-delete-flow

Conversation

@coderGtm

@coderGtm coderGtm commented May 28, 2026

Copy link
Copy Markdown
Contributor

What?

Closes #78767

Fix a bug where pressing Delete on an empty heading block incorrectly transforms the following paragraph block into a heading.

Why?

When a heading block is emptied and the user presses Delete, the expected behaviour is that the empty heading is removed and the paragraph below is left unchanged. Instead, the paragraph was being converted into a heading — matching the type of the block that was being deleted.

This only occurred with the Delete key (forward merge). Backspace was unaffected because pressing Backspace on an empty heading with no preceding block triggers switchToDefaultOrRemove(), which converts the heading itself to a paragraph without ever reaching the merge path.

How?

The bug was in the mergeBlocks action (packages/block-editor/src/store/actions.js). When mergeBlocks(blockA, blockB) is called, blockA's type is preserved and blockB's content is merged in. Two existing guard conditions short-circuit this behaviour:

  • isUnmodifiedDefaultBlock(blockA) → removes blockA (handles an empty default/paragraph block)
  • isUnmodifiedDefaultBlock(blockB) → removes blockB (handles an empty following block)

Neither condition matched for an empty heading (blockA) above a paragraph with content (blockB): a heading is not the default block type, so the first check failed, and the paragraph had content, so the second also failed. The code then fell through to the merge logic, which calls switchToBlockType(blockB, blockA.name) — transforming the paragraph into a heading before merging.

The fix adds a new guard: if blockA has a merge function but its content-role attributes are empty (isUnmodifiedBlock(blockA, 'content')), simply remove blockA and focus blockB instead of merging. A unit test covering this case is included.

Testing Instructions

  1. Open a post or page in the block editor.
  2. Insert a Heading block and type any text (e.g. "My Heading").
  3. Insert a Paragraph block below it and type any text (e.g. "My paragraph").
  4. Click into the Heading block and select all the text (Cmd/Ctrl+A), then press Delete/Backspace to clear it. The heading block should now be empty.
  5. With the cursor still in the empty heading block, press the Delete key (forward delete).
  6. Expected: The empty heading block is removed and the paragraph block below remains a paragraph with its content intact.
  7. Previous behaviour: The paragraph was converted into a heading.

Testing Instructions for Keyboard

  1. Use Tab to navigate to the Heading block and Enter to begin editing.
  2. Press Cmd/Ctrl+A to select all text, then Backspace to clear the heading content.
  3. Press Delete (forward delete).
  4. Confirm focus moves to the paragraph below and it remains a paragraph block (check the block type indicator in the toolbar).

Screenshots or screencast

Before After
Screen.Recording.2026-05-28.at.7.24.19.PM.mov
Screen.Recording.2026-05-28.at.7.31.40.PM.mov

Use of AI Tools

This PR was developed with the assistance of GitHub Copilot (code exploration, fix implementation, and test authoring). The change was reviewed and verified manually.

@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label May 28, 2026
@coderGtm
coderGtm marked this pull request as ready for review May 28, 2026 14:11
@coderGtm
coderGtm requested a review from ellatrix as a code owner May 28, 2026 14:11
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

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: coderGtm <gautam23@git.wordpress.org>
Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>
Co-authored-by: jasmussen <joen@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: Mustafabharmal <mustafabharmal@git.wordpress.org>

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

@jasmussen

Copy link
Copy Markdown
Contributor

Nice, thanks for the PR! Confirming it works for me, and I personally appreciate this a great deal as far as the writing experience.

Since this touches a sensitive part of the code, I will ultimately defer to developers familar with this code to approve or not. @ellatrix has worked a lot on writing flow.

@jasmussen
jasmussen requested review from a team and aaronrobertshaw and removed request for a team June 9, 2026 11:12
@youknowriad

Copy link
Copy Markdown
Contributor

Can you check the e2e test failures? It seems this could have introduced regressions on the list block no?

// and keep blockB focused. Without this check, merging would transform
// blockB into blockA's type (e.g. paragraph → heading), which is
// unexpected when the leading block is simply empty.
if ( blockAType.merge && isUnmodifiedBlock( blockA, 'content' ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to check if blockA.name !== blockB.name and also it would be better to have the code below the if ( ! blockAType.merge ) { block for two reasons:

  1. We remove the blockAType.merge check
  2. Preserve the if ( isUnmodifiedDefaultBlock( blockB ) ) { block flow. Placement here changes the current behavior a bit right now and we can avoid it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ntsekouras! That actually is a better way. I made the change and tested the behaviour and It works flawlessly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failing list tests seem that something different (or more) is needed. --cc @ellatrix for any quick insights due to the heavy work on this area.

@t-hamano t-hamano added [Type] Enhancement A suggestion for improvement. [Feature] Writing Flow Block selection, navigation, splitting, merging, deletion... labels Jun 10, 2026
@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Jun 10, 2026
@coderGtm

Copy link
Copy Markdown
Contributor Author

So I have fixed the logic and updated the test. There is still a keyboard testcase failing which I will try solving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Writing Flow Block selection, navigation, splitting, merging, deletion... [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Writing flow: Pressing delete in an empty heading block should delete the heading block

5 participants