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