Skip to content
Next Next commit
Try adding a grid layout type.
  • Loading branch information
tellthemachines committed Mar 22, 2023
commit 7c3ec88de3bfee89022ba48eab7afeaa71b65c34
32 changes: 32 additions & 0 deletions lib/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,38 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
);
}
}
} elseif ( 'grid' === $layout_type ) {
$minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a future proofing question: if minimumColumnWidth isn't set, this assumes a default of 12rem. If we want to enable the control over individual columns again in follow-ups, how will we tell that we shouldn't be outputting 12rem?

By which I mean, what are the trade-offs between using 12rem as an inferred value, versus explicitly defining 12rem within the Grid variation? For example, what if other blocks wished to use a different default minimum column width?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be pretty easy to introduce something like isResponsive again, or any other property that indicates that the grid type is not based on column width (which would always be the default). We'll need a check before outputting the value because the whole template-columns declaration will be different for a static grid, and even if a minimumColumnWidth is set (e.g. if we don't reset it on switching to another grid type) we will only use the value if the grid is of the responsive type (or whatever we want to call it).

I chose 12rem arbitrarily, it could be any other value but we do need to have some default value here or the auto-fill won't work.

If other blocks want to use a different value they can set it when they define the layout type, e.g. layout: { type: 'grid', minimumColumnWidth: '9rem' }.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So (just to make sure I'm following), we're essentially saying, a grid layout with absolutely no other values will default to 12rem minimum column width. For future changes, we're adding additional attributes that will be set, so it'll be easy to determine when to switch off the current logic. And for changing the 12rem default on a block-by-block basis, we provide the default type type value as you mention.

Sounds solid to me! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the idea is to have a sensible default so you can just add a grid and it'll look ok for the majority of cases? Then progressively add configurability.


// Workaround because of course core wouldn't support doing anything actually useful with grid.
$layout_styles[] = array(
'selector' => $selector,
'declarations' => array( 'grid-template-columns' => 'repeat(auto-fill, minmax(' . $minimum_column_width . ', 1fr))' ),
);

if ( $has_block_gap_support && isset( $gap_value ) ) {
$combined_gap_value = '';
$gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );

foreach ( $gap_sides as $gap_side ) {
$process_value = is_string( $gap_value ) ? $gap_value : _wp_array_get( $gap_value, array( $gap_side ), $fallback_gap_value );
// Get spacing CSS variable from preset value if provided.
if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
$index_to_splice = strrpos( $process_value, '|' ) + 1;
$slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) );
$process_value = "var(--wp--preset--spacing--$slug)";
}
$combined_gap_value .= "$process_value ";
}
$gap_value = trim( $combined_gap_value );

if ( null !== $gap_value && ! $should_skip_gap_serialization ) {
$layout_styles[] = array(
'selector' => $selector,
'declarations' => array( 'gap' => $gap_value ),
);
}
}
}

if ( ! empty( $layout_styles ) ) {
Expand Down
3 changes: 1 addition & 2 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -1341,8 +1341,7 @@ protected function get_layout_styles( $block_metadata ) {
$base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() );

if (
! empty( $class_name ) &&
! empty( $base_style_rules )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this change is to allow the block of code to be reachable if $base_style_rules is empty? Tiny nit: would it be worth retaining a check, but instead have it check is_array( $base_style_rules ) since the code below will attempt to iterate over $base_style_rules? (This is just to make sure that anything setting null or false won't throw an error)

! empty( $class_name )
) {
// Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
if (
Expand Down
19 changes: 19 additions & 0 deletions lib/experimental/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,22 @@ function allow_filter_in_styles( $allow_css, $css_test_string ) {
}

add_filter( 'safecss_filter_attr_allow_css', 'allow_filter_in_styles', 10, 2 );

/**
* Mark CSS safe if it contains grid functions
*
* This function should not be backported to core.
*
* @param bool $allow_css Whether the CSS is allowed.
* @param string $css_test_string The CSS to test.
*/
function allow_grid_functions_in_styles( $allow_css, $css_test_string ) {
if ( preg_match(
'/^grid-template-columns:\s*repeat\(auto-fill,[0-9,a-z\s\(\)]*\)$/',
$css_test_string
) ) {
return true;
}
return $allow_css;
}
add_filter( 'safecss_filter_attr_allow_css', 'allow_grid_functions_in_styles', 10, 2 );
15 changes: 15 additions & 0 deletions lib/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,21 @@
}
}
]
},
"grid": {
"name": "grid",
"slug": "grid",
"className": "is-layout-grid",
"displayMode": "grid",
"baseStyles": [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we might need to include the margin: 0 rule from baseStyles used by the flex layout, too. Otherwise the user agent stylesheet margin rules for e.g. p tags apply, affecting the gap:

image

"spacingStyles": [
{
"selector": "",
"rules": {
"gap": null
}
}
]
}
}
},
Expand Down
110 changes: 110 additions & 0 deletions packages/block-editor/src/layouts/grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

import {
Flex,
FlexItem,
__experimentalUnitControl as UnitControl,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import { appendSelectors, getBlockGapCSS } from './utils';
import { getGapCSSValue } from '../hooks/gap';
import { shouldSkipSerialization } from '../hooks/utils';

export default {
name: 'grid',
label: __( 'Grid' ),
inspectorControls: function GridLayoutInspectorControls( {
layout = {},
onChange,
} ) {
return (
<>
<Flex>
<FlexItem>
<GridLayoutMinimumWidthControl
layout={ layout }
onChange={ onChange }
/>
</FlexItem>
</Flex>
</>
);
},
toolBarControls: function DefaultLayoutToolbarControls() {
return null;
},
getLayoutStyle: function getLayoutStyle( {
selector,
layout,
style,
blockName,
hasBlockGapSupport,
layoutDefinitions,
} ) {
const { minimumColumnWidth = '12rem' } = layout;

// If a block's block.json skips serialization for spacing or spacing.blockGap,
// don't apply the user-defined value to the styles.
const blockGapValue =
style?.spacing?.blockGap &&
! shouldSkipSerialization( blockName, 'spacing', 'blockGap' )
? getGapCSSValue( style?.spacing?.blockGap, '0.5em' )
: undefined;

let output = '';
const rules = [];

if ( minimumColumnWidth ) {
rules.push(
`grid-template-columns: repeat(auto-fill, minmax(${ minimumColumnWidth }, 1fr))`
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just playing around with this again, and I noticed that if we set a minimum column width that is greater than the viewport width, that the children will overflow the container:

image

Is it worth trying to add a guardrail to the rule so that the minimum value can never be more than 100%? Would it be possible to nest min() within minmax()? So possibly something like the following:

grid-template-columns: repeat(auto-fill, minmax(min(${ minimumColumnWidth },100%), 1fr))

That seems to work for me locally:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh good catch! I'll have a play with it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worked, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay, glad it was a simple fix!

}

if ( rules.length ) {
output = `${ appendSelectors( selector ) } {
${ rules.join( '; ' ) };
}`;
}

// Output blockGap styles based on rules contained in layout definitions in theme.json.
if ( hasBlockGapSupport && blockGapValue ) {
output += getBlockGapCSS(
selector,
layoutDefinitions,
'grid',
blockGapValue
);
}
return output;
},
getOrientation() {
return null;
},
getAlignments() {
return [];
},
};

// Enables setting minimum width of grid items.
function GridLayoutMinimumWidthControl( { layout, onChange } ) {
const { minimumColumnWidth = '20rem' } = layout;

return (
<UnitControl
size={ '__unstable-large' }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a label to this component? E.g. label={ __( 'Minimum column width ' ) }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a great idea!

onChange={ ( value ) => {
onChange( {
...layout,
minimumColumnWidth: value,
} );
} }
value={ minimumColumnWidth }
/>
);
}
3 changes: 2 additions & 1 deletion packages/block-editor/src/layouts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import flex from './flex';
import flow from './flow';
import constrained from './constrained';
import grid from './grid';

const layoutTypes = [ flow, flex, constrained ];
const layoutTypes = [ flow, flex, constrained, grid ];

/**
* Retrieves a layout type by name.
Expand Down
12 changes: 11 additions & 1 deletion packages/block-library/src/group/variations.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
import { group, row, stack } from '@wordpress/icons';
import { group, row, stack, grid } from '@wordpress/icons';

const variations = [
{
Expand Down Expand Up @@ -42,6 +42,16 @@ const variations = [
blockAttributes.layout?.orientation === 'vertical',
icon: stack,
},
{
name: 'group-grid',
title: __( 'Grid' ),
description: __( 'Arrange blocks in a grid.' ),
attributes: { layout: { type: 'grid' } },
scope: [ 'block', 'inserter', 'transform' ],
isActive: ( blockAttributes ) =>
blockAttributes.layout?.type === 'grid',
icon: grid,
},
];

export default variations;