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(