|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Block states support for frontend CSS generation. |
| 4 | + * |
| 5 | + * Generates scoped CSS for per-instance pseudo-state styles (e.g., :hover, :focus) |
| 6 | + * declared in block attributes under `style[':hover']`, `style[':focus']`, etc. |
| 7 | + * |
| 8 | + * @package WordPress |
| 9 | + */ |
| 10 | + |
| 11 | +/** |
| 12 | + * Renders per-instance state styles on the frontend for blocks that declare |
| 13 | + * `__experimentalStates` support. |
| 14 | + * |
| 15 | + * @param string $block_content The block's rendered HTML. |
| 16 | + * @param array $block The block data including blockName and attrs. |
| 17 | + * @return string Modified block content with injected state styles. |
| 18 | + */ |
| 19 | +function wp_render_block_states_support( $block_content, $block ) { |
| 20 | + if ( empty( $block['blockName'] ) || empty( $block_content ) ) { |
| 21 | + return $block_content; |
| 22 | + } |
| 23 | + |
| 24 | + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); |
| 25 | + if ( ! $block_type ) { |
| 26 | + return $block_content; |
| 27 | + } |
| 28 | + |
| 29 | + $supported_states = $block_type->supports['__experimentalStates'] ?? null; |
| 30 | + if ( empty( $supported_states ) || ! is_array( $supported_states ) ) { |
| 31 | + return $block_content; |
| 32 | + } |
| 33 | + |
| 34 | + $style = $block['attrs']['style'] ?? array(); |
| 35 | + $css_rules = array(); |
| 36 | + |
| 37 | + foreach ( $supported_states as $state ) { |
| 38 | + if ( empty( $style[ $state ] ) || ! is_array( $style[ $state ] ) ) { |
| 39 | + continue; |
| 40 | + } |
| 41 | + |
| 42 | + $compiled = wp_style_engine_get_styles( $style[ $state ] ); |
| 43 | + if ( ! empty( $compiled['css'] ) ) { |
| 44 | + $css_rules[] = array( |
| 45 | + 'state' => $state, |
| 46 | + 'css' => $compiled['css'], |
| 47 | + ); |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + if ( empty( $css_rules ) ) { |
| 52 | + return $block_content; |
| 53 | + } |
| 54 | + |
| 55 | + $unique_class = 'wp-states-' . substr( md5( wp_json_encode( $css_rules ) ), 0, 8 ); |
| 56 | + $css = ''; |
| 57 | + |
| 58 | + foreach ( $css_rules as $rule ) { |
| 59 | + // Use !important to override utility classes like |
| 60 | + // .has-accent-3-background-color which are generated with !important. |
| 61 | + $declarations = str_replace( ';', ' !important;', $rule['css'] ); |
| 62 | + $css .= ".$unique_class$rule[state] { $declarations }\n"; |
| 63 | + } |
| 64 | + |
| 65 | + // Add the unique class to the interactive element so that state selectors |
| 66 | + // like `.$unique_class:hover` match directly without needing a descendant. |
| 67 | + // If the block declares selectors.root with a descendant (e.g. the button |
| 68 | + // block's ".wp-block-button .wp-block-button__link"), we extract the last |
| 69 | + // class and walk to that element. Otherwise we fall back to the wrapper. |
| 70 | + $root_selector = $block_type->selectors['root'] ?? null; |
| 71 | + $target_class = null; |
| 72 | + if ( $root_selector && preg_match( '/\.([a-zA-Z0-9_-]+)\s*$/', $root_selector, $matches ) ) { |
| 73 | + $target_class = $matches[1]; |
| 74 | + } |
| 75 | + |
| 76 | + $processor = new WP_HTML_Tag_Processor( $block_content ); |
| 77 | + if ( $target_class ) { |
| 78 | + while ( $processor->next_tag() ) { |
| 79 | + if ( $processor->has_class( $target_class ) ) { |
| 80 | + $processor->add_class( $unique_class ); |
| 81 | + break; |
| 82 | + } |
| 83 | + } |
| 84 | + } elseif ( $processor->next_tag() ) { |
| 85 | + $processor->add_class( $unique_class ); |
| 86 | + } |
| 87 | + $block_content = $processor->get_updated_html(); |
| 88 | + |
| 89 | + return '<style>' . $css . '</style>' . $block_content; |
| 90 | +} |
| 91 | +add_filter( 'render_block', 'wp_render_block_states_support', 10, 2 ); |
0 commit comments