PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
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 → trunk 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,