| @@ -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. |
| @@ -196,8 +292,97 @@ | ||
| 196 | 292 | return $root_style; |
| 197 | 293 | } |
| 198 | 294 | |
| 199 | 295 | /** |
| 296 | + * Generates all element selectors for a block root selector. | |
| 297 | + * | |
| 298 | + * @param string $root_selector The block root CSS selector. | |
| 299 | + * @return string[] Element selectors keyed by element name. | |
| 300 | + */ | |
| 301 | +function gutenberg_get_block_state_element_selectors( $root_selector ) { | |
| 302 | + if ( ! is_string( $root_selector ) || '' === trim( $root_selector ) ) { | |
| 303 | + return array(); | |
| 304 | + } | |
| 305 | + | |
| 306 | + $block_selectors = gutenberg_split_selector_list( $root_selector ); | |
| 307 | + $element_selectors = array(); | |
| 308 | + | |
| 309 | + foreach ( WP_Theme_JSON_Gutenberg::ELEMENTS as $element_name => $element_selector ) { | |
| 310 | + $selectors = array(); | |
| 311 | + | |
| 312 | + foreach ( $block_selectors as $block_selector ) { | |
| 313 | + $block_selector = trim( $block_selector ); | |
| 314 | + if ( '' === $block_selector ) { | |
| 315 | + continue; | |
| 316 | + } | |
| 317 | + | |
| 318 | + if ( $block_selector === $element_selector ) { | |
| 319 | + $selectors = array( $element_selector ); | |
| 320 | + break; | |
| 321 | + } | |
| 322 | + | |
| 323 | + $selector_prefix = "$block_selector "; | |
| 324 | + if ( ! str_contains( $element_selector, ',' ) ) { | |
| 325 | + $selectors[] = $selector_prefix . $element_selector; | |
| 326 | + continue; | |
| 327 | + } | |
| 328 | + | |
| 329 | + $prepended_selectors = array(); | |
| 330 | + foreach ( gutenberg_split_selector_list( $element_selector ) as $selector ) { | |
| 331 | + $prepended_selectors[] = $selector_prefix . $selector; | |
| 332 | + } | |
| 333 | + $selectors[] = implode( ',', $prepended_selectors ); | |
| 334 | + } | |
| 335 | + | |
| 336 | + if ( ! empty( $selectors ) ) { | |
| 337 | + $element_selectors[ $element_name ] = implode( ',', $selectors ); | |
| 338 | + } | |
| 339 | + } | |
| 340 | + | |
| 341 | + return $element_selectors; | |
| 342 | +} | |
| 343 | + | |
| 344 | +/** | |
| 345 | + * Adds a compiled state style rule to a rule list. | |
| 346 | + * | |
| 347 | + * @param array $css_rules Style rules. | |
| 348 | + * @param string $state Pseudo-state selector. | |
| 349 | + * @param string|null $selector Block, feature, or element selector. | |
| 350 | + * @param array $style Style object. | |
| 351 | + * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query. | |
| 352 | + */ | |
| 353 | +function gutenberg_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null ) { | |
| 354 | + if ( empty( $style ) || ! is_array( $style ) ) { | |
| 355 | + return; | |
| 356 | + } | |
| 357 | + | |
| 358 | + $style = gutenberg_get_state_style_with_fallback_dimension_styles( $style ); | |
| 359 | + | |
| 360 | + $compiled = gutenberg_style_engine_get_styles( | |
| 361 | + gutenberg_normalize_state_style_for_css_output( $style ) | |
| 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 | + } | |
| 369 | + | |
| 370 | + if ( empty( $declarations ) ) { | |
| 371 | + return; | |
| 372 | + } | |
| 373 | + | |
| 374 | + $css_rules[] = array( | |
| 375 | + 'state' => $state, | |
| 376 | + 'selector' => $selector, | |
| 377 | + 'declarations' => $declarations, | |
| 378 | + ); | |
| 379 | + if ( ! empty( $rules_group ) ) { | |
| 380 | + $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group; | |
| 381 | + } | |
| 382 | +} | |
| 383 | + | |
| 384 | +/** | |
| 200 | 385 | * Builds compiled state style rules, preserving the selector each rule targets. |
| 201 | 386 | * |
| 202 | 387 | * @param array $state_styles Map of state to style array. |
| 203 | 388 | * @param WP_Block_Type $block_type Block type. |
| @@ -215,22 +400,15 @@ | ||
| 215 | 400 | continue; |
| 216 | 401 | } |
| 217 | 402 | |
| 218 | 403 | foreach ( gutenberg_get_state_style_groups( $state_style, $block_selectors ) as $group ) { |
| 219 | - $compiled = gutenberg_style_engine_get_styles( | |
| 220 | - gutenberg_normalize_state_style_for_css_output( $group['style'] ) | |
| 404 | + gutenberg_add_block_state_style_rule( | |
| 405 | + $css_rules, | |
| 406 | + $state, | |
| 407 | + $group['selector'], | |
| 408 | + $group['style'], | |
| 409 | + $rules_group | |
| 221 | 410 | ); |
| 222 | - | |
| 223 | - if ( ! empty( $compiled['declarations'] ) ) { | |
| 224 | - $css_rules[] = array( | |
| 225 | - 'state' => $state, | |
| 226 | - 'selector' => $group['selector'], | |
| 227 | - 'declarations' => $compiled['declarations'], | |
| 228 | - ); | |
| 229 | - if ( ! empty( $rules_group ) ) { | |
| 230 | - $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group; | |
| 231 | - } | |
| 232 | - } | |
| 233 | 411 | } |
| 234 | 412 | } |
| 235 | 413 | |
| 236 | 414 | return $css_rules; |
| @@ -348,16 +526,31 @@ | ||
| 348 | 526 | return $block_content; |
| 349 | 527 | } |
| 350 | 528 | |
| 351 | 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 | + | |
| 352 | 544 | $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); |
| 353 | 545 | if ( ! $block_type ) { |
| 354 | 546 | return $block_content; |
| 355 | 547 | } |
| 356 | 548 | |
| 357 | - $supported_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array(); | |
| 358 | - $style = $block['attrs']['style'] ?? array(); | |
| 359 | - $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 ); | |
| 360 | 553 | |
| 361 | 554 | foreach ( $supported_pseudo_states as $pseudo_state ) { |
| 362 | 555 | if ( empty( $style[ $pseudo_state ] ) || ! is_array( $style[ $pseudo_state ] ) ) { |
| 363 | 556 | continue; |
| @@ -371,9 +564,9 @@ | ||
| 371 | 564 | ) |
| 372 | 565 | ); |
| 373 | 566 | } |
| 374 | 567 | |
| 375 | - foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) { | |
| 568 | + foreach ( $responsive_media_queries as $breakpoint => $media_query ) { | |
| 376 | 569 | if ( empty( $style[ $breakpoint ] ) || ! is_array( $style[ $breakpoint ] ) ) { |
| 377 | 570 | continue; |
| 378 | 571 | } |
| 379 | 572 | |
| @@ -392,8 +585,61 @@ | ||
| 392 | 585 | ) |
| 393 | 586 | ); |
| 394 | 587 | } |
| 395 | 588 | |
| 589 | + if ( | |
| 590 | + ! empty( $style[ $breakpoint ]['elements'] ) && | |
| 591 | + is_array( $style[ $breakpoint ]['elements'] ) | |
| 592 | + ) { | |
| 593 | + $element_selectors = gutenberg_get_block_state_element_selectors( | |
| 594 | + wp_get_block_css_selector( $block_type ) | |
| 595 | + ); | |
| 596 | + | |
| 597 | + foreach ( | |
| 598 | + $style[ $breakpoint ]['elements'] as $element_name => $element_style | |
| 599 | + ) { | |
| 600 | + if ( | |
| 601 | + empty( $element_style ) || | |
| 602 | + ! is_array( $element_style ) || | |
| 603 | + empty( $element_selectors[ $element_name ] ) | |
| 604 | + ) { | |
| 605 | + continue; | |
| 606 | + } | |
| 607 | + | |
| 608 | + $element_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] | |
| 609 | + ?? array(); | |
| 610 | + $root_element_style = gutenberg_get_root_state_style( | |
| 611 | + $element_style, | |
| 612 | + $element_pseudo_states | |
| 613 | + ); | |
| 614 | + | |
| 615 | + gutenberg_add_block_state_style_rule( | |
| 616 | + $css_rules, | |
| 617 | + '', | |
| 618 | + $element_selectors[ $element_name ], | |
| 619 | + $root_element_style, | |
| 620 | + $media_query | |
| 621 | + ); | |
| 622 | + | |
| 623 | + foreach ( $element_pseudo_states as $pseudo_state ) { | |
| 624 | + if ( | |
| 625 | + empty( $element_style[ $pseudo_state ] ) || | |
| 626 | + ! is_array( $element_style[ $pseudo_state ] ) | |
| 627 | + ) { | |
| 628 | + continue; | |
| 629 | + } | |
| 630 | + | |
| 631 | + gutenberg_add_block_state_style_rule( | |
| 632 | + $css_rules, | |
| 633 | + $pseudo_state, | |
| 634 | + $element_selectors[ $element_name ], | |
| 635 | + $element_style[ $pseudo_state ], | |
| 636 | + $media_query | |
| 637 | + ); | |
| 638 | + } | |
| 639 | + } | |
| 640 | + } | |
| 641 | + | |
| 396 | 642 | foreach ( $supported_pseudo_states as $pseudo_state ) { |
| 397 | 643 | if ( empty( $style[ $breakpoint ][ $pseudo_state ] ) || ! is_array( $style[ $breakpoint ][ $pseudo_state ] ) ) { |
| 398 | 644 | continue; |
| 399 | 645 | } |
| @@ -430,27 +676,51 @@ | ||
| 430 | 676 | * with the base layout and target the correct (inner) wrapper element. |
| 431 | 677 | */ |
| 432 | 678 | $style_rules = array(); |
| 433 | 679 | foreach ( $css_rules as $rule ) { |
| 434 | - $declarations = array(); | |
| 435 | - foreach ( $rule['declarations'] as $property => $value ) { | |
| 436 | - $declarations[ $property ] = is_string( $value ) && str_contains( $value, '!important' ) | |
| 437 | - ? $value | |
| 438 | - : $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 | + ); | |
| 439 | 691 | } |
| 440 | - $declarations = gutenberg_get_state_declarations_with_fallback_border_styles( $declarations ); | |
| 441 | - $style_rule = array( | |
| 442 | - 'selector' => gutenberg_build_state_selector( | |
| 443 | - ".$unique_class", | |
| 444 | - $rule['selector'], | |
| 445 | - $rule['state'] | |
| 446 | - ), | |
| 447 | - 'declarations' => $declarations, | |
| 692 | + $selector = gutenberg_build_state_selector( | |
| 693 | + ".$unique_class", | |
| 694 | + $rule['selector'], | |
| 695 | + $rule['state'] | |
| 448 | 696 | ); |
| 697 | + $important_style_rule = array( | |
| 698 | + 'selector' => $selector, | |
| 699 | + 'declarations' => $important_declarations, | |
| 700 | + ); | |
| 449 | 701 | if ( ! empty( $rule['rules_group'] ) ) { |
| 450 | - $style_rule['rules_group'] = $rule['rules_group']; | |
| 702 | + $important_style_rule['rules_group'] = $rule['rules_group']; | |
| 451 | 703 | } |
| 452 | - $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; | |
| 453 | 723 | } |
| 454 | 724 | |
| 455 | 725 | gutenberg_style_engine_get_stylesheet_from_css_rules( |
| 456 | 726 | $style_rules, |