Commit 7483c46357 for wordpress.org

commit 7483c463578c340adb525e952c1e307ff1474ed9
Author: isabel_brison <isabel_brison@git.wordpress.org>
Date:   Thu Oct 15 05:53:50 2026 +0000

    Editor: enable custom CSS on style states.

    Enables viewport and pseudo state-specific custom CSS on block instances.

    Props isabel_brison, ramonopoly.
    Fixes #66288.


    Built from https://develop.svn.wordpress.org/trunk@64353


    git-svn-id: http://core.svn.wordpress.org/trunk@63503 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/block-supports/custom-css.php b/wp-includes/block-supports/custom-css.php
index 66fd9469dd..f5318d075f 100644
--- a/wp-includes/block-supports/custom-css.php
+++ b/wp-includes/block-supports/custom-css.php
@@ -5,6 +5,124 @@
  * @package WordPress
  */

+/**
+ * Checks whether a style node has a non-empty custom CSS string.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param mixed $style_node A style node, possibly containing a `css` key.
+ * @return bool Whether the node has a non-empty custom CSS string.
+ */
+function wp_custom_css_has_value( $style_node ) {
+	return is_array( $style_node )
+		&& isset( $style_node['css'] )
+		&& is_string( $style_node['css'] )
+		&& '' !== trim( $style_node['css'] );
+}
+
+/**
+ * Collects custom CSS defined for a block instance across its default,
+ * pseudo-selector, and viewport states.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param array  $style                    The block's `style` attribute.
+ * @param string $block_name               Block name.
+ * @param array  $responsive_media_queries Media queries keyed by viewport state name.
+ * @return array[] Custom CSS entries with `css`, `pseudo`, and `media_query` keys.
+ */
+function wp_get_custom_css_state_entries( $style, $block_name, $responsive_media_queries ) {
+	$entries = array();
+	if ( empty( $style ) || ! is_array( $style ) ) {
+		return $entries;
+	}
+
+	$supported_pseudo_states = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
+
+	if ( wp_custom_css_has_value( $style ) ) {
+		$entries[] = array(
+			'css'         => $style['css'],
+			'pseudo'      => null,
+			'media_query' => null,
+		);
+	}
+
+	foreach ( $supported_pseudo_states as $pseudo_state ) {
+		$pseudo_style = $style[ $pseudo_state ] ?? null;
+		if ( wp_custom_css_has_value( $pseudo_style ) ) {
+			$entries[] = array(
+				'css'         => $pseudo_style['css'],
+				'pseudo'      => $pseudo_state,
+				'media_query' => null,
+			);
+		}
+	}
+
+	foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
+		$breakpoint_style = $style[ $breakpoint ] ?? null;
+		if ( ! is_array( $breakpoint_style ) ) {
+			continue;
+		}
+
+		if ( wp_custom_css_has_value( $breakpoint_style ) ) {
+			$entries[] = array(
+				'css'         => $breakpoint_style['css'],
+				'pseudo'      => null,
+				'media_query' => $media_query,
+			);
+		}
+
+		foreach ( $supported_pseudo_states as $pseudo_state ) {
+			$breakpoint_pseudo_style = $breakpoint_style[ $pseudo_state ] ?? null;
+			if ( wp_custom_css_has_value( $breakpoint_pseudo_style ) ) {
+				$entries[] = array(
+					'css'         => $breakpoint_pseudo_style['css'],
+					'pseudo'      => $pseudo_state,
+					'media_query' => $media_query,
+				);
+			}
+		}
+	}
+
+	return $entries;
+}
+
+/**
+ * Validates and processes custom CSS for each block style state.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param array[] $state_entries Custom CSS entries.
+ * @param string  $selector      Selector scoping the block instance.
+ * @return string Processed CSS.
+ */
+function wp_process_custom_css_state_entries( $state_entries, $selector ) {
+	$processed_css = '';
+
+	foreach ( $state_entries as $entry ) {
+		// Validate CSS doesn't contain HTML markup (same validation as global styles REST API).
+		if ( preg_match( '#</?\w+#', $entry['css'] ) ) {
+			continue;
+		}
+
+		$entry_selector = null !== $entry['pseudo'] ? $selector . $entry['pseudo'] : $selector;
+		$entry_css      = WP_Theme_JSON::process_blocks_custom_css( $entry['css'], $entry_selector );
+
+		if ( empty( $entry_css ) ) {
+			continue;
+		}
+
+		$processed_css .= null !== $entry['media_query']
+			? $entry['media_query'] . '{' . $entry_css . '}'
+			: $entry_css;
+	}
+
+	return $processed_css;
+}
+
 /**
  * Render the custom CSS stylesheet and add class name to block as required.
  *
@@ -39,8 +157,8 @@
  * }
  */
 function wp_render_custom_css_support_styles( $parsed_block ) {
-	$custom_css = $parsed_block['attrs']['style']['css'] ?? null;
-	if ( ! is_string( $custom_css ) || '' === trim( $custom_css ) ) {
+	$style = $parsed_block['attrs']['style'] ?? null;
+	if ( empty( $style ) || ! is_array( $style ) ) {
 		return $parsed_block;
 	}

@@ -49,13 +167,20 @@ function wp_render_custom_css_support_styles( $parsed_block ) {
 		return $parsed_block;
 	}

-	// Validate CSS doesn't contain HTML markup (same validation as global styles REST API).
-	if ( preg_match( '#</?\w+#', $custom_css ) ) {
+	$viewport_settings        = wp_get_global_settings( array( 'viewport' ) );
+	$responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
+	$state_entries            = wp_get_custom_css_state_entries( $style, $parsed_block['blockName'], $responsive_media_queries );
+	if ( empty( $state_entries ) ) {
 		return $parsed_block;
 	}

 	// Generate a unique class name for this block instance.
-	$class_name          = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
+	$class_name    = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
+	$processed_css = wp_process_custom_css_state_entries( $state_entries, '.' . $class_name );
+	if ( '' === $processed_css ) {
+		return $parsed_block;
+	}
+
 	$existing_class_name = $parsed_block['attrs']['className'] ?? null;
 	$updated_class_name  = is_string( $existing_class_name )
 		? "$existing_class_name $class_name"
@@ -63,28 +188,22 @@ function wp_render_custom_css_support_styles( $parsed_block ) {

 	$parsed_block['attrs']['className'] = $updated_class_name;

-	// Process the custom CSS using the same method as global styles.
-	$selector      = '.' . $class_name;
-	$processed_css = WP_Theme_JSON::process_blocks_custom_css( $custom_css, $selector );
-
-	if ( ! empty( $processed_css ) ) {
-		/**
-		 * Reuse one handle so identical custom CSS is enqueued only once via
-		 * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
-		 * dependency so `global-styles` is guaranteed to print after it, preventing
-		 * block default styles from unintentionally overriding global styles.
-		 */
-		$handle = 'wp-block-custom-css';
-		if ( ! wp_style_is( $handle, 'registered' ) ) {
-			wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
-		}
-		$after_styles = wp_styles()->get_data( $handle, 'after' );
-		if ( ! is_array( $after_styles ) ) {
-			$after_styles = array();
-		}
-		if ( ! in_array( $processed_css, $after_styles, true ) ) {
-			wp_add_inline_style( $handle, $processed_css );
-		}
+	/**
+	 * Reuse one handle so identical custom CSS is enqueued only once via
+	 * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
+	 * dependency so `global-styles` is guaranteed to print after it, preventing
+	 * block default styles from unintentionally overriding global styles.
+	 */
+	$handle = 'wp-block-custom-css';
+	if ( ! wp_style_is( $handle, 'registered' ) ) {
+		wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
+	}
+	$after_styles = wp_styles()->get_data( $handle, 'after' );
+	if ( ! is_array( $after_styles ) ) {
+		$after_styles = array();
+	}
+	if ( ! in_array( $processed_css, $after_styles, true ) ) {
+		wp_add_inline_style( $handle, $processed_css );
 	}

 	return $parsed_block;
@@ -181,6 +300,36 @@ function wp_register_custom_css_support( $block_type ) {
 	}
 }

+/**
+ * Removes custom CSS from a style attribute and any nested style states.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param array $style The block's `style` attribute.
+ * @return array The style attribute with all `css` keys removed.
+ */
+function wp_strip_custom_css_from_style_array( $style ) {
+	if ( ! is_array( $style ) ) {
+		return $style;
+	}
+
+	unset( $style['css'] );
+
+	foreach ( $style as $key => $value ) {
+		if ( is_string( $key ) && is_array( $value ) && ( str_starts_with( $key, ':' ) || str_starts_with( $key, '@' ) ) ) {
+			$nested_style = wp_strip_custom_css_from_style_array( $value );
+			if ( empty( $nested_style ) ) {
+				unset( $style[ $key ] );
+			} else {
+				$style[ $key ] = $nested_style;
+			}
+		}
+	}
+
+	return $style;
+}
+
 /**
  * Strips custom CSS (`style.css` in attributes) from all blocks in post content.
  *
@@ -222,14 +371,20 @@ function wp_strip_custom_css_from_blocks( $content ) {
 			continue;
 		}

-		if ( ! isset( $attrs['style']['css'] ) ) {
+		if ( ! isset( $attrs['style'] ) || ! is_array( $attrs['style'] ) ) {
+			continue;
+		}
+
+		$stripped_style = wp_strip_custom_css_from_style_array( $attrs['style'] );
+		if ( $stripped_style === $attrs['style'] ) {
 			continue;
 		}

 		// Remove css and clean up empty style.
-		unset( $attrs['style']['css'] );
-		if ( empty( $attrs['style'] ) ) {
+		if ( empty( $stripped_style ) ) {
 			unset( $attrs['style'] );
+		} else {
+			$attrs['style'] = $stripped_style;
 		}

 		// Locate the JSON portion within the token.
diff --git a/wp-includes/version.php b/wp-includes/version.php
index aaab11a042..261f66d56d 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64352';
+$wp_version = '7.2-alpha-64353';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.