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 );