From f9a0b3434fa76881a4275c403801bb2fb3db7e6a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 12 Sep 2022 15:04:36 +0100 Subject: [PATCH] Add basic ability to rename blocks --- .../components/list-view/block-contents.js | 93 ++++++++++- .../src/components/list-view/block-input.js | 144 ++++++++++++++++++ .../list-view/block-options-rename-item.js | 44 ++++++ .../list-view/block-select-button.js | 71 ++++++--- .../src/components/list-view/block.js | 23 ++- .../src/components/list-view/style.scss | 24 ++- .../use-block-display-information/index.js | 3 + packages/block-library/src/group/block.json | 1 + 8 files changed, 367 insertions(+), 36 deletions(-) create mode 100644 packages/block-editor/src/components/list-view/block-input.js create mode 100644 packages/block-editor/src/components/list-view/block-options-rename-item.js 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 ( + { __( 'Rename' ) } + ); + } } + + ); +} + +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,