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

Global styles: output :root selector for CSS custom properties #42084

Merged
merged 5 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/class-wp-duotone-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ private static function get_svg_definitions( $sources ) {
* @return string The CSS for global styles.
*/
private static function get_global_styles_presets( $sources ) {
$css = 'body{';
$css = WP_Theme_JSON_Gutenberg::ROOT_CSS_PROPERTIES_SELECTOR . '{';
foreach ( $sources as $filter_id => $filter_data ) {
$slug = $filter_data['slug'];
$colors = $filter_data['colors'];
Expand Down
39 changes: 23 additions & 16 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ class WP_Theme_JSON_Gutenberg {
*/
protected static $blocks_metadata = array();

/**
* The CSS selector for the top-level preset settings.
*
* @since 6.6.0
* @var string
*/
const ROOT_CSS_PROPERTIES_SELECTOR = ':root';

/**
* The CSS selector for the top-level styles.
*
Expand Down Expand Up @@ -1751,7 +1759,7 @@ static function ( $carry, $element ) {
* @return string The result of processing the presets.
*/
protected static function compute_preset_classes( $settings, $selector, $origins ) {
if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) {
// Classes at the global level do not need any CSS prefixed,
// and we don't want to increase its specificity.
$selector = '';
Expand Down Expand Up @@ -2276,7 +2284,7 @@ protected static function get_setting_nodes( $theme_json, $selectors = array() )
// Top-level.
$nodes[] = array(
'path' => array( 'settings' ),
'selector' => static::ROOT_BLOCK_SELECTOR,
'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
);

// Calculate paths for blocks.
Expand Down Expand Up @@ -2673,6 +2681,7 @@ static function ( $pseudo_selector ) use ( $selector ) {
* Outputs the CSS for layout rules on the root.
*
* @since 6.1.0
* @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties.
*
* @param string $selector The root node selector.
* @param array $block_metadata The metadata for the root block.
Expand All @@ -2683,16 +2692,6 @@ public function get_root_layout_rules( $selector, $block_metadata ) {
$settings = $this->theme_json['settings'] ?? array();
$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];

/*
* Reset default browser margin on the root body element.
* This is set on the root selector **before** generating the ruleset
* from the `theme.json`. This is to ensure that if the `theme.json` declares
* `margin` in its `spacing` declaration for the `body` element then these
* user-generated values take precedence in the CSS cascade.
* @link https://github.com/WordPress/gutenberg/issues/36147.
*/
$css .= 'body { margin: 0;';

/*
* If there are content and wide widths in theme.json, output them
* as custom properties on the body element so all blocks can use them.
Expand All @@ -2702,11 +2701,19 @@ public function get_root_layout_rules( $selector, $block_metadata ) {
$content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
$wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
$wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
$css .= '--wp--style--global--content-size: ' . $content_size . ';';
$css .= '--wp--style--global--wide-size: ' . $wide_size . ';';
$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
Copy link
Member Author

@ramonjd ramonjd Mar 19, 2024

Choose a reason for hiding this comment

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

One thing I've been playing with locally is moving the $use_root_padding logic from compute_style_properties to get_root_layout_rules.

This entails:

  • deleting the --wp--root--padding-* entries from PROPERTIES_METADATA
  • in compute_style_properties, skipping padding properties for the root block if the theme has opted into using the root padding, and removing all other padding related logic
  • building the rules in get_root_layout_rules.

I haven't even considered backwards compat yet 😄

Here's a diff to illustrate what I mean:
diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php
index e3c27ac82c..48b2493fe2 100644
--- a/lib/class-wp-theme-json-gutenberg.php
+++ b/lib/class-wp-theme-json-gutenberg.php
@@ -266,11 +266,6 @@ class WP_Theme_JSON_Gutenberg {
 		'padding-right'                     => array( 'spacing', 'padding', 'right' ),
 		'padding-bottom'                    => array( 'spacing', 'padding', 'bottom' ),
 		'padding-left'                      => array( 'spacing', 'padding', 'left' ),
-		'--wp--style--root--padding'        => array( 'spacing', 'padding' ),
-		'--wp--style--root--padding-top'    => array( 'spacing', 'padding', 'top' ),
-		'--wp--style--root--padding-right'  => array( 'spacing', 'padding', 'right' ),
-		'--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
-		'--wp--style--root--padding-left'   => array( 'spacing', 'padding', 'left' ),
 		'text-decoration'                   => array( 'typography', 'textDecoration' ),
 		'text-transform'                    => array( 'typography', 'textTransform' ),
 		'filter'                            => array( 'filter', 'duotone' ),
@@ -2092,6 +2087,7 @@ class WP_Theme_JSON_Gutenberg {
 	 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
 	 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
 	 * @since 6.5.0 Passing current theme JSON settings to wp_get_typography_font_size_value().
+	 * @since 6.6.0 Root padding CSS custom properties are processed in get_root_layout_rules().
 	 *
 	 * @param array   $styles Styles to process.
 	 * @param array   $settings Theme settings.
@@ -2111,28 +2107,18 @@ class WP_Theme_JSON_Gutenberg {
 			return $declarations;
 		}
 
-		$root_variable_duplicates = array();
-
 		foreach ( $properties as $css_property => $value_path ) {
 			$value = static::get_property_value( $styles, $value_path, $theme_json );
 
-			if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
-				continue;
-			}
-			// Root-level padding styles don't currently support strings with CSS shorthand values.
-			// This may change: https://github.com/WordPress/gutenberg/issues/40132.
-			if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
-				continue;
-			}
-
-			if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
-				$root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
-			}
-
-			// Look up protected properties, keyed by value path.
-			// Skip protected properties that are explicitly set to `null`.
 			if ( is_array( $value_path ) ) {
 				$path_string = implode( '.', $value_path );
+				// Skip padding properties for the root block if the theme has opted into using the root padding.
+				// The CSS custom properties for root padding are processed in get_root_layout_rules() and use static::ROOT_CSS_PROPERTIES_SELECTOR selector.
+				if ( static::ROOT_BLOCK_SELECTOR === $selector && $use_root_padding && str_starts_with( $path_string, 'spacing.padding.' ) ) {
+					continue;
+				}
+				// Look up protected properties, keyed by value path.
+				// Skip protected properties that are explicitly set to `null`.
 				if (
 					isset( static::PROTECTED_PROPERTIES[ $path_string ] ) &&
 					_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
@@ -2181,14 +2167,6 @@ class WP_Theme_JSON_Gutenberg {
 			);
 		}
 
-		// If a variable value is added to the root, the corresponding property should be removed.
-		foreach ( $root_variable_duplicates as $duplicate ) {
-			$discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
-			if ( is_numeric( $discard ) ) {
-				array_splice( $declarations, $discard, 1 );
-			}
-		}
-
 		return $declarations;
 	}
 
@@ -2688,9 +2666,10 @@ class WP_Theme_JSON_Gutenberg {
 	 * @return string The additional root rules CSS.
 	 */
 	public function get_root_layout_rules( $selector, $block_metadata ) {
-		$css              = '';
-		$settings         = $this->theme_json['settings'] ?? array();
-		$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
+		$css               = '';
+		$settings          = $this->theme_json['settings'] ?? array();
+		$use_root_padding  = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
+		$root_declarations = array();
 
 		/*
 		* If there are content and wide widths in theme.json, output them
@@ -2701,8 +2680,37 @@ class WP_Theme_JSON_Gutenberg {
 			$content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
 			$wide_size    = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
 			$wide_size    = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
-			$css         .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
-			$css         .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
+			$root_declarations[] = array(
+				'name'  => '--wp--style--global--content-size',
+				'value' => $content_size,
+			);
+			$root_declarations[] = array(
+				'name'  => '--wp--style--global--wide-size',
+				'value' => $wide_size,
+			);
+		}
+
+		if ( $use_root_padding ) {
+			$root_padding = array(
+				'--wp--style--root--padding-top'    => static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'padding', 'top' ) ),
+				'--wp--style--root--padding-right'  => static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'padding', 'right' ) ),
+				'--wp--style--root--padding-bottom' => static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'padding', 'bottom' ) ),
+				'--wp--style--root--padding-left'   => static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'padding', 'left' ) ),
+			);
+
+			$root_padding = array_filter( $root_padding, 'strlen' );
+			if ( ! empty( $root_padding ) ) {
+				foreach ( $root_padding as $property => $value ) {
+					$root_declarations[] = array(
+						'name'  => $property,
+						'value' => $value,
+					);
+				}
+			}
+		}
+
+		if ( ! empty( $root_declarations ) ) {
+			$css .= static::to_ruleset( static::ROOT_CSS_PROPERTIES_SELECTOR, $root_declarations );
 		}
 
 		/*

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we should do the above, but maybe in another PR, especially if we have to test for backwards compat.

Copy link
Contributor

Choose a reason for hiding this comment

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

Follow-up PR sounds like a good idea 👍

$css .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
}

$css .= ' }';
/*
* Reset default browser margin on the body element.
* This is set on the body selector **before** generating the ruleset
* from the `theme.json`. This is to ensure that if the `theme.json` declares
* `margin` in its `spacing` declaration for the `body` element then these
* user-generated values take precedence in the CSS cascade.
* @link https://github.com/WordPress/gutenberg/issues/36147.
*/
$css .= 'body { margin: 0; }';

if ( $use_root_padding ) {
// Top and bottom padding are applied to the outer block container.
Expand Down Expand Up @@ -2738,7 +2745,7 @@ public function get_root_layout_rules( $selector, $block_metadata ) {
$css .= ':where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }';

// For backwards compatibility, ensure the legacy block gap CSS variable is still available.
$css .= "$selector { --wp--style--block-gap: $block_gap_value; }";
$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
}
$css .= $this->get_layout_styles( $block_metadata );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
getStylesDeclarations,
processCSSNesting,
} from '../use-global-styles-output';
import { ROOT_BLOCK_SELECTOR } from '../utils';
import { ROOT_BLOCK_SELECTOR, ROOT_CSS_PROPERTIES_SELECTOR } from '../utils';

describe( 'global styles renderer', () => {
describe( 'getNodesWithStyles', () => {
Expand Down Expand Up @@ -254,7 +254,7 @@ describe( 'global styles renderer', () => {
],
},
},
selector: ROOT_BLOCK_SELECTOR,
selector: ROOT_CSS_PROPERTIES_SELECTOR,
},
{
presets: {
Expand Down Expand Up @@ -342,7 +342,7 @@ describe( 'global styles renderer', () => {
};

expect( toCustomProperties( tree, blockSelectors ) ).toEqual(
'body{--wp--preset--color--white: white;--wp--preset--color--black: black;--wp--preset--color--white-2-black: value;--wp--custom--white-2-black: value;--wp--custom--font-primary: value;--wp--custom--line-height--body: 1.7;--wp--custom--line-height--heading: 1.3;}h1,h2,h3,h4,h5,h6{--wp--preset--font-size--small: 12px;--wp--preset--font-size--medium: 23px;}'
':root{--wp--preset--color--white: white;--wp--preset--color--black: black;--wp--preset--color--white-2-black: value;--wp--custom--white-2-black: value;--wp--custom--font-primary: value;--wp--custom--line-height--body: 1.7;--wp--custom--line-height--heading: 1.3;}h1,h2,h3,h4,h5,h6{--wp--preset--font-size--small: 12px;--wp--preset--font-size--medium: 23px;}'
);
} );
} );
Expand Down Expand Up @@ -614,7 +614,7 @@ describe( 'global styles renderer', () => {
},
};
expect( toStyles( Object.freeze( tree ), 'body' ) ).toEqual(
'body {margin: 0; --wp--style--global--content-size: 840px; --wp--style--global--wide-size: 1100px;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'
':root { --wp--style--global--content-size: 840px; --wp--style--global--wide-size: 1100px;}body {margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'
);
} );
} );
Expand Down Expand Up @@ -734,7 +734,7 @@ describe( 'global styles renderer', () => {
} );

expect( layoutStyles ).toEqual(
':where(body .is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:where(body .is-layout-flow) > * + * { margin-block-start: 0.5em; margin-block-end: 0; }:where(body .is-layout-flex) { gap: 0.5em; }body { --wp--style--block-gap: 0.5em; }body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }'
':where(body .is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:where(body .is-layout-flow) > * + * { margin-block-start: 0.5em; margin-block-end: 0; }:where(body .is-layout-flex) { gap: 0.5em; }:root { --wp--style--block-gap: 0.5em; }body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }'
);
} );

Expand All @@ -751,7 +751,7 @@ describe( 'global styles renderer', () => {
} );

expect( layoutStyles ).toEqual(
':where(body .is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:where(body .is-layout-flow) > * + * { margin-block-start: 12px; margin-block-end: 0; }:where(body .is-layout-flex) { gap: 12px; }body { --wp--style--block-gap: 12px; }body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }'
':where(body .is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:where(body .is-layout-flow) > * + * { margin-block-start: 12px; margin-block-end: 0; }:where(body .is-layout-flex) { gap: 12px; }:root { --wp--style--block-gap: 12px; }body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }'
);
} );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components';
import {
PRESET_METADATA,
ROOT_BLOCK_SELECTOR,
ROOT_CSS_PROPERTIES_SELECTOR,
scopeSelector,
appendToSelector,
getBlockStyleVariationSelector,
Expand Down Expand Up @@ -543,7 +544,7 @@ export function getLayoutStyles( {
);
// For backwards compatibility, ensure the legacy block gap CSS variable is still available.
if ( selector === ROOT_BLOCK_SELECTOR && hasBlockGapSupport ) {
ruleset += `${ selector } { --wp--style--block-gap: ${ gapValue }; }`;
ruleset += `${ ROOT_CSS_PROPERTIES_SELECTOR } { --wp--style--block-gap: ${ gapValue }; }`;
}
}

Expand Down Expand Up @@ -729,7 +730,7 @@ export const getNodesWithSettings = ( tree, blockSelectors ) => {
nodes.push( {
presets,
custom,
selector: ROOT_BLOCK_SELECTOR,
selector: ROOT_CSS_PROPERTIES_SELECTOR,
} );
}

Expand Down Expand Up @@ -781,24 +782,28 @@ export const toStyles = (
const nodesWithSettings = getNodesWithSettings( tree, blockSelectors );
const useRootPaddingAlign = tree?.settings?.useRootPaddingAwareAlignments;
const { contentSize, wideSize } = tree?.settings?.layout || {};
let ruleset = '';

if ( contentSize || wideSize ) {
ruleset += ':root {';
ramonjd marked this conversation as resolved.
Show resolved Hide resolved
ruleset = contentSize
? ruleset + ` --wp--style--global--content-size: ${ contentSize };`
: ruleset;
ruleset = wideSize
? ruleset + ` --wp--style--global--wide-size: ${ wideSize };`
: ruleset;
ruleset += '}';
}

/*
* Reset default browser margin on the root body element.
* This is set on the root selector **before** generating the ruleset
* Reset default browser margin on the body element.
* This is set on the body selector **before** generating the ruleset
* from the `theme.json`. This is to ensure that if the `theme.json` declares
* `margin` in its `spacing` declaration for the `body` element then these
* user-generated values take precedence in the CSS cascade.
* @link https://github.com/WordPress/gutenberg/issues/36147.
*/
let ruleset = 'body {margin: 0;';

if ( contentSize ) {
ruleset += ` --wp--style--global--content-size: ${ contentSize };`;
}

if ( wideSize ) {
ruleset += ` --wp--style--global--wide-size: ${ wideSize };`;
}
ruleset += 'body {margin: 0;';

// Root padding styles should only be output for full templates, not patterns or template parts.
if ( useRootPaddingAlign && isTemplate ) {
Expand Down Expand Up @@ -1000,7 +1005,10 @@ export const toStyles = (
}

nodesWithSettings.forEach( ( { selector, presets } ) => {
if ( ROOT_BLOCK_SELECTOR === selector ) {
if (
ROOT_BLOCK_SELECTOR === selector ||
ROOT_CSS_PROPERTIES_SELECTOR === selector
) {
// Do not add extra specificity for top-level classes.
selector = '';
}
Expand Down Expand Up @@ -1211,6 +1219,7 @@ export function useGlobalStylesOutputWithConfig( mergedConfig = {} ) {
updatedConfig,
blockSelectors
);

const globalStyles = toStyles(
updatedConfig,
blockSelectors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getValueFromObjectPath } from '../../utils/object';

/* Supporting data. */
export const ROOT_BLOCK_SELECTOR = 'body';
export const ROOT_CSS_PROPERTIES_SELECTOR = ':root';

export const PRESET_METADATA = [
{
Expand Down
Loading
Loading