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

[WIP] Style engine: add elements and states to the backend #41619

Closed
wants to merge 14 commits into from
Closed
41 changes: 16 additions & 25 deletions lib/block-supports/elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,35 +87,26 @@ function gutenberg_render_elements_support( $block_content, $block ) {
* @return null
*/
function gutenberg_render_elements_support_styles( $pre_render, $block ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$element_block_styles = isset( $block['attrs']['style']['elements'] ) ? $block['attrs']['style']['elements'] : null;

/*
* For now we only care about link color.
* This code in the future when we have a public API
* should take advantage of WP_Theme_JSON_Gutenberg::compute_style_properties
* and work for any element and style.
*/
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$element_block_styles = isset( $block['attrs']['style']['elements'] ) ? $block['attrs']['style']['elements'] : null;
$skip_link_color_serialization = gutenberg_should_skip_block_supports_serialization( $block_type, 'color', 'link' );

if ( $skip_link_color_serialization ) {
if ( empty( $element_block_styles ) || $skip_link_color_serialization ) {
Copy link
Member Author

@ramonjd ramonjd Jun 15, 2022

Choose a reason for hiding this comment

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

When we start introducing more elements and element styles, we'll have to rebuild each element with "skip serialization" checks for all their style properties.

TODO: add a comment to the code

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeh I noticed. I'd like to understand more about why link color serlization is potentially skipped. What's the deal there?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a block supports feature introduced a while back. With it, you can specify at the block.json level whether a block receives a particular style.

See:

return null;
}
$class_name = gutenberg_get_elements_class_name( $block );
$link_block_styles = isset( $element_block_styles['link'] ) ? $element_block_styles['link'] : null;

if ( $link_block_styles ) {
$styles = gutenberg_style_engine_generate(
$link_block_styles,
array(
'selector' => ".$class_name a",
'css_vars' => true,
)
);

if ( ! empty( $styles['css'] ) ) {
gutenberg_enqueue_block_support_styles( $styles['css'] );
}
$class_name = '.' . gutenberg_get_elements_class_name( $block );
$styles = gutenberg_style_engine_generate(
array(
'elements' => $element_block_styles,
),
array(
'selector' => $class_name,
'css_vars' => true,
)
);

if ( ! empty( $styles['css'] ) ) {
gutenberg_enqueue_block_support_styles( $styles['css'] );
}

return null;
Expand Down
120 changes: 106 additions & 14 deletions packages/style-engine/class-wp-style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class WP_Style_Engine {
'default' => 'background-color',
),
'path' => array( 'color', 'background' ),
'css_vars' => array(
'--wp--preset--color--$slug' => 'color',
),
'classnames' => array(
'has-background' => true,
'has-$slug-background-color' => 'color',
Expand Down Expand Up @@ -141,6 +144,13 @@ class WP_Style_Engine {
),
),
),
'elements' => array(
'link' => array(
'path' => array( 'elements', 'link' ),
'selector' => 'a',
'states' => array( ':hover' ),
),
),
'spacing' => array(
'padding' => array(
'property_keys' => array(
Expand Down Expand Up @@ -306,21 +316,21 @@ protected static function get_classnames( $style_value, $style_definition ) {
*
* @param array $style_value A single raw style value from the generate() $block_styles array.
* @param array<string> $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
* @param boolean $should_return_css_vars Whether to try to build and return CSS var values.
* @param array $options The options array passed to $this->generate().
*
* @return array An array of CSS rules.
*/
protected static function get_css( $style_value, $style_definition, $should_return_css_vars ) {
$rules = array();

protected static function get_css( $style_value, $style_definition, $options ) {
if (
isset( $style_definition['value_func'] ) &&
is_callable( $style_definition['value_func'] )
) {
return call_user_func( $style_definition['value_func'], $style_value, $style_definition );
return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $options );
}

$style_properties = $style_definition['property_keys'];
$rules = array();
$style_properties = $style_definition['property_keys'];
$should_return_css_vars = isset( $options['css_vars'] ) && true === $options['css_vars'];

// Build CSS var values from var:? values, e.g, `var(--wp--css--rule-slug )`
// Check if the value is a CSS preset and there's a corresponding css_var pattern in the style definition.
Expand Down Expand Up @@ -375,24 +385,31 @@ public function generate( $block_styles, $options ) {
return null;
}

$css_rules = array();
$classnames = array();
$should_return_css_vars = isset( $options['css_vars'] ) && true === $options['css_vars'];
$css_rules = array();
$classnames = array();

// Elements are a special case: we need to define styles on a per-element basis using the element's selector.
// And we also need to combine selectors.
if ( array_key_exists( 'elements', $block_styles ) ) {
return static::generate_elements_styles( $block_styles, $options );
}

// Collect CSS and classnames.
foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) {
if ( empty( $block_styles[ $definition_group_key ] ) ) {
foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_definitions ) {
// Do we know about this CSS top-level key?
if ( ! array_key_exists( $definition_group_key, $block_styles ) ) {
continue;
}
foreach ( $definition_group_style as $style_definition ) {

foreach ( $definition_group_definitions as $style_definition ) {
$style_value = _wp_array_get( $block_styles, $style_definition['path'], null );

if ( ! static::is_valid_style_value( $style_value ) ) {
continue;
}

$classnames = array_merge( $classnames, static::get_classnames( $style_value, $style_definition ) );
$css_rules = array_merge( $css_rules, static::get_css( $style_value, $style_definition, $should_return_css_vars ) );
$css_rules = array_merge( $css_rules, static::get_css( $style_value, $style_definition, $options ) );
}
}

Expand All @@ -405,6 +422,7 @@ public function generate( $block_styles, $options ) {
// Generate inline style rules.
foreach ( $css_rules as $rule => $value ) {
$filtered_css = esc_html( safecss_filter_attr( "{$rule}: {$value}" ) );

if ( ! empty( $filtered_css ) ) {
$css[] = $filtered_css . ';';
}
Expand Down Expand Up @@ -448,7 +466,7 @@ public function generate( $block_styles, $options ) {
protected static function get_css_individual_property_rules( $style_value, $individual_property_definition ) {
$rules = array();

if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) {
if ( ! is_array( $style_value ) || ! static::is_valid_style_value( $style_value ) || empty( $individual_property_definition['path'] ) ) {
return $rules;
}

Expand Down Expand Up @@ -483,6 +501,77 @@ protected static function get_css_individual_property_rules( $style_value, $indi
}
return $rules;
}

/**
* Returns an CSS ruleset specifically for elements and their states.
* Styles are bundled based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA['elements'].
*
* @param array $element_styles An array of elements, each of which contain styles from a block's attributes.
* @param array $options array(
* 'selector' => (string) When a selector is passed, `generate()` will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values.
* );.
*
* @return array array(
* 'css' => (string) A CSS ruleset formatted to be placed in an HTML `style` attribute or tag. Default is a string of inline styles.
* );
*/
protected static function generate_elements_styles( $element_styles, $options = array() ) {
$css_output = array();

foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA['elements'] as $element_definition ) {
$block_styles = _wp_array_get( $element_styles, $element_definition['path'], null );

if ( empty( $block_styles ) ) {
continue;
}

$element_options = array_merge(
$options,
array(
'selector' => isset( $options['selector'] ) ? "{$options['selector']} {$element_definition['selector']}" : $element_definition['selector'],
)
);

$generated_elements_styles = self::get_instance()->generate( $block_styles, $element_options );

if ( isset( $generated_elements_styles['css'] ) ) {
$css_output[] = $generated_elements_styles['css'];
}

if ( ! empty( $element_definition['states'] ) ) {
foreach ( $element_definition['states'] as $state_selector ) {
// Try to fetch "state" styles in the incoming element's style object.
$state_styles = _wp_array_get( $element_styles, array_merge( $element_definition['path'], array( $state_selector ) ), null );

if ( empty( $state_styles ) ) {
continue;
}

$element_state_selector = "{$element_definition['selector']}$state_selector";
$state_options = array_merge(
$options,
array(
'selector' => isset( $options['selector'] ) ? "{$options['selector']} $element_state_selector" : $element_state_selector,
)
);

$generated_state_styles = self::get_instance()->generate( $state_styles, $state_options );

if ( isset( $generated_state_styles['css'] ) ) {
$css_output[] = $generated_state_styles['css'];
}
}
}
}

if ( ! empty( $css_output ) ) {
return array(
'css' => implode( ' ', $css_output ),
);
}

return $css_output;
}
}

/**
Expand Down Expand Up @@ -513,3 +602,6 @@ function wp_style_engine_generate( $block_styles, $options = array() ) {
}
return null;
}



107 changes: 105 additions & 2 deletions packages/style-engine/phpunit/class-wp-style-engine-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public function data_generate_styles_fixtures() {
),
),

'elements_with_css_var_value' => array(
'with_valid_css_value_preset_style_property' => array(
'block_styles' => array(
'color' => array(
'text' => 'var:preset|color|my-little-pony',
Expand All @@ -172,7 +172,7 @@ public function data_generate_styles_fixtures() {
),
),

'elements_with_invalid_preset_style_property' => array(
'with_invalid_css_value_preset_style_property' => array(
'block_styles' => array(
'color' => array(
'text' => 'var:preset|invalid_property|my-little-pony',
Expand Down Expand Up @@ -320,6 +320,109 @@ public function data_generate_styles_fixtures() {
'css' => 'border-bottom-color: var(--wp--preset--color--terrible-lizard);',
),
),

'elements_and_element_states_default' => array(
'block_styles' => array(
'elements' => array(
'link' => array(
'color' => array(
'text' => '#fff',
'background' => '#000',
),
':hover' => array(
'color' => array(
'text' => '#000',
'background' => '#fff',
),
),
),
),
),
'options' => array(),
'expected_output' => array(
'css' => 'a { color: #fff; background-color: #000; } a:hover { color: #000; background-color: #fff; }',
),
),

'elements_and_element_states_with_selector' => array(
'block_styles' => array(
'elements' => array(
'link' => array(
'color' => array(
'text' => '#fff',
'background' => '#000',
),
':hover' => array(
'color' => array(
'text' => '#000',
'background' => '#fff',
),
),
':focus' => array(
'color' => array(
'text' => '#000',
'background' => '#fff',
),
),
),
),
),
'options' => array( 'selector' => '.la-sinistra' ),
'expected_output' => array(
'css' => '.la-sinistra a { color: #fff; background-color: #000; } .la-sinistra a:hover { color: #000; background-color: #fff; }',
),
),

'elements_and_element_states_with_css_vars' => array(
'block_styles' => array(
'elements' => array(
'link' => array(
'color' => array(
'text' => 'var:preset|color|roastbeef',
'background' => '#000',
),
':hover' => array(
'color' => array(
'text' => 'var:preset|color|pineapple',
'background' => 'var:preset|color|goldenrod',
),
),
),
),
),
'options' => array(
'selector' => '.der-beste-link',
'css_vars' => true,
),
'expected_output' => array(
'css' => '.der-beste-link a { color: var(--wp--preset--color--roastbeef); background-color: #000; } .der-beste-link a:hover { color: var(--wp--preset--color--pineapple); background-color: var(--wp--preset--color--goldenrod); }',
),
),

'elements_and_unsupported_element_states' => array(
'block_styles' => array(
'elements' => array(
'link' => array(
'color' => array(
'text' => '#fff',
'background' => '#000',
),
// Not a supported pseudo selector (yet!).
':focus' => array(
'color' => array(
'text' => '#000',
'background' => '#fff',
),
),
),
),
),
'options' => array(),
'expected_output' => array(
// Should not contain the `:focus` rule.
'css' => 'a { color: #fff; background-color: #000; }',
),
),
);
}
}