Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2104,6 +2104,33 @@ export function editedContentOnlySection( state, action ) {
if ( action.type === 'EDIT_CONTENT_ONLY_SECTION' ) {
return action.clientId;
}

// Early return if there's no section being edited.
if ( ! state ) {
return state;
}

switch ( action.type ) {
case 'REMOVE_BLOCKS':
case 'REPLACE_BLOCKS':
// Clear if the edited section is directly among the removed/replaced blocks.
// Note: this doesn't catch the case where a parent of the edited section
// is removed, since action.clientIds only contains the top-level IDs.
// That edge case is handled by the StopEditingContentOnlySectionOnOutsideSelect
// component in block-list/index.js.
if ( action.clientIds.includes( state ) ) {
return undefined;
}
break;
case 'RESET_BLOCKS':
// When all blocks are reset (e.g. navigating to a different post),
// check whether the edited section still exists in the new block tree.
if ( ! getFlattenedClientIds( action.blocks )[ state ] ) {
return undefined;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible that the editedContentOnlySection is not in the top-level tree, but in a nested one? For example, in a template-locked mode the top-level tree is the post template, then there is a nested controlled entity, like post content or template part, and only in that nested entity is something nested even further, e.g., a pattern?

In such a case the RESET_BLOCKS' action.block won't contain the nested block ID. and the flag will be incorrectly cleared.

RESET_BLOCKS doesn't happen only on navigation, but also when the edited entity has changes coming from external source, like from RTC server. Then useBlockSync will call RESET_BLOCKS with updated blocks coming from the edited entity.

The blockEditingModes state has similar problems--what to reset and when?--and I'm trying to figure it out in #76529. That's why this PR caught my attention.

@talldan talldan Mar 20, 2026 •

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.

I think that is a possibility. The replace/remove blocks cases could also be more robust and I was planning a follow-up for that.

My idea is to lift the withInnerBlocksRemoveCascade higher order reducer up so that other reducers can also access the REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN and REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN actions. There are a few reducers across the block editor store that I think probably don't clean up clientId state properly and could benefit.

Perhaps something similar is possible with RESET_BLOCKS.

break;
}

return state;
}

Expand Down
113 changes: 113 additions & 0 deletions packages/block-editor/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3649,6 +3649,119 @@ describe( 'state', () => {
} );
} );

describe( 'editedContentOnlySection', () => {
it( 'returns undefined by default', () => {
expect(
editedContentOnlySection( undefined, { type: 'UNKNOWN' } )
).toBeUndefined();
} );

it( 'sets the clientId on EDIT_CONTENT_ONLY_SECTION', () => {
const state = editedContentOnlySection( undefined, {
type: 'EDIT_CONTENT_ONLY_SECTION',
clientId: 'block-1',
} );
expect( state ).toBe( 'block-1' );
} );

it( 'clears the clientId when EDIT_CONTENT_ONLY_SECTION has no clientId', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'EDIT_CONTENT_ONLY_SECTION',
} );
expect( state ).toBeUndefined();
} );

it( 'clears when the edited section is directly removed via REMOVE_BLOCKS', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'REMOVE_BLOCKS',
clientIds: [ 'block-1' ],
} );
expect( state ).toBeUndefined();
} );

it( 'keeps state when REMOVE_BLOCKS targets other blocks', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'REMOVE_BLOCKS',
clientIds: [ 'block-2', 'block-3' ],
} );
expect( state ).toBe( 'block-1' );
} );

it( 'clears when the edited section is directly replaced via REPLACE_BLOCKS', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'REPLACE_BLOCKS',
clientIds: [ 'block-1' ],
blocks: [ { clientId: 'block-new', innerBlocks: [] } ],
} );
expect( state ).toBeUndefined();
} );

it( 'keeps state when REPLACE_BLOCKS targets other blocks', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'REPLACE_BLOCKS',
clientIds: [ 'block-2' ],
blocks: [ { clientId: 'block-new', innerBlocks: [] } ],
} );
expect( state ).toBe( 'block-1' );
} );

it( 'clears when RESET_BLOCKS does not include the edited section', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'RESET_BLOCKS',
blocks: [
{ clientId: 'block-2', innerBlocks: [] },
{ clientId: 'block-3', innerBlocks: [] },
],
} );
expect( state ).toBeUndefined();
} );

it( 'keeps state when RESET_BLOCKS includes the edited section at the top level', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'RESET_BLOCKS',
blocks: [
{ clientId: 'block-1', innerBlocks: [] },
{ clientId: 'block-2', innerBlocks: [] },
],
} );
expect( state ).toBe( 'block-1' );
} );

it( 'keeps state when RESET_BLOCKS includes the edited section nested in innerBlocks', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'RESET_BLOCKS',
blocks: [
{
clientId: 'block-parent',
innerBlocks: [
{
clientId: 'block-1',
innerBlocks: [],
},
],
},
],
} );
expect( state ).toBe( 'block-1' );
} );

it( 'does not clear on unrelated actions when state is set', () => {
const state = editedContentOnlySection( 'block-1', {
type: 'SELECT_BLOCK',
clientId: 'block-2',
} );
expect( state ).toBe( 'block-1' );
} );

it( 'does not run cleanup logic when state is already empty', () => {
const state = editedContentOnlySection( undefined, {
type: 'REMOVE_BLOCKS',
clientIds: [ 'block-1' ],
} );
expect( state ).toBeUndefined();
} );
} );

describe( 'insertionPoint', () => {
it( 'should default to null', () => {
const state = insertionPoint( undefined, {} );
Expand Down
Loading