diff --git a/packages/block-editor/src/components/block-popover/use-popover-scroll.js b/packages/block-editor/src/components/block-popover/use-popover-scroll.js
index 5dfa74005a4081..2d3158e57d0ba7 100644
--- a/packages/block-editor/src/components/block-popover/use-popover-scroll.js
+++ b/packages/block-editor/src/components/block-popover/use-popover-scroll.js
@@ -17,14 +17,22 @@ function usePopoverScroll( contentRef ) {
const effect = useRefEffect(
( node ) => {
function onWheel( event ) {
- const { deltaX, deltaY } = event;
+ const { deltaX, deltaY, target } = event;
const contentEl = contentRef.current;
let scrollContainer = scrollContainerCache.get( contentEl );
if ( ! scrollContainer ) {
scrollContainer = getScrollContainer( contentEl );
scrollContainerCache.set( contentEl, scrollContainer );
}
- scrollContainer.scrollBy( deltaX, deltaY );
+ // Finds a scrollable ancestor of the event’s target. It's not cached because the
+ // it may not remain scrollable due to popover position changes. The cache is also
+ // less likely to be utilized because the target may be different every event.
+ const eventScrollContainer = getScrollContainer( target );
+ // Scrolls “through” the popover only if another contained scrollable area isn’t
+ // in front of it. This is to avoid scrolling both containers simultaneously.
+ if ( ! node.contains( eventScrollContainer ) ) {
+ scrollContainer.scrollBy( deltaX, deltaY );
+ }
}
// Tell the browser that we do not call event.preventDefault
// See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
diff --git a/packages/block-editor/src/components/block-tools/use-show-block-tools.js b/packages/block-editor/src/components/block-tools/use-show-block-tools.js
index 0b36786f54d5cf..533d1ffde4d108 100644
--- a/packages/block-editor/src/components/block-tools/use-show-block-tools.js
+++ b/packages/block-editor/src/components/block-tools/use-show-block-tools.js
@@ -36,7 +36,7 @@ export function useShowBlockTools() {
const hasSelectedBlock = !! clientId && !! block;
const isEmptyDefaultBlock =
hasSelectedBlock &&
- isUnmodifiedDefaultBlock( block ) &&
+ isUnmodifiedDefaultBlock( block, 'content' ) &&
getBlockMode( clientId ) !== 'html';
const _showEmptyBlockSideInserter =
clientId &&
diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
index de814152c620b0..8e2deccb9ade66 100644
--- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
+++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
@@ -151,7 +151,7 @@ function useInsertionPoint( {
if (
! isAppender &&
selectedBlock &&
- isUnmodifiedDefaultBlock( selectedBlock )
+ isUnmodifiedDefaultBlock( selectedBlock, 'content' )
) {
replaceBlocks(
selectedBlock.clientId,
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index 2a4b64f3608874..e83a251ec7e671 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -144,8 +144,6 @@ export const getEnabledClientIdsTree = createRegistrySelector( ( select ) =>
state.derivedBlockEditingModes,
state.derivedNavModeBlockEditingModes,
state.blockEditingModes,
- state.settings.templateLock,
- state.blockListSettings,
select( STORE_NAME ).__unstableGetEditorMode( state ),
] )
);
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index 5fc08ce7a37b89..ec82d5192dc6f7 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -2267,6 +2267,12 @@ function getDerivedBlockEditingModesForTree(
syncedPatternClientIds.push( clientId );
}
} );
+ const contentOnlyTemplateLockedClientIds = Object.keys(
+ state.blockListSettings
+ ).filter(
+ ( clientId ) =>
+ state.blockListSettings[ clientId ]?.templateLock === 'contentOnly'
+ );
traverseBlockTree( state, treeClientId, ( block ) => {
const { clientId, name: blockName } = block;
@@ -2457,6 +2463,23 @@ function getDerivedBlockEditingModesForTree(
derivedBlockEditingModes.set( clientId, 'disabled' );
}
}
+
+ // `templateLock: 'contentOnly'` derived modes.
+ if ( contentOnlyTemplateLockedClientIds.length ) {
+ const hasContentOnlyTemplateLockedParent =
+ !! findParentInClientIdsList(
+ state,
+ clientId,
+ contentOnlyTemplateLockedClientIds
+ );
+ if ( hasContentOnlyTemplateLockedParent ) {
+ if ( isContentBlock( blockName ) ) {
+ derivedBlockEditingModes.set( clientId, 'contentOnly' );
+ } else {
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ }
+ }
+ }
} );
return derivedBlockEditingModes;
@@ -2628,6 +2651,75 @@ export function withDerivedBlockEditingModes( reducer ) {
}
break;
}
+ case 'UPDATE_BLOCK_LIST_SETTINGS': {
+ // Handle the addition and removal of contentOnly template locked blocks.
+ const addedBlocks = [];
+ const removedClientIds = [];
+
+ const updates =
+ typeof action.clientId === 'string'
+ ? { [ action.clientId ]: action.settings }
+ : action.clientId;
+
+ for ( const clientId in updates ) {
+ const isNewContentOnlyBlock =
+ state.blockListSettings[ clientId ]?.templateLock !==
+ 'contentOnly' &&
+ nextState.blockListSettings[ clientId ]
+ ?.templateLock === 'contentOnly';
+
+ const wasContentOnlyBlock =
+ state.blockListSettings[ clientId ]?.templateLock ===
+ 'contentOnly' &&
+ nextState.blockListSettings[ clientId ]
+ ?.templateLock !== 'contentOnly';
+
+ if ( isNewContentOnlyBlock ) {
+ addedBlocks.push(
+ nextState.blocks.tree.get( clientId )
+ );
+ } else if ( wasContentOnlyBlock ) {
+ removedClientIds.push( clientId );
+ }
+ }
+
+ if ( ! addedBlocks.length && ! removedClientIds.length ) {
+ break;
+ }
+
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks,
+ removedClientIds,
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks,
+ removedClientIds,
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
case 'SET_BLOCK_EDITING_MODE':
case 'UNSET_BLOCK_EDITING_MODE':
case 'SET_HAS_CONTROLLED_INNER_BLOCKS': {
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 85ea65e5875d5c..1e18ca232a87fa 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -3076,62 +3076,36 @@ export function __unstableIsWithinBlockOverlay( state, clientId ) {
* @return {BlockEditingMode} The block editing mode. One of `'disabled'`,
* `'contentOnly'`, or `'default'`.
*/
-export const getBlockEditingMode = createRegistrySelector(
- ( select ) =>
- ( state, clientId = '' ) => {
- // Some selectors that call this provide `null` as the default
- // rootClientId, but the default rootClientId is actually `''`.
- if ( clientId === null ) {
- clientId = '';
- }
+export function getBlockEditingMode( state, clientId = '' ) {
+ // Some selectors that call this provide `null` as the default
+ // rootClientId, but the default rootClientId is actually `''`.
+ if ( clientId === null ) {
+ clientId = '';
+ }
- const isNavMode = isNavigationMode( state );
-
- // If the editor is currently not in navigation mode, check if the clientId
- // has an editing mode set in the regular derived map.
- // There may be an editing mode set here for synced patterns or in zoomed out
- // mode.
- if (
- ! isNavMode &&
- state.derivedBlockEditingModes?.has( clientId )
- ) {
- return state.derivedBlockEditingModes.get( clientId );
- }
+ const isNavMode = isNavigationMode( state );
- // If the editor *is* in navigation mode, the block editing mode states
- // are stored in the derivedNavModeBlockEditingModes map.
- if (
- isNavMode &&
- state.derivedNavModeBlockEditingModes?.has( clientId )
- ) {
- return state.derivedNavModeBlockEditingModes.get( clientId );
- }
+ // If the editor is currently not in navigation mode, check if the clientId
+ // has an editing mode set in the regular derived map.
+ // There may be an editing mode set here for synced patterns or in zoomed out
+ // mode.
+ if ( ! isNavMode && state.derivedBlockEditingModes?.has( clientId ) ) {
+ return state.derivedBlockEditingModes.get( clientId );
+ }
- // In normal mode, consider that an explicitly set editing mode takes over.
- const blockEditingMode = state.blockEditingModes.get( clientId );
- if ( blockEditingMode ) {
- return blockEditingMode;
- }
+ // If the editor *is* in navigation mode, the block editing mode states
+ // are stored in the derivedNavModeBlockEditingModes map.
+ if ( isNavMode && state.derivedNavModeBlockEditingModes?.has( clientId ) ) {
+ return state.derivedNavModeBlockEditingModes.get( clientId );
+ }
- // In normal mode, top level is default mode.
- if ( clientId === '' ) {
- return 'default';
- }
+ // In normal mode, consider that an explicitly set editing mode takes over.
+ if ( state.blockEditingModes.has( clientId ) ) {
+ return state.blockEditingModes.get( clientId );
+ }
- const rootClientId = getBlockRootClientId( state, clientId );
- const templateLock = getTemplateLock( state, rootClientId );
- // 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 { hasContentRoleAttribute } = unlock(
- select( blocksStore )
- );
- const isContent = hasContentRoleAttribute( name );
- return isContent ? 'contentOnly' : 'disabled';
- }
- return 'default';
- }
-);
+ return 'default';
+}
/**
* Indicates if a block is ungroupable.
diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js
index bf0e18c7a24d6a..20b21f2c76dd84 100644
--- a/packages/block-editor/src/store/test/private-selectors.js
+++ b/packages/block-editor/src/store/test/private-selectors.js
@@ -504,6 +504,10 @@ describe( 'private selectors', () => {
[ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ],
[ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ],
] ),
+ derivedBlockEditingModes: new Map( [
+ [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ],
+ [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ],
+ ] ),
blockListSettings: {},
};
expect(
diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js
index b0e67a5de53169..342f978d72ab3b 100644
--- a/packages/block-editor/src/store/test/reducer.js
+++ b/packages/block-editor/src/store/test/reducer.js
@@ -3575,6 +3575,7 @@ describe( 'state', () => {
blocks,
settings,
zoomLevel,
+ blockListSettings,
blockEditingModes,
} )
);
@@ -3885,6 +3886,210 @@ describe( 'state', () => {
} );
} );
+ describe( 'contentOnly template locking', () => {
+ let initialState;
+ beforeAll( () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'edit' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ // Simulates how the editor typically inserts controlled blocks,
+ // - first the pattern is inserted with no inner blocks.
+ // - next the pattern is marked as a controlled block.
+ // - finally, once the inner blocks of the pattern are received, they're inserted.
+ // This process is repeated for the two patterns in this test.
+ initialState = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: '',
+ },
+ },
+ {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-1',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-1',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-2',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-2',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'UPDATE_BLOCK_LIST_SETTINGS',
+ clientId: 'group-1',
+ settings: {
+ templateLock: 'contentOnly',
+ },
+ },
+ ],
+ testReducer,
+ initialState
+ );
+ } );
+
+ afterAll( () => {
+ select.mockRestore();
+ } );
+
+ it( 'returns the expected block editing modes for a parent block with contentOnly template locking', () => {
+ // Only the parent pattern and its own children that have bindings
+ // are in contentOnly mode. All other blocks are disabled.
+ expect( initialState.derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ 'paragraph-1': 'contentOnly',
+ 'group-2': 'disabled',
+ 'paragraph-2': 'contentOnly',
+ } )
+ )
+ );
+ } );
+
+ it( 'removes block editing modes when template locking is removed', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'UPDATE_BLOCK_LIST_SETTINGS',
+ clientId: 'group-1',
+ settings: {
+ templateLock: false,
+ },
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual( new Map() );
+ } );
+
+ it( 'allows explicitly set blockEditingModes to override the contentOnly template locking', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'SET_BLOCK_EDITING_MODE',
+ clientId: 'group-1',
+ mode: 'disabled',
+ },
+ {
+ type: 'SET_BLOCK_EDITING_MODE',
+ clientId: 'paragraph-2',
+ mode: 'disabled',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ 'paragraph-1': 'contentOnly',
+ 'group-2': 'disabled',
+ 'paragraph-2': 'contentOnly',
+ } )
+ )
+ );
+ } );
+
+ it( 'returns the expected block editing modes for synced patterns when switching to navigation mode', () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'navigation' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ const { derivedNavModeBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'SET_EDITOR_MODE',
+ mode: 'navigation',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ // Group 1 is now a section, so is set to contentOnly.
+ 'group-1': 'contentOnly',
+ 'group-2': 'disabled',
+ 'paragraph-1': 'contentOnly',
+ 'paragraph-2': 'contentOnly',
+ } )
+ )
+ );
+
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'edit' ),
+ };
+ }
+ return select( storeName );
+ } );
+ } );
+
+ it( 'returns the expected block editing modes for synced patterns when switching to zoomed out mode', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'SET_ZOOM_LEVEL',
+ zoom: 'auto-scaled',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section.
+ 'group-2': 'disabled',
+ 'paragraph-1': 'disabled',
+ 'paragraph-2': 'disabled',
+ } )
+ )
+ );
+ } );
+ } );
+
describe( 'navigation mode', () => {
let initialState;
diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js
index 587e1036e405e2..d5fedb4ea7c0b8 100644
--- a/packages/block-editor/src/store/test/selectors.js
+++ b/packages/block-editor/src/store/test/selectors.js
@@ -4380,92 +4380,9 @@ describe( '__unstableGetClientIdsTree', () => {
describe( 'getBlockEditingMode', () => {
const baseState = {
- settings: {},
- blocks: {
- byClientId: new Map( [
- [
- '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( [
- [
- '',
- [
- '6cf70164-9097-4460-bcbf-200560546988',
- 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337',
- ],
- ],
- [ '6cf70164-9097-4460-bcbf-200560546988', [] ],
- [
- 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337',
- [
- 'b26fc763-417d-4f01-b81c-2ec61e14a972',
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f',
- ],
- ],
- [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', [] ],
- [
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f',
- [
- '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', '' ],
- [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', '' ],
- [
- 'b26fc763-417d-4f01-b81c-2ec61e14a972',
- 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337',
- ],
- [
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f',
- 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337',
- ],
- [
- 'b3247f75-fd94-4fef-97f9-5bfd162cc416',
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f',
- ],
- [
- 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c',
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f',
- ],
- [
- '9b9c5c3f-2e46-4f02-9e14-9fed515b958s',
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f',
- ],
- ] ),
- },
- blockListSettings: {
- 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337': {},
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {},
- },
blockEditingModes: new Map( [] ),
derivedBlockEditingModes: new Map( [] ),
+ derivedNavModeBlockEditingModes: new Map( [] ),
};
const hasContentRoleAttribute = jest.fn( () => false );
@@ -4514,115 +4431,59 @@ describe( 'getBlockEditingMode', () => {
).toBe( 'contentOnly' );
} );
- it( 'should return disabled if explicitly set on a parent', () => {
- const state = {
- ...baseState,
- blockEditingModes: new Map( [
- [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ],
- ] ),
- derivedBlockEditingModes: new Map( [
- [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ],
- [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ],
- [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ],
- [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ],
- [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ],
- ] ),
- };
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'disabled' );
- } );
-
- it( 'should return default if parent is set to contentOnly', () => {
- const state = {
- ...baseState,
- blockEditingModes: new Map( [
- [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'contentOnly' ],
- ] ),
- };
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'default' );
- } );
-
- it( 'should return disabled if overridden by a parent', () => {
- const state = {
- ...baseState,
- blockEditingModes: new Map( [
- [ '', 'disabled' ],
- [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'default' ],
- [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ],
- ] ),
- derivedBlockEditingModes: new Map( [
- [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ],
- [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ],
- [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ],
- [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ],
- ] ),
- };
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'disabled' );
- } );
-
- it( 'should return disabled if explicitly set on root', () => {
- const state = {
- ...baseState,
- blockEditingModes: new Map( [ [ '', 'disabled' ] ] ),
- derivedBlockEditingModes: new Map( [
- [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ],
- [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ],
- [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ],
- [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ],
- [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ],
- [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ],
- [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ],
- ] ),
- };
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'disabled' );
- } );
-
- it( 'should return default if root is contentOnly', () => {
- const state = {
- ...baseState,
- blockEditingModes: new Map( [ [ '', 'contentOnly' ] ] ),
- };
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'default' );
- } );
+ describe( 'derived block editing modes override standard block editing modes', () => {
+ it( 'should return default if explicitly set', () => {
+ const state = {
+ ...baseState,
+ blockEditingModes: new Map( [
+ [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'contentOnly' ],
+ ] ),
+ derivedBlockEditingModes: new Map( [
+ [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'default' ],
+ ] ),
+ };
+ expect(
+ getBlockEditingMode(
+ state,
+ 'b3247f75-fd94-4fef-97f9-5bfd162cc416'
+ )
+ ).toBe( 'default' );
+ } );
- it( 'should return disabled if parent is locked and the block has no content role', () => {
- const state = {
- ...baseState,
- blockListSettings: {
- ...baseState.blockListSettings,
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {
- templateLock: 'contentOnly',
- },
- },
- };
- hasContentRoleAttribute.mockReturnValueOnce( false );
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'disabled' );
- } );
+ it( 'should return disabled if explicitly set', () => {
+ const state = {
+ ...baseState,
+ blockEditingModes: new Map( [
+ [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'contentOnly' ],
+ ] ),
+ derivedBlockEditingModes: new Map( [
+ [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ],
+ ] ),
+ };
+ expect(
+ getBlockEditingMode(
+ state,
+ 'b3247f75-fd94-4fef-97f9-5bfd162cc416'
+ )
+ ).toBe( 'disabled' );
+ } );
- it( 'should return contentOnly if parent is locked and the block has a content role', () => {
- const state = {
- ...baseState,
- blockListSettings: {
- ...baseState.blockListSettings,
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {
- templateLock: 'contentOnly',
- },
- },
- };
- hasContentRoleAttribute.mockReturnValueOnce( true );
- expect(
- getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
- ).toBe( 'contentOnly' );
+ it( 'should return contentOnly if explicitly set', () => {
+ const state = {
+ ...baseState,
+ blockEditingModes: new Map( [
+ [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'default' ],
+ ] ),
+ derivedBlockEditingModes: new Map( [
+ [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'contentOnly' ],
+ ] ),
+ };
+ expect(
+ getBlockEditingMode(
+ state,
+ 'b3247f75-fd94-4fef-97f9-5bfd162cc416'
+ )
+ ).toBe( 'contentOnly' );
+ } );
} );
} );
diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js
index 16695f53f67466..9e5437fea61b72 100644
--- a/packages/block-library/src/cover/test/edit.js
+++ b/packages/block-library/src/cover/test/edit.js
@@ -51,11 +51,7 @@ async function createAndSelectBlock() {
name: 'Black',
} )
);
- await userEvent.click(
- screen.getByRole( 'button', {
- name: 'Select parent block: Cover',
- } )
- );
+ await selectBlock( 'Block: Cover' );
}
describe( 'Cover block', () => {
diff --git a/packages/block-library/src/navigation-link/block-inserter.js b/packages/block-library/src/navigation-link/block-inserter.js
new file mode 100644
index 00000000000000..704641c3801e6f
--- /dev/null
+++ b/packages/block-library/src/navigation-link/block-inserter.js
@@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useSelect } from '@wordpress/data';
+import {
+ store as blockEditorStore,
+ privateApis as blockEditorPrivateApis,
+} from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import DialogWrapper from './dialog-wrapper';
+import { unlock } from '../lock-unlock';
+
+const { PrivateQuickInserter: QuickInserter } = unlock(
+ blockEditorPrivateApis
+);
+
+/**
+ * Component for inserting blocks within the Navigation Link UI.
+ *
+ * @param {Object} props Component props.
+ * @param {string} props.clientId Client ID of the navigation link block.
+ * @param {Function} props.onBack Callback when user wants to go back.
+ * @param {Function} props.onBlockInsert Callback when a block is inserted.
+ */
+function LinkUIBlockInserter( { clientId, onBack, onBlockInsert } ) {
+ const { rootBlockClientId } = useSelect(
+ ( select ) => {
+ const { getBlockRootClientId } = select( blockEditorStore );
+
+ return {
+ rootBlockClientId: getBlockRootClientId( clientId ),
+ };
+ },
+ [ clientId ]
+ );
+
+ if ( ! clientId ) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default LinkUIBlockInserter;
diff --git a/packages/block-library/src/navigation-link/dialog-wrapper.js b/packages/block-library/src/navigation-link/dialog-wrapper.js
new file mode 100644
index 00000000000000..e37166a1dfc7c2
--- /dev/null
+++ b/packages/block-library/src/navigation-link/dialog-wrapper.js
@@ -0,0 +1,74 @@
+/**
+ * WordPress dependencies
+ */
+import { Button, VisuallyHidden } from '@wordpress/components';
+import { __, isRTL } from '@wordpress/i18n';
+import { chevronLeftSmall, chevronRightSmall } from '@wordpress/icons';
+import { useInstanceId, useFocusOnMount } from '@wordpress/compose';
+
+/**
+ * Shared BackButton component for consistent navigation across LinkUI sub-components.
+ *
+ * @param {Object} props Component props.
+ * @param {string} props.className CSS class name for the button.
+ * @param {Function} props.onBack Callback when user wants to go back.
+ */
+function BackButton( { className, onBack } ) {
+ return (
+
+ );
+}
+
+/**
+ * Shared DialogWrapper component for consistent dialog structure across LinkUI sub-components.
+ *
+ * @param {Object} props Component props.
+ * @param {string} props.className CSS class name for the dialog container.
+ * @param {string} props.title Dialog title for accessibility.
+ * @param {string} props.description Dialog description for accessibility.
+ * @param {Function} props.onBack Callback when user wants to go back.
+ * @param {Object} props.children Child components to render inside the dialog.
+ */
+function DialogWrapper( { className, title, description, onBack, children } ) {
+ const dialogTitleId = useInstanceId(
+ DialogWrapper,
+ 'link-ui-dialog-title'
+ );
+ const dialogDescriptionId = useInstanceId(
+ DialogWrapper,
+ 'link-ui-dialog-description'
+ );
+ const focusOnMountRef = useFocusOnMount( 'firstElement' );
+ const backButtonClassName = `${ className }__back`;
+
+ return (
+
+
+ { title }
+ { description }
+
+
+
+
+ { children }
+
+ );
+}
+
+export default DialogWrapper;
diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js
index eca905d399c34e..c2dfccde84b2ad 100644
--- a/packages/block-library/src/navigation-link/link-ui.js
+++ b/packages/block-library/src/navigation-link/link-ui.js
@@ -8,13 +8,8 @@ import {
VisuallyHidden,
__experimentalVStack as VStack,
} from '@wordpress/components';
-import { __, isRTL } from '@wordpress/i18n';
-import {
- LinkControl,
- store as blockEditorStore,
- privateApis as blockEditorPrivateApis,
- useBlockEditingMode,
-} from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+import { LinkControl, useBlockEditingMode } from '@wordpress/block-editor';
import {
useMemo,
useState,
@@ -23,19 +18,14 @@ import {
forwardRef,
} from '@wordpress/element';
import { useResourcePermissions } from '@wordpress/core-data';
-import { useSelect } from '@wordpress/data';
-import { chevronLeftSmall, chevronRightSmall, plus } from '@wordpress/icons';
-import { useInstanceId, useFocusOnMount } from '@wordpress/compose';
+import { plus } from '@wordpress/icons';
+import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
-import { unlock } from '../lock-unlock';
import { LinkUIPageCreator } from './page-creator';
-
-const { PrivateQuickInserter: QuickInserter } = unlock(
- blockEditorPrivateApis
-);
+import LinkUIBlockInserter from './block-inserter';
/**
* Given the Link block's type attribute, return the query params to give to
@@ -75,74 +65,6 @@ export function getSuggestionsQuery( type, kind ) {
}
}
-function LinkUIBlockInserter( { clientId, onBack, onBlockInsert } ) {
- const { rootBlockClientId } = useSelect(
- ( select ) => {
- const { getBlockRootClientId } = select( blockEditorStore );
-
- return {
- rootBlockClientId: getBlockRootClientId( clientId ),
- };
- },
- [ clientId ]
- );
-
- const focusOnMountRef = useFocusOnMount( 'firstElement' );
-
- const dialogTitleId = useInstanceId(
- LinkControl,
- `link-ui-block-inserter__title`
- );
- const dialogDescriptionId = useInstanceId(
- LinkControl,
- `link-ui-block-inserter__description`
- );
-
- if ( ! clientId ) {
- return null;
- }
-
- return (
-
-
- { __( 'Add block' ) }
-
-
- { __( 'Choose a block to add to your Navigation.' ) }
-
-
-
-
-
-
-
- );
-}
-
function UnforwardedLinkUI( props, ref ) {
const { label, url, opensInNewTab, type, kind } = props.link;
const postType = type || 'page';
@@ -176,11 +98,11 @@ function UnforwardedLinkUI( props, ref ) {
const dialogTitleId = useInstanceId(
LinkUI,
- `link-ui-link-control__title`
+ 'link-ui-link-control__title'
);
const dialogDescriptionId = useInstanceId(
LinkUI,
- `link-ui-link-control__description`
+ 'link-ui-link-control__description'
);
const blockEditingMode = useBlockEditingMode();
@@ -220,8 +142,13 @@ function UnforwardedLinkUI( props, ref ) {
onChange={ props.onChange }
onRemove={ props.onRemove }
onCancel={ props.onCancel }
- renderControlBottom={ () =>
- ! link?.url?.length && (
+ renderControlBottom={ () => {
+ // Don't show the tools when there is submitted link (preview state).
+ if ( link?.url?.length ) {
+ return null;
+ }
+
+ return (
- )
- }
+ );
+ } }
/>
) }
@@ -277,8 +209,8 @@ const LinkUITools = ( {
setAddingPage,
focusAddBlockButton,
focusAddPageButton,
- canCreatePage,
- blockEditingMode,
+ canAddPage,
+ canAddBlock,
} ) => {
const blockInserterAriaRole = 'listbox';
const addBlockButtonRef = useRef();
@@ -298,9 +230,14 @@ const LinkUITools = ( {
}
}, [ focusAddPageButton ] );
+ // Don't render anything if neither button should be shown
+ if ( ! canAddPage && ! canAddBlock ) {
+ return null;
+ }
+
return (
- { canCreatePage && (
+ { canAddPage && (