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
93 changes: 62 additions & 31 deletions packages/block-editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,40 +475,17 @@ export const getContentLockingParent = ( state, clientId ) => {
};

/**
* Retrieves the client ID of the parent section block.
* Checks whether a block meets the raw criteria to be a section block,
* without considering contextual factors like nesting or the edited
* content-only section. Used internally by `isSectionBlock` and
* `getParentSectionBlock` to avoid circular calls between them.
*
* @param {Object} state Global application state.
* @param {string} clientId Client Id of the block.
*
* @return {?string} Client ID of the ancestor block that is a contentOnly section.
* @return {boolean} Whether the block is a candidate section block.
*/
export const getParentSectionBlock = ( state, clientId ) => {
let current = clientId;
let result;

// If sections are nested, return the top level section block.
// Don't return early.
while ( ( current = state.blocks.parents.get( current ) ) ) {
if ( isSectionBlock( state, current ) ) {
result = current;
}
}
return result;
};

/**
* Returns whether the block is a contentOnly section.
*
* @param {Object} state Global application state.
* @param {string} clientId Client Id of the block.
*
* @return {boolean} Whether the block is a contentOnly section.
*/
export function isSectionBlock( state, clientId ) {
if ( clientId === state.editedContentOnlySection ) {
return false;
}

function isSectionBlockCandidate( state, clientId ) {
const blockName = getBlockName( state, clientId );
if ( blockName === 'core/block' ) {
return true;
Expand Down Expand Up @@ -536,18 +513,72 @@ export function isSectionBlock( state, clientId ) {
// TemplateLock cascades to all inner parent blocks. Only the top-level
// block that's contentOnly templateLocked is the true contentLocker,
// all the others are mere imitators.
const hasContentOnlyTempateLock =
const hasContentOnlyTemplateLock =
getTemplateLock( state, clientId ) === 'contentOnly';
const rootClientId = getBlockRootClientId( state, clientId );
const hasRootContentOnlyTemplateLock =
getTemplateLock( state, rootClientId ) === 'contentOnly';
if ( hasContentOnlyTempateLock && ! hasRootContentOnlyTemplateLock ) {
if ( hasContentOnlyTemplateLock && ! hasRootContentOnlyTemplateLock ) {
return true;
}

return false;
}

/**
* Retrieves the client ID of the parent section block.
*
* @param {Object} state Global application state.
* @param {string} clientId Client Id of the block.
*
* @return {?string} Client ID of the ancestor block that is a contentOnly section.
*/
export const getParentSectionBlock = ( state, clientId ) => {
// If this block is within the edited content-only section,
// it has no parent section — it's temporarily fully editable.
if ( isWithinEditedContentOnlySection( state, clientId ) ) {
return undefined;
}

let current = clientId;
let result;

// If sections are nested, return the top level section block.
// Don't return early.
while ( ( current = state.blocks.parents.get( current ) ) ) {
if ( isSectionBlockCandidate( state, current ) ) {
result = current;
}
}
return result;
};

/**
* Returns whether the block is a contentOnly section.
*
* @param {Object} state Global application state.
* @param {string} clientId Client Id of the block.
*
* @return {boolean} Whether the block is a contentOnly section.
*/
export function isSectionBlock( state, clientId ) {
// isWithinEditedContentOnlySection -
// If the section is being edited or a parent section is being edited,
// this block is temporarily not considered a section.
//
// getParentSectionBlock -
// Only the top level section is considered the section,
// a nested section is managed by its parent section.
if (
isWithinEditedContentOnlySection( state, clientId ) ||
getParentSectionBlock( state, clientId )
) {
return false;
}

return isSectionBlockCandidate( state, clientId );
}

/**
* Retrieves the client ID of the block that is a contentOnly section but is
* currently being temporarily edited (contentOnly is deactivated).
Expand Down
239 changes: 239 additions & 0 deletions packages/block-editor/src/store/test/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isBlockHiddenAtViewport,
getViewportModalClientIds,
isSectionBlock,
getParentSectionBlock,
} from '../private-selectors';
import { getBlockEditingMode } from '../selectors';
import { deviceTypeKey } from '../private-keys';
Expand Down Expand Up @@ -1413,5 +1414,243 @@ describe( 'private selectors', () => {
} );
expect( isSectionBlock( state, 'block-1' ) ).toBe( true );
} );

it( 'returns false when nested inside another section block', () => {
const state = {
blocks: {
byClientId: new Map( [
[ 'outer-pattern', { name: 'core/group' } ],
[ 'inner-pattern', { name: 'core/group' } ],
] ),
attributes: new Map( [
[
'outer-pattern',
{ metadata: { patternName: 'outer' } },
],
[
'inner-pattern',
{ metadata: { patternName: 'inner' } },
],
] ),
parents: new Map( [
[ 'outer-pattern', '' ],
[ 'inner-pattern', 'outer-pattern' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: undefined,
};
// inner-pattern is nested inside outer-pattern (also a section),
// so it is not considered a section itself.
expect( isSectionBlock( state, 'inner-pattern' ) ).toBe( false );
} );

it( 'returns false when the block itself is the editedContentOnlySection', () => {
const state = {
...createState( {
patternName: 'my-pattern',
} ),
editedContentOnlySection: 'block-1',
};
expect( isSectionBlock( state, 'block-1' ) ).toBe( false );
} );

it( 'returns false when the block is nested within the editedContentOnlySection', () => {
// Create a nested structure: outer-pattern > block-1 (with patternName)
const state = {
blocks: {
byClientId: new Map( [
[ 'outer-pattern', { name: 'core/group' } ],
[ 'block-1', { name: 'core/group' } ],
] ),
attributes: new Map( [
[
'outer-pattern',
{ metadata: { patternName: 'outer' } },
],
[ 'block-1', { metadata: { patternName: 'inner' } } ],
] ),
parents: new Map( [
[ 'outer-pattern', '' ],
[ 'block-1', 'outer-pattern' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: 'outer-pattern',
};
// block-1 has a patternName, so would normally be a section,
// but since its parent (outer-pattern) is being edited, it's not.
expect( isSectionBlock( state, 'block-1' ) ).toBe( false );
} );

it( 'returns true for section blocks outside the editedContentOnlySection', () => {
// Create a structure with two sibling patterns
const state = {
blocks: {
byClientId: new Map( [
[ 'pattern-a', { name: 'core/group' } ],
[ 'pattern-b', { name: 'core/group' } ],
] ),
attributes: new Map( [
[ 'pattern-a', { metadata: { patternName: 'a' } } ],
[ 'pattern-b', { metadata: { patternName: 'b' } } ],
] ),
parents: new Map( [
[ 'pattern-a', '' ],
[ 'pattern-b', '' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: 'pattern-a',
};
// pattern-b is not the edited section and not within it
expect( isSectionBlock( state, 'pattern-b' ) ).toBe( true );
} );
} );

describe( 'getParentSectionBlock', () => {
it( 'returns undefined when there are no parent section blocks', () => {
const state = {
blocks: {
byClientId: new Map( [
[ 'block-1', { name: 'core/paragraph' } ],
] ),
attributes: new Map( [ [ 'block-1', {} ] ] ),
parents: new Map( [ [ 'block-1', '' ] ] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: undefined,
};
expect( getParentSectionBlock( state, 'block-1' ) ).toBeUndefined();
} );

it( 'returns the parent section block clientId', () => {
const state = {
blocks: {
byClientId: new Map( [
[ 'pattern-block', { name: 'core/group' } ],
[ 'inner-block', { name: 'core/paragraph' } ],
] ),
attributes: new Map( [
[
'pattern-block',
{ metadata: { patternName: 'my-pattern' } },
],
[ 'inner-block', {} ],
] ),
parents: new Map( [
[ 'pattern-block', '' ],
[ 'inner-block', 'pattern-block' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: undefined,
};
expect( getParentSectionBlock( state, 'inner-block' ) ).toBe(
'pattern-block'
);
} );

it( 'returns undefined when the parent is the editedContentOnlySection', () => {
const state = {
blocks: {
byClientId: new Map( [
[ 'pattern-block', { name: 'core/group' } ],
[ 'inner-block', { name: 'core/paragraph' } ],
] ),
attributes: new Map( [
[
'pattern-block',
{ metadata: { patternName: 'my-pattern' } },
],
[ 'inner-block', {} ],
] ),
parents: new Map( [
[ 'pattern-block', '' ],
[ 'inner-block', 'pattern-block' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: 'pattern-block',
};
// Since pattern-block is the edited section, it's no longer
// considered a parent section for inner-block
expect(
getParentSectionBlock( state, 'inner-block' )
).toBeUndefined();
} );

it( 'returns undefined for deeply nested blocks when an ancestor is the editedContentOnlySection', () => {
// Structure: outer-pattern > nested-pattern > deep-block
const state = {
blocks: {
byClientId: new Map( [
[ 'outer-pattern', { name: 'core/group' } ],
[ 'nested-pattern', { name: 'core/group' } ],
[ 'deep-block', { name: 'core/paragraph' } ],
] ),
attributes: new Map( [
[
'outer-pattern',
{ metadata: { patternName: 'outer' } },
],
[
'nested-pattern',
{ metadata: { patternName: 'nested' } },
],
[ 'deep-block', {} ],
] ),
parents: new Map( [
[ 'outer-pattern', '' ],
[ 'nested-pattern', 'outer-pattern' ],
[ 'deep-block', 'nested-pattern' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: 'outer-pattern',
};
// When outer-pattern is being edited, nested-pattern is no longer
// a section (tested above), so deep-block has no parent section
expect(
getParentSectionBlock( state, 'deep-block' )
).toBeUndefined();
} );

it( 'returns the correct parent when editedContentOnlySection is set but not in ancestry', () => {
// Structure: pattern-a > inner-block, pattern-b (sibling)
const state = {
blocks: {
byClientId: new Map( [
[ 'pattern-a', { name: 'core/group' } ],
[ 'inner-block', { name: 'core/paragraph' } ],
[ 'pattern-b', { name: 'core/group' } ],
] ),
attributes: new Map( [
[ 'pattern-a', { metadata: { patternName: 'a' } } ],
[ 'inner-block', {} ],
[ 'pattern-b', { metadata: { patternName: 'b' } } ],
] ),
parents: new Map( [
[ 'pattern-a', '' ],
[ 'inner-block', 'pattern-a' ],
[ 'pattern-b', '' ],
] ),
},
blockListSettings: {},
settings: {},
editedContentOnlySection: 'pattern-b',
};
// pattern-a is not being edited, so inner-block still has pattern-a as parent
expect( getParentSectionBlock( state, 'inner-block' ) ).toBe(
'pattern-a'
);
} );
} );
} );
Loading