| @@ -3,9 +3,9 @@ | ||
| 3 | 3 | * Block state support for frontend CSS generation. |
| 4 | 4 | * |
| 5 | 5 | * Generates scoped CSS for per-instance state styles declared in block attributes, |
| 6 | 6 | * including pseudo-states (e.g., `style[':hover']`) and responsive states |
| 7 | - * (e.g., `style['mobile']` and `style['mobile'][':hover']`). | |
| 7 | + * (e.g., `style['@mobile']` and `style['@mobile'][':hover']`). | |
| 8 | 8 | * |
| 9 | 9 | * @package WordPress |
| 10 | 10 | */ |
| 11 | 11 | |
| @@ -31,10 +31,23 @@ | ||
| 31 | 31 | if ( ! is_string( $value ) || ! str_starts_with( $value, 'var:preset|' ) ) { |
| 32 | 32 | return $value; |
| 33 | 33 | } |
| 34 | 34 | |
| 35 | - $unwrapped_name = str_replace( '|', '--', substr( $value, strlen( 'var:' ) ) ); | |
| 36 | - return "var(--wp--$unwrapped_name)"; | |
| 35 | + $parts = explode( '|', substr( $value, strlen( 'var:' ) ) ); | |
| 36 | + | |
| 37 | + /* | |
| 38 | + * The slug is kebab-cased when the preset's custom property is generated, | |
| 39 | + * so a reference has to be converted the same way or it points at a | |
| 40 | + * property that does not exist (`--wp--preset--font-size--3xl` for a preset | |
| 41 | + * generated as `--wp--preset--font-size--3-xl`). Mirrors | |
| 42 | + * `WP_Theme_JSON_Gutenberg::convert_custom_properties()` and the JS style | |
| 43 | + * engine's `getCSSValueFromRawStyle()`. | |
| 44 | + */ | |
| 45 | + if ( 3 === count( $parts ) ) { | |
| 46 | + $parts[2] = _wp_to_kebab_case( $parts[2] ); | |
| 47 | + } | |
| 48 | + | |
| 49 | + return 'var(--wp--' . implode( '--', $parts ) . ')'; | |
| 37 | 50 | } |
| 38 | 51 | |
| 39 | 52 | /** |
| 40 | 53 | * Normalizes a state style object before generating CSS declarations. |
| @@ -90,8 +103,91 @@ | ||
| 90 | 103 | return $declarations; |
| 91 | 104 | } |
| 92 | 105 | |
| 93 | 106 | /** |
| 107 | + * Adds background reset declarations to prevent gradient/solid color conflicts. | |
| 108 | + * | |
| 109 | + * When a state sets a solid background-color, any gradient applied to the | |
| 110 | + * default state (via `background` shorthand or `background-image`) must be | |
| 111 | + * explicitly cleared. Without this, the gradient image layer remains visible | |
| 112 | + * on top of the solid hover color even when `!important` is used, because | |
| 113 | + * `background-color` and `background-image` are separate CSS properties. | |
| 114 | + * | |
| 115 | + * @param array $declarations CSS declarations generated by the style engine. | |
| 116 | + * @return array CSS declarations with background resets applied where needed. | |
| 117 | + */ | |
| 118 | +function gutenberg_get_state_declarations_with_background_resets( $declarations ) { | |
| 119 | + if ( ! is_array( $declarations ) ) { | |
| 120 | + return $declarations; | |
| 121 | + } | |
| 122 | + | |
| 123 | + $has_background_color = isset( $declarations['background-color'] ) && '' !== $declarations['background-color']; | |
| 124 | + $has_background = isset( $declarations['background'] ) && '' !== $declarations['background']; | |
| 125 | + $has_background_image = isset( $declarations['background-image'] ) && '' !== $declarations['background-image']; | |
| 126 | + | |
| 127 | + /* | |
| 128 | + * When the state sets a solid background-color but no gradient of its own, | |
| 129 | + * emit `background-image: unset` to clear any gradient (whether stored as | |
| 130 | + * the `background` shorthand or as `background-image`) that was applied to | |
| 131 | + * the default / normal state via an inline style attribute. The declaration | |
| 132 | + * is marked important when the state rule is registered with the style engine. | |
| 133 | + */ | |
| 134 | + if ( $has_background_color && ! $has_background && ! $has_background_image ) { | |
| 135 | + $declarations['background-image'] = 'unset'; | |
| 136 | + } | |
| 137 | + | |
| 138 | + return $declarations; | |
| 139 | +} | |
| 140 | + | |
| 141 | +/** | |
| 142 | + * Adds fallback dimension styles for aspectRatio and height block-support values. | |
| 143 | + * | |
| 144 | + * @param array $state_style State style object. | |
| 145 | + * @return array State style object with fallback dimension styles applied where needed. | |
| 146 | + */ | |
| 147 | +function gutenberg_get_state_style_with_fallback_dimension_styles( $state_style ) { | |
| 148 | + if ( ! is_array( $state_style ) ) { | |
| 149 | + return $state_style; | |
| 150 | + } | |
| 151 | + | |
| 152 | + $dimensions = isset( $state_style['dimensions'] ) && is_array( $state_style['dimensions'] ) | |
| 153 | + ? $state_style['dimensions'] | |
| 154 | + : array(); | |
| 155 | + | |
| 156 | + if ( empty( $dimensions ) ) { | |
| 157 | + return $state_style; | |
| 158 | + } | |
| 159 | + | |
| 160 | + if ( gutenberg_is_explicit_aspect_ratio_value( $dimensions['aspectRatio'] ?? null ) ) { | |
| 161 | + return array_replace_recursive( | |
| 162 | + $state_style, | |
| 163 | + array( | |
| 164 | + 'dimensions' => array( | |
| 165 | + 'minHeight' => 'unset', | |
| 166 | + 'height' => 'unset', | |
| 167 | + ), | |
| 168 | + ) | |
| 169 | + ); | |
| 170 | + } | |
| 171 | + | |
| 172 | + $has_min_height = isset( $dimensions['minHeight'] ) && ( is_string( $dimensions['minHeight'] ) || is_numeric( $dimensions['minHeight'] ) ) && '' !== trim( (string) $dimensions['minHeight'] ); | |
| 173 | + $has_height = isset( $dimensions['height'] ) && ( is_string( $dimensions['height'] ) || is_numeric( $dimensions['height'] ) ) && '' !== trim( (string) $dimensions['height'] ); | |
| 174 | + | |
| 175 | + if ( $has_min_height || $has_height ) { | |
| 176 | + return array_replace_recursive( | |
| 177 | + $state_style, | |
| 178 | + array( | |
| 179 | + 'dimensions' => array( | |
| 180 | + 'aspectRatio' => 'unset', | |
| 181 | + ), | |
| 182 | + ) | |
| 183 | + ); | |
| 184 | + } | |
| 185 | + | |
| 186 | + return $state_style; | |
| 187 | +} | |
| 188 | + | |
| 189 | +/** | |
| 94 | 190 | * Adds a style fragment to a selector-keyed state style group. |
| 95 | 191 | * |
| 96 | 192 | * @param array $groups Selector-keyed style groups. |
| 97 | 193 | * @param string|null $selector Block or feature selector. |
| @@ -258,13 +354,21 @@ | ||
| 258 | 354 | if ( empty( $style ) || ! is_array( $style ) ) { |
| 259 | 355 | return; |
| 260 | 356 | } |
| 261 | 357 | |
| 262 | - $compiled = gutenberg_style_engine_get_styles( | |
| 358 | + $style = gutenberg_get_state_style_with_fallback_dimension_styles( $style ); | |
| 359 | + | |
| 360 | + $compiled = gutenberg_style_engine_get_styles( | |
| 263 | 361 | gutenberg_normalize_state_style_for_css_output( $style ) |
| 264 | 362 | ); |
| 363 | + $declarations = $compiled['declarations'] ?? array(); | |
| 364 | + $text_align = $style['typography']['textAlign'] ?? null; | |
| 365 | + // Base text alignment is class-based, so state styles need a declaration. | |
| 366 | + if ( is_string( $text_align ) && '' !== trim( $text_align ) ) { | |
| 367 | + $declarations['text-align'] = $text_align; | |
| 368 | + } | |
| 265 | 369 | |
| 266 | - if ( empty( $compiled['declarations'] ) ) { | |
| 370 | + if ( empty( $declarations ) ) { | |
| 267 | 371 | return; |
| 268 | 372 | } |
| 269 | 373 | |
| 270 | 374 | $css_rules[] = array( |
| @@ -269,9 +373,9 @@ | ||
| 269 | 373 | |
| 270 | 374 | $css_rules[] = array( |
| 271 | 375 | 'state' => $state, |
| 272 | 376 | 'selector' => $selector, |
| 273 | - 'declarations' => $compiled['declarations'], | |
| 377 | + 'declarations' => $declarations, | |
| 274 | 378 | ); |
| 275 | 379 | if ( ! empty( $rules_group ) ) { |
| 276 | 380 | $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group; |
| 277 | 381 | } |
| @@ -422,16 +526,31 @@ | ||
| 422 | 526 | return $block_content; |
| 423 | 527 | } |
| 424 | 528 | |
| 425 | 529 | $block_name = $block['blockName']; |
| 530 | + | |
| 531 | + /* | |
| 532 | + * Every CSS rule this function can produce is keyed off the block's `style` | |
| 533 | + * attribute. Without it there is nothing to generate, so bail before doing | |
| 534 | + * any of the lookups below — this runs for every block on every request. | |
| 535 | + */ | |
| 536 | + $raw_style = $block['attrs']['style'] ?? array(); | |
| 537 | + if ( empty( $raw_style ) || ! is_array( $raw_style ) ) { | |
| 538 | + return $block_content; | |
| 539 | + } | |
| 540 | + | |
| 541 | + // Keep the existing block registry lookup here. | |
| 542 | + $style = gutenberg_resolve_style_state_aliases( $raw_style, $block_name ); | |
| 543 | + | |
| 426 | 544 | $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); |
| 427 | 545 | if ( ! $block_type ) { |
| 428 | 546 | return $block_content; |
| 429 | 547 | } |
| 430 | 548 | |
| 431 | - $supported_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array(); | |
| 432 | - $style = $block['attrs']['style'] ?? array(); | |
| 433 | - $css_rules = array(); | |
| 549 | + $supported_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array(); | |
| 550 | + $css_rules = array(); | |
| 551 | + $viewport_settings = gutenberg_get_global_settings( array( 'viewport' ) ); | |
| 552 | + $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings ); | |
| 434 | 553 | |
| 435 | 554 | foreach ( $supported_pseudo_states as $pseudo_state ) { |
| 436 | 555 | if ( empty( $style[ $pseudo_state ] ) || ! is_array( $style[ $pseudo_state ] ) ) { |
| 437 | 556 | continue; |
| @@ -445,9 +564,9 @@ | ||
| 445 | 564 | ) |
| 446 | 565 | ); |
| 447 | 566 | } |
| 448 | 567 | |
| 449 | - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) { | |
| 568 | + foreach ( $responsive_media_queries as $breakpoint => $media_query ) { | |
| 450 | 569 | if ( empty( $style[ $breakpoint ] ) || ! is_array( $style[ $breakpoint ] ) ) { |
| 451 | 570 | continue; |
| 452 | 571 | } |
| 453 | 572 | |
| @@ -557,27 +676,51 @@ | ||
| 557 | 676 | * with the base layout and target the correct (inner) wrapper element. |
| 558 | 677 | */ |
| 559 | 678 | $style_rules = array(); |
| 560 | 679 | foreach ( $css_rules as $rule ) { |
| 561 | - $declarations = array(); | |
| 562 | - foreach ( $rule['declarations'] as $property => $value ) { | |
| 563 | - $declarations[ $property ] = is_string( $value ) && str_contains( $value, '!important' ) | |
| 564 | - ? $value | |
| 565 | - : $value . ' !important'; | |
| 680 | + $declarations = $rule['declarations']; | |
| 681 | + $important_declaration_values = gutenberg_get_state_declarations_with_background_resets( $declarations ); | |
| 682 | + $important_declarations = new WP_Style_Engine_CSS_Declarations_Gutenberg(); | |
| 683 | + foreach ( $important_declaration_values as $property => $value ) { | |
| 684 | + $important_declarations->add_declaration( | |
| 685 | + $property, | |
| 686 | + $value, | |
| 687 | + array( | |
| 688 | + 'important' => true, | |
| 689 | + ) | |
| 690 | + ); | |
| 566 | 691 | } |
| 567 | - $declarations = gutenberg_get_state_declarations_with_fallback_border_styles( $declarations ); | |
| 568 | - $style_rule = array( | |
| 569 | - 'selector' => gutenberg_build_state_selector( | |
| 570 | - ".$unique_class", | |
| 571 | - $rule['selector'], | |
| 572 | - $rule['state'] | |
| 573 | - ), | |
| 574 | - 'declarations' => $declarations, | |
| 692 | + $selector = gutenberg_build_state_selector( | |
| 693 | + ".$unique_class", | |
| 694 | + $rule['selector'], | |
| 695 | + $rule['state'] | |
| 575 | 696 | ); |
| 697 | + $important_style_rule = array( | |
| 698 | + 'selector' => $selector, | |
| 699 | + 'declarations' => $important_declarations, | |
| 700 | + ); | |
| 576 | 701 | if ( ! empty( $rule['rules_group'] ) ) { |
| 577 | - $style_rule['rules_group'] = $rule['rules_group']; | |
| 702 | + $important_style_rule['rules_group'] = $rule['rules_group']; | |
| 578 | 703 | } |
| 579 | - $style_rules[] = $style_rule; | |
| 704 | + $style_rules[] = $important_style_rule; | |
| 705 | + | |
| 706 | + $fallback_declarations = gutenberg_get_state_declarations_with_fallback_border_styles( $declarations ); | |
| 707 | + foreach ( array_keys( $declarations ) as $property ) { | |
| 708 | + unset( $fallback_declarations[ $property ] ); | |
| 709 | + } | |
| 710 | + | |
| 711 | + if ( empty( $fallback_declarations ) ) { | |
| 712 | + continue; | |
| 713 | + } | |
| 714 | + | |
| 715 | + $fallback_style_rule = array( | |
| 716 | + 'selector' => $selector, | |
| 717 | + 'declarations' => $fallback_declarations, | |
| 718 | + ); | |
| 719 | + if ( ! empty( $rule['rules_group'] ) ) { | |
| 720 | + $fallback_style_rule['rules_group'] = $rule['rules_group']; | |
| 721 | + } | |
| 722 | + $style_rules[] = $fallback_style_rule; | |
| 580 | 723 | } |
| 581 | 724 | |
| 582 | 725 | gutenberg_style_engine_get_stylesheet_from_css_rules( |
| 583 | 726 | $style_rules, |