diff --git a/packages/block-editor/src/components/block-types-list/index.js b/packages/block-editor/src/components/block-types-list/index.js
index ca54df0d24b78f..28c538ac52c7d1 100644
--- a/packages/block-editor/src/components/block-types-list/index.js
+++ b/packages/block-editor/src/components/block-types-list/index.js
@@ -2,15 +2,20 @@
* WordPress dependencies
*/
import { getBlockMenuDefaultClassName } from '@wordpress/blocks';
-import {
- __unstableComposite as Composite,
- __unstableUseCompositeState as useCompositeState,
-} from '@wordpress/components';
/**
* Internal dependencies
*/
import InserterListItem from '../inserter-list-item';
+import { InserterListboxGroup, InserterListboxRow } from '../inserter-listbox';
+
+function chunk( array, size ) {
+ const chunks = [];
+ for ( let i = 0, j = array.length; i < j; i += size ) {
+ chunks.push( array.slice( i, i + size ) );
+ }
+ return chunks;
+}
function BlockTypesList( {
items = [],
@@ -20,35 +25,30 @@ function BlockTypesList( {
label,
isDraggable = true,
} ) {
- const composite = useCompositeState();
return (
- /*
- * Disable reason: The `list` ARIA role is redundant but
- * Safari+VoiceOver won't announce the list otherwise.
- */
- /* eslint-disable jsx-a11y/no-redundant-roles */
-
- { items.map( ( item ) => {
- return (
-
- );
- } ) }
+ { chunk( items, 3 ).map( ( row, i ) => (
+
+ { row.map( ( item, j ) => (
+
+ ) ) }
+
+ ) ) }
{ children }
-
- /* eslint-enable jsx-a11y/no-redundant-roles */
+
);
}
diff --git a/packages/block-editor/src/components/block-types-list/style.scss b/packages/block-editor/src/components/block-types-list/style.scss
index 1f197db18126bd..f1f22fe504fbe4 100644
--- a/packages/block-editor/src/components/block-types-list/style.scss
+++ b/packages/block-editor/src/components/block-types-list/style.scss
@@ -1,5 +1,4 @@
-.block-editor-block-types-list {
- list-style: none;
+.block-editor-block-types-list > [role="presentation"] {
padding: 4px;
margin-left: -4px;
margin-right: -4px;
diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js
index e2fb004fedbe26..5a13c27c8616f9 100644
--- a/packages/block-editor/src/components/inserter-list-item/index.js
+++ b/packages/block-editor/src/components/inserter-list-item/index.js
@@ -7,10 +7,6 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { useMemo, useRef, memo } from '@wordpress/element';
-import {
- Button,
- __unstableCompositeItem as CompositeItem,
-} from '@wordpress/components';
import {
createBlock,
createBlocksFromInnerBlocksTemplate,
@@ -21,6 +17,7 @@ import { ENTER } from '@wordpress/keycodes';
* Internal dependencies
*/
import BlockIcon from '../block-icon';
+import { InserterListboxItem } from '../inserter-listbox';
import InserterDraggableBlocks from '../inserter-draggable-blocks';
/**
@@ -41,7 +38,7 @@ function isAppleOS( _window = window ) {
function InserterListItem( {
className,
- composite,
+ isFirst,
item,
onSelect,
onHover,
@@ -89,10 +86,8 @@ function InserterListItem( {
}
} }
>
- onHover( null ) }
onBlur={ () => onHover( null ) }
- // Use the CompositeItem `focusable` prop over Button's
- // isFocusable. The latter was shown to cause an issue
- // with tab order in the inserter list.
- focusable
{ ...props }
>
{ item.title }
-
+
) }
diff --git a/packages/block-editor/src/components/inserter-listbox/context.js b/packages/block-editor/src/components/inserter-listbox/context.js
new file mode 100644
index 00000000000000..aaaac34f05f61f
--- /dev/null
+++ b/packages/block-editor/src/components/inserter-listbox/context.js
@@ -0,0 +1,8 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext } from '@wordpress/element';
+
+const InserterListboxContext = createContext();
+
+export default InserterListboxContext;
diff --git a/packages/block-editor/src/components/inserter-listbox/group.js b/packages/block-editor/src/components/inserter-listbox/group.js
new file mode 100644
index 00000000000000..11e98cf01b202f
--- /dev/null
+++ b/packages/block-editor/src/components/inserter-listbox/group.js
@@ -0,0 +1,40 @@
+/**
+ * WordPress dependencies
+ */
+import { forwardRef, useEffect, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { speak } from '@wordpress/a11y';
+
+function InserterListboxGroup( props, ref ) {
+ const [ shouldSpeak, setShouldSpeak ] = useState( false );
+
+ useEffect( () => {
+ if ( shouldSpeak ) {
+ speak(
+ __( 'Use left and right arrow keys to move through blocks' )
+ );
+ }
+ }, [ shouldSpeak ] );
+
+ return (
+
{
+ setShouldSpeak( true );
+ } }
+ onBlur={ ( event ) => {
+ const focusingOutsideGroup = ! event.currentTarget.contains(
+ event.relatedTarget
+ );
+ if ( focusingOutsideGroup ) {
+ setShouldSpeak( false );
+ }
+ } }
+ { ...props }
+ />
+ );
+}
+
+export default forwardRef( InserterListboxGroup );
diff --git a/packages/block-editor/src/components/inserter-listbox/index.js b/packages/block-editor/src/components/inserter-listbox/index.js
new file mode 100644
index 00000000000000..6345cb38c494ac
--- /dev/null
+++ b/packages/block-editor/src/components/inserter-listbox/index.js
@@ -0,0 +1,27 @@
+/**
+ * WordPress dependencies
+ */
+import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import InserterListboxContext from './context';
+
+export { default as InserterListboxGroup } from './group';
+export { default as InserterListboxRow } from './row';
+export { default as InserterListboxItem } from './item';
+
+function InserterListbox( { children } ) {
+ const compositeState = useCompositeState( {
+ shift: true,
+ wrap: 'horizontal',
+ } );
+ return (
+
+ { children }
+
+ );
+}
+
+export default InserterListbox;
diff --git a/packages/block-editor/src/components/inserter-listbox/item.js b/packages/block-editor/src/components/inserter-listbox/item.js
new file mode 100644
index 00000000000000..50adb4a7880387
--- /dev/null
+++ b/packages/block-editor/src/components/inserter-listbox/item.js
@@ -0,0 +1,52 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ Button,
+ __unstableCompositeItem as CompositeItem,
+} from '@wordpress/components';
+import { forwardRef, useContext } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import InserterListboxContext from './context';
+
+function InserterListboxItem(
+ { isFirst, as: Component, children, ...props },
+ ref
+) {
+ const state = useContext( InserterListboxContext );
+ return (
+
+ { ( htmlProps ) => {
+ const propsWithTabIndex = {
+ ...htmlProps,
+ tabIndex: isFirst ? 0 : htmlProps.tabIndex,
+ };
+ if ( Component ) {
+ return (
+
+ { children }
+
+ );
+ }
+ if ( typeof children === 'function' ) {
+ return children( propsWithTabIndex );
+ }
+ return ;
+ } }
+
+ );
+}
+
+export default forwardRef( InserterListboxItem );
diff --git a/packages/block-editor/src/components/inserter-listbox/row.js b/packages/block-editor/src/components/inserter-listbox/row.js
new file mode 100644
index 00000000000000..710267660199d7
--- /dev/null
+++ b/packages/block-editor/src/components/inserter-listbox/row.js
@@ -0,0 +1,24 @@
+/**
+ * WordPress dependencies
+ */
+import { forwardRef, useContext } from '@wordpress/element';
+import { __unstableCompositeGroup as CompositeGroup } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import InserterListboxContext from './context';
+
+function InserterListboxRow( props, ref ) {
+ const state = useContext( InserterListboxContext );
+ return (
+
+ );
+}
+
+export default forwardRef( InserterListboxRow );
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 00b51a4ed2165d..886e9e3835a47c 100644
--- a/packages/block-editor/src/components/inserter/block-types-tab.js
+++ b/packages/block-editor/src/components/inserter/block-types-tab.js
@@ -15,6 +15,7 @@ import { useMemo, useEffect } from '@wordpress/element';
import BlockTypesList from '../block-types-list';
import InserterPanel from './panel';
import useBlockTypesState from './hooks/use-block-types-state';
+import InserterListbox from '../inserter-listbox';
const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ];
@@ -71,75 +72,77 @@ export function BlockTypesTab( {
useEffect( () => () => onHover( null ), [] );
return (
-
- { showMostUsedBlocks && !! suggestedItems.length && (
-
-
-
- ) }
-
- { map( categories, ( category ) => {
- const categoryItems = itemsPerCategory[ category.slug ];
- if ( ! categoryItems || ! categoryItems.length ) {
- return null;
- }
- return (
-
+
+
+ { showMostUsedBlocks && !! suggestedItems.length && (
+
- );
- } ) }
-
- { ! uncategorizedItems.length && (
-
-
-
- ) }
-
- { map( collections, ( collection, namespace ) => {
- const collectionItems = itemsPerCollection[ namespace ];
- if ( ! collectionItems || ! collectionItems.length ) {
- return null;
- }
-
- return (
+ ) }
+
+ { map( categories, ( category ) => {
+ const categoryItems = itemsPerCategory[ category.slug ];
+ if ( ! categoryItems || ! categoryItems.length ) {
+ return null;
+ }
+ return (
+
+
+
+ );
+ } ) }
+
+ { ! uncategorizedItems.length && (
- );
- } ) }
-
+ ) }
+
+ { map( collections, ( collection, namespace ) => {
+ const collectionItems = itemsPerCollection[ namespace ];
+ if ( ! collectionItems || ! collectionItems.length ) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ } ) }
+
+
);
}
diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js
index 0dae29a2305561..03bf7b7c2da998 100644
--- a/packages/block-editor/src/components/inserter/search-results.js
+++ b/packages/block-editor/src/components/inserter/search-results.js
@@ -24,6 +24,7 @@ import useInsertionPoint from './hooks/use-insertion-point';
import usePatternsState from './hooks/use-patterns-state';
import useBlockTypesState from './hooks/use-block-types-state';
import { searchBlockItems, searchItems } from './search-items';
+import InserterListbox from '../inserter-listbox';
function InserterSearchResults( {
filterValue,
@@ -104,7 +105,7 @@ function InserterSearchResults( {
! isEmpty( filteredBlockTypes ) || ! isEmpty( filteredBlockPatterns );
return (
- <>
+
{ ! showBlockDirectory && ! hasItems && }
{ !! filteredBlockTypes.length && (
@@ -168,7 +169,7 @@ function InserterSearchResults( {
} }
) }
- >
+
);
}
diff --git a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
index efe1a049a78207..378503b5a152df 100644
--- a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
@@ -293,7 +293,7 @@ describe( 'adding blocks', () => {
// We need to wait a bit after typing otherwise we might an "early" result
// that is going to be "detached" when trying to click on it
// eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( 100 );
+ await page.waitForTimeout( 200 );
const coverBlock = await page.waitForSelector(
'.block-editor-block-types-list .editor-block-list-item-cover'
);