diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index b53a90cde21855..b741c1905e59f5 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -572,17 +572,85 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $child_layout_declarations = array(); $child_layout_styles = array(); - $self_stretch = isset( $block['attrs']['style']['layout']['selfStretch'] ) ? $block['attrs']['style']['layout']['selfStretch'] : null; + $self_stretch = isset( $child_layout['selfStretch'] ) ? $child_layout['selfStretch'] : null; + $self_align = isset( $child_layout['selfAlign'] ) ? $child_layout['selfAlign'] : null; + $height = isset( $child_layout['height'] ) ? $child_layout['height'] : null; + $width = isset( $child_layout['width'] ) ? $child_layout['width'] : null; + + $parent_layout_type = 'default'; + if ( isset( $block['parentLayout']['type'] ) ) { + $parent_layout_type = $block['parentLayout']['type']; + } elseif ( isset( $block['parentLayout']['default']['type'] ) ) { + $parent_layout_type = $block['parentLayout']['default']['type']; + } + + // Orientation is only used for flex layouts so its default is horizontal. + $parent_orientation = isset( $block['parentLayout']['orientation'] ) ? $block['parentLayout']['orientation'] : 'horizontal'; + $has_vertical_parent_layout = in_array( $parent_layout_type, array( 'constrained', 'default' ), true ) || ( 'flex' === $parent_layout_type && 'vertical' === $parent_orientation ); - if ( 'fixed' === $self_stretch && isset( $block['attrs']['style']['layout']['flexSize'] ) ) { - $child_layout_declarations['flex-basis'] = $block['attrs']['style']['layout']['flexSize']; + // Support for legacy flexSize value. + if ( 'fixed' === $self_stretch && isset( $child_layout['flexSize'] ) ) { + $child_layout_declarations['flex-basis'] = $child_layout['flexSize']; $child_layout_declarations['box-sizing'] = 'border-box'; } elseif ( 'fill' === $self_stretch ) { $child_layout_declarations['flex-grow'] = '1'; } - $column_start = isset( $block['attrs']['style']['layout']['columnStart'] ) ? $block['attrs']['style']['layout']['columnStart'] : null; - $column_span = isset( $block['attrs']['style']['layout']['columnSpan'] ) ? $block['attrs']['style']['layout']['columnSpan'] : null; + if ( $has_vertical_parent_layout ) { + // Width styles. + if ( 'fixed' === $self_align && $width ) { + /** + * !important is a (hopefully) temporary override for + * the constrained layout styles, the specificity of + * which should be lowered soon. + */ + $child_layout_declarations['max-width'] = "$width !important"; + } elseif ( 'fixedNoShrink' === $self_align && $width ) { + $child_layout_declarations['width'] = $width; + if ( 'constrained' === $parent_layout_type ) { + $child_layout_declarations['max-width'] = 'none !important'; + } + } elseif ( 'fill' === $self_align ) { + $child_layout_declarations['align-self'] = 'stretch'; + } elseif ( 'fit' === $self_align ) { + $child_layout_declarations['width'] = 'fit-content'; + } + // Height styles. + if ( 'fixed' === $self_stretch && $height ) { + $child_layout_declarations['max-height'] = $height; + $child_layout_declarations['flex-basis'] = $height; + } elseif ( 'fixedNoShrink' === $self_stretch && $height ) { + $child_layout_declarations['height'] = $height; + $child_layout_declarations['flex-shrink'] = '0'; + $child_layout_declarations['flex-basis'] = $height; + } elseif ( 'fill' === $self_stretch ) { + $child_layout_declarations['flex-grow'] = '1'; + } + } elseif ( 'grid' !== $parent_layout_type ) { + // Width styles. + if ( 'fixed' === $self_stretch && $width ) { + $child_layout_declarations['flex-basis'] = $width; + } elseif ( 'fixedNoShrink' === $self_stretch && $width ) { + $child_layout_declarations['flex-shrink'] = '0'; + $child_layout_declarations['flex-basis'] = $width; + } elseif ( 'fill' === $self_stretch ) { + $child_layout_declarations['flex-grow'] = '1'; + } + // Height styles. + if ( 'fixed' === $self_align && $height ) { + $child_layout_declarations['max-height'] = $height; + } elseif ( 'fixedNoShrink' === $self_align && $height ) { + $child_layout_declarations['height'] = $height; + } elseif ( 'fill' === $self_align ) { + $child_layout_declarations['align-self'] = 'stretch'; + } + } + + // Grid specific styles. + + $column_start = isset( $child_layout['columnStart'] ) ? $child_layout['columnStart'] : null; + $column_span = isset( $child_layout['columnSpan'] ) ? $child_layout['columnSpan'] : null; + if ( $column_start && $column_span ) { $child_layout_declarations['grid-column'] = "$column_start / span $column_span"; } elseif ( $column_start ) { @@ -591,8 +659,8 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $child_layout_declarations['grid-column'] = "span $column_span"; } - $row_start = isset( $block['attrs']['style']['layout']['rowStart'] ) ? $block['attrs']['style']['layout']['rowStart'] : null; - $row_span = isset( $block['attrs']['style']['layout']['rowSpan'] ) ? $block['attrs']['style']['layout']['rowSpan'] : null; + $row_start = isset( $child_layout['rowStart'] ) ? $child_layout['rowStart'] : null; + $row_span = isset( $child_layout['rowSpan'] ) ? $child_layout['rowSpan'] : null; if ( $row_start && $row_span ) { $child_layout_declarations['grid-row'] = "$row_start / span $row_span"; } elseif ( $row_start ) { @@ -606,8 +674,8 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { 'declarations' => $child_layout_declarations, ); - $minimum_column_width = isset( $block['attrs']['style']['layout']['minimumColumnWidth'] ) ? $block['attrs']['style']['layout']['minimumColumnWidth'] : null; - $column_count = isset( $block['attrs']['style']['layout']['columnCount'] ) ? $block['attrs']['style']['layout']['columnCount'] : null; + $minimum_column_width = isset( $child_layout['minimumColumnWidth'] ) ? $child_layout['minimumColumnWidth'] : null; + $column_count = isset( $child_layout['columnCount'] ) ? $child_layout['columnCount'] : null; /* * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set, @@ -922,6 +990,8 @@ function ( $parsed_block, $source_block, $parent_block ) { */ if ( $parent_block && isset( $parent_block->parsed_block['attrs']['layout'] ) ) { $parsed_block['parentLayout'] = $parent_block->parsed_block['attrs']['layout']; + } elseif ( $parent_block && isset( $parent_block->block_type->supports['layout'] ) ) { + $parsed_block['parentLayout'] = $parent_block->block_type->supports['layout']; } return $parsed_block; }, diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 4e94a8a427510d..fcac905c084f59 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -42,11 +42,14 @@ export default function BlockEdit( { attributes = {}, __unstableLayoutClassNames, } = props; - const { layout = null, metadata = {} } = attributes; + const { layout = null, align = null, metadata = {} } = attributes; const { bindings } = metadata; const layoutSupport = hasBlockSupport( name, 'layout', false ) || hasBlockSupport( name, '__experimentalLayout', false ); + + const hasAlignSupport = hasBlockSupport( name, 'align', false ); + return ( !! selfStretch; - const flexResetLabel = - orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); - const resetFlex = () => { + const isFlowOrConstrained = + parentLayoutType === 'default' || + parentLayoutType === 'constrained' || + parentLayoutType === undefined; + + const widthProp = + isFlowOrConstrained || orientation === 'vertical' + ? 'selfAlign' + : 'selfStretch'; + const heightProp = + isFlowOrConstrained || orientation === 'vertical' + ? 'selfStretch' + : 'selfAlign'; + + //ToolsPanelItem-specific functions. + const resetWidthValue = () => { + // If alignment has been set via the width control, unset it. + if ( + ( selfAlign === 'wide' && currentAlignment === 'wide' ) || + ( selfAlign === 'fill' && currentAlignment === 'full' ) + ) { + onChangeAlignment( undefined ); + } onChange( { selfStretch: undefined, flexSize: undefined, + selfAlign: undefined, + width: undefined, + } ); + }; + const resetHeightValue = () => { + onChange( { + selfStretch: undefined, + flexSize: undefined, + selfAlign: undefined, + height: undefined, } ); }; - - const hasStartValue = () => !! columnStart || !! rowStart; - const hasSpanValue = () => !! columnSpan || !! rowSpan; const resetGridStarts = () => { onChange( { columnStart: undefined, @@ -87,73 +130,360 @@ export default function ChildLayoutControl( { rowSpan: undefined, } ); }; + const hasStartValue = () => !! columnStart || !! rowStart; + const hasSpanValue = () => !! columnSpan || !! rowSpan; + const hasWidthValue = () => !! childLayout[ widthProp ]; + const hasHeightValue = () => !! childLayout[ heightProp ]; + + const widthOptions = []; + + if ( parentLayoutType === 'constrained' ) { + if ( contentSize ) { + widthOptions.push( { + key: 'content', + value: 'content', + name: __( 'Default' ), + } ); + } + if ( + wideSize && + supportedAlignments?.includes( 'wide' ) && + ( parentAlignment === 'wide' || parentAlignment === 'full' ) + ) { + widthOptions.push( { + key: 'wide', + value: 'wide', + name: __( 'Wide' ), + } ); + } + // If no contentSize is defined, fill should be the default. + if ( + ( supportedAlignments?.includes( 'full' ) && + parentAlignment === 'full' ) || + ! contentSize + ) { + widthOptions.push( { + key: 'fill', + value: 'fill', + name: __( 'Fill' ), + } ); + } + widthOptions.push( + { + key: 'fit', + value: 'fit', + name: __( 'Fit' ), + }, + { + key: 'fixedNoShrink', + value: 'fixedNoShrink', + name: __( 'Fixed' ), + }, + { + key: 'fixed', + value: 'fixed', + name: __( 'Max Width' ), + } + ); + } else if ( + parentLayoutType === 'default' || + ( parentLayoutType === 'flex' && orientation === 'vertical' ) + ) { + widthOptions.push( + { + key: 'fit', + value: 'fit', + name: __( 'Fit' ), + }, + { + key: 'fill', + value: 'fill', + name: __( 'Fill' ), + }, + { + key: 'fixedNoShrink', + value: 'fixedNoShrink', + name: __( 'Fixed' ), + }, + { + key: 'fixed', + value: 'fixed', + name: __( 'Max Width' ), + } + ); + } else if ( parentLayoutType === 'flex' && orientation === 'horizontal' ) { + widthOptions.push( + { + key: 'fit', + value: 'fit', + name: __( 'Fit' ), + }, + { + key: 'fill', + value: 'fill', + name: __( 'Fill' ), + }, + { + key: 'fixed', + value: 'fixed', + name: __( 'Max Width' ), + }, + { + key: 'fixedNoShrink', + value: 'fixedNoShrink', + name: __( 'Fixed' ), + } + ); + } + + const heightOptions = [ + { + key: 'fit', + value: 'fit', + name: __( 'Fit' ), + }, + ]; + + if ( parentLayoutType === 'flex' ) { + heightOptions.push( + { + key: 'fixed', + value: 'fixed', + name: __( 'Max Height' ), + }, + { + key: 'fixedNoShrink', + value: 'fixedNoShrink', + name: __( 'Fixed' ), + }, + { + key: 'fill', + value: 'fill', + name: __( 'Fill' ), + } + ); + } else { + heightOptions.push( { + key: 'fixedNoShrink', + value: 'fixedNoShrink', + name: __( 'Fixed' ), + } ); + } + + const selectedWidth = () => { + let selectedValue; + if ( isFlowOrConstrained ) { + // Replace "full" with "fill" for full width alignments. + if ( + currentAlignment === 'full' && + parentLayoutType === 'constrained' + ) { + selectedValue = 'fill'; + } else if ( + wideSize && + currentAlignment === 'wide' && + parentLayoutType === 'constrained' + ) { + selectedValue = 'wide'; + } else if ( selfAlign === 'fixedNoShrink' ) { + selectedValue = 'fixedNoShrink'; + } else if ( selfAlign === 'fixed' ) { + selectedValue = 'fixed'; + } else if ( selfAlign === 'fit' ) { + selectedValue = 'fit'; + } else if ( parentLayoutType === 'constrained' ) { + selectedValue = 'content'; + } else { + selectedValue = 'fill'; + } + } else if ( + parentLayoutType === 'flex' && + orientation === 'vertical' + ) { + // If the parent layout is justified stretch, children should be fill by default. + const defaultSelfAlign = + justifyContent === 'stretch' ? 'fill' : 'fit'; + selectedValue = selfAlign || defaultSelfAlign; + } else if ( + parentLayoutType === 'flex' && + orientation === 'horizontal' + ) { + selectedValue = selfStretch || 'fit'; + } else { + selectedValue = 'fill'; + } + + return widthOptions.find( ( _value ) => _value?.key === selectedValue ); + }; + + const selectedHeight = () => { + let selectedValue; + if ( + isFlowOrConstrained || + ( parentLayoutType === 'flex' && orientation === 'vertical' ) + ) { + selectedValue = childLayout[ heightProp ] || 'fit'; + } else if ( parentLayoutType === 'flex' ) { + const defaultSelfAlign = + verticalAlignment === 'stretch' ? 'fill' : 'fit'; + selectedValue = childLayout[ heightProp ] || defaultSelfAlign; + } else { + selectedValue = 'fit'; + } + return heightOptions.find( + ( _value ) => _value?.key === selectedValue + ); + }; + + const onChangeWidth = ( newWidth ) => { + const { selectedItem } = newWidth; + const { key } = selectedItem; + if ( isFlowOrConstrained ) { + if ( key === 'fill' ) { + onChange( { ...childLayout, [ widthProp ]: key } ); + /** + * Fill exists for both flow and constrained layouts but + * should only change alignment for constrained layouts. + * "fill" in flow layout is the default state of its children. + */ + if ( parentLayoutType === 'constrained' ) { + onChangeAlignment( 'full' ); + } + } else if ( key === 'wide' ) { + onChange( { ...childLayout, [ widthProp ]: key } ); + onChangeAlignment( 'wide' ); + } else if ( key === 'fixedNoShrink' ) { + onChange( { + ...childLayout, + [ widthProp ]: key, + } ); + onChangeAlignment( undefined ); + } else { + onChange( { ...childLayout, [ widthProp ]: key } ); + onChangeAlignment( undefined ); + } + } else if ( parentLayoutType === 'flex' ) { + // if the layout is horizontal, reset any flexSize when changing width. + const resetFlexSize = + orientation !== 'vertical' ? undefined : flexSize; + onChange( { + ...childLayout, + [ widthProp ]: key, + flexSize: resetFlexSize, + } ); + } + }; + + const onChangeHeight = ( newHeight ) => { + // If the layout is vertical, reset any flexSize when changing height. + const resetFlexSize = orientation === 'vertical' ? undefined : flexSize; + onChange( { + ...childLayout, + [ heightProp ]: newHeight.selectedItem.key, + flexSize: resetFlexSize, + } ); + }; useEffect( () => { - if ( selfStretch === 'fixed' && ! flexSize ) { + if ( + ( childLayout[ heightProp ] === 'fixed' || + childLayout[ heightProp ] === 'fixedNoShrink' ) && + ! height + ) { onChange( { ...childLayout, - selfStretch: 'fit', + [ heightProp ]: undefined, + } ); + } + if ( + ( childLayout[ widthProp ] === 'fixed' || + childLayout[ widthProp ] === 'fixedNoShrink' ) && + ! width + ) { + onChange( { + ...childLayout, + [ widthProp ]: undefined, } ); } }, [] ); return ( <> - { parentLayoutType === 'flex' && ( - - { - const newFlexSize = - value !== 'fixed' ? null : flexSize; - onChange( { - selfStretch: value, - flexSize: newFlexSize, - } ); - } } - isBlock + { parentLayoutType !== 'grid' && ( + <> + - - - - - { selfStretch === 'fixed' && ( - { - onChange( { - selfStretch, - flexSize: value, - } ); - } } - value={ flexSize } - /> - ) } - + + + + + { ( childLayout[ widthProp ] === 'fixed' || + childLayout[ widthProp ] === 'fixedNoShrink' ) && ( + + { + onChange( { + ...childLayout, + width: _value, + } ); + } } + value={ width } + /> + + ) } + + + + + + + { ( childLayout[ heightProp ] === 'fixed' || + childLayout[ heightProp ] === 'fixedNoShrink' ) && ( + + { + onChange( { + ...childLayout, + height: _value, + } ); + } } + value={ height } + /> + + ) } + + ) } { parentLayoutType === 'grid' && ( <> diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 94e53eec163721..d4907b6ce0d42f 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -18,6 +18,7 @@ import { } from '@wordpress/components'; import { Icon, positionCenter, stretchWide } from '@wordpress/icons'; import { useCallback, Platform } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -29,6 +30,7 @@ import ChildLayoutControl from '../child-layout-control'; import AspectRatioTool from '../dimensions-tool/aspect-ratio-tool'; import { cleanEmptyObject } from '../../hooks/utils'; import { setImmutably } from '../../utils/object'; +import { store as blockEditorStore } from '../../store'; const AXIAL_SIDES = [ 'horizontal', 'vertical' ]; @@ -84,19 +86,35 @@ function useHasAspectRatio( settings ) { } function useHasChildLayout( settings ) { + const { themeSupportsLayout } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return { + themeSupportsLayout: getSettings().supportsLayout, + }; + }, [] ); + const { - type: parentLayoutType = 'default', - default: { type: defaultParentLayoutType = 'default' } = {}, allowSizingOnChildren = false, + type, + default: { type: defaultType = 'default' } = {}, } = settings?.parentLayout ?? {}; - - const support = - ( defaultParentLayoutType === 'flex' || - parentLayoutType === 'flex' || - defaultParentLayoutType === 'grid' || - parentLayoutType === 'grid' ) && - allowSizingOnChildren; - return !! settings?.layout && support; + const layoutType = type || defaultType; + const isFlowOrConstrained = + layoutType === 'default' || layoutType === 'constrained'; + + const support = allowSizingOnChildren; + + /* + * If the theme supports layout and parent block supports sizing on children, + * the child layout control is always shown. + * If the theme does not support layout, the child layout control is shown + * only if the parent layout is not flow or constrained. + */ + return ( + !! settings?.layout && + support && + ( themeSupportsLayout || ! isFlowOrConstrained ) + ); } function useHasSpacingPresets( settings ) { @@ -207,6 +225,7 @@ const DEFAULT_CONTROLS = { export default function DimensionsPanel( { as: Wrapper = DimensionsToolsPanel, value, + alignments = null, onChange, inheritedValue = value, settings, @@ -397,12 +416,7 @@ export default function DimensionsPanel( { const childLayout = inheritedValue?.layout; const setChildLayout = ( newChildLayout ) => { - onChange( { - ...value, - layout: { - ...newChildLayout, - }, - } ); + onChange( setImmutably( value, [ 'layout' ], newChildLayout ) ); }; const resetAllFilter = useCallback( ( previousValue ) => { @@ -414,6 +428,9 @@ export default function DimensionsPanel( { wideSize: undefined, selfStretch: undefined, flexSize: undefined, + selfAlign: undefined, + width: undefined, + height: undefined, columnStart: undefined, rowStart: undefined, columnSpan: undefined, @@ -637,11 +654,12 @@ export default function DimensionsPanel( { value={ childLayout } onChange={ setChildLayout } parentLayout={ settings?.parentLayout } - panelId={ panelId } + alignments={ alignments } isShownByDefault={ defaultControls.childLayout ?? DEFAULT_CONTROLS.childLayout } + panelId={ panelId } /> ) } { showMinHeightControl && ( diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index deb4328212b105..9d7964f91bc1ad 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -70,6 +70,7 @@ function UncontrolledInnerBlocks( props ) { renderAppender, orientation, placeholder, + align, layout, name, blockType, @@ -104,19 +105,18 @@ function UncontrolledInnerBlocks( props ) { getBlockSupport( name, '__experimentalLayout' ) || EMPTY_OBJECT; - const { allowSizingOnChildren = false } = defaultLayoutBlockSupport; - const usedLayout = layout || defaultLayoutBlockSupport; - const memoedLayout = useMemo( () => ( { - // Default layout will know about any content/wide size defined by the theme. + // Default layout contains theme.json settings such as content and wide size. ...defaultLayout, - ...usedLayout, - ...( allowSizingOnChildren && { - allowSizingOnChildren: true, - } ), + // Any layout settings added to the block. + ...layout, + // The layout settings from the block's block.json. + ...defaultLayoutBlockSupport, + // Any alignment set on the block. + ...( align && { alignWidth: align } ), } ), - [ defaultLayout, usedLayout, allowSizingOnChildren ] + [ defaultLayout, layout, defaultLayoutBlockSupport, align ] ); // For controlled inner blocks, we don't want a change in blocks to @@ -185,6 +185,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { } = options; const { clientId, + align = null, layout = null, __unstableLayoutClassNames: layoutClassNames = '', } = useBlockEditContext(); @@ -259,6 +260,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const innerBlocksProps = { __experimentalCaptureToolbars, + align, layout, name, blockType, diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index f78a39230e89bb..e79e95b2967c41 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -70,11 +70,19 @@ function DimensionsInspectorControl( { children, resetAllFilter } ) { export function DimensionsPanel( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasDimensionsPanel( settings ); - const value = useSelect( - ( select ) => - select( blockEditorStore ).getBlockAttributes( clientId )?.style, + const { value, align } = useSelect( + ( select ) => { + const blockAttributes = + select( blockEditorStore ).getBlockAttributes( clientId ); + + return { + value: blockAttributes?.style, + align: blockAttributes?.align, + }; + }, [ clientId ] ); + const [ visualizedProperty, setVisualizedProperty ] = useVisualizer(); const onChange = ( newStyle ) => { setAttributes( { @@ -99,6 +107,14 @@ export function DimensionsPanel( { clientId, name, setAttributes, settings } ) { ...defaultSpacingControls, }; + /** + * Alignments are needed for the child layout control. + */ + const supportedAlignments = getBlockSupport( name, 'align' ); + const onChangeAlignment = ( newAlign ) => { + setAttributes( { align: newAlign } ); + }; + return ( <>