diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index a50e1fb883717..8bda090263037 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -316,13 +316,61 @@ function gutenberg_get_classnames_from_last_tag( $html ) { * @return string Filtered block content. */ function gutenberg_render_layout_support_flag( $block_content, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $support_layout = block_has_support( $block_type, array( '__experimentalLayout' ), false ); + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $support_layout = block_has_support( $block_type, array( '__experimentalLayout' ), false ); + $has_child_layout = isset( $block['attrs']['style']['layout']['selfStretch'] ); - if ( ! $support_layout ) { + if ( ! $support_layout + && ! $has_child_layout ) { return $block_content; } + $outer_class_names = array(); + + if ( $has_child_layout && ( 'fixed' === $block['attrs']['style']['layout']['selfStretch'] || 'fill' === $block['attrs']['style']['layout']['selfStretch'] ) ) { + + $container_content_class = wp_unique_id( 'wp-container-content-' ); + + $child_layout_styles = array(); + + if ( 'fixed' === $block['attrs']['style']['layout']['selfStretch'] && isset( $block['attrs']['style']['layout']['flexSize'] ) ) { + $child_layout_styles[] = array( + 'selector' => ".$container_content_class", + 'declarations' => array( + 'flex-shrink' => '0', + 'flex-basis' => $block['attrs']['style']['layout']['flexSize'], + 'box-sizing' => 'border-box', + ), + ); + } elseif ( 'fill' === $block['attrs']['style']['layout']['selfStretch'] ) { + $child_layout_styles[] = array( + 'selector' => ".$container_content_class", + 'declarations' => array( + 'flex-grow' => '1', + ), + ); + } + + gutenberg_style_engine_get_stylesheet_from_css_rules( + $child_layout_styles, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + + $outer_class_names[] = $container_content_class; + + } + + // Return early if only child layout exists. + if ( ! $support_layout && ! empty( $outer_class_names ) ) { + $content = new WP_HTML_Tag_Processor( $block_content ); + $content->next_tag(); + $content->add_class( implode( ' ', $outer_class_names ) ); + return (string) $content; + } + $block_gap = gutenberg_get_global_settings( array( 'spacing', 'blockGap' ) ); $global_layout_settings = gutenberg_get_global_settings( array( 'layout' ) ); $has_block_gap_support = isset( $block_gap ) ? null !== $block_gap : false; @@ -428,13 +476,26 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { } } + $content_with_outer_classnames = ''; + + if ( ! empty( $outer_class_names ) ) { + $content_with_outer_classnames = new WP_HTML_Tag_Processor( $block_content ); + $content_with_outer_classnames->next_tag(); + foreach ( $outer_class_names as $outer_class_name ) { + $content_with_outer_classnames->add_class( $outer_class_name ); + } + + $content_with_outer_classnames = (string) $content_with_outer_classnames; + } + /** * The first chunk of innerContent contains the block markup up until the inner blocks start. * We want to target the opening tag of the inner blocks wrapper, which is the last tag in that chunk. */ $inner_content_classnames = isset( $block['innerContent'][0] ) && 'string' === gettype( $block['innerContent'][0] ) ? gutenberg_get_classnames_from_last_tag( $block['innerContent'][0] ) : ''; - $content = new WP_HTML_Tag_Processor( $block_content ); + $content = $content_with_outer_classnames ? new WP_HTML_Tag_Processor( $content_with_outer_classnames ) : new WP_HTML_Tag_Processor( $block_content ); + if ( $inner_content_classnames ) { $content->next_tag( array( 'class_name' => $inner_content_classnames ) ); foreach ( $class_names as $class_name ) { @@ -442,7 +503,9 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { } } else { $content->next_tag(); - $content->add_class( implode( ' ', $class_names ) ); + foreach ( $class_names as $class_name ) { + $content->add_class( $class_name ); + } } return (string) $content; diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 9e4c9755ec225..c32d929392d8d 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -40,7 +40,7 @@ import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; import { useBlockProps } from './use-block-props'; import { store as blockEditorStore } from '../../store'; - +import { useLayout } from './layout'; export const BlockListBlockContext = createContext(); /** @@ -130,6 +130,8 @@ function BlockListBlock( { const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); + const parentLayout = useLayout(); + // We wrap the BlockEdit component in a div that hides it when editing in // HTML mode. This allows us to render all of the ancillary pieces // (InspectorControls, etc.) which are inside `BlockEdit` but not @@ -148,6 +150,7 @@ function BlockListBlock( { isSelectionEnabled={ isSelectionEnabled } toggleSelection={ toggleSelection } __unstableLayoutClassNames={ layoutClassNames } + __unstableParentLayout={ parentLayout } /> ); diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index bf62fe9c24e13..991f08ec58a17 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -33,6 +33,7 @@ import BlockInvalidWarning from './block-invalid-warning'; import BlockMobileToolbar from '../block-mobile-toolbar'; import { store as blockEditorStore } from '../../store'; import BlockDraggable from '../block-draggable'; +import { useLayout } from './layout'; const emptyArray = []; function BlockForType( { @@ -78,6 +79,8 @@ function BlockForType( { ), ] ); + const parentLayout = useLayout(); + return ( diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index c03a8616fed65..80362754a394f 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -10,6 +10,7 @@ import { useViewportMatch, useMergeRefs } from '@wordpress/compose'; import { forwardRef } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { + getBlockSupport, getBlockType, store as blocksStore, __unstableGetInnerBlocksProps as getInnerBlocksProps, @@ -74,27 +75,33 @@ function UncontrolledInnerBlocks( props ) { templateInsertUpdatesSelection ); - const context = useSelect( + const { context, name } = useSelect( ( select ) => { const block = select( blockEditorStore ).getBlock( clientId ); // This check is here to avoid the Redux zombie bug where a child subscription // is called before a parent, causing potential JS errors when the child has been removed. if ( ! block ) { - return; + return {}; } const blockType = getBlockType( block.name ); if ( ! blockType || ! blockType.providesContext ) { - return; + return {}; } - return getBlockContext( block.attributes, blockType ); + return { + context: getBlockContext( block.attributes, blockType ), + name: block.name, + }; }, [ clientId ] ); + const { allowSizingOnChildren = false } = + getBlockSupport( name, '__experimentalLayout' ) || {}; + // This component needs to always be synchronous as it's the one changing // the async mode depending on the block selection. return ( @@ -103,7 +110,10 @@ function UncontrolledInnerBlocks( props ) { rootClientId={ clientId } renderAppender={ renderAppender } __experimentalAppenderTagName={ __experimentalAppenderTagName } - __experimentalLayout={ __experimentalLayout } + __experimentalLayout={ { + ...__experimentalLayout, + allowSizingOnChildren, + } } wrapperRef={ wrapperRef } placeholder={ placeholder } /> diff --git a/packages/block-editor/src/hooks/child-layout.js b/packages/block-editor/src/hooks/child-layout.js new file mode 100644 index 0000000000000..89e5c345085a8 --- /dev/null +++ b/packages/block-editor/src/hooks/child-layout.js @@ -0,0 +1,172 @@ +/** + * WordPress dependencies + */ +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import useSetting from '../components/use-setting'; + +function helpText( selfStretch ) { + switch ( selfStretch ) { + case 'fill': + return __( 'Stretch to fill available space.' ); + case 'fixed': + return __( 'Specify a fixed width.' ); + default: + return __( 'Fit contents.' ); + } +} + +/** + * Inspector controls containing the child layout related configuration. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block attributes. + * @param {Object} props.setAttributes Function to set block attributes. + * @param {Object} props.__unstableParentLayout + * + * @return {WPElement} child layout edit element. + */ +export function ChildLayoutEdit( { + attributes, + setAttributes, + __unstableParentLayout: parentLayout, +} ) { + const { style = {} } = attributes; + const { layout: childLayout = {} } = style; + const { selfStretch, flexSize } = childLayout; + + return ( + <> + { + const newFlexSize = value !== 'fixed' ? null : flexSize; + setAttributes( { + style: { + ...style, + layout: { + ...childLayout, + selfStretch: value, + flexSize: newFlexSize, + }, + }, + } ); + } } + isBlock={ true } + > + + + + + { selfStretch === 'fixed' && ( + { + setAttributes( { + style: { + ...style, + layout: { + ...childLayout, + flexSize: value, + }, + }, + } ); + } } + value={ flexSize } + /> + ) } + + ); +} + +/** + * Determines if there is child layout support. + * + * @param {Object} props Block Props object. + * @param {Object} props.__unstableParentLayout Parent layout. + * + * @return {boolean} Whether there is support. + */ +export function hasChildLayoutSupport( { + __unstableParentLayout: parentLayout = {}, +} ) { + const { + type: parentLayoutType = 'default', + allowSizingOnChildren = false, + } = parentLayout; + const support = parentLayoutType === 'flex' && allowSizingOnChildren; + + return support; +} + +/** + * Checks if there is a current value in the child layout attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a child layout value set. + */ +export function hasChildLayoutValue( props ) { + return props.attributes.style?.layout !== undefined; +} + +/** + * Resets the child layout attribute. This can be used when disabling + * child layout controls for a block via a progressive discovery panel. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block attributes. + * @param {Object} props.setAttributes Function to set block attributes. + */ +export function resetChildLayout( { attributes = {}, setAttributes } ) { + const { style } = attributes; + + setAttributes( { + style: { + ...style, + layout: undefined, + }, + } ); +} + +/** + * Custom hook that checks if child layout settings have been disabled. + * + * @param {Object} props Block props. + * + * @return {boolean} Whether the child layout setting is disabled. + */ +export function useIsChildLayoutDisabled( props ) { + const isDisabled = ! useSetting( 'layout' ); + + return ! hasChildLayoutSupport( props ) || isDisabled; +} + +export function childLayoutOrientation( parentLayout ) { + const { orientation = 'horizontal' } = parentLayout; + + return orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); +} diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 44a9fd83278a9..c5b6398890833 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -6,7 +6,10 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __experimentalToolsPanelItem as ToolsPanelItem } from '@wordpress/components'; +import { + __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalVStack as VStack, +} from '@wordpress/components'; import { Platform, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getBlockSupport } from '@wordpress/blocks'; @@ -46,6 +49,14 @@ import { resetPadding, useIsPaddingDisabled, } from './padding'; +import { + ChildLayoutEdit, + hasChildLayoutSupport, + hasChildLayoutValue, + resetChildLayout, + useIsChildLayoutDisabled, + childLayoutOrientation, +} from './child-layout'; import useSetting from '../components/use-setting'; import { store as blockEditorStore } from '../store'; @@ -85,8 +96,9 @@ export function DimensionsPanel( props ) { const isPaddingDisabled = useIsPaddingDisabled( props ); const isMarginDisabled = useIsMarginDisabled( props ); const isMinHeightDisabled = useIsMinHeightDisabled( props ); + const isChildLayoutDisabled = useIsChildLayoutDisabled( props ); const isDisabled = useIsDimensionsDisabled( props ); - const isSupported = hasDimensionsSupport( props.name ); + const isSupported = hasDimensionsSupport( props ); const spacingSizes = useSetting( 'spacing.spacingSizes' ); const paddingMouseOver = useVisualizerMouseOver(); const marginMouseOver = useVisualizerMouseOver(); @@ -121,6 +133,8 @@ export function DimensionsPanel( props ) { 'tools-panel-item-spacing': spacingSizes && spacingSizes.length > 0, } ); + const { __unstableParentLayout: parentLayout } = props; + return ( <> @@ -198,6 +212,23 @@ export function DimensionsPanel( props ) { ) } + { ! isChildLayoutDisabled && ( + hasChildLayoutValue( props ) } + label={ childLayoutOrientation( parentLayout ) } + onDeselect={ () => resetChildLayout( props ) } + resetAllFilter={ createResetAllFilter( + 'selfStretch', + 'layout' + ) } + isShownByDefault={ false } + panelId={ props.clientId } + > + + + ) } { ! isPaddingDisabled && ( { const minHeightDisabled = useIsMinHeightDisabled( props ); const paddingDisabled = useIsPaddingDisabled( props ); const marginDisabled = useIsMarginDisabled( props ); + const childLayoutDisabled = useIsChildLayoutDisabled( props ); return ( - gapDisabled && minHeightDisabled && paddingDisabled && marginDisabled + gapDisabled && + minHeightDisabled && + paddingDisabled && + marginDisabled && + childLayoutDisabled ); }; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index a2058cb24677e..8ee7a6615c367 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -416,6 +416,62 @@ export const withLayoutStyles = createHigherOrderComponent( } ); +/** + * Override the default block element to add the child layout styles. + * + * @param {Function} BlockListBlock Original component. + * + * @return {Function} Wrapped component. + */ +export const withChildLayoutStyles = createHigherOrderComponent( + ( BlockListBlock ) => ( props ) => { + const { attributes } = props; + const { style: { layout = {} } = {} } = attributes; + const { selfStretch, flexSize } = layout; + const hasChildLayout = selfStretch || flexSize; + const disableLayoutStyles = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return !! getSettings().disableLayoutStyles; + } ); + const shouldRenderChildLayoutStyles = + hasChildLayout && ! disableLayoutStyles; + + const element = useContext( BlockList.__unstableElementContext ); + const id = useInstanceId( BlockListBlock ); + const selector = `.wp-container-content-${ id }`; + + let css = ''; + + if ( selfStretch === 'fixed' && flexSize ) { + css += `${ selector } { + flex-shrink: 0; + flex-basis: ${ flexSize }; + box-sizing: border-box; + }`; + } else if ( selfStretch === 'fill' ) { + css += `${ selector } { + flex-grow: 1; + }`; + } + + // Attach a `wp-container-content` id-based classname. + const className = classnames( props?.className, { + [ `wp-container-content-${ id }` ]: + shouldRenderChildLayoutStyles && !! css, // Only attach a container class if there is generated CSS to be attached. + } ); + + return ( + <> + { shouldRenderChildLayoutStyles && + element && + !! css && + createPortal( , element ) } + + + ); + } +); + addFilter( 'blocks.registerBlockType', 'core/layout/addAttribute', @@ -426,6 +482,11 @@ addFilter( 'core/editor/layout/with-layout-styles', withLayoutStyles ); +addFilter( + 'editor.BlockListBlock', + 'core/editor/layout/with-child-layout-styles', + withChildLayoutStyles +); addFilter( 'editor.BlockEdit', 'core/editor/layout/with-inspector-controls', diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index 3f409ba052c95..a3997db0621c5 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -69,7 +69,9 @@ "fontSize": true } }, - "__experimentalLayout": true + "__experimentalLayout": { + "allowSizingOnChildren": true + } }, "editorStyle": "wp-block-group-editor", "style": "wp-block-group"