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