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"