diff --git a/packages/block-editor/src/components/block-rename/index.js b/packages/block-editor/src/components/block-rename/index.js new file mode 100644 index 00000000000000..0379893d412ec9 --- /dev/null +++ b/packages/block-editor/src/components/block-rename/index.js @@ -0,0 +1,3 @@ +export { default as BlockRenameControl } from './rename-control'; +export { default as BlockRenameModal } from './modal'; +export { default as useBlockRename } from './use-block-rename'; diff --git a/packages/block-editor/src/components/block-rename/is-empty-string.js b/packages/block-editor/src/components/block-rename/is-empty-string.js new file mode 100644 index 00000000000000..42d88be77b96e5 --- /dev/null +++ b/packages/block-editor/src/components/block-rename/is-empty-string.js @@ -0,0 +1,3 @@ +export default function isEmptyString( testString ) { + return testString?.trim()?.length === 0; +} diff --git a/packages/block-editor/src/components/block-rename/modal.js b/packages/block-editor/src/components/block-rename/modal.js new file mode 100644 index 00000000000000..a1e9193f348fd0 --- /dev/null +++ b/packages/block-editor/src/components/block-rename/modal.js @@ -0,0 +1,115 @@ +/** + * WordPress dependencies + */ +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + Button, + TextControl, + Modal, +} from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { __, sprintf } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import isEmptyString from './is-empty-string'; + +export default function BlockRenameModal( { + blockName, + originalBlockName, + onClose, + onSave, +} ) { + const [ editedBlockName, setEditedBlockName ] = useState( blockName ); + + const nameHasChanged = editedBlockName !== blockName; + const nameIsOriginal = editedBlockName === originalBlockName; + const nameIsEmpty = isEmptyString( editedBlockName ); + + const isNameValid = nameHasChanged || nameIsOriginal; + + const autoSelectInputText = ( event ) => event.target.select(); + + const dialogDescription = useInstanceId( + BlockRenameModal, + `block-editor-rename-modal__description` + ); + + const handleSubmit = () => { + const message = + nameIsOriginal || nameIsEmpty + ? sprintf( + /* translators: %s: new name/label for the block */ + __( 'Block name reset to: "%s".' ), + editedBlockName + ) + : sprintf( + /* translators: %s: new name/label for the block */ + __( 'Block name changed to: "%s".' ), + editedBlockName + ); + + // Must be assertive to immediately announce change. + speak( message, 'assertive' ); + onSave( editedBlockName ); + + // Immediate close avoids ability to hit save multiple times. + onClose(); + }; + + return ( + +

+ { __( 'Enter a custom name for this block.' ) } +

+
{ + e.preventDefault(); + + if ( ! isNameValid ) { + return; + } + + handleSubmit(); + } } + > + + + + + + + + +
+
+ ); +} diff --git a/packages/block-editor/src/components/block-rename/rename-control.js b/packages/block-editor/src/components/block-rename/rename-control.js new file mode 100644 index 00000000000000..1f646126d14a4b --- /dev/null +++ b/packages/block-editor/src/components/block-rename/rename-control.js @@ -0,0 +1,80 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { useBlockDisplayInformation } from '..'; +import isEmptyString from './is-empty-string'; +import BlockRenameModal from './modal'; + +export default function BlockRenameControl( { clientId } ) { + const [ renamingBlock, setRenamingBlock ] = useState( false ); + + const { metadata } = useSelect( + ( select ) => { + const { getBlockAttributes } = select( blockEditorStore ); + + const _metadata = getBlockAttributes( clientId )?.metadata; + return { + metadata: _metadata, + }; + }, + [ clientId ] + ); + + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const customName = metadata?.name; + + function onChange( newName ) { + updateBlockAttributes( [ clientId ], { + metadata: { + ...( metadata && metadata ), + name: newName, + }, + } ); + } + + const blockInformation = useBlockDisplayInformation( clientId ); + + return ( + <> + { + setRenamingBlock( true ); + } } + aria-expanded={ renamingBlock } + aria-haspopup="dialog" + > + { __( 'Rename' ) } + + { renamingBlock && ( + setRenamingBlock( false ) } + onSave={ ( newName ) => { + // If the new value is the block's original name (e.g. `Group`) + // or it is an empty string then assume the intent is to reset + // the value. Therefore reset the metadata. + if ( + newName === blockInformation?.title || + isEmptyString( newName ) + ) { + newName = undefined; + } + + onChange( newName ); + } } + /> + ) } + + ); +} diff --git a/packages/block-editor/src/hooks/block-rename-ui.scss b/packages/block-editor/src/components/block-rename/style.scss similarity index 100% rename from packages/block-editor/src/hooks/block-rename-ui.scss rename to packages/block-editor/src/components/block-rename/style.scss diff --git a/packages/block-editor/src/components/block-rename/use-block-rename.js b/packages/block-editor/src/components/block-rename/use-block-rename.js new file mode 100644 index 00000000000000..1d75b6e56addbf --- /dev/null +++ b/packages/block-editor/src/components/block-rename/use-block-rename.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { getBlockSupport } from '@wordpress/blocks'; + +export default function useBlockRename( name ) { + const metaDataSupport = getBlockSupport( + name, + '__experimentalMetadata', + false + ); + + const supportsBlockNaming = !! ( + true === metaDataSupport || metaDataSupport?.name + ); + + return { + canRename: supportsBlockNaming, + }; +} diff --git a/packages/block-editor/src/components/block-settings-menu-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js index 53b3835fad1a1b..d098aecebc1e1a 100644 --- a/packages/block-editor/src/components/block-settings-menu-controls/index.js +++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js @@ -22,6 +22,8 @@ import { BlockLockMenuItem, useBlockLock } from '../block-lock'; import { store as blockEditorStore } from '../../store'; import BlockModeToggle from '../block-settings-menu/block-mode-toggle'; +import { BlockRenameControl, useBlockRename } from '../block-rename'; + const { Fill, Slot } = createSlotFill( 'BlockSettingsMenuControls' ); const BlockSettingsMenuControlsSlot = ( { @@ -44,7 +46,9 @@ const BlockSettingsMenuControlsSlot = ( { ); const { canLock } = useBlockLock( selectedClientIds[ 0 ] ); + const { canRename } = useBlockRename( selectedBlocks[ 0 ] ); const showLockButton = selectedClientIds.length === 1 && canLock; + const showRenameButton = selectedClientIds.length === 1 && canRename; // Check if current selection of blocks is Groupable or Ungroupable // and pass this props down to ConvertToGroupButton. @@ -84,6 +88,11 @@ const BlockSettingsMenuControlsSlot = ( { clientId={ selectedClientIds[ 0 ] } /> ) } + { showRenameButton && ( + + ) } { fills } { fillProps?.canMove && ! fillProps?.onlyBlock && ( testString?.trim()?.length === 0; - -function RenameModal( { blockName, originalBlockName, onClose, onSave } ) { - const [ editedBlockName, setEditedBlockName ] = useState( blockName ); - - const nameHasChanged = editedBlockName !== blockName; - const nameIsOriginal = editedBlockName === originalBlockName; - const nameIsEmpty = emptyString( editedBlockName ); - - const isNameValid = nameHasChanged || nameIsOriginal; - - const autoSelectInputText = ( event ) => event.target.select(); - - const dialogDescription = useInstanceId( - RenameModal, - `block-editor-rename-modal__description` - ); - - const handleSubmit = () => { - const message = - nameIsOriginal || nameIsEmpty - ? sprintf( - /* translators: %s: new name/label for the block */ - __( 'Block name reset to: "%s".' ), - editedBlockName - ) - : sprintf( - /* translators: %s: new name/label for the block */ - __( 'Block name changed to: "%s".' ), - editedBlockName - ); - - // Must be assertive to immediately announce change. - speak( message, 'assertive' ); - onSave( editedBlockName ); - - // Immediate close avoids ability to hit save multiple times. - onClose(); - }; - - return ( - -

- { __( 'Enter a custom name for this block.' ) } -

-
{ - e.preventDefault(); - - if ( ! isNameValid ) { - return; - } - - handleSubmit(); - } } - > - - - - - - - - -
-
- ); -} - -function BlockRenameControl( props ) { - const [ renamingBlock, setRenamingBlock ] = useState( false ); - - const { clientId, customName, onChange } = props; - - const blockInformation = useBlockDisplayInformation( clientId ); - - return ( - <> - - - - - { ( { selectedClientIds } ) => { - // 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. - if ( ! canRename ) { - return null; - } - - return ( - { - setRenamingBlock( true ); - } } - aria-expanded={ renamingBlock } - aria-haspopup="dialog" - > - { __( 'Rename' ) } - - ); - } } - - - { renamingBlock && ( - setRenamingBlock( false ) } - onSave={ ( newName ) => { - // If the new value is the block's original name (e.g. `Group`) - // or it is an empty string then assume the intent is to reset - // the value. Therefore reset the metadata. - if ( - newName === blockInformation?.title || - emptyString( newName ) - ) { - newName = undefined; - } - - onChange( newName ); - } } - /> - ) } - - ); -} - -export const withBlockRenameControl = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { clientId, name, attributes, setAttributes } = props; - - const metaDataSupport = getBlockSupport( - name, - '__experimentalMetadata', - false - ); - - const supportsBlockNaming = !! ( - true === metaDataSupport || metaDataSupport?.name - ); - - return ( - <> - { supportsBlockNaming && ( - <> - { - setAttributes( { - metadata: { - ...( attributes?.metadata && - attributes?.metadata ), - name: newName, - }, - } ); - } } - /> - - ) } - - - - ); - }, - 'withToolbarControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/block-rename-ui/with-block-rename-control', - withBlockRenameControl -); diff --git a/packages/block-editor/src/hooks/block-rename.js b/packages/block-editor/src/hooks/block-rename.js new file mode 100644 index 00000000000000..d8f414a3e9ae80 --- /dev/null +++ b/packages/block-editor/src/hooks/block-rename.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { createHigherOrderComponent } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; +import { __ } from '@wordpress/i18n'; +import { hasBlockSupport } from '@wordpress/blocks'; +import { TextControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { InspectorControls } from '../components'; + +export const withBlockRenameControl = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { name, attributes, setAttributes, isSelected } = props; + + const supportsBlockNaming = hasBlockSupport( name, 'renaming', true ); + + return ( + <> + { isSelected && supportsBlockNaming && ( + + { + setAttributes( { + metadata: { + ...attributes?.metadata, + name: newName, + }, + } ); + } } + /> + + ) } + + + + ); + }, + 'withToolbarControls' +); + +addFilter( + 'editor.BlockEdit', + 'core/block-rename-ui/with-block-rename-control', + withBlockRenameControl +); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 8ae5c1dbe3a7e7..460014701c8ad2 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -22,7 +22,7 @@ import './metadata'; import './metadata-name'; import './custom-fields'; import './block-hooks'; -import './block-rename-ui'; +import './block-rename'; export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 93ab3b69a7aad3..a55756ae6f53d7 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -15,6 +15,7 @@ @import "./components/block-patterns-paging/style.scss"; @import "./components/block-popover/style.scss"; @import "./components/block-preview/style.scss"; +@import "./components/block-rename/style.scss"; @import "./components/block-settings-menu/style.scss"; @import "./components/block-styles/style.scss"; @import "./components/block-switcher/style.scss"; @@ -56,7 +57,6 @@ @import "./hooks/padding.scss"; @import "./hooks/position.scss"; @import "./hooks/typography.scss"; -@import "./hooks/block-rename-ui.scss"; @import "./components/block-toolbar/style.scss"; @import "./components/inserter/style.scss";