Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try adding a grid layout type #49018

Merged
merged 11 commits into from
Mar 24, 2023
31 changes: 31 additions & 0 deletions lib/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,37 @@ 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.


$layout_styles[] = array(
'selector' => $selector,
'declarations' => array( 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 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
2 changes: 1 addition & 1 deletion lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@ protected function get_layout_styles( $block_metadata ) {

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)

is_array( $base_style_rules )
) {
// 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\([0-9,a-z-\s\(\)]*\)$/',
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks good to me for now, but just wondering what the fix in core will be for it? I suppose we don't need to figure that out right now, but I was curious if it'll just be a matter of adding repeat to this line in core, or if it'll be more complex.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Judging by the previous update it should be pretty straightforward.

$css_test_string
) ) {
return true;
}
return $allow_css;
}
add_filter( 'safecss_filter_attr_allow_css', 'allow_grid_functions_in_styles', 10, 2 );
22 changes: 22 additions & 0 deletions lib/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,28 @@
}
}
]
},
"grid": {
"name": "grid",
"slug": "grid",
"className": "is-layout-grid",
"displayMode": "grid",
"baseStyles": [
{
"selector": " > *",
"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 seems there are more nuances about margin/padding when set in individual blocks inside the grid(example heading with margin inside grid). Can we contain them inside the grid column?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I added that margin override due to this comment pointing out that default block margins can interfere with the grid gap. It's the same we have for flex blocks; do you think this might be more of an issue with grid than with flex layouts?

There's also a whole discussion around margins set in global styles being overridden by layout rules, that we're trying to address in #47858.

Copy link
Contributor

Choose a reason for hiding this comment

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

do you think this might be more of an issue with grid than with flex layouts?

Probably the same :). Thanks for linking the discussions/issues.

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

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

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

const RANGE_CONTROL_MAX_VALUES = {
px: 600,
'%': 100,
vw: 100,
vh: 100,
em: 38,
rem: 38,
};

export default {
name: 'grid',
label: __( 'Grid' ),
inspectorControls: function GridLayoutInspectorControls( {
layout = {},
onChange,
} ) {
return (
<GridLayoutMinimumWidthControl
layout={ layout }
onChange={ onChange }
/>
);
},
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(min(${ minimumColumnWidth }, 100%), 1fr))`
);
}

if ( rules.length ) {
// Reason to disable: the extra line breaks added by prettier mess with the unit tests.
// eslint-disable-next-line prettier/prettier
output = `${ appendSelectors( selector ) } { ${ rules.join(
'; '
) }; }`;
Comment on lines +76 to +80
Copy link
Contributor

Choose a reason for hiding this comment

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

Another option (totally feel free to ignore this!), if we don't want to disable prettier on this line could be to perform the output over two lines. So, something like:

Suggested change
// Reason to disable: the extra line breaks added by prettier mess with the unit tests.
// eslint-disable-next-line prettier/prettier
output = `${ appendSelectors( selector ) } { ${ rules.join(
'; '
) }; }`;
output += `${ appendSelectors( selector ) } { `;
output += `${ rules.join( '; ' ) }; }`;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't want to dig too deeply into it in this PR, but it feels a bit absurd to have prettier formatting multi-line strings at all, because sometimes the spacing is there for a specific reason. Something to look at later perhaps?

Copy link
Contributor

@andrewserong andrewserong Mar 23, 2023

Choose a reason for hiding this comment

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

it feels a bit absurd to have prettier formatting multi-line strings

Totally, and not a blocker at all. This was just in case you didn't want the extra disable in there. I doubt we can really configure prettier here, though, as far as I know there's not a lot of configurability.

}

// 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 'horizontal';
},
getAlignments() {
return [];
},
};

// Enables setting minimum width of grid items.
function GridLayoutMinimumWidthControl( { layout, onChange } ) {
const { minimumColumnWidth: value = '12rem' } = layout;
const [ quantity, unit ] = parseQuantityAndUnitFromRawValue( value );

const handleSliderChange = ( next ) => {
onChange( {
...layout,
minimumColumnWidth: [ next, unit ].join( '' ),
} );
};

// Mostly copied from HeightControl.
const handleUnitChange = ( newUnit ) => {
// Attempt to smooth over differences between currentUnit and newUnit.
// This should slightly improve the experience of switching between unit types.
let newValue;

if ( [ 'em', 'rem' ].includes( newUnit ) && unit === 'px' ) {
// Convert pixel value to an approximate of the new unit, assuming a root size of 16px.
newValue = ( quantity / 16 ).toFixed( 2 ) + newUnit;
} else if ( [ 'em', 'rem' ].includes( unit ) && newUnit === 'px' ) {
// Convert to pixel value assuming a root size of 16px.
newValue = Math.round( quantity * 16 ) + newUnit;
} else if (
[ 'vh', 'vw', '%' ].includes( newUnit ) &&
quantity > 100
) {
// When converting to `vh`, `vw`, or `%` units, cap the new value at 100.
newValue = 100 + newUnit;
}

onChange( {
...layout,
minimumColumnWidth: newValue,
} );
};

return (
<fieldset>
<BaseControl.VisualLabel as="legend">
{ __( 'Minimum column width' ) }
</BaseControl.VisualLabel>
<Flex gap={ 4 }>
<FlexItem isBlock>
<UnitControl
size={ '__unstable-large' }
onChange={ ( newValue ) => {
onChange( {
...layout,
minimumColumnWidth: newValue,
} );
} }
onUnitChange={ handleUnitChange }
value={ value }
min={ 0 }
/>
</FlexItem>
<FlexItem isBlock>
<RangeControl
andrewserong marked this conversation as resolved.
Show resolved Hide resolved
onChange={ handleSliderChange }
value={ quantity }
min={ 0 }
max={ RANGE_CONTROL_MAX_VALUES[ unit ] || 600 }
withInputField={ false }
/>
</FlexItem>
</Flex>
</fieldset>
);
}
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
21 changes: 21 additions & 0 deletions packages/block-editor/src/layouts/test/grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Internal dependencies
*/
import grid from '../grid';

describe( 'getLayoutStyle', () => {
it( 'should return a single `grid-template-columns` property if no non-default params are provided', () => {
const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); }`;

const result = grid.getLayoutStyle( {
selector: '.my-container',
layout: {},
style: {},
blockName: 'test-block',
hasBlockGapSupport: false,
layoutDefinitions: undefined,
} );

expect( result ).toBe( expected );
} );
} );
14 changes: 13 additions & 1 deletion packages/block-library/src/group/placeholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ const getGroupPlaceholderIcons = ( name = 'group' ) => {
<Path d="M42 0H2C.9 0 0 .9 0 2v12.5c0 .6.4 1 1 1h42c.6 0 1-.4 1-1V2c0-1.1-.9-2-2-2zm1 16.5H1c-.6 0-1 .4-1 1V30c0 1.1.9 2 2 2h40c1.1 0 2-.9 2-2V17.5c0-.6-.4-1-1-1z" />
</SVG>
),
'group-grid': (
<SVG
xmlns="http://www.w3.org/2000/svg"
width="44"
height="32"
viewBox="0 0 44 32"
>
<Path d="m20.30137,-0.00025l-18.9728,0c-0.86524,0.07234 -1.41711,0.79149 -1.41711,1.89149l0,12.64468c0,0.6 0.73401,0.96383 1.0304,0.96383l19.67469,0.03617c0.29639,0 1.0304,-0.4 1.0304,-1l-0.03576,-12.7532c0,-1.1 -0.76644,-1.78297 -1.30983,-1.78297zm0.52975,16.60851l-19.99654,-0.03617c-0.29639,0 -0.92312,0.36383 -0.92312,0.96383l-0.03576,12.68085c0,1.1 0.8022,1.81915 1.34559,1.81915l19.00857,0c0.54339,0 1.45287,-0.71915 1.45287,-1.81915l0,-12.53617c0,-0.6 -0.5552,-1.07234 -0.8516,-1.07234z" />
<Path d="m42.73056,-0.03617l-18.59217,0c-0.84788,0.07234 -1.38868,0.79149 -1.38868,1.89149l0,12.64468c0,0.6 0.71928,0.96383 1.00973,0.96383l19.27997,0.03617c0.29045,0 1.00973,-0.4 1.00973,-1l-0.03504,-12.7532c0,-1.1 -0.75106,-1.78297 -1.28355,-1.78297zm0.51912,16.60851l-19.59537,-0.03617c-0.29045,0 -0.9046,0.36383 -0.9046,0.96383l-0.03504,12.68085c0,1.1 0.78611,1.81915 1.31859,1.81915l18.62721,0c0.53249,0 1.42372,-0.71915 1.42372,-1.81915l0,-12.53617c0,-0.6 -0.54407,-1.07234 -0.83451,-1.07234z" />
</SVG>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I improvised a grid icon here but we'll probably want a proper one at some point 😄

),
};
return icons?.[ name ];
};
Expand Down Expand Up @@ -85,7 +96,8 @@ export function useShouldShowPlaceHolder( {
! fontSize &&
! textColor &&
! style &&
usedLayoutType !== 'flex'
usedLayoutType !== 'flex' &&
usedLayoutType !== 'grid'
);

useEffect( () => {
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;