diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php
index b1ba529baae75c..fa63beef0710b3 100644
--- a/lib/block-supports/layout.php
+++ b/lib/block-supports/layout.php
@@ -64,16 +64,14 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
$style .= "$selector .alignfull { max-width: none; }";
}
- $style .= "$selector > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }";
- $style .= "$selector > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }";
- $style .= "$selector > .aligncenter { margin-left: auto !important; margin-right: auto !important; }";
if ( $has_block_gap_support ) {
if ( is_array( $gap_value ) ) {
$gap_value = isset( $gap_value['top'] ) ? $gap_value['top'] : null;
}
- $gap_style = $gap_value && ! $should_skip_gap_serialization ? $gap_value : 'var( --wp--style--block-gap )';
- $style .= "$selector > * { margin-block-start: 0; margin-block-end: 0; }";
- $style .= "$selector > * + * { margin-block-start: $gap_style; margin-block-end: 0; }";
+ if ( $gap_value && ! $should_skip_gap_serialization ) {
+ $style .= "$selector > * { margin-block-start: 0; margin-block-end: 0; }";
+ $style .= "$selector > * + * { margin-block-start: $gap_value; margin-block-end: 0; }";
+ }
}
} elseif ( 'flex' === $layout_type ) {
$layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal';
@@ -94,26 +92,23 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
$justify_content_options += array( 'space-between' => 'space-between' );
}
- $flex_wrap_options = array( 'wrap', 'nowrap' );
- $flex_wrap = ! empty( $layout['flexWrap'] ) && in_array( $layout['flexWrap'], $flex_wrap_options, true ) ?
- $layout['flexWrap'] :
- 'wrap';
+ if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) {
+ $style .= "$selector { flex-wrap: nowrap; }";
+ }
- $style = "$selector {";
- $style .= 'display: flex;';
if ( $has_block_gap_support ) {
if ( is_array( $gap_value ) ) {
$gap_row = isset( $gap_value['top'] ) ? $gap_value['top'] : $fallback_gap_value;
$gap_column = isset( $gap_value['left'] ) ? $gap_value['left'] : $fallback_gap_value;
$gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
}
- $gap_style = $gap_value && ! $should_skip_gap_serialization ? $gap_value : "var( --wp--style--block-gap, $fallback_gap_value )";
- $style .= "gap: $gap_style;";
- } else {
- $style .= "gap: $fallback_gap_value;";
+ if ( $gap_value && ! $should_skip_gap_serialization ) {
+ $style .= "$selector {";
+ $style .= "gap: $gap_value;";
+ $style .= '}';
+ }
}
- $style .= "flex-wrap: $flex_wrap;";
if ( 'horizontal' === $layout_orientation ) {
/**
* Add this style only if is not empty for backwards compatibility,
@@ -121,25 +116,26 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
* by custom css.
*/
if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
+ $style .= "$selector {";
$style .= "justify-content: {$justify_content_options[ $layout['justifyContent'] ]};";
+ $style .= '}';
}
if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
+ $style .= "$selector {";
$style .= "align-items: {$vertical_alignment_options[ $layout['verticalAlignment'] ]};";
- } else {
- $style .= 'align-items: center;';
+ $style .= '}';
}
} else {
+ $style .= "$selector {";
$style .= 'flex-direction: column;';
if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
$style .= "align-items: {$justify_content_options[ $layout['justifyContent'] ]};";
} else {
$style .= 'align-items: flex-start;';
}
+ $style .= '}';
}
- $style .= '}';
-
- $style .= "$selector > * { margin: 0; }";
}
return $style;
@@ -160,21 +156,23 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
return $block_content;
}
- $block_gap = gutenberg_get_global_settings( array( 'spacing', 'blockGap' ) );
- $default_layout = gutenberg_get_global_settings( array( 'layout' ) );
- $has_block_gap_support = isset( $block_gap ) ? null !== $block_gap : false;
- $default_block_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() );
- $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout;
+ $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;
+ $default_block_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() );
+ $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout;
if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] ) {
- if ( ! $default_layout ) {
+ if ( ! $global_layout_settings ) {
return $block_content;
}
- $used_layout = $default_layout;
+ $used_layout = $global_layout_settings;
}
- $class_names = array();
- $container_class = wp_unique_id( 'wp-container-' );
- $class_names[] = $container_class;
+ $class_names = array();
+ $layout_definitions = _wp_array_get( $global_layout_settings, array( 'definitions' ), array() );
+ $block_classname = wp_get_block_default_classname( $block['blockName'] );
+ $container_class = wp_unique_id( 'wp-container-' );
+ $layout_classname = '';
// The following section was added to reintroduce a small set of layout classnames that were
// removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
@@ -192,6 +190,17 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
$class_names[] = 'is-nowrap';
}
+ // Get classname for layout type.
+ if ( isset( $used_layout['type'] ) ) {
+ $layout_classname = _wp_array_get( $layout_definitions, array( $used_layout['type'], 'className' ), '' );
+ } else {
+ $layout_classname = _wp_array_get( $layout_definitions, array( 'default', 'className' ), '' );
+ }
+
+ if ( $layout_classname && is_string( $layout_classname ) ) {
+ $class_names[] = sanitize_title( $layout_classname );
+ }
+
$gap_value = _wp_array_get( $block, array( 'attrs', 'style', 'spacing', 'blockGap' ) );
// Skip if gap value contains unsupported characters.
// Regex for CSS value borrowed from `safecss_filter_attr`, and used here
@@ -209,7 +218,14 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
// If a block's block.json skips serialization for spacing or spacing.blockGap,
// don't apply the user-defined value to the styles.
$should_skip_gap_serialization = gutenberg_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
- $style = gutenberg_get_layout_style( ".$container_class", $used_layout, $has_block_gap_support, $gap_value, $should_skip_gap_serialization, $fallback_gap_value );
+ $style = gutenberg_get_layout_style( ".$block_classname.$container_class", $used_layout, $has_block_gap_support, $gap_value, $should_skip_gap_serialization, $fallback_gap_value );
+
+ // Only add container class and enqueue block support styles if unique styles were generated.
+ if ( ! empty( $style ) ) {
+ $class_names[] = $container_class;
+ wp_enqueue_block_support_styles( $style );
+ }
+
// This assumes the hook only applies to blocks with a single wrapper.
// I think this is a reasonable limitation for that particular hook.
$content = preg_replace(
@@ -219,8 +235,6 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
1
);
- wp_enqueue_block_support_styles( $style );
-
return $content;
}
diff --git a/lib/compat/wordpress-6.0/get-global-styles-and-settings.php b/lib/compat/wordpress-6.0/get-global-styles-and-settings.php
index 5768b4cba80986..1dfafe9f7631d3 100644
--- a/lib/compat/wordpress-6.0/get-global-styles-and-settings.php
+++ b/lib/compat/wordpress-6.0/get-global-styles-and-settings.php
@@ -63,75 +63,6 @@ function gutenberg_get_global_styles( $path = array(), $context = array() ) {
return _wp_array_get( $styles, $path, $styles );
}
-/**
- * Returns the stylesheet resulting of merging core, theme, and user data.
- *
- * @param array $types Types of styles to load. Optional.
- * It accepts 'variables', 'styles', 'presets' as values.
- * If empty, it'll load all for themes with theme.json support
- * and only [ 'variables', 'presets' ] for themes without theme.json support.
- *
- * @return string Stylesheet.
- */
-function gutenberg_get_global_stylesheet( $types = array() ) {
- // Return cached value if it can be used and exists.
- // It's cached by theme to make sure that theme switching clears the cache.
- $can_use_cached = (
- ( empty( $types ) ) &&
- ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) &&
- ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) &&
- ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) &&
- ! is_admin()
- );
- $transient_name = 'gutenberg_global_styles_' . get_stylesheet();
- if ( $can_use_cached ) {
- $cached = get_transient( $transient_name );
- if ( $cached ) {
- return $cached;
- }
- }
- $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
- $supports_theme_json = WP_Theme_JSON_Resolver_Gutenberg::theme_has_support();
- if ( empty( $types ) ) {
- $types = array( 'variables', 'styles', 'presets' );
- }
-
- /*
- * If variables are part of the stylesheet,
- * we add them for all origins (default, theme, user).
- * This is so themes without a theme.json still work as before 5.9:
- * they can override the default presets.
- * See https://core.trac.wordpress.org/ticket/54782
- */
- $styles_variables = '';
- if ( in_array( 'variables', $types, true ) ) {
- $styles_variables = $tree->get_stylesheet( array( 'variables' ) );
- $types = array_diff( $types, array( 'variables' ) );
- }
-
- /*
- * For the remaining types (presets, styles), we do consider origins:
- *
- * - themes without theme.json: only the classes for the presets defined by core
- * - themes with theme.json: the presets and styles classes, both from core and the theme
- */
- $styles_rest = '';
- if ( ! empty( $types ) ) {
- $origins = array( 'default', 'theme', 'custom' );
- if ( ! $supports_theme_json ) {
- $origins = array( 'default' );
- }
- $styles_rest = $tree->get_stylesheet( $types, $origins );
- }
- $stylesheet = $styles_variables . $styles_rest;
- if ( $can_use_cached ) {
- // Cache for a minute.
- // This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites.
- set_transient( $transient_name, $stylesheet, MINUTE_IN_SECONDS );
- }
- return $stylesheet;
-}
-
/**
* Returns a string containing the SVGs to be referenced as filters (duotone).
*
diff --git a/lib/compat/wordpress-6.1/block-editor-settings.php b/lib/compat/wordpress-6.1/block-editor-settings.php
index ed591cf60196ae..de61109ed3ac73 100644
--- a/lib/compat/wordpress-6.1/block-editor-settings.php
+++ b/lib/compat/wordpress-6.1/block-editor-settings.php
@@ -81,6 +81,18 @@ function gutenberg_get_block_editor_settings( $settings ) {
$block_classes['css'] = $actual_css;
$new_global_styles[] = $block_classes;
}
+ } else {
+ // If there is no `theme.json` file, ensure base layout styles are still available.
+ $block_classes = array(
+ 'css' => 'base-layout-styles',
+ '__unstableType' => 'theme',
+ 'isGlobalStyles' => true,
+ );
+ $actual_css = gutenberg_get_global_stylesheet( array( $block_classes['css'] ) );
+ if ( '' !== $actual_css ) {
+ $block_classes['css'] = $actual_css;
+ $new_global_styles[] = $block_classes;
+ }
}
$settings['styles'] = array_merge( $new_global_styles, $styles_without_existing_global_styles );
diff --git a/lib/compat/wordpress-6.1/blocks.php b/lib/compat/wordpress-6.1/blocks.php
index 85e124306161a1..ed41c5aa8cd098 100644
--- a/lib/compat/wordpress-6.1/blocks.php
+++ b/lib/compat/wordpress-6.1/blocks.php
@@ -5,6 +5,26 @@
* @package gutenberg
*/
+/**
+ * Update allowed inline style attributes list.
+ *
+ * Note: This should be removed when the minimum required WP version is >= 6.1.
+ *
+ * @param string[] $attrs Array of allowed CSS attributes.
+ * @return string[] CSS attributes.
+ */
+function gutenberg_safe_style_attrs_6_1( $attrs ) {
+ $attrs[] = 'flex-wrap';
+ $attrs[] = 'gap';
+ $attrs[] = 'margin-block-start';
+ $attrs[] = 'margin-block-end';
+ $attrs[] = 'margin-inline-start';
+ $attrs[] = 'margin-inline-end';
+
+ return $attrs;
+}
+add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs_6_1' );
+
/**
* Registers view scripts for core blocks if handling is missing in WordPress core.
*
diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
index 189e3694dd87f6..5bb0c4fa944231 100644
--- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
+++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
@@ -25,7 +25,58 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 {
'link' => array( ':hover', ':focus', ':active', ':visited' ),
);
- /*
+ /**
+ * Metadata for style properties.
+ *
+ * Each element is a direct mapping from the CSS property name to the
+ * path to the value in theme.json & block attributes.
+ */
+ const PROPERTIES_METADATA = array(
+ 'background' => array( 'color', 'gradient' ),
+ 'background-color' => array( 'color', 'background' ),
+ 'border-radius' => array( 'border', 'radius' ),
+ 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ),
+ 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ),
+ 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ),
+ 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
+ 'border-color' => array( 'border', 'color' ),
+ 'border-width' => array( 'border', 'width' ),
+ 'border-style' => array( 'border', 'style' ),
+ 'border-top-color' => array( 'border', 'top', 'color' ),
+ 'border-top-width' => array( 'border', 'top', 'width' ),
+ 'border-top-style' => array( 'border', 'top', 'style' ),
+ 'border-right-color' => array( 'border', 'right', 'color' ),
+ 'border-right-width' => array( 'border', 'right', 'width' ),
+ 'border-right-style' => array( 'border', 'right', 'style' ),
+ 'border-bottom-color' => array( 'border', 'bottom', 'color' ),
+ 'border-bottom-width' => array( 'border', 'bottom', 'width' ),
+ 'border-bottom-style' => array( 'border', 'bottom', 'style' ),
+ 'border-left-color' => array( 'border', 'left', 'color' ),
+ 'border-left-width' => array( 'border', 'left', 'width' ),
+ 'border-left-style' => array( 'border', 'left', 'style' ),
+ 'color' => array( 'color', 'text' ),
+ 'font-family' => array( 'typography', 'fontFamily' ),
+ 'font-size' => array( 'typography', 'fontSize' ),
+ 'font-style' => array( 'typography', 'fontStyle' ),
+ 'font-weight' => array( 'typography', 'fontWeight' ),
+ 'letter-spacing' => array( 'typography', 'letterSpacing' ),
+ 'line-height' => array( 'typography', 'lineHeight' ),
+ 'margin' => array( 'spacing', 'margin' ),
+ 'margin-top' => array( 'spacing', 'margin', 'top' ),
+ 'margin-right' => array( 'spacing', 'margin', 'right' ),
+ 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
+ 'margin-left' => array( 'spacing', 'margin', 'left' ),
+ 'padding' => array( 'spacing', 'padding' ),
+ 'padding-top' => array( 'spacing', 'padding', 'top' ),
+ 'padding-right' => array( 'spacing', 'padding', 'right' ),
+ 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
+ 'padding-left' => array( 'spacing', 'padding', 'left' ),
+ 'text-decoration' => array( 'typography', 'textDecoration' ),
+ 'text-transform' => array( 'typography', 'textTransform' ),
+ 'filter' => array( 'filter', 'duotone' ),
+ );
+
+ /**
* The valid elements that can be found under styles.
*
* @var string[]
@@ -151,8 +202,6 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n
return $output;
}
-
-
/**
* Removes insecure data from theme.json.
*
@@ -230,6 +279,47 @@ public static function remove_insecure_properties( $theme_json ) {
return $theme_json;
}
+ /**
+ * The valid properties under the styles key.
+ *
+ * @var array
+ */
+ const VALID_STYLES = array(
+ 'border' => array(
+ 'color' => null,
+ 'radius' => null,
+ 'style' => null,
+ 'width' => null,
+ 'top' => null,
+ 'right' => null,
+ 'bottom' => null,
+ 'left' => null,
+ ),
+ 'color' => array(
+ 'background' => null,
+ 'gradient' => null,
+ 'text' => null,
+ ),
+ 'filter' => array(
+ 'duotone' => null,
+ ),
+ 'spacing' => array(
+ 'margin' => null,
+ 'padding' => null,
+ 'blockGap' => null,
+ ),
+ 'typography' => array(
+ 'fontFamily' => null,
+ 'fontSize' => null,
+ 'fontStyle' => null,
+ 'fontWeight' => null,
+ 'letterSpacing' => null,
+ 'lineHeight' => null,
+ 'textDecoration' => null,
+ 'textTransform' => null,
+ ),
+ );
+
/**
* Returns the metadata for each block.
*
@@ -452,6 +542,73 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) {
return $nodes;
}
+ /**
+ * Returns the stylesheet that results of processing
+ * the theme.json structure this object represents.
+ *
+ * @param array $types Types of styles to load. Will load all by default. It accepts:
+ * 'variables': only the CSS Custom Properties for presets & custom ones.
+ * 'styles': only the styles section in theme.json.
+ * 'presets': only the classes for the presets.
+ * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
+ * @return string Stylesheet.
+ */
+ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
+ if ( null === $origins ) {
+ $origins = static::VALID_ORIGINS;
+ }
+
+ if ( is_string( $types ) ) {
+ // Dispatch error and map old arguments to new ones.
+ _deprecated_argument( __FUNCTION__, '5.9' );
+ if ( 'block_styles' === $types ) {
+ $types = array( 'styles', 'presets' );
+ } elseif ( 'css_variables' === $types ) {
+ $types = array( 'variables' );
+ } else {
+ $types = array( 'variables', 'styles', 'presets' );
+ }
+ }
+
+ $blocks_metadata = static::get_blocks_metadata();
+ $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
+ $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
+
+ $stylesheet = '';
+
+ if ( in_array( 'variables', $types, true ) ) {
+ $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
+ }
+
+ if ( in_array( 'styles', $types, true ) ) {
+ $stylesheet .= $this->get_block_classes( $style_nodes );
+ } elseif ( in_array( 'base-layout-styles', $types, true ) ) {
+ // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
+ // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
+ $base_styles_nodes = array(
+ array(
+ 'path' => array( 'styles' ),
+ 'selector' => static::ROOT_BLOCK_SELECTOR,
+ ),
+ array(
+ 'path' => array( 'styles', 'blocks', 'core/columns' ),
+ 'selector' => '.wp-block-columns',
+ 'name' => 'core/columns',
+ ),
+ );
+
+ foreach ( $base_styles_nodes as $base_style_node ) {
+ $stylesheet .= $this->get_layout_styles( $base_style_node );
+ }
+ }
+
+ if ( in_array( 'presets', $types, true ) ) {
+ $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
+ }
+
+ return $stylesheet;
+ }
+
/**
* Gets the CSS rules for a particular block from theme.json.
*
@@ -530,7 +687,17 @@ function( $pseudo_selector ) use ( $selector ) {
$block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone );
}
+ // 4. Generate Layout block gap styles.
+ if (
+ static::ROOT_BLOCK_SELECTOR !== $selector &&
+ ! empty( $block_metadata['name'] )
+ ) {
+ $block_rules .= $this->get_layout_styles( $block_metadata );
+ }
+
if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
+ $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' );
+
$block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
$block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
$block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
@@ -538,8 +705,11 @@ function( $pseudo_selector ) use ( $selector ) {
$has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
if ( $has_block_gap_support ) {
$block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
- $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
+ $block_rules .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }";
+ // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
+ $block_rules .= "$selector { --wp--style--block-gap: $block_gap_value; }";
}
+ $block_rules .= $this->get_layout_styles( $block_metadata );
}
return $block_rules;
@@ -688,7 +858,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null
return $value;
}
- /*
+ /**
* Presets are a set of values that serve
* to bootstrap some styles: colors, font sizes, etc.
*
@@ -826,6 +996,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null
'custom' => null,
'customDuotone' => null,
'customGradient' => null,
+ 'defaultDuotone' => null,
'defaultGradients' => null,
'defaultPalette' => null,
'duotone' => null,
@@ -837,6 +1008,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null
'custom' => null,
'layout' => array(
'contentSize' => null,
+ 'definitions' => null,
'wideSize' => null,
),
'spacing' => array(
@@ -959,4 +1131,169 @@ public function set_spacing_sizes() {
_wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), array_merge( $below_sizes, $above_sizes ) );
}
+
+ /**
+ * Get the CSS layout rules for a particular block from theme.json layout definitions.
+ *
+ * @param array $block_metadata Metadata about the block to get styles for.
+ *
+ * @return string Layout styles for the block.
+ */
+ protected function get_layout_styles( $block_metadata ) {
+ $block_rules = '';
+ $block_type = null;
+
+ if ( isset( $block_metadata['name'] ) ) {
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
+ if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) {
+ return $block_rules;
+ }
+ }
+
+ $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
+ $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
+ $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support.
+ $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
+ $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() );
+ $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, and child combinator selectors.
+
+ // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
+ // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
+ if ( $has_block_gap_support || $has_fallback_gap_support ) {
+ $block_gap_value = null;
+ // Use a fallback gap value if block gap support is not available.
+ if ( ! $has_block_gap_support ) {
+ $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
+ if ( ! empty( $block_type ) ) {
+ $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null );
+ }
+ } else {
+ $block_gap_value = _wp_array_get( $node, array( 'spacing', 'blockGap' ), null );
+ }
+
+ // Support split row / column values and concatenate to a shorthand value.
+ if ( is_array( $block_gap_value ) ) {
+ if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
+ $gap_row = $block_gap_value['top'];
+ $gap_column = $block_gap_value['left'];
+ $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
+ } else {
+ // Skip outputting gap value if not all sides are provided.
+ $block_gap_value = null;
+ }
+ }
+
+ if ( $block_gap_value ) {
+ foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
+ // Allow skipping default layout for themes that opt-in to block styles, but opt-out of blockGap.
+ if ( ! $has_block_gap_support && 'default' === $layout_definition_key ) {
+ continue;
+ }
+
+ $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) );
+ $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() );
+
+ if (
+ ! empty( $class_name ) &&
+ ! empty( $spacing_rules )
+ ) {
+ foreach ( $spacing_rules as $spacing_rule ) {
+ $declarations = array();
+ if (
+ isset( $spacing_rule['selector'] ) &&
+ preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
+ ! empty( $spacing_rule['rules'] )
+ ) {
+ // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
+ foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
+ $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
+ if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
+ $declarations[] = array(
+ 'name' => $css_property,
+ 'value' => $current_css_value,
+ );
+ }
+ }
+
+ $format = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s';
+ $layout_selector = sprintf(
+ $format,
+ $selector,
+ $class_name,
+ $spacing_rule['selector']
+ );
+ $block_rules .= static::to_ruleset( $layout_selector, $declarations );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Output base styles.
+ if (
+ static::ROOT_BLOCK_SELECTOR === $selector
+ ) {
+ $valid_display_modes = array( 'block', 'flex', 'grid' );
+ foreach ( $layout_definitions as $layout_definition ) {
+ $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) );
+ $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() );
+
+ if (
+ ! empty( $class_name ) &&
+ ! empty( $base_style_rules )
+ ) {
+ // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
+ if (
+ ! empty( $layout_definition['displayMode'] ) &&
+ is_string( $layout_definition['displayMode'] ) &&
+ in_array( $layout_definition['displayMode'], $valid_display_modes, true )
+ ) {
+ $layout_selector = sprintf(
+ '%s .%s',
+ $selector,
+ $class_name
+ );
+ $block_rules .= static::to_ruleset(
+ $layout_selector,
+ array(
+ array(
+ 'name' => 'display',
+ 'value' => $layout_definition['displayMode'],
+ ),
+ )
+ );
+ }
+
+ foreach ( $base_style_rules as $base_style_rule ) {
+ $declarations = array();
+
+ if (
+ isset( $base_style_rule['selector'] ) &&
+ preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
+ ! empty( $base_style_rule['rules'] )
+ ) {
+ foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
+ if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
+ $declarations[] = array(
+ 'name' => $css_property,
+ 'value' => $css_value,
+ );
+ }
+ }
+
+ $layout_selector = sprintf(
+ '%s .%s%s',
+ $selector,
+ $class_name,
+ $base_style_rule['selector']
+ );
+ $block_rules .= static::to_ruleset( $layout_selector, $declarations );
+ }
+ }
+ }
+ }
+ }
+ return $block_rules;
+ }
}
diff --git a/lib/compat/wordpress-6.1/get-global-styles-and-settings.php b/lib/compat/wordpress-6.1/get-global-styles-and-settings.php
index dd699edc269b91..5dd950c627a641 100644
--- a/lib/compat/wordpress-6.1/get-global-styles-and-settings.php
+++ b/lib/compat/wordpress-6.1/get-global-styles-and-settings.php
@@ -47,3 +47,74 @@ function ( $item ) {
}
}
}
+
+/**
+ * Returns the stylesheet resulting of merging core, theme, and user data.
+ *
+ * @param array $types Types of styles to load. Optional.
+ * It accepts 'variables', 'styles', 'presets' as values.
+ * If empty, it'll load all for themes with theme.json support
+ * and only [ 'variables', 'presets' ] for themes without theme.json support.
+ *
+ * @return string Stylesheet.
+ */
+function gutenberg_get_global_stylesheet( $types = array() ) {
+ // Return cached value if it can be used and exists.
+ // It's cached by theme to make sure that theme switching clears the cache.
+ $can_use_cached = (
+ ( empty( $types ) ) &&
+ ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) &&
+ ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) &&
+ ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) &&
+ ! is_admin()
+ );
+ $transient_name = 'gutenberg_global_styles_' . get_stylesheet();
+ if ( $can_use_cached ) {
+ $cached = get_transient( $transient_name );
+ if ( $cached ) {
+ return $cached;
+ }
+ }
+ $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
+ $supports_theme_json = WP_Theme_JSON_Resolver_Gutenberg::theme_has_support();
+ if ( empty( $types ) && ! $supports_theme_json ) {
+ $types = array( 'variables', 'presets', 'base-layout-styles' );
+ } elseif ( empty( $types ) ) {
+ $types = array( 'variables', 'styles', 'presets' );
+ }
+
+ /*
+ * If variables are part of the stylesheet,
+ * we add them for all origins (default, theme, user).
+ * This is so themes without a theme.json still work as before 5.9:
+ * they can override the default presets.
+ * See https://core.trac.wordpress.org/ticket/54782
+ */
+ $styles_variables = '';
+ if ( in_array( 'variables', $types, true ) ) {
+ $styles_variables = $tree->get_stylesheet( array( 'variables' ) );
+ $types = array_diff( $types, array( 'variables' ) );
+ }
+
+ /*
+ * For the remaining types (presets, styles), we do consider origins:
+ *
+ * - themes without theme.json: only the classes for the presets defined by core
+ * - themes with theme.json: the presets and styles classes, both from core and the theme
+ */
+ $styles_rest = '';
+ if ( ! empty( $types ) ) {
+ $origins = array( 'default', 'theme', 'custom' );
+ if ( ! $supports_theme_json ) {
+ $origins = array( 'default' );
+ }
+ $styles_rest = $tree->get_stylesheet( $types, $origins );
+ }
+ $stylesheet = $styles_variables . $styles_rest;
+ if ( $can_use_cached ) {
+ // Cache for a minute.
+ // This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites.
+ set_transient( $transient_name, $stylesheet, MINUTE_IN_SECONDS );
+ }
+ return $stylesheet;
+}
diff --git a/lib/compat/wordpress-6.1/theme.json b/lib/compat/wordpress-6.1/theme.json
index 928758be88e121..426adc6195085d 100644
--- a/lib/compat/wordpress-6.1/theme.json
+++ b/lib/compat/wordpress-6.1/theme.json
@@ -185,6 +185,85 @@
],
"text": true
},
+ "layout": {
+ "definitions": {
+ "default": {
+ "name": "default",
+ "slug": "flow",
+ "className": "is-layout-flow",
+ "baseStyles": [
+ {
+ "selector": " > .alignleft",
+ "rules": {
+ "float": "left",
+ "margin-inline-start": "0",
+ "margin-inline-end": "2em"
+ }
+ },
+ {
+ "selector": " > .alignright",
+ "rules": {
+ "float": "right",
+ "margin-inline-start": "2em",
+ "margin-inline-end": "0"
+ }
+ },
+ {
+ "selector": " > .aligncenter",
+ "rules": {
+ "margin-left": "auto !important",
+ "margin-right": "auto !important"
+ }
+ }
+ ],
+ "spacingStyles": [
+ {
+ "selector": " > *",
+ "rules": {
+ "margin-block-start": "0",
+ "margin-block-end": "0"
+ }
+ },
+ {
+ "selector": " > * + *",
+ "rules": {
+ "margin-block-start": null,
+ "margin-block-end": "0"
+ }
+ }
+ ]
+ },
+ "flex": {
+ "name": "flex",
+ "slug": "flex",
+ "className": "is-layout-flex",
+ "displayMode": "flex",
+ "baseStyles": [
+ {
+ "selector": "",
+ "rules": {
+ "flex-wrap": "wrap",
+ "align-items": "center"
+ }
+ },
+ {
+ "selector": " > *",
+ "rules": {
+ "margin": "0"
+ }
+ }
+ ],
+ "spacingStyles": [
+ {
+ "selector": "",
+ "rules": {
+ "gap": null
+ }
+ }
+ ]
+ }
+ }
+ },
"spacing": {
"blockGap": null,
"margin": false,
diff --git a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php
index 2007893afea7fc..adf5803de561aa 100644
--- a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php
+++ b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php
@@ -111,6 +111,15 @@ public static function get_block_data() {
if ( isset( $block_type->supports['__experimentalStyle'] ) ) {
$config['styles']['blocks'][ $block_name ] = static::remove_JSON_comments( $block_type->supports['__experimentalStyle'] );
}
+
+ if (
+ isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) &&
+ null === _wp_array_get( $config, array( 'styles', 'blocks', $block_name, 'spacing', 'blockGap' ), null )
+ ) {
+ // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value.
+ // The real blockGap value to be used will be determined when the styles are rendered for output.
+ $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null;
+ }
}
// Core here means it's the lower level part of the styles chain.
diff --git a/packages/block-editor/src/components/block-list/layout.js b/packages/block-editor/src/components/block-list/layout.js
index 79ff15811ab521..d7a9113ebfa0f3 100644
--- a/packages/block-editor/src/components/block-list/layout.js
+++ b/packages/block-editor/src/components/block-list/layout.js
@@ -7,6 +7,7 @@ import { createContext, useContext } from '@wordpress/element';
* Internal dependencies
*/
import { getLayoutType } from '../../layouts';
+import useSetting from '../use-setting';
export const defaultLayout = { type: 'default' };
@@ -24,12 +25,23 @@ export function useLayout() {
return useContext( Layout );
}
-export function LayoutStyle( { layout = {}, ...props } ) {
+export function LayoutStyle( { layout = {}, css, ...props } ) {
const layoutType = getLayoutType( layout.type );
+ const blockGapSupport = useSetting( 'spacing.blockGap' );
+ const hasBlockGapSupport = blockGapSupport !== null;
if ( layoutType ) {
- return ;
+ if ( css ) {
+ return ;
+ }
+ const layoutStyle = layoutType.getLayoutStyle?.( {
+ hasBlockGapSupport,
+ layout,
+ ...props,
+ } );
+ if ( layoutStyle ) {
+ return ;
+ }
}
-
return null;
}
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index 9548f5ea152ecb..272e79e78dbdd3 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -20,4 +20,5 @@ export { useCustomSides } from './dimensions';
export { getBorderClassesAndStyles, useBorderProps } from './use-border-props';
export { getColorClassesAndStyles, useColorProps } from './use-color-props';
export { getSpacingClassesAndStyles } from './use-spacing-props';
+export { getGapCSSValue } from './gap';
export { useCachedTruthy } from './use-cached-truthy';
diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js
index c5072200869ed2..65a44b6b8df546 100644
--- a/packages/block-editor/src/hooks/layout.js
+++ b/packages/block-editor/src/hooks/layout.js
@@ -9,7 +9,11 @@ import { has, kebabCase } from 'lodash';
*/
import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
-import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks';
+import {
+ getBlockDefaultClassName,
+ getBlockSupport,
+ hasBlockSupport,
+} from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import {
Button,
@@ -40,35 +44,31 @@ const layoutBlockSupportKey = '__experimentalLayout';
* have the style engine generate a more extensive list of utility classnames which
* will then replace this method.
*
- * @param { Array } attributes Array of block attributes.
+ * @param { Object } layout Layout object.
+ * @param { Object } layoutDefinitions An object containing layout definitions, stored in theme.json.
*
* @return { Array } Array of CSS classname strings.
*/
-function getLayoutClasses( attributes ) {
+function getLayoutClasses( layout, layoutDefinitions ) {
const layoutClassnames = [];
- if ( ! attributes.layout ) {
- return layoutClassnames;
- }
-
- if ( attributes?.layout?.orientation ) {
+ if ( layoutDefinitions?.[ layout?.type || 'default' ]?.className ) {
layoutClassnames.push(
- `is-${ kebabCase( attributes.layout.orientation ) }`
+ layoutDefinitions?.[ layout?.type || 'default' ]?.className
);
}
- if ( attributes?.layout?.justifyContent ) {
+ if ( layout?.orientation ) {
+ layoutClassnames.push( `is-${ kebabCase( layout.orientation ) }` );
+ }
+
+ if ( layout?.justifyContent ) {
layoutClassnames.push(
- `is-content-justification-${ kebabCase(
- attributes.layout.justifyContent
- ) }`
+ `is-content-justification-${ kebabCase( layout.justifyContent ) }`
);
}
- if (
- attributes?.layout?.flexWrap &&
- attributes.layout.flexWrap === 'nowrap'
- ) {
+ if ( layout?.flexWrap && layout.flexWrap === 'nowrap' ) {
layoutClassnames.push( 'is-nowrap' );
}
@@ -267,12 +267,36 @@ export const withLayoutStyles = createHigherOrderComponent(
? defaultThemeLayout
: layout || defaultBlockLayout || {};
const layoutClasses = shouldRenderLayoutStyles
- ? getLayoutClasses( attributes )
+ ? getLayoutClasses( usedLayout, defaultThemeLayout?.definitions )
: null;
+ const selector = `.${ getBlockDefaultClassName(
+ name
+ ) }.wp-container-${ id }`;
+ const blockGapSupport = useSetting( 'spacing.blockGap' );
+ const hasBlockGapSupport = blockGapSupport !== null;
+
+ // Get CSS string for the current layout type.
+ // The CSS and `style` element is only output if it is not empty.
+ let css;
+ if ( shouldRenderLayoutStyles ) {
+ const fullLayoutType = getLayoutType(
+ usedLayout?.type || 'default'
+ );
+ css = fullLayoutType?.getLayoutStyle?.( {
+ blockName: name,
+ selector,
+ layout: usedLayout,
+ layoutDefinitions: defaultThemeLayout?.definitions,
+ style: attributes?.style,
+ hasBlockGapSupport,
+ } );
+ }
+
+ // Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`.
const className = classnames(
props?.className,
{
- [ `wp-container-${ id }` ]: shouldRenderLayoutStyles,
+ [ `wp-container-${ id }` ]: shouldRenderLayoutStyles && !! css, // Only attach a container class if there is generated CSS to be attached.
},
layoutClasses
);
@@ -281,10 +305,12 @@ export const withLayoutStyles = createHigherOrderComponent(
<>
{ shouldRenderLayoutStyles &&
element &&
+ !! css &&
createPortal(
,
diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js
index b2fe8aec5e1ff0..c3d55ce8962c7a 100644
--- a/packages/block-editor/src/index.js
+++ b/packages/block-editor/src/index.js
@@ -9,6 +9,7 @@ export {
useColorProps as __experimentalUseColorProps,
useCustomSides as __experimentalUseCustomSides,
getSpacingClassesAndStyles as __experimentalGetSpacingClassesAndStyles,
+ getGapCSSValue as __experimentalGetGapCSSValue,
useCachedTruthy,
} from './hooks';
export * from './components';
diff --git a/packages/block-editor/src/layouts/flex.js b/packages/block-editor/src/layouts/flex.js
index 4be6528eed16c7..2e90f10f511f0b 100644
--- a/packages/block-editor/src/layouts/flex.js
+++ b/packages/block-editor/src/layouts/flex.js
@@ -11,14 +11,12 @@ import {
arrowDown,
} from '@wordpress/icons';
import { Button, ToggleControl, Flex, FlexItem } from '@wordpress/components';
-import { getBlockSupport } from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import { appendSelectors } from './utils';
+import { appendSelectors, getBlockGapCSS } from './utils';
import { getGapCSSValue } from '../hooks/gap';
-import useSetting from '../components/use-setting';
import {
BlockControls,
JustifyContentControl,
@@ -107,59 +105,67 @@ export default {
);
},
- save: function FlexLayoutStyle( { selector, layout, style, blockName } ) {
+ getLayoutStyle: function getLayoutStyle( {
+ selector,
+ layout,
+ style,
+ blockName,
+ hasBlockGapSupport,
+ layoutDefinitions,
+ } ) {
const { orientation = 'horizontal' } = layout;
- const blockGapSupport = useSetting( 'spacing.blockGap' );
- const fallbackValue =
- getBlockSupport( blockName, [
- 'spacing',
- 'blockGap',
- '__experimentalDefault',
- ] ) || '0.5em';
- const hasBlockGapStylesSupport = blockGapSupport !== null;
// 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, fallbackValue )
- : `var( --wp--style--block-gap, ${ fallbackValue } )`;
- const justifyContent =
- justifyContentMap[ layout.justifyContent ] ||
- justifyContentMap.left;
+ ? getGapCSSValue( style?.spacing?.blockGap )
+ : undefined;
+ const justifyContent = justifyContentMap[ layout.justifyContent ];
const flexWrap = flexWrapOptions.includes( layout.flexWrap )
? layout.flexWrap
: 'wrap';
const verticalAlignment =
- verticalAlignmentMap[ layout.verticalAlignment ] ||
- verticalAlignmentMap.center;
- const rowOrientation = `
- flex-direction: row;
- align-items: ${ verticalAlignment };
- justify-content: ${ justifyContent };
- `;
+ verticalAlignmentMap[ layout.verticalAlignment ];
const alignItems =
alignItemsMap[ layout.justifyContent ] || alignItemsMap.left;
- const columnOrientation = `
- flex-direction: column;
- align-items: ${ alignItems };
- `;
- return (
-
- );
+ if ( flexWrap && flexWrap !== 'wrap' ) {
+ rules.push( `flex-wrap: ${ flexWrap }` );
+ }
+
+ if ( orientation === 'horizontal' ) {
+ if ( verticalAlignment ) {
+ rules.push( `align-items: ${ verticalAlignment }` );
+ }
+ if ( justifyContent ) {
+ rules.push( `justify-content: ${ justifyContent }` );
+ }
+ } else {
+ rules.push( 'flex-direction: column' );
+ rules.push( `align-items: ${ alignItems }` );
+ }
+
+ 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,
+ 'flex',
+ blockGapValue
+ );
+ }
+ return output;
},
getOrientation( layout ) {
const { orientation = 'horizontal' } = layout;
diff --git a/packages/block-editor/src/layouts/flow.js b/packages/block-editor/src/layouts/flow.js
index 82851cb0150756..1d8ea7d7a6aea2 100644
--- a/packages/block-editor/src/layouts/flow.js
+++ b/packages/block-editor/src/layouts/flow.js
@@ -13,7 +13,7 @@ import { Icon, positionCenter, stretchWide } from '@wordpress/icons';
* Internal dependencies
*/
import useSetting from '../components/use-setting';
-import { appendSelectors } from './utils';
+import { appendSelectors, getBlockGapCSS } from './utils';
import { getGapBoxControlValueFromStyle } from '../hooks/gap';
import { shouldSkipSerialization } from '../hooks/utils';
@@ -107,15 +107,15 @@ export default {
toolBarControls: function DefaultLayoutToolbarControls() {
return null;
},
- save: function DefaultLayoutStyle( {
+ getLayoutStyle: function getLayoutStyle( {
selector,
layout = {},
style,
blockName,
+ hasBlockGapSupport,
+ layoutDefinitions,
} ) {
const { contentSize, wideSize } = layout;
- const blockGapSupport = useSetting( 'spacing.blockGap' );
- const hasBlockGapStylesSupport = blockGapSupport !== null;
const blockGapStyleValue = getGapBoxControlValueFromStyle(
style?.spacing?.blockGap
);
@@ -125,7 +125,7 @@ export default {
blockGapStyleValue?.top &&
! shouldSkipSerialization( blockName, 'spacing', 'blockGap' )
? blockGapStyleValue?.top
- : 'var( --wp--style--block-gap )';
+ : '';
let output =
!! contentSize || !! wideSize
@@ -147,37 +147,16 @@ export default {
`
: '';
- output += `
- ${ appendSelectors( selector, '> .alignleft' ) } {
- float: left;
- margin-inline-start: 0;
- margin-inline-end: 2em;
- }
- ${ appendSelectors( selector, '> .alignright' ) } {
- float: right;
- margin-inline-start: 2em;
- margin-inline-end: 0;
- }
-
- ${ appendSelectors( selector, '> .aligncenter' ) } {
- margin-left: auto !important;
- margin-right: auto !important;
- }
- `;
-
- if ( hasBlockGapStylesSupport ) {
- output += `
- ${ appendSelectors( selector, '> *' ) } {
- margin-block-start: 0;
- margin-block-end: 0;
- }
- ${ appendSelectors( selector, '> * + *' ) } {
- margin-block-start: ${ blockGapValue };
- }
- `;
+ // Output blockGap styles based on rules contained in layout definitions in theme.json.
+ if ( hasBlockGapSupport && blockGapValue ) {
+ output += getBlockGapCSS(
+ selector,
+ layoutDefinitions,
+ 'default',
+ blockGapValue
+ );
}
-
- return ;
+ return output;
},
getOrientation() {
return 'vertical';
diff --git a/packages/block-editor/src/layouts/test/flex.js b/packages/block-editor/src/layouts/test/flex.js
new file mode 100644
index 00000000000000..01bd8735dc782c
--- /dev/null
+++ b/packages/block-editor/src/layouts/test/flex.js
@@ -0,0 +1,21 @@
+/**
+ * Internal dependencies
+ */
+import flex from '../flex';
+
+describe( 'getLayoutStyle', () => {
+ it( 'should return an empty string if no non-default params are provided', () => {
+ const expected = '';
+
+ const result = flex.getLayoutStyle( {
+ selector: '.my-container',
+ layout: {},
+ style: {},
+ blockName: 'test-block',
+ hasBlockGapSupport: false,
+ layoutDefinitions: undefined,
+ } );
+
+ expect( result ).toBe( expected );
+ } );
+} );
diff --git a/packages/block-editor/src/layouts/test/flow.js b/packages/block-editor/src/layouts/test/flow.js
new file mode 100644
index 00000000000000..727ac214af6286
--- /dev/null
+++ b/packages/block-editor/src/layouts/test/flow.js
@@ -0,0 +1,21 @@
+/**
+ * Internal dependencies
+ */
+import flow from '../flow';
+
+describe( 'getLayoutStyle', () => {
+ it( 'should return an empty string if no non-default params are provided', () => {
+ const expected = '';
+
+ const result = flow.getLayoutStyle( {
+ selector: '.my-container',
+ layout: {},
+ style: {},
+ blockName: 'test-block',
+ hasBlockGapSupport: false,
+ layoutDefinitions: undefined,
+ } );
+
+ expect( result ).toBe( expected );
+ } );
+} );
diff --git a/packages/block-editor/src/layouts/test/utils.js b/packages/block-editor/src/layouts/test/utils.js
new file mode 100644
index 00000000000000..529e1bf74e24c9
--- /dev/null
+++ b/packages/block-editor/src/layouts/test/utils.js
@@ -0,0 +1,138 @@
+/**
+ * Internal dependencies
+ */
+import { appendSelectors, getBlockGapCSS } from '../utils';
+
+const layoutDefinitions = {
+ default: {
+ spacingStyles: [
+ {
+ selector: ' > *',
+ rules: {
+ 'margin-block-start': '0',
+ 'margin-block-end': '0',
+ },
+ },
+ {
+ selector: ' > * + *',
+ rules: {
+ 'margin-block-start': null,
+ 'margin-block-end': '0',
+ },
+ },
+ ],
+ },
+ flex: {
+ spacingStyles: [
+ {
+ selector: '',
+ rules: {
+ gap: null,
+ },
+ },
+ ],
+ },
+};
+
+describe( 'getBlockGapCSS', () => {
+ it( 'should output default blockGap rules', () => {
+ const expected =
+ '.editor-styles-wrapper .my-container > * { margin-block-start: 0; margin-block-end: 0; }.editor-styles-wrapper .my-container > * + * { margin-block-start: 3em; margin-block-end: 0; }';
+
+ const result = getBlockGapCSS(
+ '.my-container',
+ layoutDefinitions,
+ 'default',
+ '3em'
+ );
+
+ expect( result ).toBe( expected );
+ } );
+
+ it( 'should output flex blockGap rules', () => {
+ const expected = '.editor-styles-wrapper .my-container { gap: 3em; }';
+
+ const result = getBlockGapCSS(
+ '.my-container',
+ layoutDefinitions,
+ 'flex',
+ '3em'
+ );
+
+ expect( result ).toBe( expected );
+ } );
+
+ it( 'should return an empty string if layout type cannot be found', () => {
+ const expected = '';
+
+ const result = getBlockGapCSS(
+ '.my-container',
+ layoutDefinitions,
+ 'aTypeThatDoesNotExist',
+ '3em'
+ );
+
+ expect( result ).toBe( expected );
+ } );
+
+ it( 'should return an empty string if layout definitions cannot be found', () => {
+ const expected = '';
+
+ const result = getBlockGapCSS(
+ '.my-container',
+ undefined,
+ 'flex',
+ '3em'
+ );
+
+ expect( result ).toBe( expected );
+ } );
+
+ it( 'should return an empty string if blockGap is empty', () => {
+ const expected = '';
+
+ const result = getBlockGapCSS(
+ '.my-container',
+ layoutDefinitions,
+ 'flex',
+ null
+ );
+
+ expect( result ).toBe( expected );
+ } );
+
+ it( 'should treat a blockGap string containing 0 as a valid value', () => {
+ const expected = '.editor-styles-wrapper .my-container { gap: 0; }';
+
+ const result = getBlockGapCSS(
+ '.my-container',
+ layoutDefinitions,
+ 'flex',
+ '0'
+ );
+
+ expect( result ).toBe( expected );
+ } );
+} );
+
+describe( 'appendSelectors', () => {
+ it( 'should append a subselector without an appended selector', () => {
+ expect( appendSelectors( '.original-selector' ) ).toBe(
+ '.editor-styles-wrapper .original-selector'
+ );
+ } );
+
+ it( 'should append a subselector to a single selector', () => {
+ expect( appendSelectors( '.original-selector', '.appended' ) ).toBe(
+ '.editor-styles-wrapper .original-selector .appended'
+ );
+ } );
+
+ it( 'should append a subselector to multiple selectors', () => {
+ expect(
+ appendSelectors( '.first-selector,.second-selector', '.appended' )
+ ).toBe(
+ '.editor-styles-wrapper .first-selector .appended,.editor-styles-wrapper .second-selector .appended'
+ );
+ } );
+} );
diff --git a/packages/block-editor/src/layouts/utils.js b/packages/block-editor/src/layouts/utils.js
index 89e83fdbfb41e9..e2a27f7d7b121a 100644
--- a/packages/block-editor/src/layouts/utils.js
+++ b/packages/block-editor/src/layouts/utils.js
@@ -1,8 +1,8 @@
/**
* Utility to generate the proper CSS selector for layout styles.
*
- * @param {string|string[]} selectors - CSS selectors
- * @param {boolean} append - string to append.
+ * @param {string} selectors CSS selector, also supports multiple comma-separated selectors.
+ * @param {string} append The string to append.
*
* @return {string} - CSS selector.
*/
@@ -17,7 +17,48 @@ export function appendSelectors( selectors, append = '' ) {
.split( ',' )
.map(
( subselector ) =>
- `.editor-styles-wrapper ${ subselector } ${ append }`
+ `.editor-styles-wrapper ${ subselector }${
+ append ? ` ${ append }` : ''
+ }`
)
.join( ',' );
}
+
+/**
+ * Get generated blockGap CSS rules based on layout definitions provided in theme.json
+ * Falsy values in the layout definition's spacingStyles rules will be swapped out
+ * with the provided `blockGapValue`.
+ *
+ * @param {string} selector The CSS selector to target for the generated rules.
+ * @param {Object} layoutDefinitions Layout definitions object from theme.json.
+ * @param {string} layoutType The layout type (e.g. `default` or `flex`).
+ * @param {string} blockGapValue The current blockGap value to be applied.
+ * @return {string} The generated CSS rules.
+ */
+export function getBlockGapCSS(
+ selector,
+ layoutDefinitions,
+ layoutType,
+ blockGapValue
+) {
+ let output = '';
+ if (
+ layoutDefinitions?.[ layoutType ]?.spacingStyles?.length &&
+ blockGapValue
+ ) {
+ layoutDefinitions[ layoutType ].spacingStyles.forEach( ( gapStyle ) => {
+ output += `${ appendSelectors(
+ selector,
+ gapStyle.selector.trim()
+ ) } { `;
+ output += Object.entries( gapStyle.rules )
+ .map(
+ ( [ cssProperty, value ] ) =>
+ `${ cssProperty }: ${ value ? value : blockGapValue }`
+ )
+ .join( '; ' );
+ output += '; }';
+ } );
+ }
+ return output;
+}
diff --git a/packages/block-library/src/gallery/index.php b/packages/block-library/src/gallery/index.php
index 81010c9064f830..e6eecb7dda4122 100644
--- a/packages/block-library/src/gallery/index.php
+++ b/packages/block-library/src/gallery/index.php
@@ -81,7 +81,7 @@ function block_core_gallery_render( $attributes, $content ) {
}
// Set the CSS variable to the column value, and the `gap` property to the combined gap value.
- $style = '.' . $class . '{ --wp--style--unstable-gallery-gap: ' . $gap_column . '; gap: ' . $gap_value . '}';
+ $style = '.wp-block-gallery.' . $class . '{ --wp--style--unstable-gallery-gap: ' . $gap_column . '; gap: ' . $gap_value . '}';
gutenberg_enqueue_block_support_styles( $style, 11 );
return $content;
diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js
index ba41424b375efa..e45613bbba25d3 100644
--- a/packages/block-library/src/group/edit.js
+++ b/packages/block-library/src/group/edit.js
@@ -52,9 +52,7 @@ function GroupEdit( { attributes, setAttributes, clientId } ) {
const { type = 'default' } = usedLayout;
const layoutSupportEnabled = themeSupportsLayout || type !== 'default';
- const blockProps = useBlockProps( {
- className: `is-layout-${ type }`,
- } );
+ const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps(
layoutSupportEnabled
diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js
index b7e9d11524ac77..fb8580ef30ef0a 100644
--- a/packages/blocks/src/api/constants.js
+++ b/packages/blocks/src/api/constants.js
@@ -199,10 +199,6 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = {
support: [ 'typography', '__experimentalLetterSpacing' ],
useEngine: true,
},
- '--wp--style--block-gap': {
- value: [ 'spacing', 'blockGap' ],
- support: [ 'spacing', 'blockGap' ],
- },
};
export const __EXPERIMENTAL_ELEMENTS = {
diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js
index b5a2b0a4a4b971..77054884472a67 100644
--- a/packages/edit-post/src/components/visual-editor/index.js
+++ b/packages/edit-post/src/components/visual-editor/index.js
@@ -245,6 +245,7 @@ export default function VisualEditor( { styles } ) {
) }
{ ! isTemplateMode && (
@@ -260,7 +261,7 @@ export default function VisualEditor( { styles } ) {
className={
isTemplateMode
? 'wp-site-blocks'
- : undefined
+ : 'is-layout-flow' // Ensure root level blocks receive default/flow blockGap styling rules.
}
__experimentalLayout={ layout }
/>
diff --git a/packages/edit-site/src/components/global-styles/dimensions-panel.js b/packages/edit-site/src/components/global-styles/dimensions-panel.js
index 80c6c25e1d64cb..0cd4571f1a3a41 100644
--- a/packages/edit-site/src/components/global-styles/dimensions-panel.js
+++ b/packages/edit-site/src/components/global-styles/dimensions-panel.js
@@ -43,13 +43,8 @@ function useHasMargin( name ) {
function useHasGap( name ) {
const supports = getSupportedGlobalStylesPanels( name );
const [ settings ] = useSetting( 'spacing.blockGap', name );
- // Do not show the gap control panel for block-level global styles
- // as they do not work on the frontend.
- // See: https://github.com/WordPress/gutenberg/pull/39845.
- // We can revert this condition when they're working again.
- return !! name
- ? false
- : settings && supports.includes( '--wp--style--block-gap' );
+
+ return settings && supports.includes( 'blockGap' );
}
function filterValuesBySides( values, sides ) {
diff --git a/packages/edit-site/src/components/global-styles/hooks.js b/packages/edit-site/src/components/global-styles/hooks.js
index da7a9ccf6de234..69bc0762f3a43c 100644
--- a/packages/edit-site/src/components/global-styles/hooks.js
+++ b/packages/edit-site/src/components/global-styles/hooks.js
@@ -182,6 +182,21 @@ export function getSupportedGlobalStylesPanels( name ) {
}
const supportKeys = [];
+
+ // Check for blockGap support.
+ // Block spacing support doesn't map directly to a single style property, so needs to be handled separately.
+ // Also, only allow `blockGap` support if serialization has not been skipped, to be sure global spacing can be applied.
+ if (
+ blockType?.supports?.spacing?.blockGap &&
+ blockType?.supports?.spacing?.__experimentalSkipSerialization !==
+ true &&
+ ! blockType?.supports?.spacing?.__experimentalSkipSerialization?.some?.(
+ ( spacingType ) => spacingType === 'blockGap'
+ )
+ ) {
+ supportKeys.push( 'blockGap' );
+ }
+
Object.keys( STYLE_PROPERTY ).forEach( ( styleName ) => {
if ( ! STYLE_PROPERTY[ styleName ].support ) {
return;
diff --git a/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js
index 10e1d533e025ba..b165d5bb917420 100644
--- a/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js
+++ b/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js
@@ -7,6 +7,7 @@ import { __EXPERIMENTAL_ELEMENTS as ELEMENTS } from '@wordpress/blocks';
* Internal dependencies
*/
import {
+ getLayoutStyles,
getNodesWithSettings,
getNodesWithStyles,
toCustomProperties,
@@ -450,4 +451,171 @@ describe( 'global styles renderer', () => {
);
} );
} );
+
+ describe( 'getLayoutStyles', () => {
+ const layoutDefinitionsTree = {
+ settings: {
+ layout: {
+ definitions: {
+ default: {
+ name: 'default',
+ slug: 'flow',
+ className: 'is-layout-flow',
+ baseStyles: [
+ {
+ selector: ' > .alignleft',
+ rules: {
+ float: 'left',
+ 'margin-inline-start': '0',
+ 'margin-inline-end': '2em',
+ },
+ },
+ {
+ selector: ' > .alignright',
+ rules: {
+ float: 'right',
+ 'margin-inline-start': '2em',
+ 'margin-inline-end': '0',
+ },
+ },
+ {
+ selector: ' > .aligncenter',
+ rules: {
+ 'margin-left': 'auto !important',
+ 'margin-right': 'auto !important',
+ },
+ },
+ ],
+ spacingStyles: [
+ {
+ selector: ' > *',
+ rules: {
+ 'margin-block-start': '0',
+ 'margin-block-end': '0',
+ },
+ },
+ {
+ selector: ' > * + *',
+ rules: {
+ 'margin-block-start': null,
+ 'margin-block-end': '0',
+ },
+ },
+ ],
+ },
+ flex: {
+ name: 'flex',
+ slug: 'flex',
+ className: 'is-layout-flex',
+ displayMode: 'flex',
+ baseStyles: [
+ {
+ selector: '',
+ rules: {
+ 'flex-wrap': 'wrap',
+ 'align-items': 'center',
+ },
+ },
+ {
+ selector: ' > *',
+ rules: {
+ margin: '0',
+ },
+ },
+ ],
+ spacingStyles: [
+ {
+ selector: '',
+ rules: {
+ gap: null,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ };
+
+ it( 'should return fallback gap flex layout style, and all base styles, if block styles are enabled and blockGap is disabled', () => {
+ const style = { spacing: { blockGap: '12px' } };
+
+ const layoutStyles = getLayoutStyles( {
+ tree: layoutDefinitionsTree,
+ style,
+ selector: 'body',
+ hasBlockGapSupport: false,
+ hasFallbackGapSupport: true,
+ } );
+
+ expect( layoutStyles ).toEqual(
+ 'body .is-layout-flex { 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; }'
+ );
+ } );
+
+ it( 'should return fallback gap layout styles, and base styles, if blockGap is enabled, but there is no blockGap value', () => {
+ const style = {};
+
+ const layoutStyles = getLayoutStyles( {
+ tree: layoutDefinitionsTree,
+ style,
+ selector: 'body',
+ hasBlockGapSupport: true,
+ hasFallbackGapSupport: true,
+ } );
+
+ expect( layoutStyles ).toEqual(
+ 'body .is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }body .is-layout-flow > * + * { margin-block-start: 0.5em; margin-block-end: 0; }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; }'
+ );
+ } );
+
+ it( 'should return real gap layout style if blockGap is enabled, and base styles', () => {
+ const style = { spacing: { blockGap: '12px' } };
+
+ const layoutStyles = getLayoutStyles( {
+ tree: layoutDefinitionsTree,
+ style,
+ selector: 'body',
+ hasBlockGapSupport: true,
+ hasFallbackGapSupport: true,
+ } );
+
+ expect( layoutStyles ).toEqual(
+ 'body .is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }body .is-layout-flow > * + * { margin-block-start: 12px; margin-block-end: 0; }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; }'
+ );
+ } );
+
+ it( 'should return real gap layout style if blockGap is enabled', () => {
+ const style = { spacing: { blockGap: '12px' } };
+
+ const layoutStyles = getLayoutStyles( {
+ tree: layoutDefinitionsTree,
+ style,
+ selector: '.wp-block-group',
+ hasBlockGapSupport: true,
+ hasFallbackGapSupport: true,
+ } );
+
+ expect( layoutStyles ).toEqual(
+ '.wp-block-group.is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.wp-block-group.is-layout-flow > * + * { margin-block-start: 12px; margin-block-end: 0; }.wp-block-group.is-layout-flex { gap: 12px; }'
+ );
+ } );
+
+ it( 'should return fallback gap flex layout style for a block if blockGap is disabled, and a fallback value is provided', () => {
+ const style = { spacing: { blockGap: '12px' } };
+
+ const layoutStyles = getLayoutStyles( {
+ tree: layoutDefinitionsTree,
+ style,
+ selector: '.wp-block-group',
+ hasBlockGapSupport: false, // This means that the fallback value will be used instead of the "real" one.
+ hasFallbackGapSupport: true,
+ fallbackGapValue: '2em',
+ } );
+
+ expect( layoutStyles ).toEqual(
+ '.wp-block-group.is-layout-flex { gap: 2em; }'
+ );
+ } );
+ } );
} );
diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js
index 9a8ad4f54282b7..9db98c46a75ac6 100644
--- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js
+++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js
@@ -24,7 +24,10 @@ import {
} from '@wordpress/blocks';
import { useEffect, useState, useContext } from '@wordpress/element';
import { getCSSRules } from '@wordpress/style-engine';
-import { __unstablePresetDuotoneFilter as PresetDuotoneFilter } from '@wordpress/block-editor';
+import {
+ __unstablePresetDuotoneFilter as PresetDuotoneFilter,
+ __experimentalGetGapCSSValue as getGapCSSValue,
+} from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -231,6 +234,135 @@ function getStylesDeclarations( blockStyles = {} ) {
return output;
}
+/**
+ * Get generated CSS for layout styles by looking up layout definitions provided
+ * in theme.json, and outputting common layout styles, and specific blockGap values.
+ *
+ * @param {Object} props
+ * @param {Object} props.tree A theme.json tree containing layout definitions.
+ * @param {Object} props.style A style object containing spacing values.
+ * @param {string} props.selector Selector used to group together layout styling rules.
+ * @param {boolean} props.hasBlockGapSupport Whether or not the theme opts-in to blockGap support.
+ * @param {boolean} props.hasFallbackGapSupport Whether or not the theme allows fallback gap styles.
+ * @param {?string} props.fallbackGapValue An optional fallback gap value if no real gap value is available.
+ * @return {string} Generated CSS rules for the layout styles.
+ */
+export function getLayoutStyles( {
+ tree,
+ style,
+ selector,
+ hasBlockGapSupport,
+ hasFallbackGapSupport,
+ fallbackGapValue,
+} ) {
+ let ruleset = '';
+ let gapValue = hasBlockGapSupport
+ ? getGapCSSValue( style?.spacing?.blockGap )
+ : '';
+
+ // Ensure a fallback gap value for the root layout definitions,
+ // and use a fallback value if one is provided for the current block.
+ if ( hasFallbackGapSupport ) {
+ if ( selector === ROOT_BLOCK_SELECTOR ) {
+ gapValue = ! gapValue ? '0.5em' : gapValue;
+ } else if ( ! hasBlockGapSupport && fallbackGapValue ) {
+ gapValue = fallbackGapValue;
+ }
+ }
+
+ if ( gapValue && tree?.settings?.layout?.definitions ) {
+ Object.values( tree.settings.layout.definitions ).forEach(
+ ( { className, name, spacingStyles } ) => {
+ // Allow skipping default layout for themes that opt-in to block styles, but opt-out of blockGap.
+ if ( ! hasBlockGapSupport && 'default' === name ) {
+ return;
+ }
+
+ if ( spacingStyles?.length ) {
+ spacingStyles.forEach( ( spacingStyle ) => {
+ const declarations = [];
+
+ if ( spacingStyle.rules ) {
+ Object.entries( spacingStyle.rules ).forEach(
+ ( [ cssProperty, cssValue ] ) => {
+ declarations.push(
+ `${ cssProperty }: ${
+ cssValue ? cssValue : gapValue
+ }`
+ );
+ }
+ );
+ }
+
+ if ( declarations.length ) {
+ const combinedSelector =
+ selector === ROOT_BLOCK_SELECTOR
+ ? `${ selector } .${ className }${
+ spacingStyle?.selector || ''
+ }`
+ : `${ selector }.${ className }${
+ spacingStyle?.selector || ''
+ }`;
+ ruleset += `${ combinedSelector } { ${ declarations.join(
+ '; '
+ ) }; }`;
+ }
+ } );
+ }
+ }
+ );
+ // 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 }; }`;
+ }
+ }
+
+ // Output base styles
+ if (
+ selector === ROOT_BLOCK_SELECTOR &&
+ tree?.settings?.layout?.definitions
+ ) {
+ const validDisplayModes = [ 'block', 'flex', 'grid' ];
+ Object.values( tree.settings.layout.definitions ).forEach(
+ ( { className, displayMode, baseStyles } ) => {
+ if (
+ displayMode &&
+ validDisplayModes.includes( displayMode )
+ ) {
+ ruleset += `${ selector } .${ className } { display:${ displayMode }; }`;
+ }
+
+ if ( baseStyles?.length ) {
+ baseStyles.forEach( ( baseStyle ) => {
+ const declarations = [];
+
+ if ( baseStyle.rules ) {
+ Object.entries( baseStyle.rules ).forEach(
+ ( [ cssProperty, cssValue ] ) => {
+ declarations.push(
+ `${ cssProperty }: ${ cssValue }`
+ );
+ }
+ );
+ }
+
+ if ( declarations.length ) {
+ const combinedSelector = `${ selector } .${ className }${
+ baseStyle?.selector || ''
+ }`;
+ ruleset += `${ combinedSelector } { ${ declarations.join(
+ '; '
+ ) }; }`;
+ }
+ } );
+ }
+ }
+ );
+ }
+
+ return ruleset;
+}
+
export const getNodesWithStyles = ( tree, blockSelectors ) => {
const nodes = [];
@@ -267,9 +399,11 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => {
const blockStyles = pickStyleKeys( node );
if ( !! blockStyles && !! blockSelectors?.[ blockName ]?.selector ) {
nodes.push( {
- styles: blockStyles,
- selector: blockSelectors[ blockName ].selector,
duotoneSelector: blockSelectors[ blockName ].duotoneSelector,
+ fallbackGapValue: blockSelectors[ blockName ].fallbackGapValue,
+ hasLayoutSupport: blockSelectors[ blockName ].hasLayoutSupport,
+ selector: blockSelectors[ blockName ].selector,
+ styles: blockStyles,
} );
}
@@ -364,7 +498,12 @@ export const toCustomProperties = ( tree, blockSelectors ) => {
return ruleset;
};
-export const toStyles = ( tree, blockSelectors, hasBlockGapSupport ) => {
+export const toStyles = (
+ tree,
+ blockSelectors,
+ hasBlockGapSupport,
+ hasFallbackGapSupport
+) => {
const nodesWithStyles = getNodesWithStyles( tree, blockSelectors );
const nodesWithSettings = getNodesWithSettings( tree, blockSelectors );
@@ -377,63 +516,90 @@ export const toStyles = ( tree, blockSelectors, hasBlockGapSupport ) => {
* @link https://github.com/WordPress/gutenberg/issues/36147.
*/
let ruleset = 'body {margin: 0;}';
- nodesWithStyles.forEach( ( { selector, duotoneSelector, styles } ) => {
- const duotoneStyles = {};
- if ( styles?.filter ) {
- duotoneStyles.filter = styles.filter;
- delete styles.filter;
- }
+ nodesWithStyles.forEach(
+ ( {
+ selector,
+ duotoneSelector,
+ styles,
+ fallbackGapValue,
+ hasLayoutSupport,
+ } ) => {
+ const duotoneStyles = {};
+ if ( styles?.filter ) {
+ duotoneStyles.filter = styles.filter;
+ delete styles.filter;
+ }
- // Process duotone styles (they use color.__experimentalDuotone selector).
- if ( duotoneSelector ) {
- const duotoneDeclarations = getStylesDeclarations( duotoneStyles );
- if ( duotoneDeclarations.length === 0 ) {
- return;
+ // Process duotone styles (they use color.__experimentalDuotone selector).
+ if ( duotoneSelector ) {
+ const duotoneDeclarations =
+ getStylesDeclarations( duotoneStyles );
+ if ( duotoneDeclarations.length === 0 ) {
+ return;
+ }
+ ruleset =
+ ruleset +
+ `${ duotoneSelector }{${ duotoneDeclarations.join(
+ ';'
+ ) };}`;
}
- ruleset =
- ruleset +
- `${ duotoneSelector }{${ duotoneDeclarations.join( ';' ) };}`;
- }
- // Process the remaning block styles (they use either normal block class or __experimentalSelector).
- const declarations = getStylesDeclarations( styles );
- if ( declarations?.length ) {
- ruleset = ruleset + `${ selector }{${ declarations.join( ';' ) };}`;
- }
+ // Process blockGap and layout styles.
+ if ( ROOT_BLOCK_SELECTOR === selector || hasLayoutSupport ) {
+ ruleset += getLayoutStyles( {
+ tree,
+ style: styles,
+ selector,
+ hasBlockGapSupport,
+ hasFallbackGapSupport,
+ fallbackGapValue,
+ } );
+ }
- // Check for pseudo selector in `styles` and handle separately.
- const psuedoSelectorStyles = Object.entries( styles ).filter(
- ( [ key ] ) => key.startsWith( ':' )
- );
+ // Process the remaining block styles (they use either normal block class or __experimentalSelector).
+ const declarations = getStylesDeclarations( styles );
+ if ( declarations?.length ) {
+ ruleset =
+ ruleset + `${ selector }{${ declarations.join( ';' ) };}`;
+ }
- if ( psuedoSelectorStyles?.length ) {
- psuedoSelectorStyles.forEach( ( [ pseudoKey, pseudoRule ] ) => {
- const pseudoDeclarations = getStylesDeclarations( pseudoRule );
+ // Check for pseudo selector in `styles` and handle separately.
+ const pseudoSelectorStyles = Object.entries( styles ).filter(
+ ( [ key ] ) => key.startsWith( ':' )
+ );
- if ( ! pseudoDeclarations?.length ) {
- return;
- }
+ if ( pseudoSelectorStyles?.length ) {
+ pseudoSelectorStyles.forEach(
+ ( [ pseudoKey, pseudoStyle ] ) => {
+ const pseudoDeclarations =
+ getStylesDeclarations( pseudoStyle );
- // `selector` maybe provided in a form
- // where block level selectors have sub element
- // selectors appended to them as a comma seperated
- // string.
- // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`;
- // Split and append pseudo selector to create
- // the proper rules to target the elements.
- const _selector = selector
- .split( ',' )
- .map( ( sel ) => sel + pseudoKey )
- .join( ',' );
-
- const psuedoRule = `${ _selector }{${ pseudoDeclarations.join(
- ';'
- ) };}`;
-
- ruleset = ruleset + psuedoRule;
- } );
+ if ( ! pseudoDeclarations?.length ) {
+ return;
+ }
+
+ // `selector` maybe provided in a form
+ // where block level selectors have sub element
+ // selectors appended to them as a comma seperated
+ // string.
+ // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`;
+ // Split and append pseudo selector to create
+ // the proper rules to target the elements.
+ const _selector = selector
+ .split( ',' )
+ .map( ( sel ) => sel + pseudoKey )
+ .join( ',' );
+
+ const pseudoRule = `${ _selector }{${ pseudoDeclarations.join(
+ ';'
+ ) };}`;
+
+ ruleset = ruleset + pseudoRule;
+ }
+ );
+ }
}
- } );
+ );
/* Add alignment / layout styles */
ruleset =
@@ -447,12 +613,15 @@ export const toStyles = ( tree, blockSelectors, hasBlockGapSupport ) => {
'.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
if ( hasBlockGapSupport ) {
+ // Use fallback of `0.5em` just in case, however if there is blockGap support, there should nearly always be a real value.
+ const gapValue =
+ getGapCSSValue( tree?.styles?.spacing?.blockGap ) || '0.5em';
ruleset =
ruleset +
'.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
ruleset =
ruleset +
- '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
+ `.wp-site-blocks > * + * { margin-block-start: ${ gapValue }; }`;
}
nodesWithSettings.forEach( ( { selector, presets } ) => {
@@ -486,10 +655,15 @@ const getBlockSelectors = ( blockTypes ) => {
'.wp-block-' + name.replace( 'core/', '' ).replace( '/', '-' );
const duotoneSelector =
blockType?.supports?.color?.__experimentalDuotone ?? null;
+ const hasLayoutSupport = !! blockType?.supports?.__experimentalLayout;
+ const fallbackGapValue =
+ blockType?.supports?.spacing?.blockGap?.__experimentalDefault;
result[ name ] = {
+ duotoneSelector,
+ fallbackGapValue,
+ hasLayoutSupport,
name,
selector,
- duotoneSelector,
};
} );
@@ -503,6 +677,7 @@ export function useGlobalStylesOutput() {
const { merged: mergedConfig } = useContext( GlobalStylesContext );
const [ blockGap ] = useSetting( 'spacing.blockGap' );
const hasBlockGapSupport = blockGap !== null;
+ const hasFallbackGapSupport = ! hasBlockGapSupport; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback styles support.
useEffect( () => {
if ( ! mergedConfig?.styles || ! mergedConfig?.settings ) {
@@ -517,7 +692,8 @@ export function useGlobalStylesOutput() {
const globalStyles = toStyles(
mergedConfig,
blockSelectors,
- hasBlockGapSupport
+ hasBlockGapSupport,
+ hasFallbackGapSupport
);
const filters = toSvgFilters( mergedConfig, blockSelectors );
setStylesheets( [
@@ -532,7 +708,7 @@ export function useGlobalStylesOutput() {
] );
setSettings( mergedConfig.settings );
setSvgFilters( filters );
- }, [ mergedConfig ] );
+ }, [ hasBlockGapSupport, hasFallbackGapSupport, mergedConfig ] );
return [ stylesheets, settings, svgFilters, hasBlockGapSupport ];
}
diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php
index a662b8efb1d9e6..0c830d8de9a9c9 100644
--- a/phpunit/class-wp-theme-json-test.php
+++ b/phpunit/class-wp-theme-json-test.php
@@ -7,7 +7,184 @@
*/
class WP_Theme_JSON_Gutenberg_Test extends WP_UnitTestCase {
+ /**
+ * @dataProvider data_get_layout_definitions
+ *
+ * @param array $layout_definitions Layout definitions as stored in core theme.json.
+ */
+ public function test_get_stylesheet_generates_layout_styles( $layout_definitions ) {
+ $theme_json = new WP_Theme_JSON_Gutenberg(
+ array(
+ 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
+ 'settings' => array(
+ 'layout' => array(
+ 'definitions' => $layout_definitions,
+ ),
+ 'spacing' => array(
+ 'blockGap' => true,
+ ),
+ ),
+ 'styles' => array(
+ 'spacing' => array(
+ 'blockGap' => '1em',
+ ),
+ ),
+ ),
+ 'default'
+ );
+
+ // Results also include root site blocks styles.
+ $this->assertEquals(
+ '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; }.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }.wp-site-blocks > * + * { margin-block-start: 1em; }body { --wp--style--block-gap: 1em; }body .is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}body .is-layout-flow > * + *{margin-block-start: 1em;margin-block-end: 0;}body .is-layout-flex{gap: 1em;}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;}',
+ $theme_json->get_stylesheet( array( 'styles' ) )
+ );
+ }
+
+ /**
+ * @dataProvider data_get_layout_definitions
+ *
+ * @param array $layout_definitions Layout definitions as stored in core theme.json.
+ */
+ public function test_get_stylesheet_generates_fallback_gap_layout_styles( $layout_definitions ) {
+ $theme_json = new WP_Theme_JSON_Gutenberg(
+ array(
+ 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
+ 'settings' => array(
+ 'layout' => array(
+ 'definitions' => $layout_definitions,
+ ),
+ 'spacing' => array(
+ 'blockGap' => null,
+ ),
+ ),
+ 'styles' => array(
+ 'spacing' => array(
+ 'blockGap' => '1em',
+ ),
+ ),
+ ),
+ 'default'
+ );
+ $stylesheet = $theme_json->get_stylesheet( array( 'styles' ) );
+
+ // Results also include root site blocks styles.
+ $this->assertEquals(
+ '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; }body .is-layout-flex{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;}',
+ $stylesheet
+ );
+ }
+
+ /**
+ * @dataProvider data_get_layout_definitions
+ *
+ * @param array $layout_definitions Layout definitions as stored in core theme.json.
+ */
+ public function test_get_stylesheet_generates_base_fallback_gap_layout_styles( $layout_definitions ) {
+ $theme_json = new WP_Theme_JSON_Gutenberg(
+ array(
+ 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
+ 'settings' => array(
+ 'layout' => array(
+ 'definitions' => $layout_definitions,
+ ),
+ 'spacing' => array(
+ 'blockGap' => null,
+ ),
+ ),
+ ),
+ 'default'
+ );
+ $stylesheet = $theme_json->get_stylesheet( array( 'base-layout-styles' ) );
+ // Note the `base-layout-styles` includes a fallback gap for the Columns block for backwards compatibility.
+ $this->assertEquals(
+ 'body .is-layout-flex{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;}.wp-block-columns.is-layout-flex{gap: 2em;}',
+ $stylesheet
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_get_layout_definitions() {
+ return array(
+ 'layout definitions' => array(
+ array(
+ 'default' => array(
+ 'name' => 'default',
+ 'slug' => 'flow',
+ 'className' => 'is-layout-flow',
+ 'baseStyles' => array(
+ array(
+ 'selector' => ' > .alignleft',
+ 'rules' => array(
+ 'float' => 'left',
+ 'margin-inline-start' => '0',
+ 'margin-inline-end' => '2em',
+ ),
+ ),
+ array(
+ 'selector' => ' > .alignright',
+ 'rules' => array(
+ 'float' => 'right',
+ 'margin-inline-start' => '2em',
+ 'margin-inline-end' => '0',
+ ),
+ ),
+ array(
+ 'selector' => ' > .aligncenter',
+ 'rules' => array(
+ 'margin-left' => 'auto !important',
+ 'margin-right' => 'auto !important',
+ ),
+ ),
+ ),
+ 'spacingStyles' => array(
+ array(
+ 'selector' => ' > *',
+ 'rules' => array(
+ 'margin-block-start' => '0',
+ 'margin-block-end' => '0',
+ ),
+ ),
+ array(
+ 'selector' => ' > * + *',
+ 'rules' => array(
+ 'margin-block-start' => null,
+ 'margin-block-end' => '0',
+ ),
+ ),
+ ),
+ ),
+ 'flex' => array(
+ 'name' => 'flex',
+ 'slug' => 'flex',
+ 'className' => 'is-layout-flex',
+ 'displayMode' => 'flex',
+ 'baseStyles' => array(
+ array(
+ 'selector' => '',
+ 'rules' => array(
+ 'flex-wrap' => 'wrap',
+ 'align-items' => 'center',
+ ),
+ ),
+ ),
+ 'spacingStyles' => array(
+ array(
+ 'selector' => '',
+ 'rules' => array(
+ 'gap' => null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
function test_get_stylesheet_handles_whitelisted_element_pseudo_selectors() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(