diff --git a/packages/block-editor/src/components/inserter/block-list.js b/packages/block-editor/src/components/inserter/block-list.js
index d1512e11e6571d..854f3788a966c8 100644
--- a/packages/block-editor/src/components/inserter/block-list.js
+++ b/packages/block-editor/src/components/inserter/block-list.js
@@ -8,8 +8,6 @@ import { map, findIndex, flow, sortBy, groupBy, isEmpty } from 'lodash';
*/
import { __, _x, _n, sprintf } from '@wordpress/i18n';
import { withSpokenMessages } from '@wordpress/components';
-import { addQueryArgs } from '@wordpress/url';
-import { controlsRepeat } from '@wordpress/icons';
import { useMemo, useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
@@ -61,12 +59,6 @@ export function InserterBlockList( {
return items.slice( 0, MAX_SUGGESTED_ITEMS );
}, [ items ] );
- const reusableItems = useMemo( () => {
- return filteredItems.filter(
- ( { category } ) => category === 'reusable'
- );
- }, [ filteredItems ] );
-
const uncategorizedItems = useMemo( () => {
return filteredItems.filter( ( item ) => ! item.category );
}, [ filteredItems ] );
@@ -200,28 +192,6 @@ export function InserterBlockList( {
);
} ) }
- { ! hasChildItems && !! reusableItems.length && (
-
-
-
- { __( 'Manage all reusable blocks' ) }
-
-
- ) }
-
<__experimentalInserterMenuExtension.Slot
fillProps={ {
onSelect: onSelectItem,
diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index c8f55eaadaabef..18baaf0063c7b3 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -21,6 +21,7 @@ import InserterPreviewPanel from './preview-panel';
import InserterBlockList from './block-list';
import BlockPatterns from './block-patterns';
import useInsertionPoint from './hooks/use-insertion-point';
+import InserterReusableBlockList from './reusable-block-list';
const stopKeyPropagation = ( event ) => event.stopPropagation();
@@ -105,6 +106,17 @@ function InserterMenu( {
);
+ const reusableBlocksTab = (
+
+
+
+ );
+
// Disable reason (no-autofocus): The inserter menu is a modal display, not one which
// is always visible, and one which already incurs this behavior of autoFocus via
// Popover's focusOnMount.
@@ -133,13 +145,20 @@ function InserterMenu( {
/* translators: Patterns tab title in the block inserter. */
title: __( 'Patterns' ),
},
+ {
+ name: 'reusable',
+ /* translators: Reusable blocks tab title in the block inserter. */
+ title: __( 'Reusable' ),
+ },
] }
>
{ ( tab ) => {
if ( tab.name === 'blocks' ) {
return blocksTab;
+ } else if ( tab.name === 'patterns' ) {
+ return patternsTab;
}
- return patternsTab;
+ return reusableBlocksTab;
} }
) }
diff --git a/packages/block-editor/src/components/inserter/reusable-block-list.js b/packages/block-editor/src/components/inserter/reusable-block-list.js
new file mode 100644
index 00000000000000..74220a45667514
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/reusable-block-list.js
@@ -0,0 +1,111 @@
+/**
+ * External dependencies
+ */
+import { isEmpty } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { withSpokenMessages } from '@wordpress/components';
+import { useMemo, useEffect } from '@wordpress/element';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import BlockTypesList from '../block-types-list';
+import __experimentalInserterMenuExtension from '../inserter-menu-extension';
+import { searchBlockItems } from './search-items';
+import InserterPanel from './panel';
+import InserterNoResults from './no-results';
+import useBlockTypesState from './hooks/use-block-types-state';
+
+/**
+ * List of reusable blocks shown in the "Reusable" tab of the inserter.
+ *
+ * @param {Object} props Component props.
+ * @param {?string} props.rootClientId Client id of block to insert into.
+ * @param {Function} props.onInsert Callback to run when item is inserted.
+ * @param {Function} props.onHover Callback to run when item is hovered.
+ * @param {?string} props.filterValue Search term.
+ * @param {Function} props.debouncedSpeak Debounced speak function.
+ *
+ * @return {WPComponent} The component.
+ */
+export function InserterReusableBlockList( {
+ rootClientId,
+ onInsert,
+ onHover,
+ filterValue,
+ debouncedSpeak,
+} ) {
+ const [ items, categories, collections, onSelectItem ] = useBlockTypesState(
+ rootClientId,
+ onInsert
+ );
+
+ const filteredItems = useMemo( () => {
+ return searchBlockItems(
+ items,
+ categories,
+ collections,
+ filterValue
+ ).filter( ( { category } ) => category === 'reusable' );
+ }, [ filterValue, items, categories, collections ] );
+
+ // Announce search results on change.
+ useEffect( () => {
+ const resultsFoundMessage = sprintf(
+ /* translators: %d: number of results. */
+ _n( '%d result found.', '%d results found.', filteredItems.length ),
+ filteredItems.length
+ );
+ debouncedSpeak( resultsFoundMessage );
+ }, [ filterValue, debouncedSpeak ] );
+
+ const hasItems = ! isEmpty( filteredItems );
+
+ return (
+
+ { filteredItems.length > 0 && (
+
+
+
+ { __( 'Manage all reusable blocks' ) }
+
+
+ ) }
+
+ <__experimentalInserterMenuExtension.Slot
+ fillProps={ {
+ onSelect: onSelectItem,
+ onHover,
+ filterValue,
+ hasItems,
+ } }
+ >
+ { ( fills ) => {
+ if ( fills.length ) {
+ return fills;
+ }
+ if ( ! hasItems ) {
+ return
;
+ }
+ return null;
+ } }
+
+
+ );
+}
+
+export default withSpokenMessages( InserterReusableBlockList );