diff --git a/packages/block-editor/src/components/list-view/appender.js b/packages/block-editor/src/components/list-view/appender.js new file mode 100644 index 00000000000000..a006b91e860c20 --- /dev/null +++ b/packages/block-editor/src/components/list-view/appender.js @@ -0,0 +1,101 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { speak } from '@wordpress/a11y'; +import { useSelect } from '@wordpress/data'; +import { forwardRef, useState, useEffect } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import useBlockDisplayTitle from '../block-title/use-block-display-title'; +import Inserter from '../inserter'; + +export const Appender = forwardRef( + ( { nestingLevel, blockCount, clientId, ...props }, ref ) => { + const [ insertedBlock, setInsertedBlock ] = useState( null ); + + const instanceId = useInstanceId( Appender ); + const { hideInserter } = useSelect( + ( select ) => { + const { getTemplateLock, __unstableGetEditorMode } = + select( blockEditorStore ); + + return { + hideInserter: + !! getTemplateLock( clientId ) || + __unstableGetEditorMode() === 'zoom-out', + }; + }, + [ clientId ] + ); + + const blockTitle = useBlockDisplayTitle( { + clientId, + context: 'list-view', + } ); + + const insertedBlockTitle = useBlockDisplayTitle( { + clientId: insertedBlock?.clientId, + context: 'list-view', + } ); + + useEffect( () => { + if ( ! insertedBlockTitle?.length ) { + return; + } + + speak( + sprintf( + // translators: %s: name of block being inserted (i.e. Paragraph, Image, Group etc) + __( '%s block inserted' ), + insertedBlockTitle + ), + 'assertive' + ); + }, [ insertedBlockTitle ] ); + + if ( hideInserter ) { + return null; + } + + const descriptionId = `list-view-appender__${ instanceId }`; + const description = sprintf( + /* translators: 1: The name of the block. 2: The numerical position of the block. 3: The level of nesting for the block. */ + __( 'Append to %1$s block at position %2$d, Level %3$d' ), + blockTitle, + blockCount + 1, + nestingLevel + ); + + return ( +
+ { + if ( maybeInsertedBlock?.clientId ) { + setInsertedBlock( maybeInsertedBlock ); + } + } } + /> +
+ { description } +
+
+ ); + } +); diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js index 83fc99dca24504..4438b57c70d69e 100644 --- a/packages/block-editor/src/components/list-view/branch.js +++ b/packages/block-editor/src/components/list-view/branch.js @@ -1,12 +1,17 @@ /** * WordPress dependencies */ +import { + __experimentalTreeGridRow as TreeGridRow, + __experimentalTreeGridCell as TreeGridCell, +} from '@wordpress/components'; import { memo } from '@wordpress/element'; import { AsyncModeProvider, useSelect } from '@wordpress/data'; /** * Internal dependencies */ +import { Appender } from './appender'; import ListViewBlock from './block'; import { useListViewContext } from './context'; import { isClientIdSelected } from './utils'; @@ -93,6 +98,7 @@ function ListViewBranch( props ) { parentId, shouldShowInnerBlocks = true, isSyncedBranch = false, + showAppender: showAppenderProp = true, } = props; const parentBlockInformation = useBlockDisplayInformation( parentId ); @@ -120,8 +126,12 @@ function ListViewBranch( props ) { return null; } + // Only show the appender at the first level. + const showAppender = showAppenderProp && level === 1; const filteredBlocks = blocks.filter( Boolean ); const blockCount = filteredBlocks.length; + // The appender means an extra row in List View, so add 1 to the row count. + const rowCount = showAppender ? blockCount + 1 : blockCount; let nextPosition = listPosition; return ( @@ -175,7 +185,7 @@ function ListViewBranch( props ) { isDragged={ isDragged } level={ level } position={ position } - rowCount={ blockCount } + rowCount={ rowCount } siblingBlockCount={ blockCount } showBlockMovers={ showBlockMovers } path={ updatedPath } @@ -209,6 +219,25 @@ function ListViewBranch( props ) { ); } ) } + { showAppender && ( + + + { ( treeGridCellProps ) => ( + + ) } + + + ) } ); } diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 5425290809a18b..524c8ca4dc29be 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -55,10 +55,17 @@ export const BLOCK_LIST_ITEM_HEIGHT = 36; * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. * @param {boolean} props.showBlockMovers Flag to enable block movers * @param {boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. + * @param {boolean} props.showAppender Flag to show or hide the block appender. * @param {Object} ref Forwarded ref */ -function ListView( - { id, blocks, showBlockMovers = false, isExpanded = false }, +function ListViewComponent( + { + id, + blocks, + showBlockMovers = false, + isExpanded = false, + showAppender = false, + }, ref ) { const { clientIdsTree, draggedClientIds, selectedClientIds } = @@ -204,10 +211,15 @@ function ListView( selectedClientIds={ selectedClientIds } isExpanded={ isExpanded } shouldShowInnerBlocks={ shouldShowInnerBlocks } + showAppender={ showAppender } /> ); } -export default forwardRef( ListView ); +export const PrivateListView = forwardRef( ListViewComponent ); + +export default forwardRef( ( props, ref ) => { + return ; +} ); diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 0bedb39061af4c..9a1b2f501d4dca 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -410,3 +410,22 @@ $block-navigation-max-indent: 8; height: 36px; } +.list-view-appender .block-editor-inserter__toggle { + background-color: #1e1e1e; + color: #fff; + margin: $grid-unit-10 0 0 24px; + border-radius: 2px; + height: 24px; + min-width: 24px; + padding: 0; + + &:hover, + &:focus { + background: var(--wp-admin-theme-color); + color: #fff; + } +} + +.list-view-appender__description { + display: none; +} diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index b34f35adc5c911..567849f66f019e 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -7,6 +7,7 @@ import { lock } from './lock-unlock'; import OffCanvasEditor from './components/off-canvas-editor'; import LeafMoreMenu from './components/off-canvas-editor/leaf-more-menu'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; +import { PrivateListView } from './components/list-view'; /** * Private @wordpress/block-editor APIs. @@ -18,4 +19,5 @@ lock( privateApis, { LeafMoreMenu, OffCanvasEditor, PrivateInserter, + PrivateListView, } ); diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index eaad82acafa273..dc33ffb649c7de 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __experimentalListView as ListView } from '@wordpress/block-editor'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; import { useFocusOnMount, @@ -18,6 +18,7 @@ import { ESCAPE } from '@wordpress/keycodes'; * Internal dependencies */ import { store as editSiteStore } from '../../store'; +import { unlock } from '../../private-apis'; export default function ListViewSidebar() { const { setIsListViewOpened } = useDispatch( editSiteStore ); @@ -33,7 +34,7 @@ export default function ListViewSidebar() { const instanceId = useInstanceId( ListViewSidebar ); const labelId = `edit-site-editor__list-view-panel-label-${ instanceId }`; - + const { PrivateListView } = unlock( blockEditorPrivateApis ); return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
- +
);