PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/block-supports/states.php +305 -35 23.3.2 → 24.1.0 View file →
@@ -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,