diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js
index b78eeed5b1d6a1..bb8990da56c09a 100644
--- a/packages/block-editor/src/components/content-only-controls/index.js
+++ b/packages/block-editor/src/components/content-only-controls/index.js
@@ -9,6 +9,8 @@ import {
__experimentalHStack as HStack,
Icon,
Navigator,
+ __experimentalToolsPanel as ToolsPanel,
+ useNavigator,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
@@ -24,8 +26,11 @@ import { store as blockEditorStore } from '../../store';
import BlockIcon from '../block-icon';
import useBlockDisplayTitle from '../block-title/use-block-display-title';
import useBlockDisplayInformation from '../use-block-display-information';
+import { PrivateListView } from '../list-view';
+import InspectorControls from '../inspector-controls';
const { fieldsKey } = unlock( blocksPrivateApis );
import FieldsDropdownMenu from './fields-dropdown-menu';
+import LeafMoreMenu from './leaf-more-menu';
// controls
import RichText from './rich-text';
@@ -173,6 +178,55 @@ function denormalizeLinkValue( value, fieldDef ) {
return result;
}
+function ContentOnlyBackButton() {
+ return (
+
+
+
+
+ { __( 'Back' ) }
+
+
+
+ );
+}
+
+function NavigationListView( { clientId } ) {
+ const blockTitle = useBlockDisplayTitle( {
+ clientId,
+ context: 'list-view',
+ } );
+ const blockInformation = useBlockDisplayInformation( clientId );
+ const { goTo } = useNavigator();
+
+ const handleSelect = ( block ) => {
+ goTo( `/${ clientId }/${ block.clientId }` );
+ };
+
+ return (
+
+
+ { blockTitle }
+
+ }
+ panelId={ clientId }
+ resetAll={ () => {} }
+ >
+
+
+ );
+}
+
function BlockFields( { clientId } ) {
const { attributes, blockType } = useSelect(
( select ) => {
@@ -416,12 +470,15 @@ function ContentOnlyControlsScreen( {
parentClientIds,
isNested,
} ) {
- const isRootContentBlock = useSelect(
+ const { isRootContentBlock, isNavigationBlock } = useSelect(
( select ) => {
const { getBlockName } = select( blockEditorStore );
const blockName = getBlockName( rootClientId );
const { hasContentRoleAttribute } = unlock( select( blocksStore ) );
- return hasContentRoleAttribute( blockName );
+ return {
+ isRootContentBlock: hasContentRoleAttribute( blockName ),
+ isNavigationBlock: blockName === 'core/navigation',
+ };
},
[ rootClientId ]
);
@@ -430,18 +487,19 @@ function ContentOnlyControlsScreen( {
return null;
}
+ // Special case: If this is a navigation block drilldown, show the NavigationListView
+ if ( isNavigationBlock && isNested ) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
return (
<>
- { isNested && (
-
-
-
-
- { __( 'Back' ) }
-
-
-
- ) }
+ { isNested && }
{ isRootContentBlock && }
{ contentClientIds.map( ( clientId ) => {
if ( parentClientIds?.[ clientId ] ) {
@@ -460,84 +518,124 @@ function ContentOnlyControlsScreen( {
}
export default function ContentOnlyControls( { rootClientId } ) {
- const { updatedRootClientId, nestedContentClientIds, contentClientIds } =
- useSelect(
- ( select ) => {
- const { getClientIdsOfDescendants, getBlockEditingMode } =
- select( blockEditorStore );
-
- // _nestedContentClientIds is for content blocks within 'drilldowns'.
- // It's an object where the key is the parent clientId, and the element is
- // an array of child clientIds whose controls are shown within the drilldown.
- const _nestedContentClientIds = {};
-
- // _contentClientIds is the list of contentClientIds for blocks being
- // shown at the root level. Includes parent blocks that might have a drilldown,
- // but not the children of those blocks.
- const _contentClientIds = [];
-
- // An array of all nested client ids. Used for ensuring blocks within drilldowns
- // don't appear at the root level.
- let allNestedClientIds = [];
-
- // A flattened list of all content clientIds to arrange into the
- // groups above.
- const allContentClientIds = getClientIdsOfDescendants(
- rootClientId
- ).filter(
- ( clientId ) =>
- getBlockEditingMode( clientId ) === 'contentOnly'
+ const {
+ updatedRootClientId,
+ nestedContentClientIds,
+ contentClientIds,
+ navigationBlockIds,
+ } = useSelect(
+ ( select ) => {
+ const {
+ getClientIdsOfDescendants,
+ getBlockEditingMode,
+ getBlockName,
+ } = select( blockEditorStore );
+
+ // _nestedContentClientIds is for content blocks within 'drilldowns'.
+ // It's an object where the key is the parent clientId, and the element is
+ // an array of child clientIds whose controls are shown within the drilldown.
+ const _nestedContentClientIds = {};
+
+ // _contentClientIds is the list of contentClientIds for blocks being
+ // shown at the root level. Includes parent blocks that might have a drilldown,
+ // but not the children of those blocks.
+ const _contentClientIds = [];
+
+ // An array of all nested client ids. Used for ensuring blocks within drilldowns
+ // don't appear at the root level.
+ let allNestedClientIds = [];
+
+ // A flattened list of all content clientIds to arrange into the
+ // groups above.
+ const allDescendants = getClientIdsOfDescendants( rootClientId );
+
+ // Exclude Navigation block children (but not the navigation block itself)
+ // Navigation blocks will show their own list view controls
+ const navigationChildren = new Set();
+
+ // Check if root is a navigation block
+ if ( getBlockName( rootClientId ) === 'core/navigation' ) {
+ allDescendants.forEach( ( childId ) =>
+ navigationChildren.add( childId )
);
+ }
- for ( const clientId of allContentClientIds ) {
- const childClientIds = getClientIdsOfDescendants(
- clientId
- ).filter(
- ( childClientId ) =>
- getBlockEditingMode( childClientId ) ===
- 'contentOnly'
+ // Check for navigation blocks within descendants and exclude only their children
+ allDescendants.forEach( ( clientId ) => {
+ if ( getBlockName( clientId ) === 'core/navigation' ) {
+ // Don't exclude the navigation block itself, only its children
+ const navChildren = getClientIdsOfDescendants( clientId );
+ navChildren.forEach( ( childId ) =>
+ navigationChildren.add( childId )
);
+ }
+ } );
- // If there's more than one child block, use a drilldown.
- if (
- childClientIds.length > 1 &&
- ! allNestedClientIds.includes( clientId )
- ) {
- _nestedContentClientIds[ clientId ] = childClientIds;
- allNestedClientIds = [
- allNestedClientIds,
- ...childClientIds,
- ];
- }
+ const allContentClientIds = allDescendants.filter(
+ ( clientId ) =>
+ getBlockEditingMode( clientId ) === 'contentOnly' &&
+ ! navigationChildren.has( clientId )
+ );
- if ( ! allNestedClientIds.includes( clientId ) ) {
- _contentClientIds.push( clientId );
- }
- }
+ for ( const clientId of allContentClientIds ) {
+ const childClientIds = getClientIdsOfDescendants(
+ clientId
+ ).filter(
+ ( childClientId ) =>
+ getBlockEditingMode( childClientId ) === 'contentOnly'
+ );
- // Avoid showing only one drilldown block at the root.
+ // If there's more than one child block, use a drilldown.
+ // For navigation blocks, we'll show the NavigationListView in the drilldown.
if (
- _contentClientIds.length === 1 &&
- Object.keys( _nestedContentClientIds ).length === 1
+ childClientIds.length > 1 &&
+ ! allNestedClientIds.includes( clientId )
) {
- const onlyParentClientId = Object.keys(
- _nestedContentClientIds
- )[ 0 ];
- return {
- updatedRootClientId: onlyParentClientId,
- contentClientIds:
- _nestedContentClientIds[ onlyParentClientId ],
- nestedContentClientIds: {},
- };
+ _nestedContentClientIds[ clientId ] = childClientIds;
+ allNestedClientIds = [
+ allNestedClientIds,
+ ...childClientIds,
+ ];
}
+ if ( ! allNestedClientIds.includes( clientId ) ) {
+ _contentClientIds.push( clientId );
+ }
+ }
+
+ // Identify which parent blocks are navigation blocks
+ const _navigationBlockIds = new Set();
+ Object.keys( _nestedContentClientIds ).forEach( ( parentId ) => {
+ if ( getBlockName( parentId ) === 'core/navigation' ) {
+ _navigationBlockIds.add( parentId );
+ }
+ } );
+
+ // Avoid showing only one drilldown block at the root.
+ if (
+ _contentClientIds.length === 1 &&
+ Object.keys( _nestedContentClientIds ).length === 1
+ ) {
+ const onlyParentClientId = Object.keys(
+ _nestedContentClientIds
+ )[ 0 ];
return {
- nestedContentClientIds: _nestedContentClientIds,
- contentClientIds: _contentClientIds,
+ updatedRootClientId: onlyParentClientId,
+ contentClientIds:
+ _nestedContentClientIds[ onlyParentClientId ],
+ nestedContentClientIds: {},
+ navigationBlockIds: _navigationBlockIds,
};
- },
- [ rootClientId ]
- );
+ }
+
+ return {
+ nestedContentClientIds: _nestedContentClientIds,
+ contentClientIds: _contentClientIds,
+ navigationBlockIds: _navigationBlockIds,
+ };
+ },
+ [ rootClientId ]
+ );
return (
@@ -564,6 +662,23 @@ export default function ContentOnlyControls( { rootClientId } ) {
/>
) ) }
+ { /* Create screens for navigation children to show their inspectors */ }
+ { Array.from( navigationBlockIds ).flatMap( ( parentId ) =>
+ nestedContentClientIds[ parentId ].map( ( childId ) => (
+
+
+
+ ) )
+ ) }
);
}
diff --git a/packages/block-editor/src/components/content-only-controls/leaf-more-menu.js b/packages/block-editor/src/components/content-only-controls/leaf-more-menu.js
new file mode 100644
index 00000000000000..804edcf6061ae8
--- /dev/null
+++ b/packages/block-editor/src/components/content-only-controls/leaf-more-menu.js
@@ -0,0 +1,201 @@
+/**
+ * WordPress dependencies
+ */
+import { createBlock } from '@wordpress/blocks';
+import {
+ addSubmenu,
+ chevronUp,
+ chevronDown,
+ moreVertical,
+} from '@wordpress/icons';
+import { DropdownMenu, MenuItem, MenuGroup } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import BlockTitle from '../block-title';
+import { store as blockEditorStore } from '../../store';
+
+const POPOVER_PROPS = {
+ className: 'block-editor-block-settings-menu__popover',
+ placement: 'bottom-start',
+};
+
+const BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU = [
+ 'core/navigation-link',
+ 'core/navigation-submenu',
+];
+
+const DEFAULT_BLOCK = {
+ name: 'core/navigation-link',
+ attributes: {
+ kind: 'post-type',
+ type: 'page',
+ },
+};
+
+function AddSubmenuItem( {
+ block,
+ onClose,
+ expandedState,
+ expand,
+ setInsertedBlock,
+} ) {
+ const { insertBlock, replaceBlock, replaceInnerBlocks } =
+ useDispatch( blockEditorStore );
+
+ const clientId = block.clientId;
+ const isDisabled = ! BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU.includes(
+ block.name
+ );
+ return (
+
+ );
+}
+
+export default function LeafMoreMenu( props ) {
+ const {
+ block,
+ expand,
+ expandedState,
+ setInsertedBlock,
+ icon = moreVertical,
+ label = __( 'Options' ),
+ popoverProps,
+ toggleProps,
+ ...restProps
+ } = props;
+ const { clientId } = block;
+
+ const { moveBlocksDown, moveBlocksUp, removeBlocks } =
+ useDispatch( blockEditorStore );
+
+ const removeLabel = sprintf(
+ /* translators: %s: block name */
+ __( 'Remove %s' ),
+ BlockTitle( { clientId, maximumLength: 25 } )
+ );
+
+ const rootClientId = useSelect(
+ ( select ) => {
+ const { getBlockRootClientId } = select( blockEditorStore );
+
+ return getBlockRootClientId( clientId );
+ },
+ [ clientId ]
+ );
+
+ // Merge default popover props with passed-in props
+ const mergedPopoverProps = {
+ ...POPOVER_PROPS,
+ ...popoverProps,
+ };
+
+ return (
+
+ { ( { onClose } ) => (
+ <>
+
+
+
+
+
+
+
+
+ >
+ ) }
+
+ );
+}
diff --git a/packages/block-editor/src/components/content-only-controls/styles.scss b/packages/block-editor/src/components/content-only-controls/styles.scss
index 105dc3c0c11f1e..648c184052f987 100644
--- a/packages/block-editor/src/components/content-only-controls/styles.scss
+++ b/packages/block-editor/src/components/content-only-controls/styles.scss
@@ -42,3 +42,8 @@
padding: $grid-unit-10 0;
margin-bottom: $grid-unit-05;
}
+
+.block-editor-content-only-controls__navigation-list-view {
+ // Ensure the list view takes full width
+ grid-column: 1/-1;
+}