diff --git a/packages/block-editor/src/components/custom-class-name-control/index.js b/packages/block-editor/src/components/custom-class-name-control/index.js new file mode 100644 index 00000000000000..c02a0bc8ad5cce --- /dev/null +++ b/packages/block-editor/src/components/custom-class-name-control/index.js @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + DropdownMenu, + MenuGroup, + MenuItem, + TextControl, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { store as blocksStore } from '@wordpress/blocks'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { check, moreVertical } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { InspectorControls } from '../'; +import { getActiveStyle, replaceActiveStyle } from '../block-styles/utils'; +import { store as blockEditorStore } from '../../store'; + +/** + * @typedef {Object} CustomClassNameMenuDropDownMenuProps + * @property {string} activeStyle The currently active style. + * @property {Object} blockStyles A collection of Block Styles. + * @property {Function} onSelectStyleClassName An onClick handler. + */ + +/** + * Returns a DropDownMenu component. + * + * @param {CustomClassNameMenuDropDownMenuProps} props The component. + * @return {WPComponent} The menu item component. + */ +function CustomClassNameMenuDropDownMenu( { + activeStyle, + blockStyles, + onSelectStyleClassName, +} ) { + return ( + + { ( { onClose } ) => ( + + { blockStyles.map( ( style ) => { + const isSelected = activeStyle?.name === style.name; + const icon = isSelected ? check : null; + return ( + { + onSelectStyleClassName( style ); + onClose(); + } } + role="menuitemcheckbox" + > + { style?.label } + + ); + } ) } + + ) } + + ); +} + +/** + * @typedef {Object} CustomClassNameControlProps + * @property {string} clientId Selected Block clientId. + * @property {string} name Selected Block name. + * @property {Object} attributes Selected Block's attributes. + * @property {Function} setAttributes Set attributes callback. + */ + +/** + * Control to display custom class name control dropdown and text input. + * + * @param {CustomClassNameControlProps} props Component props. + * + * @return {WPElement} Font appearance control. + */ +export default function CustomClassNameControl( { + clientId, + name, + attributes, + setAttributes, +} ) { + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const blockStyles = useSelect( + ( select ) => select( blocksStore ).getBlockStyles( name ), + [ name, attributes.className ] + ); + + const hasBlockStyles = blockStyles && !! blockStyles.length; + + const activeStyle = hasBlockStyles + ? getActiveStyle( blockStyles, attributes.className || '' ) + : null; + + const onSelectStyleClassName = ( style ) => { + const styleClassName = replaceActiveStyle( + attributes.className, + activeStyle, + style + ); + updateBlockAttributes( clientId, { + className: styleClassName, + } ); + }; + + const additionalClassNameContainerClasses = classnames( + 'additional-class-name-control__container', + { + 'has-block-styles': hasBlockStyles, + } + ); + + return ( + +
+ { + setAttributes( { + className: nextValue !== '' ? nextValue : undefined, + } ); + } } + help={ __( 'Separate multiple classes with spaces.' ) } + > + { hasBlockStyles && ( + + ) } + +
+
+ ); +} diff --git a/packages/block-editor/src/components/custom-class-name-control/style.scss b/packages/block-editor/src/components/custom-class-name-control/style.scss new file mode 100644 index 00000000000000..4ee866bc66cc64 --- /dev/null +++ b/packages/block-editor/src/components/custom-class-name-control/style.scss @@ -0,0 +1,28 @@ +.additional-class-name-control__container.has-block-styles { + .additional-class-name-control__text-control > .components-base-control__field { + position: relative; + } +} + +.additional-class-name-control__block-style-dropdown { + position: absolute; + top: auto; + right: 2px; + bottom: 2px; + background: $white; + + button.components-button.has-icon { + min-width: 24px; + height: 24px; + padding: 0; + + svg { + height: 27px; + width: 27px; + @include break-medium() { + height: 18px; + width: 18px; + } + } + } +} diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index 0ed09f8975c5cb..492560ae13a1c3 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -7,15 +7,13 @@ import classnames from 'classnames'; * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { TextControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; /** * Internal dependencies */ -import { InspectorControls } from '../components'; +import CustomClassNameControl from '../components/custom-class-name-control'; /** * Filters registered block settings, extending attributes with anchor using ID @@ -55,28 +53,12 @@ export const withInspectorControl = createHigherOrderComponent( 'customClassName', true ); + if ( hasCustomClassName && props.isSelected ) { return ( <> - - { - props.setAttributes( { - className: - nextValue !== '' - ? nextValue - : undefined, - } ); - } } - help={ __( - 'Separate multiple classes with spaces.' - ) } - /> - + ); } diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 6f20a3b1b29012..afe7ef3989ac4b 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -29,6 +29,7 @@ @import "./components/button-block-appender/style.scss"; @import "./components/colors-gradients/style.scss"; @import "./components/contrast-checker/style.scss"; +@import "./components/custom-class-name-control/style.scss"; @import "./components/default-block-appender/style.scss"; @import "./components/duotone-control/style.scss"; @import "./components/font-appearance-control/style.scss"; diff --git a/packages/components/src/text-control/README.md b/packages/components/src/text-control/README.md index 11834ddefbd21e..f960279ee53352 100644 --- a/packages/components/src/text-control/README.md +++ b/packages/components/src/text-control/README.md @@ -132,6 +132,13 @@ A function that receives the value of the input. - Type: `function` - Required: Yes +#### children + +The content to be displayed within the `BaseControl`, and as a sibling of `input`. + +- Type: `Element` +- Required: No + ## Related components - To offer users more constrained options for input, use SelectControl, RadioControl, CheckboxControl, or RangeControl. diff --git a/packages/components/src/text-control/index.js b/packages/components/src/text-control/index.js index 394da3644ce490..12f1c0ac935e05 100644 --- a/packages/components/src/text-control/index.js +++ b/packages/components/src/text-control/index.js @@ -18,6 +18,7 @@ import BaseControl from '../base-control'; * @property {string} [className] Classname passed to BaseControl wrapper * @property {(value: string) => void} onChange Handle changes. * @property {string} [type='text'] Type of the input. + * @property {WPElement[]} children Children. */ /** @typedef {OwnProps & import('react').ComponentProps<'input'>} Props */ @@ -36,6 +37,7 @@ function TextControl( className, onChange, type = 'text', + children, ...props }, ref @@ -65,6 +67,7 @@ function TextControl( ref={ ref } { ...props } /> + { children } ); } diff --git a/packages/components/src/text-control/stories/index.js b/packages/components/src/text-control/stories/index.js index e381fd36be57a7..7368f1099ac941 100644 --- a/packages/components/src/text-control/stories/index.js +++ b/packages/components/src/text-control/stories/index.js @@ -12,6 +12,7 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import TextControl from '../'; +import Button from '../../button'; export default { title: 'Components/TextControl', @@ -22,18 +23,34 @@ export default { }; const TextControlWithState = ( props ) => { - const [ value, setValue ] = useState(); + const [ value, setValue ] = useState( '' ); return ; }; -export const _default = () => { - const label = text( 'Label', 'Label Text' ); - const hideLabelFromVision = boolean( 'Hide Label From Vision', false ); - const help = text( 'Help Text', 'Help text to explain the input.' ); - const type = text( 'Input Type', 'text' ); - const className = text( 'Class Name', '' ); +const TextControlWithStateAndChildren = ( props ) => { + const [ value, setValue ] = useState( '' ); + return ( + + + + ); +}; + +const label = text( 'Label', 'Label Text' ); +const hideLabelFromVision = boolean( 'Hide Label From Vision', false ); +const help = text( 'Help Text', 'Help text to explain the input.' ); +const type = text( 'Input Type', 'text' ); +const className = text( 'Class Name', '' ); + +export const _default = () => { return ( { /> ); }; + +export const withChildren = () => { + return ( + + ); +};