diff --git a/packages/block-editor/src/components/list-view/block-rename-ui.js b/packages/block-editor/src/components/list-view/block-rename-ui.js
new file mode 100644
index 0000000000000..87da4ffa455a1
--- /dev/null
+++ b/packages/block-editor/src/components/list-view/block-rename-ui.js
@@ -0,0 +1,163 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalInputControl as InputControl,
+ Popover,
+ VisuallyHidden,
+ Button,
+} from '@wordpress/components';
+import { speak } from '@wordpress/a11y';
+import { useInstanceId, useFocusOnMount } from '@wordpress/compose';
+import { useState, forwardRef, useEffect } from '@wordpress/element';
+import { ENTER, ESCAPE } from '@wordpress/keycodes';
+import { __, sprintf } from '@wordpress/i18n';
+import { keyboardReturn, close } from '@wordpress/icons';
+
+const ListViewBlockRenameUI = forwardRef(
+ ( { blockTitle, onSubmit, onCancel }, ref ) => {
+ const inputRef = useFocusOnMount();
+
+ const inputDescriptionId = useInstanceId(
+ ListViewBlockRenameUI,
+ `block-editor-list-view-block-node__input-description`
+ );
+
+ const dialogTitle = useInstanceId(
+ ListViewBlockRenameUI,
+ `block-editor-list-view-block-rename-dialog__title`
+ );
+
+ const dialogDescription = useInstanceId(
+ ListViewBlockRenameUI,
+ `block-editor-list-view-block-rename-dialog__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 ) {
+ handleCancel();
+ }
+
+ if ( event.keyCode === ENTER ) {
+ handleSubmit();
+ }
+ }
+ };
+
+ const handleCancel = () => {
+ // Reset the input's local state to avoid
+ // stale values.
+ setInputValue( blockTitle );
+
+ onCancel();
+
+ // Must be assertive to immediately announce change.
+ speak( __( 'Leaving block name edit mode' ), 'assertive' );
+ };
+
+ const handleSubmit = () => {
+ let successAnnouncement;
+
+ if ( inputValue === '' ) {
+ successAnnouncement = __( 'Block name reset.' );
+ } else {
+ 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' );
+
+ // Submit changes only for ENTER.
+ onSubmit( inputValue );
+ };
+
+ const autoSelectInputText = ( event ) => event.target.select();
+
+ useEffect( () => {
+ speak( __( 'Entering block name edit mode' ), 'assertive' );
+ }, [] );
+
+ return (
+
+
+ Rename Block
+
+ { __( 'Choose a custom name for this block.' ) }
+
+
+
+
+ );
+ }
+);
+
+export default ListViewBlockRenameUI;
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 cec5d4699c7a2..ca74c078b76d4 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
@@ -13,7 +13,7 @@ import {
__experimentalTruncate as Truncate,
Tooltip,
} from '@wordpress/components';
-import { forwardRef } from '@wordpress/element';
+import { forwardRef, useRef, useState } 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';
@@ -28,13 +28,16 @@ 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 ListViewBlockRenameUI from './block-rename-ui';
+import { store as blockEditorStore } from '../../store';
+
+const SINGLE_CLICK = 1;
function ListViewBlockSelectButton(
{
className,
- block: { clientId },
+ block: { clientId, attributes: blockAttributes, name: blockName },
onClick,
onToggleExpanded,
tabIndex,
@@ -49,6 +52,7 @@ function ListViewBlockSelectButton(
},
ref
) {
+ const blockNameElementRef = useRef();
const blockInformation = useBlockDisplayInformation( clientId );
const blockTitle = useBlockDisplayTitle( {
clientId,
@@ -64,7 +68,9 @@ function ListViewBlockSelectButton(
getBlocksByClientId,
canRemoveBlocks,
} = useSelect( blockEditorStore );
- const { duplicateBlocks, removeBlocks } = useDispatch( blockEditorStore );
+ const { duplicateBlocks, removeBlocks, updateBlockAttributes } =
+ useDispatch( blockEditorStore );
+
const isMatch = useShortcutEventMatch();
const isSticky = blockInformation?.positionType === 'sticky';
const images = useListViewImages( { clientId, isExpanded } );
@@ -77,6 +83,14 @@ function ListViewBlockSelectButton(
)
: '';
+ const [ isRenamingBlock, setBlockBeingRenamed ] = useState( false );
+
+ const supportsBlockNaming = hasBlockSupport(
+ blockName,
+ 'renaming',
+ true // default value
+ );
+
// The `href` attribute triggers the browser's native HTML drag operations.
// When the link is dragged, the element's outerHTML is set in DataTransfer object as text/html.
// We need to clear any HTML drag data to prevent `pasteHandler` from firing
@@ -193,7 +207,24 @@ function ListViewBlockSelectButton(
'block-editor-list-view-block-select-button',
className
) }
- onClick={ onClick }
+ onClick={ ( event ) => {
+ // Avoid click delays for blocks that don't support naming interaction.
+ if ( ! supportsBlockNaming ) {
+ onClick( event );
+ return;
+ }
+
+ if ( event.detail === SINGLE_CLICK ) {
+ onClick( event );
+ }
+ } }
+ onDoubleClick={ ( event ) => {
+ event.preventDefault();
+ if ( ! supportsBlockNaming ) {
+ return;
+ }
+ setBlockBeingRenamed( true );
+ } }
onKeyDown={ onKeyDownHandler }
ref={ ref }
tabIndex={ tabIndex }
@@ -218,9 +249,12 @@ function ListViewBlockSelectButton(
justify="flex-start"
spacing={ 1 }
>
-
- { blockTitle }
-
+
+ { blockTitle }
+
{ blockInformation?.anchor && (
+
+ { isRenamingBlock && (
+ setBlockBeingRenamed( false ) }
+ onSubmit={ ( newName ) => {
+ if ( newName === undefined ) {
+ setBlockBeingRenamed( false );
+ }
+
+ setBlockBeingRenamed( false );
+ updateBlockAttributes( clientId, {
+ // Include existing metadata (if present) to avoid overwriting existing.
+ metadata: {
+ ...( blockAttributes?.metadata &&
+ blockAttributes?.metadata ),
+ name: newName,
+ },
+ } );
+ } }
+ />
+ ) }
>
);
}
diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss
index 7afa7a1c98431..458476d750f12 100644
--- a/packages/block-editor/src/components/list-view/style.scss
+++ b/packages/block-editor/src/components/list-view/style.scss
@@ -1,3 +1,6 @@
+
+$list-view-item-content-height: 32px;
+
.block-editor-list-view-tree {
width: 100%;
border-collapse: collapse;
@@ -11,6 +14,61 @@
}
}
+
+// Render in Popover.
+// Todo: find a better way to match height and avoid important.
+.block-editor-list-view-block-rename__form {
+ position: relative;
+
+ .components-input-control__input {
+ height: $list-view-item-content-height !important; // force match height of block title UI.
+ min-height: $list-view-item-content-height !important; // force match height of block title UI.
+ padding-right: 30px !important; // allow space for submit button.
+ }
+
+ // Cancel focused styles as contrast is already sufficiently high.
+ .components-input-control__backdrop {
+ box-shadow: none !important;
+ border-color: initial !important;
+ }
+
+ .block-editor-list-view-block-rename__actions {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 9999;
+
+ .block-editor-list-view-block-rename__action {
+ height: $list-view-item-content-height;
+
+ &:not(:focus) {
+ border: 0;
+ clip: rect(1px, 1px, 1px, 1px);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ word-wrap: normal;
+ }
+ }
+
+ .block-editor-list-view-block-rename__action--submit {
+ position: relative;
+ top: -2px; // Ensure that the submit button is always visible.
+ }
+ }
+}
+
+
+.block-editor-list-view-block-rename__popover {
+ // Important required to overide inline positioning
+ // until such time as we can specify a horizontal offset.
+ left: -8px !important; // todo: use input padding instead.
+}
+
+
.block-editor-list-view-leaf {
// Use position relative for row animation.
position: relative;
@@ -131,7 +189,7 @@
align-items: center;
width: 100%;
height: auto;
- padding: ($grid-unit-15 * 0.5) $grid-unit-05 ($grid-unit-15 * 0.5) 0;
+ padding: 2px $grid-unit-05 2px 0;
text-align: left;
border-radius: $radius-block-ui;
position: relative;
@@ -301,11 +359,16 @@
.block-editor-list-view-block-select-button__label-wrapper {
min-width: 120px;
+
}
+
.block-editor-list-view-block-select-button__title {
flex: 1;
position: relative;
+ height: $list-view-item-content-height;
+ display: flex;
+ align-items: center;
.components-truncate {
position: absolute;
diff --git a/test/e2e/specs/editor/various/block-renaming.spec.js b/test/e2e/specs/editor/various/block-renaming.spec.js
index 4150be64bd33d..da3f7ac65798d 100644
--- a/test/e2e/specs/editor/various/block-renaming.spec.js
+++ b/test/e2e/specs/editor/various/block-renaming.spec.js
@@ -14,13 +14,6 @@ test.describe( 'Block Renaming', () => {
page,
pageUtils,
} ) => {
- // Turn on block list view by default.
- await page.evaluate( () => {
- window.wp.data
- .dispatch( 'core/preferences' )
- .set( 'core/edit-site', 'showListViewByDefault', true );
- } );
-
const listView = page.getByRole( 'treegrid', {
name: 'Block navigation structure',
} );
@@ -168,6 +161,82 @@ test.describe( 'Block Renaming', () => {
} );
} );
+ test.describe( 'List View renaming', () => {
+ test( 'should allow renaming via double click in List View', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: { content: 'First Paragraph' },
+ } );
+
+ await pageUtils.pressKeys( 'access+o' );
+ const listView = page.getByRole( 'treegrid', {
+ name: 'Block navigation structure',
+ } );
+
+ await expect( listView ).toBeVisible();
+
+ const listViewNode = listView.getByRole( 'link', {
+ name: 'Paragraph',
+ } );
+
+ // double click to trigger rename
+ await listViewNode.dblclick();
+
+ const renameModal = page.getByRole( 'dialog', {
+ name: 'Rename block',
+ } );
+
+ // Check the Modal is perceivable.
+ await expect( renameModal ).toBeVisible();
+
+ const nameInput = renameModal.getByRole( 'textbox', {
+ name: 'Block name',
+ } );
+
+ // Check focus is transferred into modal.
+ await expect( nameInput ).toBeFocused();
+
+ const saveButton = renameModal.getByRole( 'button', {
+ name: 'Save',
+ type: 'submit',
+ } );
+
+ // await expect( saveButton ).toBeDisabled();
+
+ await expect( nameInput ).toHaveValue( 'Paragraph' );
+
+ await nameInput.fill( 'My new name' );
+
+ await expect( saveButton ).toBeEnabled();
+
+ await saveButton.click();
+
+ await expect( renameModal ).toBeHidden();
+
+ // Check that focus is transferred back to original "Rename" menu item.
+ await expect(
+ listView.getByRole( 'link', {
+ name: 'My new name',
+ } )
+ ).toBeFocused();
+
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: {
+ metadata: {
+ name: 'My new name',
+ },
+ },
+ },
+ ] );
+ } );
+ } );
+
test.describe( 'Block inspector renaming', () => {
test( 'allows renaming of blocks that support the feature via "Advanced" section of block inspector tools', async ( {
editor,