Skip to content

Commit

Permalink
Add Layout controls to children of Flex layout blocks (#45364)
Browse files Browse the repository at this point in the history
* Add Layout controls to children of Flex layout blocks

* Child layout styles on the editor

* Try to get it working on server

* Fix style generation

* Fix classname logic

* Add hug option and change names to accommodate vertical orientation

* Don't trash existing styles

* Merge layout panels

* Move controls to dimensions panel

* Use VStack for margins.

* Change "hug" label and add help text.

* Wipe fixed width when other setting is chosen.

* Update doc comment.

* Try parent layout logic in native BlockEdit

* Add sizing controls to transformed Rows and Stacks

* Failsafe if parent layout doesn't exist

* Fixed controls not rendering for certain block types

* Actual 40px size

* Make opt-in a default property of layout support

* Fix error when block doesn't exist

* Update lib/block-supports/layout.php

string content ftw

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
  • Loading branch information
tellthemachines and andrewserong committed Nov 25, 2022
1 parent 0b9bf24 commit 220e945
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 18 deletions.
73 changes: 68 additions & 5 deletions lib/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -428,21 +476,36 @@ 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 ) {
$content->add_class( $class_name );
}
} else {
$content->next_tag();
$content->add_class( implode( ' ', $class_names ) );
foreach ( $class_names as $class_name ) {
$content->add_class( $class_name );
}
}

return (string) $content;
Expand Down
5 changes: 4 additions & 1 deletion packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand Down Expand Up @@ -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
Expand All @@ -148,6 +150,7 @@ function BlockListBlock( {
isSelectionEnabled={ isSelectionEnabled }
toggleSelection={ toggleSelection }
__unstableLayoutClassNames={ layoutClassNames }
__unstableParentLayout={ parentLayout }
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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( {
Expand Down Expand Up @@ -81,6 +82,8 @@ function BlockForType( {
),
] );

const parentLayout = useLayout();

return (
<GlobalStylesContext.Provider value={ mergedStyle }>
<BlockEdit
Expand All @@ -102,6 +105,7 @@ function BlockForType( {
onDeleteBlock={ onDeleteBlock }
blockWidth={ blockWidth }
parentBlockAlignment={ parentBlockAlignment }
__unstableParentLayout={ parentLayout }
/>
<View onLayout={ getBlockWidth } />
</GlobalStylesContext.Provider>
Expand Down
20 changes: 15 additions & 5 deletions packages/block-editor/src/components/inner-blocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand All @@ -103,7 +110,10 @@ function UncontrolledInnerBlocks( props ) {
rootClientId={ clientId }
renderAppender={ renderAppender }
__experimentalAppenderTagName={ __experimentalAppenderTagName }
__experimentalLayout={ __experimentalLayout }
__experimentalLayout={ {
...__experimentalLayout,
allowSizingOnChildren,
} }
wrapperRef={ wrapperRef }
placeholder={ placeholder }
/>
Expand Down
172 changes: 172 additions & 0 deletions packages/block-editor/src/hooks/child-layout.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<ToggleGroupControl
size={ '__unstable-large' }
label={ childLayoutOrientation( parentLayout ) }
value={ selfStretch || 'fit' }
help={ helpText( selfStretch ) }
onChange={ ( value ) => {
const newFlexSize = value !== 'fixed' ? null : flexSize;
setAttributes( {
style: {
...style,
layout: {
...childLayout,
selfStretch: value,
flexSize: newFlexSize,
},
},
} );
} }
isBlock={ true }
>
<ToggleGroupControlOption
key={ 'fit' }
value={ 'fit' }
label={ __( 'Fit' ) }
/>
<ToggleGroupControlOption
key={ 'fill' }
value={ 'fill' }
label={ __( 'Fill' ) }
/>
<ToggleGroupControlOption
key={ 'fixed' }
value={ 'fixed' }
label={ __( 'Fixed' ) }
/>
</ToggleGroupControl>
{ selfStretch === 'fixed' && (
<UnitControl
size={ '__unstable-large' }
style={ { height: 'auto' } }
onChange={ ( value ) => {
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' );
}
Loading

0 comments on commit 220e945

Please sign in to comment.