From 3cb19e9abaf5e9df772710270202debdfafeec76 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Thu, 14 May 2026 15:26:05 +1000 Subject: [PATCH] Add support for styling block states --- src/wp-includes/block-supports/states.php | 229 ++++++++++++++++++++++ src/wp-settings.php | 1 + 2 files changed, 230 insertions(+) create mode 100644 src/wp-includes/block-supports/states.php diff --git a/src/wp-includes/block-supports/states.php b/src/wp-includes/block-supports/states.php new file mode 100644 index 0000000000000..70f0e431c20f2 --- /dev/null +++ b/src/wp-includes/block-supports/states.php @@ -0,0 +1,229 @@ + $nested_value ) { + $value[ $key ] = wp_normalize_state_preset_vars( $nested_value ); + } + return $value; + } + + if ( ! is_string( $value ) || ! str_starts_with( $value, 'var:preset|' ) ) { + return $value; + } + + $unwrapped_name = str_replace( '|', '--', substr( $value, strlen( 'var:' ) ) ); + return "var(--wp--$unwrapped_name)"; +} + +/** + * Normalizes a state style object before generating CSS declarations. + * + * @since 7.1.0 + * + * @param array $style State style object. + * @return array Normalized state style object. + */ +function wp_normalize_state_style_for_css_output( $style ) { + return wp_normalize_state_preset_vars( $style ); +} + +/** + * Adds fallback border-style declarations for visible border declarations. + * + * CSS does not render border color or width unless a border style is also set. + * State styles are emitted as stylesheet rules rather than inline styles, so + * they cannot rely on the block-library inline-style attribute fallback rules. + * + * @since 7.1.0 + * + * @param array $declarations CSS declarations generated by the style engine. + * @return array CSS declarations with fallback border styles applied where needed. + */ +function wp_get_state_declarations_with_fallback_border_styles( $declarations ) { + if ( ! is_array( $declarations ) ) { + return $declarations; + } + + $has_border_style = isset( $declarations['border-style'] ) && '' !== $declarations['border-style']; + $has_border_color = isset( $declarations['border-color'] ) && '' !== $declarations['border-color']; + $has_border_width = isset( $declarations['border-width'] ) && '' !== $declarations['border-width']; + + if ( ! $has_border_style && ( $has_border_color || $has_border_width ) ) { + $declarations['border-style'] = 'solid'; + } + + $sides = array( 'top', 'right', 'bottom', 'left' ); + foreach ( $sides as $side ) { + $side_style_property = "border-$side-style"; + $side_color_property = "border-$side-color"; + $side_width_property = "border-$side-width"; + + $has_side_style = isset( $declarations[ $side_style_property ] ) && '' !== $declarations[ $side_style_property ]; + $has_side_color = isset( $declarations[ $side_color_property ] ) && '' !== $declarations[ $side_color_property ]; + $has_side_width = isset( $declarations[ $side_width_property ] ) && '' !== $declarations[ $side_width_property ]; + + if ( ! $has_border_style && ! $has_side_style && ( $has_side_color || $has_side_width ) ) { + $declarations[ $side_style_property ] = 'solid'; + } + } + + return $declarations; +} + +/** + * Renders per-instance pseudo-state styles on the frontend for blocks with + * configured pseudo-state support. + * + * @since 7.1.0 + * + * @param string $block_content The block's rendered HTML. + * @param array $block The block data including blockName and attrs. + * @return string Modified block content with injected pseudo-state styles. + */ +function wp_render_block_states_support( $block_content, $block ) { + if ( empty( $block['blockName'] ) || empty( $block_content ) ) { + return $block_content; + } + + $block_name = $block['blockName']; + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); + if ( ! $block_type ) { + return $block_content; + } + + $supported_states = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? null; + if ( empty( $supported_states ) || ! is_array( $supported_states ) ) { + return $block_content; + } + + $style = $block['attrs']['style'] ?? array(); + $css_rules = array(); + + foreach ( $supported_states as $state ) { + if ( empty( $style[ $state ] ) || ! is_array( $style[ $state ] ) ) { + continue; + } + + $compiled = wp_style_engine_get_styles( + wp_normalize_state_style_for_css_output( $style[ $state ] ) + ); + if ( ! empty( $compiled['declarations'] ) ) { + $css_rules[] = array( + 'state' => $state, + 'declarations' => $compiled['declarations'], + ); + } + } + + if ( empty( $css_rules ) ) { + return $block_content; + } + + $unique_class = 'wp-states-' . substr( md5( wp_json_encode( $css_rules ) ), 0, 8 ); + + /* + * Register each pseudo-state's CSS rules with the block-supports style engine store. + * The store deduplicates rules by selector — two block instances with identical + * pseudo-state styles share the same hash class and therefore the same selector, + * so only one CSS rule is emitted. The store is flushed to the page by + * wp_enqueue_stored_styles() rather than injected inline here. + * + * Some block support declarations need !important to apply reliably. Preset-backed + * declarations need to override preset utility classes such as .has-accent-3-background-color, + * while border declarations need to override base styles that can be serialized inline. + * Properties that do not have either conflict do not need !important. + */ + $important_properties = array( + 'color', + 'background-color', + 'border-color', + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + 'border-width', + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + 'border-style', + 'border-top-style', + 'border-right-style', + 'border-bottom-style', + 'border-left-style', + 'background', + 'font-size', + 'font-family', + ); + + $style_rules = array(); + foreach ( $css_rules as $rule ) { + $declarations = array(); + foreach ( $rule['declarations'] as $property => $value ) { + $declarations[ $property ] = in_array( $property, $important_properties, true ) + ? $value . ' !important' + : $value; + } + $declarations = wp_get_state_declarations_with_fallback_border_styles( $declarations ); + $style_rules[] = array( + 'selector' => ".$unique_class{$rule['state']}", + 'declarations' => $declarations, + ); + } + + wp_style_engine_get_stylesheet_from_css_rules( + $style_rules, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + + // Add the unique class to the interactive element so that pseudo-state + // selectors like `.$unique_class:hover` match directly without needing a descendant. + // If the block declares selectors.root with a descendant (e.g. the button + // block's ".wp-block-button .wp-block-button__link"), we extract the last + // class and walk to that element. Otherwise we fall back to the wrapper. + $root_selector = $block_type->selectors['root'] ?? null; + $target_class = null; + if ( $root_selector && preg_match( '/\.([a-zA-Z0-9_-]+)\s*$/', $root_selector, $matches ) ) { + $target_class = $matches[1]; + } + + $processor = new WP_HTML_Tag_Processor( $block_content ); + if ( $target_class ) { + while ( $processor->next_tag() ) { + if ( $processor->has_class( $target_class ) ) { + $processor->add_class( $unique_class ); + break; + } + } + } elseif ( $processor->next_tag() ) { + $processor->add_class( $unique_class ); + } + return $processor->get_updated_html(); +} +add_filter( 'render_block', 'wp_render_block_states_support', 10, 2 ); diff --git a/src/wp-settings.php b/src/wp-settings.php index b2736bddadc3c..0935e2762619c 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -426,6 +426,7 @@ require ABSPATH . WPINC . '/block-supports/anchor.php'; require ABSPATH . WPINC . '/block-supports/block-visibility.php'; require ABSPATH . WPINC . '/block-supports/custom-css.php'; +require ABSPATH . WPINC . '/block-supports/states.php'; require ABSPATH . WPINC . '/style-engine.php'; require ABSPATH . WPINC . '/style-engine/class-wp-style-engine.php'; require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-css-declarations.php';