diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js
index 50a8b46b46427..844d5dd341437 100644
--- a/packages/block-editor/src/components/inserter/block-types-tab.js
+++ b/packages/block-editor/src/components/inserter/block-types-tab.js
@@ -186,7 +186,7 @@ export function BlockTypesTab(
continue;
}
- if ( rootClientId && item.rootClientId === rootClientId ) {
+ if ( rootClientId && item.isAllowedInCurrentRoot ) {
itemsForCurrentRoot.push( item );
} else {
itemsRemaining.push( item );
diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js
index 8db23267eee8f..6f11060c75c49 100644
--- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js
+++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js
@@ -2,19 +2,23 @@
* WordPress dependencies
*/
import {
+ getBlockType,
createBlock,
createBlocksFromInnerBlocksTemplate,
store as blocksStore,
parse,
} from '@wordpress/blocks';
-import { useSelect } from '@wordpress/data';
+import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback, useMemo } from '@wordpress/element';
+import { store as noticesStore } from '@wordpress/notices';
+import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../../store';
-import { withRootClientIdOptionKey } from '../../../store/utils';
+import { isFiltered } from '../../../store/utils';
+import { unlock } from '../../../lock-unlock';
/**
* Retrieves the block types inserter state.
@@ -26,7 +30,7 @@ import { withRootClientIdOptionKey } from '../../../store/utils';
*/
const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
const options = useMemo(
- () => ( { [ withRootClientIdOptionKey ]: ! isQuick } ),
+ () => ( { [ isFiltered ]: !! isQuick } ),
[ isQuick ]
);
const [ items ] = useSelect(
@@ -38,6 +42,10 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
],
[ rootClientId, options ]
);
+ const { getClosestAllowedInsertionPoint } = unlock(
+ useSelect( blockEditorStore )
+ );
+ const { createErrorNotice } = useDispatch( noticesStore );
const [ categories, collections ] = useSelect( ( select ) => {
const { getCategories, getCollections } = select( blocksStore );
@@ -46,16 +54,29 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
const onSelectItem = useCallback(
(
- {
- name,
- initialAttributes,
- innerBlocks,
- syncStatus,
- content,
- rootClientId: _rootClientId,
- },
+ { name, initialAttributes, innerBlocks, syncStatus, content },
shouldFocusBlock
) => {
+ const destinationClientId = getClosestAllowedInsertionPoint(
+ name,
+ rootClientId
+ );
+ if ( destinationClientId === null ) {
+ const title = getBlockType( name )?.title ?? name;
+ createErrorNotice(
+ sprintf(
+ /* translators: %s: block pattern title. */
+ __( 'Block "%s" can\'t be inserted.' ),
+ title
+ ),
+ {
+ type: 'snackbar',
+ id: 'inserter-notice',
+ }
+ );
+ return;
+ }
+
const insertedBlock =
syncStatus === 'unsynced'
? parse( content, {
@@ -66,15 +87,14 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
initialAttributes,
createBlocksFromInnerBlocksTemplate( innerBlocks )
);
-
onInsert(
insertedBlock,
undefined,
shouldFocusBlock,
- _rootClientId
+ destinationClientId
);
},
- [ onInsert ]
+ [ onInsert, getClosestAllowedInsertionPoint, rootClientId ]
);
return [ items, categories, collections, onSelectItem ];
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 24074ec500456..0cd71bf77b983 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
@@ -71,7 +71,11 @@ function useInsertionPoint( {
selectBlockOnInsert = true,
} ) {
const registry = useRegistry();
- const { getSelectedBlock } = useSelect( blockEditorStore );
+ const {
+ getSelectedBlock,
+ getClosestAllowedInsertionPoint,
+ isBlockInsertionPointVisible,
+ } = unlock( useSelect( blockEditorStore ) );
const { destinationRootClientId, destinationIndex } = useSelect(
( select ) => {
const {
@@ -193,21 +197,30 @@ function useInsertionPoint( {
const onToggleInsertionPoint = useCallback(
( item ) => {
- if ( item?.hasOwnProperty( 'rootClientId' ) ) {
- showInsertionPoint(
- item.rootClientId,
- getIndex( {
- destinationRootClientId,
- destinationIndex,
- rootClientId: item.rootClientId,
- registry,
- } )
- );
+ if ( item && ! isBlockInsertionPointVisible() ) {
+ const allowedDestinationRootClientId =
+ getClosestAllowedInsertionPoint(
+ item.name,
+ destinationRootClientId
+ );
+ if ( allowedDestinationRootClientId !== null ) {
+ showInsertionPoint(
+ allowedDestinationRootClientId,
+ getIndex( {
+ destinationRootClientId,
+ destinationIndex,
+ rootClientId: allowedDestinationRootClientId,
+ registry,
+ } )
+ );
+ }
} else {
hideInsertionPoint();
}
},
[
+ getClosestAllowedInsertionPoint,
+ isBlockInsertionPointVisible,
showInsertionPoint,
hideInsertionPoint,
destinationRootClientId,
diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js
index 6483dc58ae8b9..f8b083d4eedf1 100644
--- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js
+++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js
@@ -87,7 +87,7 @@ const usePatternsState = ( onInsert, rootClientId, selectedCategory ) => {
),
{
type: 'snackbar',
- id: 'block-pattern-inserted-notice',
+ id: 'inserter-notice',
}
);
},
diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js
index 64088f45fa1c3..a890e5fe8dc13 100644
--- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js
+++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js
@@ -184,13 +184,16 @@ export function MediaPreview( { media, onClick, category } ) {
} );
createSuccessNotice(
__( 'Image uploaded and inserted.' ),
- { type: 'snackbar' }
+ { type: 'snackbar', id: 'inserter-notice' }
);
setIsInserting( false );
},
allowedTypes: ALLOWED_MEDIA_TYPES,
onError( message ) {
- createErrorNotice( message, { type: 'snackbar' } );
+ createErrorNotice( message, {
+ type: 'snackbar',
+ id: 'inserter-notice',
+ } );
setIsInserting( false );
},
} );
@@ -281,6 +284,7 @@ export function MediaPreview( { media, onClick, category } ) {
onClick( cloneBlock( block ) );
createSuccessNotice( __( 'Image inserted.' ), {
type: 'snackbar',
+ id: 'inserter-notice',
} );
setShowExternalUploadModal( false );
} }
diff --git a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js
deleted file mode 100644
index 925570130359a..0000000000000
--- a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * External dependencies
- */
-import { render } from 'test/helpers';
-
-/**
- * WordPress dependencies
- */
-import { useSelect } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import items from './fixtures';
-import BlockTypesTab from '../block-types-tab';
-
-jest.mock( '../hooks/use-clipboard-block' );
-jest.mock( '@wordpress/data/src/components/use-select' );
-
-const selectMock = {
- getCategories: jest.fn().mockReturnValue( [] ),
- getCollections: jest.fn().mockReturnValue( [] ),
- getInserterItems: jest.fn().mockReturnValue( [] ),
- canInsertBlockType: jest.fn(),
- getBlockType: jest.fn(),
- getClipboard: jest.fn(),
- getSettings: jest.fn( () => ( { impressions: {} } ) ),
-};
-
-describe( 'BlockTypesTab component', () => {
- beforeEach( () => {
- useSelect.mockImplementation( ( callback ) =>
- callback( () => selectMock )
- );
- } );
-
- it( 'renders without crashing', () => {
- const component = render(
-
- );
- expect( component ).toBeTruthy();
- } );
-
- it( 'shows block items', () => {
- selectMock.getInserterItems.mockReturnValue( items );
-
- const blockItems = items.filter(
- ( { id, category } ) =>
- category !== 'reusable' && id !== 'core-embed/a-paragraph-embed'
- );
- const component = render(
-
- );
-
- blockItems.forEach( ( item ) => {
- expect( component.getByText( item.title ) ).toBeTruthy();
- } );
- } );
-} );
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index 9e99176819ae8..02a37b94ec27f 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -16,6 +16,7 @@ import {
getTemplateLock,
getClientIdsWithDescendants,
isNavigationMode,
+ getBlockRootClientId,
} from './selectors';
import {
checkAllowListRecursive,
@@ -637,3 +638,40 @@ export function getZoomLevel( state ) {
export function isZoomOut( state ) {
return getZoomLevel( state ) < 100;
}
+
+/**
+ * Finds the closest block where the block is allowed to be inserted.
+ *
+ * @param {Object} state Editor state.
+ * @param {string} name Block name.
+ * @param {string} clientId Default insertion point.
+ *
+ * @return {string} clientID of the closest container when the block name can be inserted.
+ */
+export function getClosestAllowedInsertionPoint( state, name, clientId = '' ) {
+ // If we're trying to insert at the root level and it's not allowed
+ // Try the section root instead.
+ if ( ! clientId ) {
+ if ( canInsertBlockType( state, name, clientId ) ) {
+ return clientId;
+ }
+
+ const sectionRootClientId = getSectionRootClientId( state );
+ if (
+ sectionRootClientId &&
+ canInsertBlockType( state, name, sectionRootClientId )
+ ) {
+ return sectionRootClientId;
+ }
+ return null;
+ }
+
+ // Traverse the block tree up until we find a place where we can insert.
+ let current = clientId;
+ while ( current !== null && ! canInsertBlockType( state, name, current ) ) {
+ const parentClientId = getBlockRootClientId( state, current );
+ current = parentClientId;
+ }
+
+ return current;
+}
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 20d6627398886..3163bb5257a9a 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -21,7 +21,7 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
* Internal dependencies
*/
import {
- withRootClientIdOptionKey,
+ isFiltered,
checkAllowListRecursive,
checkAllowList,
getAllPatternsDependants,
@@ -80,7 +80,9 @@ const EMPTY_ARRAY = [];
*/
const EMPTY_SET = new Set();
-const EMPTY_OBJECT = {};
+const DEFAULT_INSERTER_OPTIONS = {
+ [ isFiltered ]: true,
+};
/**
* Returns a block's name given its client ID, or null if no block exists with
@@ -2008,7 +2010,7 @@ const buildBlockTypeItem =
*/
export const getInserterItems = createRegistrySelector( ( select ) =>
createSelector(
- ( state, rootClientId = null, options = EMPTY_OBJECT ) => {
+ ( state, rootClientId = null, options = DEFAULT_INSERTER_OPTIONS ) => {
const buildReusableBlockInserterItem = ( reusableBlock ) => {
const icon = ! reusableBlock.wp_pattern_sync_status
? {
@@ -2056,56 +2058,7 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
)
.map( buildBlockTypeInserterItem );
- if ( options[ withRootClientIdOptionKey ] ) {
- blockTypeInserterItems = blockTypeInserterItems.reduce(
- ( accumulator, item ) => {
- item.rootClientId = rootClientId ?? '';
-
- while (
- ! canInsertBlockTypeUnmemoized(
- state,
- item.name,
- item.rootClientId
- )
- ) {
- if ( ! item.rootClientId ) {
- let sectionRootClientId;
- try {
- sectionRootClientId =
- getSectionRootClientId( state );
- } catch ( e ) {}
- if (
- sectionRootClientId &&
- canInsertBlockTypeUnmemoized(
- state,
- item.name,
- sectionRootClientId
- )
- ) {
- item.rootClientId = sectionRootClientId;
- } else {
- delete item.rootClientId;
- }
- break;
- } else {
- const parentClientId = getBlockRootClientId(
- state,
- item.rootClientId
- );
- item.rootClientId = parentClientId;
- }
- }
-
- // We could also add non insertable items and gray them out.
- if ( item.hasOwnProperty( 'rootClientId' ) ) {
- accumulator.push( item );
- }
-
- return accumulator;
- },
- []
- );
- } else {
+ if ( options[ isFiltered ] !== false ) {
blockTypeInserterItems = blockTypeInserterItems.filter(
( blockType ) =>
canIncludeBlockTypeInInserter(
@@ -2114,6 +2067,17 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
rootClientId
)
);
+ } else {
+ blockTypeInserterItems = blockTypeInserterItems.map(
+ ( blockType ) => ( {
+ ...blockType,
+ isAllowedInCurrentRoot: canIncludeBlockTypeInInserter(
+ state,
+ blockType,
+ rootClientId
+ ),
+ } )
+ );
}
const items = blockTypeInserterItems.reduce(
diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js
index 79e15255e6cc1..9b83a8f74cf9a 100644
--- a/packages/block-editor/src/store/utils.js
+++ b/packages/block-editor/src/store/utils.js
@@ -12,8 +12,7 @@ import { unlock } from '../lock-unlock';
import { STORE_NAME } from './constants';
import { getSectionRootClientId } from './private-selectors';
-export const withRootClientIdOptionKey = Symbol( 'withRootClientId' );
-
+export const isFiltered = Symbol( 'isFiltered' );
const parsedPatternCache = new WeakMap();
const grammarMapCache = new WeakMap();