diff --git a/packages/block-editor/src/components/list-view/block-contents.js b/packages/block-editor/src/components/list-view/block-contents.js
index 8d5b03395f3e20..84b6d016b9298a 100644
--- a/packages/block-editor/src/components/list-view/block-contents.js
+++ b/packages/block-editor/src/components/list-view/block-contents.js
@@ -6,16 +6,21 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useSelect } from '@wordpress/data';
-import { forwardRef } from '@wordpress/element';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { forwardRef, useState } from '@wordpress/element';
+import { getBlockSupport } from '@wordpress/blocks';
/**
* Internal dependencies
*/
+import { useBlockLock } from '../block-lock';
import ListViewBlockSelectButton from './block-select-button';
+import ListViewBlockInput from './block-input';
import BlockDraggable from '../block-draggable';
import { store as blockEditorStore } from '../../store';
import { useListViewContext } from './context';
+import useBlockDisplayTitle from '../block-title/use-block-display-title';
+import useBlockDisplayInformation from '../use-block-display-information';
const ListViewBlockContents = forwardRef(
(
@@ -35,6 +40,9 @@ const ListViewBlockContents = forwardRef(
) => {
const { clientId } = block;
+ // Setting managed via `toggleLabelEditingMode` handler.
+ const [ labelEditingMode, setLabelEditingMode ] = useState( false );
+
const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect(
( select ) => {
const { hasBlockMovingClientId, getSelectedBlockClientId } =
@@ -53,10 +61,6 @@ const ListViewBlockContents = forwardRef(
const isBlockMoveTarget =
blockMovingClientId && selectedBlockInBlockEditor === clientId;
- const className = classnames( 'block-editor-list-view-block-contents', {
- 'is-dropping-before': isBlockMoveTarget,
- } );
-
// Only include all selected blocks if the currently clicked on block
// is one of the selected blocks. This ensures that if a user attempts
// to drag a block that isn't part of the selection, they're still able
@@ -65,6 +69,79 @@ const ListViewBlockContents = forwardRef(
? selectedClientIds
: [ clientId ];
+ const { blockName, blockAttributes } = useSelect(
+ ( select ) => {
+ const blockObject =
+ select( blockEditorStore ).getBlock( clientId );
+ return {
+ blockName: blockObject?.name,
+ blockAttributes: blockObject?.attributes,
+ };
+ },
+ [ clientId ]
+ );
+
+ const { updateBlockAttributes } = useDispatch( blockEditorStore );
+
+ const metaDataSupport = getBlockSupport(
+ blockName,
+ '__experimentalMetadata',
+ false
+ );
+
+ const supportsBlockNaming = !! (
+ true === metaDataSupport || metaDataSupport?.name
+ );
+
+ const { isLocked } = useBlockLock( clientId );
+
+ const toggleLabelEditingMode = ( value ) => {
+ if ( ! supportsBlockNaming ) {
+ return;
+ }
+
+ setLabelEditingMode( value );
+ };
+
+ const blockTitle = useBlockDisplayTitle( {
+ clientId,
+ context: 'list-view',
+ } );
+
+ const blockInformation = useBlockDisplayInformation( clientId );
+
+ const className = classnames( 'block-editor-list-view-block-contents', {
+ 'is-dropping-before': isBlockMoveTarget,
+ 'has-block-naming-support': supportsBlockNaming,
+ } );
+
+ function inputSubmitHandler( updatedInputValue ) {
+ updateBlockAttributes( clientId, {
+ // Include existing metadata (if present) to avoid overwriting existing.
+ metadata: {
+ ...( blockAttributes?.metadata &&
+ blockAttributes?.metadata ),
+ name: updatedInputValue,
+ },
+ } );
+ }
+
+ if ( labelEditingMode ) {
+ return (
+
+ );
+ }
+
return (
<>
{ AdditionalBlockContent && (
@@ -90,6 +167,10 @@ const ListViewBlockContents = forwardRef(
onDragStart={ onDragStart }
onDragEnd={ onDragEnd }
isExpanded={ isExpanded }
+ labelEditingMode={ labelEditingMode }
+ toggleLabelEditingMode={ toggleLabelEditingMode }
+ supportsBlockNaming={ supportsBlockNaming }
+ blockTitle={ blockTitle }
{ ...props }
/>
) }
diff --git a/packages/block-editor/src/components/list-view/block-input.js b/packages/block-editor/src/components/list-view/block-input.js
new file mode 100644
index 00000000000000..bb1d9c44d75f86
--- /dev/null
+++ b/packages/block-editor/src/components/list-view/block-input.js
@@ -0,0 +1,144 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalHStack as HStack,
+ __experimentalTruncate as Truncate,
+ __experimentalInputControl as InputControl,
+ VisuallyHidden,
+} from '@wordpress/components';
+import { speak } from '@wordpress/a11y';
+import { useInstanceId, useFocusOnMount } from '@wordpress/compose';
+import { forwardRef, useState } from '@wordpress/element';
+import { ENTER, ESCAPE } from '@wordpress/keycodes';
+import { __, sprintf } from '@wordpress/i18n';
+import { Icon, lock } from '@wordpress/icons';
+
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import BlockIcon from '../block-icon';
+import ListViewExpander from './expander';
+
+const ListViewBlockInput = forwardRef(
+ (
+ {
+ className,
+ onToggleExpanded,
+ toggleLabelEditingMode,
+ blockInformation,
+ isLocked,
+ blockTitle,
+ onSubmit,
+ },
+ ref
+ ) => {
+ const inputRef = useFocusOnMount();
+ const inputDescriptionId = useInstanceId(
+ ListViewBlockInput,
+ `block-editor-list-view-block-node__input-description`
+ );
+
+ // Local state for value of input **pre-submission**.
+ const [ inputValue, setInputValue ] = useState( blockTitle );
+
+ const onKeyDownHandler = ( event ) => {
+ // Trap events to input when editing to avoid
+ // default list view key handing (e.g. arrow
+ // keys for navigation).
+ event.stopPropagation();
+
+ // Handle ENTER and ESC exits editing mode.
+ if ( event.keyCode === ENTER || event.keyCode === ESCAPE ) {
+ if ( event.keyCode === ESCAPE ) {
+ // Must be assertive to immediately announce change.
+ speak( 'Leaving edit mode', 'assertive' );
+ }
+
+ if ( event.keyCode === ENTER ) {
+ // Submit changes only for ENTER.
+ onSubmit( inputValue );
+ const successAnnouncement = sprintf(
+ /* translators: %s: new name/label for the block */
+ __( 'Block name updated to: "%s".' ),
+ inputValue
+ );
+ // Must be assertive to immediately announce change.
+ speak( successAnnouncement, 'assertive' );
+ }
+
+ toggleLabelEditingMode( false );
+ }
+ };
+
+ return (
+
+
+
+
+
+ {
+ setInputValue( nextValue ?? '' );
+ } }
+ onBlur={ () => {
+ toggleLabelEditingMode( false );
+
+ // Reset the input's local state to avoid
+ // stale values.
+ setInputValue( blockTitle );
+ } }
+ onKeyDown={ onKeyDownHandler }
+ aria-describedby={ inputDescriptionId }
+ />
+
+
+ { __(
+ 'Press the ENTER key to submit or the ESCAPE key to cancel.'
+ ) }
+
+
+
+ { blockInformation?.anchor && (
+
+
+ { blockInformation.anchor }
+
+
+ ) }
+ { isLocked && (
+
+
+
+ ) }
+
+
+ );
+ }
+);
+
+export default ListViewBlockInput;
diff --git a/packages/block-editor/src/components/list-view/block-options-rename-item.js b/packages/block-editor/src/components/list-view/block-options-rename-item.js
new file mode 100644
index 00000000000000..098e0c30dab66d
--- /dev/null
+++ b/packages/block-editor/src/components/list-view/block-options-rename-item.js
@@ -0,0 +1,44 @@
+/**
+ * WordPress dependencies
+ */
+import { MenuItem } from '@wordpress/components';
+
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+
+import BlockSettingsMenuControls from '../block-settings-menu-controls';
+
+function BlockOptionsRenameItem( { clientId, onClick } ) {
+ return (
+
+ { ( { selectedClientIds, __unstableDisplayLocation } ) => {
+ // Only enabled for single selections.
+ const canRename =
+ selectedClientIds.length === 1 &&
+ clientId === selectedClientIds[ 0 ];
+
+ // This check ensures
+ // - the `BlockSettingsMenuControls` fill
+ // doesn't render multiple times and also that it renders for
+ // the block from which the menu was triggered.
+ // - `Rename` only appears in the ListView options.
+ // - `Rename` only appears for blocks that support renaming.
+ if (
+ __unstableDisplayLocation !== 'list-view' ||
+ ! canRename
+ ) {
+ return null;
+ }
+
+ return (
+
+ );
+ } }
+
+ );
+}
+
+export default BlockOptionsRenameItem;
diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js
index 930720fe565825..78af4d3ddb273c 100644
--- a/packages/block-editor/src/components/list-view/block-select-button.js
+++ b/packages/block-editor/src/components/list-view/block-select-button.js
@@ -12,32 +12,31 @@ import {
__experimentalTruncate as Truncate,
Tooltip,
} from '@wordpress/components';
-import { forwardRef } from '@wordpress/element';
import { Icon, lockSmall as lock, pinSmall } from '@wordpress/icons';
import { SPACE, ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts';
import { __, sprintf } from '@wordpress/i18n';
+import { forwardRef, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import BlockIcon from '../block-icon';
-import useBlockDisplayInformation from '../use-block-display-information';
-import useBlockDisplayTitle from '../block-title/use-block-display-title';
+
import ListViewExpander from './expander';
import { useBlockLock } from '../block-lock';
import { store as blockEditorStore } from '../../store';
import useListViewImages from './use-list-view-images';
+import BlockOptionsRenameItem from './block-options-rename-item';
+
+const SINGLE_CLICK = 1;
function ListViewBlockSelectButton(
{
className,
- block: { clientId },
onClick,
onToggleExpanded,
- tabIndex,
- onFocus,
onDragStart,
onDragEnd,
draggable,
@@ -45,14 +44,17 @@ function ListViewBlockSelectButton(
ariaLabel,
ariaDescribedBy,
updateFocusAndSelection,
+ labelEditingMode,
+ toggleLabelEditingMode,
+ supportsBlockNaming,
+ blockTitle,
+ clientId,
+ blockInformation,
+ tabIndex,
+ onFocus,
},
ref
) {
- const blockInformation = useBlockDisplayInformation( clientId );
- const blockTitle = useBlockDisplayTitle( {
- clientId,
- context: 'list-view',
- } );
const { isLocked } = useBlockLock( clientId );
const {
getSelectedBlockClientIds,
@@ -134,16 +136,41 @@ function ListViewBlockSelectButton(
}
}
+ useEffect( () => {
+ if ( ! labelEditingMode ) {
+ // Re-focus button element when existing edit mode.
+ ref?.current?.focus();
+ }
+ }, [ labelEditingMode ] );
+
return (
<>
>
);
diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js
index d4845dc769c7fa..b41edfbe062b73 100644
--- a/packages/block-editor/src/components/list-view/block.js
+++ b/packages/block-editor/src/components/list-view/block.js
@@ -102,7 +102,22 @@ function ListViewBlock( {
level
);
- const blockAriaLabel = isLocked
+ let blockAriaLabel = __( 'Link' );
+ if ( blockInformation ) {
+ blockAriaLabel = isLocked
+ ? sprintf(
+ // translators: %s: The title of the block. This string indicates a link to select the locked block.
+ __( '%s link (locked)' ),
+ blockInformation.name || blockInformation.title
+ )
+ : sprintf(
+ // translators: %s: The title of the block. This string indicates a link to select the block.
+ __( '%s link' ),
+ blockInformation.name || blockInformation.title
+ );
+ }
+
+ const settingsAriaLabel = blockInformation
? sprintf(
// translators: %s: The title of the block. This string indicates a link to select the locked block.
__( '%s (locked)' ),
@@ -110,12 +125,6 @@ function ListViewBlock( {
)
: blockTitle;
- const settingsAriaLabel = sprintf(
- // translators: %s: The title of the block.
- __( 'Options for %s' ),
- blockTitle
- );
-
const {
isTreeGridMounted,
expand,
diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss
index 49d404cfc817c6..83a20657f4e431 100644
--- a/packages/block-editor/src/components/list-view/style.scss
+++ b/packages/block-editor/src/components/list-view/style.scss
@@ -299,11 +299,11 @@
}
}
- .block-editor-list-view-block-select-button__label-wrapper {
+ .block-editor-list-view-block-node__label-wrapper {
min-width: 120px;
}
- .block-editor-list-view-block-select-button__title {
+ .block-editor-list-view-block-node__title {
flex: 1;
position: relative;
@@ -312,15 +312,23 @@
width: 100%;
transform: translateY(-50%);
}
+
+ /* Match input's text position with non-editable visual state */
+ .components-input-control {
+ height: 24px;
+ position: relative;
+ left: -8px;
+ border-radius: $radius-block-ui * 2;
+ }
}
- .block-editor-list-view-block-select-button__anchor-wrapper {
+ .block-editor-list-view-block-node__anchor-wrapper {
position: relative;
max-width: min(110px, 40%);
width: 100%;
}
- .block-editor-list-view-block-select-button__anchor {
+ .block-editor-list-view-block-node__anchor {
position: absolute;
right: 0;
transform: translateY(-50%);
@@ -328,10 +336,11 @@
border-radius: $radius-block-ui;
padding: 2px 6px;
max-width: 100%;
+ overflow: hidden;
box-sizing: border-box;
}
- &.is-selected .block-editor-list-view-block-select-button__anchor {
+ &.is-selected .block-editor-list-view-block-node__anchor {
background: rgba($black, 0.3);
}
@@ -366,6 +375,11 @@
}
}
+.block-editor-list-view-block-node__description,
+.block-editor-list-view-appender__description {
+ display: none;
+}
+
.block-editor-list-view-block__contents-cell,
.block-editor-list-view-appender__cell {
.block-editor-list-view-block__contents-container,
diff --git a/packages/block-editor/src/components/use-block-display-information/index.js b/packages/block-editor/src/components/use-block-display-information/index.js
index 1cff9da4bc04a9..73515d39500881 100644
--- a/packages/block-editor/src/components/use-block-display-information/index.js
+++ b/packages/block-editor/src/components/use-block-display-information/index.js
@@ -26,6 +26,7 @@ import { store as blockEditorStore } from '../../store';
* @property {WPIcon} icon Block type icon.
* @property {string} description A detailed block type description.
* @property {string} anchor HTML anchor.
+ * @property {?string} name Human-readable, user-provided custom name for the block.
*/
/**
@@ -94,6 +95,7 @@ export default function useBlockDisplayInformation( clientId ) {
anchor: attributes?.anchor,
positionLabel,
positionType: attributes?.style?.position?.type,
+ name: attributes?.metadata?.name,
};
if ( ! match ) return blockTypeInfo;
@@ -105,6 +107,7 @@ export default function useBlockDisplayInformation( clientId ) {
anchor: attributes?.anchor,
positionLabel,
positionType: attributes?.style?.position?.type,
+ name: attributes?.metadata?.name,
};
},
[ clientId ]
diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json
index 4f8de8802ea70c..63df123fbb0cf3 100644
--- a/packages/block-library/src/group/block.json
+++ b/packages/block-library/src/group/block.json
@@ -21,6 +21,7 @@
}
},
"supports": {
+ "__experimentalMetadata": true,
"__experimentalOnEnter": true,
"__experimentalOnMerge": true,
"__experimentalSettings": true,