diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js
index 1afae5f0de0877..28e25145f15f13 100644
--- a/packages/blocks/src/store/selectors.js
+++ b/packages/blocks/src/store/selectors.js
@@ -332,7 +332,8 @@ export function isMatchingSearchTerm( state, nameOrType, searchTerm ) {
return (
isSearchMatch( blockType.title ) ||
some( blockType.keywords, isSearchMatch ) ||
- isSearchMatch( blockType.category )
+ isSearchMatch( blockType.category ) ||
+ isSearchMatch( blockType.description )
);
}
diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js
index 47b521f05bc30e..32ebe238373582 100644
--- a/packages/blocks/src/store/test/selectors.js
+++ b/packages/blocks/src/store/test/selectors.js
@@ -586,6 +586,7 @@ describe( 'selectors', () => {
title: 'Paragraph',
category: 'text',
keywords: [ 'body' ],
+ description: 'writing flow',
};
const state = {
@@ -598,6 +599,10 @@ describe( 'selectors', () => {
[ 'name', name ],
[ 'block type', blockType ],
[ 'block type without category', omit( blockType, 'category' ) ],
+ [
+ 'block type without description',
+ omit( blockType, 'description' ),
+ ],
] )( 'by %s', ( label, nameOrType ) => {
it( 'should return false if not match', () => {
const result = isMatchingSearchTerm(
@@ -670,6 +675,18 @@ describe( 'selectors', () => {
expect( result ).toBe( true );
} );
}
+
+ if ( nameOrType.description ) {
+ it( 'should return true if match using the description', () => {
+ const result = isMatchingSearchTerm(
+ state,
+ nameOrType,
+ 'flow'
+ );
+
+ expect( result ).toBe( true );
+ } );
+ }
} );
} );
diff --git a/packages/e2e-tests/specs/site-editor/global-styles-sidebar.test.js b/packages/e2e-tests/specs/site-editor/global-styles-sidebar.test.js
new file mode 100644
index 00000000000000..36b615d202926b
--- /dev/null
+++ b/packages/e2e-tests/specs/site-editor/global-styles-sidebar.test.js
@@ -0,0 +1,42 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ deleteAllTemplates,
+ activateTheme,
+ visitSiteEditor,
+ toggleGlobalStyles,
+ openGlobalStylesPanel,
+} from '@wordpress/e2e-test-utils';
+
+describe( 'Global styles sidebar', () => {
+ beforeAll( async () => {
+ await activateTheme( 'emptytheme' );
+ await Promise.all( [
+ deleteAllTemplates( 'wp_template' ),
+ deleteAllTemplates( 'wp_template_part' ),
+ ] );
+ } );
+ afterAll( async () => {
+ await Promise.all( [
+ deleteAllTemplates( 'wp_template' ),
+ deleteAllTemplates( 'wp_template_part' ),
+ ] );
+ await activateTheme( 'twentytwentyone' );
+ } );
+ beforeEach( async () => {
+ await visitSiteEditor();
+ } );
+ describe( 'blocks list', () => {
+ it( 'should filter results properly', async () => {
+ await toggleGlobalStyles();
+ await openGlobalStylesPanel( 'Blocks' );
+ await page.focus( '.edit-site-block-types-search input' );
+ await page.keyboard.type( 'heading' );
+ const results = await page.$$(
+ '.edit-site-block-types-item-list div[role="listitem"]'
+ );
+ expect( results.length ).toEqual( 1 );
+ } );
+ } );
+} );
diff --git a/packages/edit-site/src/components/global-styles/screen-block-list.js b/packages/edit-site/src/components/global-styles/screen-block-list.js
index ac50ce5f672ee3..194192c8b113bf 100644
--- a/packages/edit-site/src/components/global-styles/screen-block-list.js
+++ b/packages/edit-site/src/components/global-styles/screen-block-list.js
@@ -2,13 +2,17 @@
* WordPress dependencies
*/
import { store as blocksStore } from '@wordpress/blocks';
-import { useSelect } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf, _n } from '@wordpress/i18n';
import {
FlexItem,
+ SearchControl,
__experimentalHStack as HStack,
} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { useState, useMemo, useEffect, useRef } from '@wordpress/element';
import { BlockIcon } from '@wordpress/block-editor';
+import { useDebounce } from '@wordpress/compose';
+import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
@@ -68,6 +72,45 @@ function BlockMenuItem( { block } ) {
function ScreenBlockList() {
const sortedBlockTypes = useSortedBlockTypes();
+ const [ filterValue, setFilterValue ] = useState( '' );
+ const debouncedSpeak = useDebounce( speak, 500 );
+ const isMatchingSearchTerm = useSelect(
+ ( select ) => select( blocksStore ).isMatchingSearchTerm,
+ []
+ );
+ const filteredBlockTypes = useMemo( () => {
+ if ( ! filterValue ) {
+ return sortedBlockTypes;
+ }
+ return sortedBlockTypes.filter( ( blockType ) =>
+ isMatchingSearchTerm( blockType, filterValue )
+ );
+ }, [ filterValue, sortedBlockTypes, isMatchingSearchTerm ] );
+
+ const blockTypesListRef = useRef();
+
+ // Announce search results on change
+ useEffect( () => {
+ if ( ! filterValue ) {
+ return;
+ }
+ // We extract the results from the wrapper div's `ref` because
+ // filtered items can contain items that will eventually not
+ // render and there is no reliable way to detect when a child
+ // will return `null`.
+ // TODO: We should find a better way of handling this as it's
+ // fragile and depends on the number of rendered elements of `BlockMenuItem`,
+ // which is now one.
+ // @see https://github.com/WordPress/gutenberg/pull/39117#discussion_r816022116
+ const count = blockTypesListRef.current.childElementCount;
+ const resultsFoundMessage = sprintf(
+ /* translators: %d: number of results. */
+ _n( '%d result found.', '%d results found.', count ),
+ count
+ );
+ debouncedSpeak( resultsFoundMessage, count );
+ }, [ filterValue, debouncedSpeak ] );
+
return (
<>