diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 806e2b5f5993eb..20d6627398886c 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2901,6 +2901,14 @@ export function __unstableIsWithinBlockOverlay( state, clientId ) { return false; } +function isWithinBlock( state, clientId, parentClientId ) { + let parent = state.blocks.parents.get( clientId ); + while ( !! parent && parent !== parentClientId ) { + parent = state.blocks.parents.get( parent ); + } + return parent === parentClientId; +} + /** * @typedef {import('../components/block-editing-mode').BlockEditingMode} BlockEditingMode */ @@ -2968,36 +2976,58 @@ export const getBlockEditingMode = createRegistrySelector( return 'disabled'; } + if ( editorMode === 'navigation' ) { + const sectionRootClientId = getSectionRootClientId( state ); + + // The root section is "default mode" + if ( clientId === sectionRootClientId ) { + return 'default'; + } + + // Sections should always be contentOnly in navigation mode. + const sectionsClientIds = getBlockOrder( + state, + sectionRootClientId + ); + if ( sectionsClientIds.includes( clientId ) ) { + return 'contentOnly'; + } + + // Blocks outside sections should be disabled. + const isWithinSectionRoot = isWithinBlock( + state, + clientId, + sectionRootClientId + ); + if ( ! isWithinSectionRoot ) { + return 'disabled'; + } + + // The rest of the blocks depend on whether they are content blocks or not. + // This "flattens" the sections tree. + const name = getBlockName( state, clientId ); + const isContent = + select( blocksStore ).__experimentalHasContentRoleAttribute( + name + ); + return isContent ? 'contentOnly' : 'disabled'; + } + + // In normal mode, consider that an explicitely set editing mode takes over. const blockEditingMode = state.blockEditingModes.get( clientId ); if ( blockEditingMode ) { return blockEditingMode; } + + // In normal mode, top level is default mode. if ( ! clientId ) { return 'default'; } - const sectionRootClientId = getSectionRootClientId( state ); - if ( - editorMode === 'navigation' && - clientId === sectionRootClientId - ) { - return 'default'; - } - const sectionsClientIds = getBlockOrder( - state, - sectionRootClientId - ); + const rootClientId = getBlockRootClientId( state, clientId ); const templateLock = getTemplateLock( state, rootClientId ); - if ( - templateLock === 'contentOnly' || - editorMode === 'navigation' - ) { - // Sections should always be contentOnly in navigation mode. - // This will also cause them to display in List View providing - // a structure. - if ( sectionsClientIds.includes( clientId ) ) { - return 'contentOnly'; - } + // If the parent of the block is contentOnly locked, check whether it's a content block. + if ( templateLock === 'contentOnly' ) { const name = getBlockName( state, clientId ); const isContent = select( blocksStore ).__experimentalHasContentRoleAttribute( @@ -3005,6 +3035,7 @@ export const getBlockEditingMode = createRegistrySelector( ); return isContent ? 'contentOnly' : 'disabled'; } + // Otherwise, check if there's an ancestor that is contentOnly const parentMode = getBlockEditingMode( state, rootClientId ); return parentMode === 'contentOnly' ? 'default' : parentMode; } diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 04899a9ee243fd..2895f6573dc54f 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -15,6 +15,7 @@ import { select, dispatch } from '@wordpress/data'; */ import * as selectors from '../selectors'; import { store } from '../'; +import { sectionRootClientIdKey } from '../private-keys'; const { getBlockName, @@ -4382,12 +4383,28 @@ describe( 'getBlockEditingMode', () => { settings: {}, blocks: { byClientId: new Map( [ - [ '6cf70164-9097-4460-bcbf-200560546988', {} ], // Header - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', {} ], // Group - [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', {} ], // | Post Title - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', {} ], // | Post Content - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', {} ], // | | Paragraph - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', {} ], // | | Paragraph + [ + '6cf70164-9097-4460-bcbf-200560546988', + { name: 'core/template-part' }, + ], // Header + [ + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', + { name: 'core/group' }, + ], // Group + [ + 'b26fc763-417d-4f01-b81c-2ec61e14a972', + { name: 'core/post-title' }, + ], // | Post Title + [ + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + { name: 'core/group' }, + ], // | Group + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', { name: 'core/p' } ], // | | Paragraph + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', { name: 'core/p' } ], // | | Paragraph + [ + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', + { name: 'core/group' }, + ], // | | Group ] ), order: new Map( [ [ @@ -4411,10 +4428,12 @@ describe( 'getBlockEditingMode', () => { [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', ], ], [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', [] ], [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', [] ], + [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', [] ], ] ), parents: new Map( [ [ '6cf70164-9097-4460-bcbf-200560546988', '' ], @@ -4435,6 +4454,10 @@ describe( 'getBlockEditingMode', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', ], + [ + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + ], ] ), }, blockListSettings: { @@ -4444,7 +4467,18 @@ describe( 'getBlockEditingMode', () => { blockEditingModes: new Map( [] ), }; - const __experimentalHasContentRoleAttribute = jest.fn( () => false ); + const navigationModeStateWithRootSection = { + ...baseState, + editorMode: 'navigation', + settings: { + [ sectionRootClientIdKey ]: 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', // The group is the "main" container + }, + }; + + const __experimentalHasContentRoleAttribute = jest.fn( ( name ) => { + // consider paragraphs as content blocks. + return name === 'core/p'; + } ); getBlockEditingMode.registry = { select: jest.fn( () => ( { __experimentalHasContentRoleAttribute, @@ -4573,4 +4607,61 @@ describe( 'getBlockEditingMode', () => { getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) ).toBe( 'contentOnly' ); } ); + + it( 'in navigation mode, the root section container is default', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' + ) + ).toBe( 'default' ); + } ); + + it( 'in navigation mode, anything outside the section container is disabled', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + '6cf70164-9097-4460-bcbf-200560546988' + ) + ).toBe( 'disabled' ); + } ); + + it( 'in navigation mode, sections are contentOnly', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'b26fc763-417d-4f01-b81c-2ec61e14a972' + ) + ).toBe( 'contentOnly' ); + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' + ) + ).toBe( 'contentOnly' ); + } ); + + it( 'in navigation mode, blocks with content attributes within sections are contentOnly', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'contentOnly' ); + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c' + ) + ).toBe( 'contentOnly' ); + } ); + + it( 'in navigation mode, blocks without content attributes within sections are disabled', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s' + ) + ).toBe( 'disabled' ); + } ); } );