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
61 changes: 32 additions & 29 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2559,30 +2559,6 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
traverseBlockTree( state, treeClientId, ( block ) => {
const { clientId, name: blockName } = block;

// Set the edited section and all blocks within it to 'default', so that all changes can be made.
if ( state.editedContentOnlySection ) {
// If this is the edited section, use the default mode.
if ( state.editedContentOnlySection === clientId ) {
derivedBlockEditingModes.set( clientId, 'default' );
return;
}

// If the block is within the edited section also use the default mode.
const parentTempEditedClientId = findParentInClientIdsList(
state,
clientId,
[ state.editedContentOnlySection ]
);
if ( parentTempEditedClientId ) {
derivedBlockEditingModes.set( clientId, 'default' );
return;
}

// Disable blocks that are outside of the edited section.
derivedBlockEditingModes.set( clientId, 'disabled' );
return;
}

// If the block already has an explicit block editing mode set,
// don't override it.
if ( state.blockEditingModes.has( clientId ) ) {
Expand Down Expand Up @@ -2643,7 +2619,8 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
if ( syncedPatternClientIds.length ) {
// Synced pattern blocks (core/block).
if ( syncedPatternClientIds.includes( clientId ) ) {
// This is a pattern nested in another pattern, it should be disabled.
// This is a synced pattern nested in another synced pattern,
// disable the core/block itself.
if (
findParentInClientIdsList(
state,
Expand All @@ -2660,17 +2637,18 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
}

// Inner blocks of synced patterns.
const parentPatternClientId = findParentInClientIdsList(
const parentSyncedPatternClientId = findParentInClientIdsList(
state,
clientId,
syncedPatternClientIds
);
if ( parentPatternClientId ) {
// This is a pattern nested in another pattern, it should be disabled.
if ( parentSyncedPatternClientId ) {
// This is an inner block of a synced pattern that's nested in another synced pattern,
// disable its contents.
if (
findParentInClientIdsList(
state,
parentPatternClientId,
parentSyncedPatternClientId,
syncedPatternClientIds
)
) {
Expand All @@ -2687,9 +2665,34 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
// from the instance, the user has to edit the pattern source,
// so return 'disabled'.
derivedBlockEditingModes.set( clientId, 'disabled' );
return;
Copy link
Member

Choose a reason for hiding this comment

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

Just checking so I understand - this is the magic that prevents synced pattern blocks from showing?

Copy link
Contributor Author

@talldan talldan Feb 24, 2026

Choose a reason for hiding this comment

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

This makes sure the derivedBlockEditingModes that were just set for synced patterns on the few lines above are not overwritten by the code below.

Previously the execution would continue on to the rest of the logic in this function, it'd hit the if ( hasContentOnlyParent ) {, which would be true when the synced pattern pattern is nested in an unsynced one, and then new block editing modes would be set for the blocks in the synced pattern, but following the rules for unsynced patterns.

}
}

// Set the edited section and all blocks within it to 'default', so that all changes can be made.
if ( state.editedContentOnlySection ) {
// If this is the edited section, use the default mode.
if ( state.editedContentOnlySection === clientId ) {
derivedBlockEditingModes.set( clientId, 'default' );
return;
}

// If the block is within the edited section also use the default mode.
const parentTempEditedClientId = findParentInClientIdsList(
state,
clientId,
[ state.editedContentOnlySection ]
);
if ( parentTempEditedClientId ) {
derivedBlockEditingModes.set( clientId, 'default' );
return;
}

// Disable blocks that are outside of the edited section.
derivedBlockEditingModes.set( clientId, 'disabled' );
return;
}

// Handle `templateLock=contentOnly` blocks and unsynced patterns.
if ( contentOnlyParents.length ) {
const hasContentOnlyParent = !! findParentInClientIdsList(
Expand Down
242 changes: 242 additions & 0 deletions packages/block-editor/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3946,6 +3946,248 @@ describe( 'state', () => {
expect( derivedBlockEditingModes ).toEqual( new Map() );
} );

it( 'synced pattern inner blocks keep their editing modes when inside an editedContentOnlySection', () => {
// Set up an unsynced pattern containing a synced pattern.
// When the unsynced pattern is the editedContentOnlySection,
// synced pattern inner blocks should retain their locked modes
// rather than becoming fully editable ('default').
const stateWithSyncedInUnsynced = dispatchActions(
[
{
type: 'UPDATE_SETTINGS',
settings: {
[ sectionRootClientIdKey ]: '',
},
},
{
type: 'RESET_BLOCKS',
blocks: [
{
name: 'core/group',
clientId: 'unsynced-pattern-group',
attributes: {
metadata: {
patternName: 'test-pattern',
},
},
innerBlocks: [
{
name: 'core/paragraph',
clientId: 'paragraph-in-unsynced',
attributes: {},
innerBlocks: [],
},
{
name: 'core/block',
clientId: 'synced-in-unsynced',
attributes: {},
innerBlocks: [],
},
],
},
],
},
{
type: 'SET_HAS_CONTROLLED_INNER_BLOCKS',
clientId: 'synced-in-unsynced',
hasControlledInnerBlocks: true,
},
{
type: 'REPLACE_INNER_BLOCKS',
rootClientId: 'synced-in-unsynced',
blocks: [
{
name: 'core/paragraph',
clientId: 'synced-inner-paragraph',
attributes: {},
innerBlocks: [],
},
{
name: 'core/group',
clientId: 'synced-inner-group',
attributes: {},
innerBlocks: [
{
name: 'core/paragraph',
clientId:
'synced-inner-paragraph-with-overrides',
attributes: {
metadata: {
bindings: {
__default:
'core/pattern-overrides',
},
},
},
innerBlocks: [],
},
],
},
],
},
],
testReducer
);

// Start editing the unsynced pattern section.
const editingState = dispatchActions(
[
{
type: 'EDIT_CONTENT_ONLY_SECTION',
clientId: 'unsynced-pattern-group',
},
],
testReducer,
stateWithSyncedInUnsynced
);

expect( editingState.derivedBlockEditingModes ).toEqual(
new Map(
Object.entries( {
// Root is outside the edited section.
'': 'disabled',
// The edited section itself is fully editable.
'unsynced-pattern-group': 'default',
// Non-synced child of the edited section is fully editable.
'paragraph-in-unsynced': 'default',
// synced-in-unsynced (core/block) has no derived mode —
// the synced pattern logic returns early without setting one.
// Inner blocks of the synced pattern retain their locked modes.
'synced-inner-paragraph': 'disabled',
'synced-inner-group': 'disabled',
'synced-inner-paragraph-with-overrides':
'contentOnly',
} )
)
);
} );

it( 'nested synced patterns remain disabled when inside an editedContentOnlySection', () => {
// Set up an unsynced pattern containing a synced pattern,
// which itself contains another synced pattern.
// All doubly-nested synced pattern blocks should remain disabled.
const stateWithNestedSynced = dispatchActions(
[
{
type: 'UPDATE_SETTINGS',
settings: {
[ sectionRootClientIdKey ]: '',
},
},
{
type: 'RESET_BLOCKS',
blocks: [
{
name: 'core/group',
clientId: 'unsynced-pattern-group',
attributes: {
metadata: {
patternName: 'test-pattern',
},
},
innerBlocks: [
{
name: 'core/paragraph',
clientId: 'paragraph-in-unsynced',
attributes: {},
innerBlocks: [],
},
{
name: 'core/block',
clientId: 'synced-in-unsynced',
attributes: {},
innerBlocks: [],
},
],
},
],
},
{
type: 'SET_HAS_CONTROLLED_INNER_BLOCKS',
clientId: 'synced-in-unsynced',
hasControlledInnerBlocks: true,
},
{
type: 'REPLACE_INNER_BLOCKS',
rootClientId: 'synced-in-unsynced',
blocks: [
{
name: 'core/paragraph',
clientId:
'synced-inner-paragraph-with-overrides',
attributes: {
metadata: {
bindings: {
__default:
'core/pattern-overrides',
},
},
},
innerBlocks: [],
},
{
name: 'core/block',
clientId: 'nested-synced',
attributes: {},
innerBlocks: [],
},
],
},
{
type: 'SET_HAS_CONTROLLED_INNER_BLOCKS',
clientId: 'nested-synced',
hasControlledInnerBlocks: true,
},
{
type: 'REPLACE_INNER_BLOCKS',
rootClientId: 'nested-synced',
blocks: [
{
name: 'core/paragraph',
clientId: 'deeply-nested-paragraph',
attributes: {},
innerBlocks: [],
},
],
},
],
testReducer
);

// Start editing the unsynced pattern section.
const editingState = dispatchActions(
[
{
type: 'EDIT_CONTENT_ONLY_SECTION',
clientId: 'unsynced-pattern-group',
},
],
testReducer,
stateWithNestedSynced
);

expect( editingState.derivedBlockEditingModes ).toEqual(
new Map(
Object.entries( {
// Root is outside the edited section.
'': 'disabled',
// The edited section itself is fully editable.
'unsynced-pattern-group': 'default',
// Non-synced child of the edited section is fully editable.
'paragraph-in-unsynced': 'default',
// synced-in-unsynced (core/block) has no derived mode.
// Its direct inner block with bindings retains contentOnly.
'synced-inner-paragraph-with-overrides':
'contentOnly',
// The doubly-nested synced pattern and its inner blocks
// are all disabled.
'nested-synced': 'disabled',
'deeply-nested-paragraph': 'disabled',
} )
)
);
} );

it( 'returns the expected block editing modes for synced patterns when switching to zoomed out mode', () => {
const { derivedBlockEditingModes } = dispatchActions(
[
Expand Down
Loading