Make WordPress Core

Changeset 61607


Ignore:
Timestamp:
02/10/2026 03:19:36 PM (7 weeks ago)
Author:
scruffian
Message:

Editor: Add support for pseudo elements for the block and its variations on theme.json.

Adds support for pseudo elements on the core/button block for ( ':hover', ':focus', ':focus-visible', ':active' ) at the theme.json level. This is also allowing the block's variations to control the same pseudo elements, so now we can style hover for the outline variation too.

Example usage:

"styles": {
	"blocks": {
		"core/button": {
			"color": {
				"background": "blue"
			},
			":hover": {
				"color": {
					"background": "green"
				}
			},
			":focus": {
				"color": {
					"background": "purple"
				}
			},
			"variations": {
				"outline": {
					":hover": {
						"color": {
							"background": "pink"
						}
					}
				}
			}
		}
	}
}

Reviewed by palak678, getdave, MaggieCabrera.

Props MaggieCabrera, scruffian, palak678. joedolson, getdave, mikachan.
Fixes #64263.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-theme-json.php

    r61570 r61607  
    611611
    612612    /**
     613     * The valid pseudo-selectors that can be used for blocks.
     614     *
     615     * @since 7.0
     616     * @var array
     617     */
     618    const VALID_BLOCK_PSEUDO_SELECTORS = array(
     619        'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ),
     620    );
     621
     622    /**
    613623     * The valid elements that can be found under styles.
    614624     *
     
    700710    }
    701711
     712
     713    /**
     714     * Processes pseudo-selectors for any node (block or variation).
     715     *
     716     * @param array  $node The node data (block or variation).
     717     * @param string $base_selector The base selector.
     718     * @param array  $settings The theme settings.
     719     * @param string $block_name The block name.
     720     * @return array Array of pseudo-selector declarations.
     721     */
     722    private static function process_pseudo_selectors( $node, $base_selector, $settings, $block_name ) {
     723        $pseudo_declarations = array();
     724
     725        if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
     726            return $pseudo_declarations;
     727        }
     728
     729        foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
     730            if ( isset( $node[ $pseudo_selector ] ) ) {
     731                $combined_selector                         = static::append_to_selector( $base_selector, $pseudo_selector );
     732                $declarations                              = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null );
     733                $pseudo_declarations[ $combined_selector ] = $declarations;
     734            }
     735        }
     736
     737        return $pseudo_declarations;
     738    }
     739
     740
    702741    /**
    703742     * Returns a class name by an element name.
     
    10191058            $schema_styles_blocks[ $block ]             = $styles_non_top_level;
    10201059            $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
     1060
     1061            // Add pseudo-selectors for blocks that support them
     1062            if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
     1063                foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
     1064                    $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
     1065                }
     1066            }
    10211067        }
    10221068
     
    10411087            $schema_styles_variations = array();
    10421088            if ( ! empty( $style_variation_names ) ) {
    1043                 $schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles );
     1089                foreach ( $style_variation_names as $variation_name ) {
     1090                    $variation_schema = $block_style_variation_styles;
     1091
     1092                    // Add pseudo-selectors to variations for blocks that support them
     1093                    if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
     1094                        foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
     1095                            $variation_schema[ $pseudo_selector ] = $styles_non_top_level;
     1096                        }
     1097                    }
     1098
     1099                    $schema_styles_variations[ $variation_name ] = $variation_schema;
     1100                }
    10441101            }
    10451102
     
    27432800                    'css'        => $selector,
    27442801                );
     2802
     2803                // Handle any pseudo selectors for the block.
     2804                if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
     2805                    foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
     2806                        if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) {
     2807                            $nodes[] = array(
     2808                                'name'       => $name,
     2809                                'path'       => array( 'styles', 'blocks', $name, $pseudo_selector ),
     2810                                'selector'   => static::append_to_selector( $selector, $pseudo_selector ),
     2811                                'selectors'  => $feature_selectors,
     2812                                'duotone'    => $duotone_selector,
     2813                                'variations' => $variation_selectors,
     2814                                'css'        => static::append_to_selector( $selector, $pseudo_selector ),
     2815                            );
     2816                        }
     2817                    }
     2818                }
    27452819            }
    27462820
     
    28392913                // Compute declarations for remaining styles not covered by feature level selectors.
    28402914                $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
     2915
     2916                // Process pseudo-selectors for this variation (e.g., :hover, :focus)
     2917                $block_name                    = isset( $block_metadata['name'] ) ? $block_metadata['name'] : ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ? $block_metadata['path'][2] : null );
     2918                $variation_pseudo_declarations = static::process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name );
     2919                $style_variation_declarations  = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
     2920
    28412921                // Store custom CSS for the style variation.
    28422922                if ( isset( $style_variation_node['css'] ) ) {
     
    28712951        if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
    28722952            $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
     2953        }
     2954
     2955        /*
     2956         * Check if we're processing a block pseudo-selector.
     2957         * $block_metadata['path'] = array( 'styles', 'blocks', 'core/button', ':hover' );
     2958         */
     2959        $is_processing_block_pseudo = false;
     2960        $block_pseudo_selector      = null;
     2961        if ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 4 ) {
     2962            $block_name        = $block_metadata['path'][2]; // 'core/button'
     2963            $last_path_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; // ':hover'
     2964
     2965            if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) &&
     2966                in_array( $last_path_element, static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ], true ) ) {
     2967                $is_processing_block_pseudo = true;
     2968                $block_pseudo_selector      = $last_path_element;
     2969            }
    28732970        }
    28742971
     
    29022999        ) {
    29033000            $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
     3001        } elseif ( $is_processing_block_pseudo ) {
     3002            // Process block pseudo-selector styles
     3003            // For block pseudo-selectors, we need to get the block data first, then access the pseudo-selector
     3004            $block_name  = $block_metadata['path'][2]; // 'core/button'
     3005            $block_data  = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $block_name ), array() );
     3006            $pseudo_data = isset( $block_data[ $block_pseudo_selector ] ) ? $block_data[ $block_pseudo_selector ] : array();
     3007
     3008            $declarations = static::compute_style_properties( $pseudo_data, $settings, null, $this->theme_json, $selector, $use_root_padding );
    29043009        } else {
    29053010            $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
  • trunk/src/wp-includes/global-styles-and-settings.php

    r61473 r61607  
    277277
    278278        if ( $can_use_cached ) {
    279             // Use the block name as the key for cached CSS data. Otherwise, use a hash of the metadata.
    280             $cache_node_key = $metadata['name'] ?? md5( wp_json_encode( $metadata ) );
     279            // Generate a unique cache key based on the full metadata to ensure pseudo-selectors and other variations get unique keys.
     280            $cache_node_key = md5( wp_json_encode( $metadata ) );
    281281
    282282            if ( isset( $cached['blocks'][ $cache_node_key ] ) ) {
  • trunk/tests/phpunit/tests/theme/wpThemeJson.php

    r61573 r61607  
    60706070     * @ticket 60613
    60716071     *
    6072      * @covers WP_Theme_JSON_Gutenberg::resolve_variables
    6073      * @covers WP_Theme_JSON_Gutenberg::convert_variables_to_value
     6072     * @covers WP_Theme_JSON::resolve_variables
     6073     * @covers WP_Theme_JSON::convert_variables_to_value
    60746074     */
    60756075    public function test_resolve_variables() {
     
    66946694        $this->assertEqualSetsWithIndex( $expected, $actual );
    66956695    }
     6696
     6697    /**
     6698     * Test that block pseudo selectors are processed correctly.
     6699     */
     6700    public function test_block_pseudo_selectors_are_processed() {
     6701        $theme_json = new WP_Theme_JSON(
     6702            array(
     6703                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     6704                'styles'  => array(
     6705                    'blocks' => array(
     6706                        'core/button' => array(
     6707                            'color'  => array(
     6708                                'text'       => 'white',
     6709                                'background' => 'blue',
     6710                            ),
     6711                            ':hover' => array(
     6712                                'color' => array(
     6713                                    'text'       => 'blue',
     6714                                    'background' => 'white',
     6715                                ),
     6716                            ),
     6717                            ':focus' => array(
     6718                                'color' => array(
     6719                                    'text'       => 'red',
     6720                                    'background' => 'yellow',
     6721                                ),
     6722                            ),
     6723                        ),
     6724                    ),
     6725                ),
     6726            )
     6727        );
     6728
     6729        $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}:root :where(.wp-block-button .wp-block-button__link:focus){background-color: yellow;color: red;}';
     6730        $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
     6731    }
     6732
     6733    /**
     6734     * Test that block pseudo selectors are processed correctly within variations.
     6735     */
     6736    public function test_block_variation_pseudo_selectors_are_processed() {
     6737        register_block_style(
     6738            'core/button',
     6739            array(
     6740                'name'  => 'outline',
     6741                'label' => 'Outline',
     6742            )
     6743        );
     6744
     6745        $theme_json = new WP_Theme_JSON(
     6746            array(
     6747                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     6748                'styles'  => array(
     6749                    'blocks' => array(
     6750                        'core/button' => array(
     6751                            'color'      => array(
     6752                                'text'       => 'white',
     6753                                'background' => 'blue',
     6754                            ),
     6755                            'variations' => array(
     6756                                'outline' => array(
     6757                                    'color'  => array(
     6758                                        'text'       => 'currentColor',
     6759                                        'background' => 'transparent',
     6760                                    ),
     6761                                    'border' => array(
     6762                                        'color' => 'currentColor',
     6763                                        'width' => '1px',
     6764                                        'style' => 'solid',
     6765                                    ),
     6766                                    ':hover' => array(
     6767                                        'color' => array(
     6768                                            'text'       => 'white',
     6769                                            'background' => 'red',
     6770                                        ),
     6771                                    ),
     6772                                    ':focus' => array(
     6773                                        'color' => array(
     6774                                            'text'       => 'black',
     6775                                            'background' => 'yellow',
     6776                                        ),
     6777                                    ),
     6778                                ),
     6779                            ),
     6780                        ),
     6781                    ),
     6782                ),
     6783            )
     6784        );
     6785
     6786        $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link){background-color: transparent;border-color: currentColor;border-width: 1px;border-style: solid;color: currentColor;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link:hover){background-color: red;color: white;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link:focus){background-color: yellow;color: black;}';
     6787        $actual   = $theme_json->get_stylesheet(
     6788            array( 'styles' ),
     6789            null,
     6790            array(
     6791                'skip_root_layout_styles'        => true,
     6792                'include_block_style_variations' => true,
     6793            )
     6794        );
     6795
     6796        unregister_block_style( 'core/button', 'outline' );
     6797
     6798        $this->assertSame( $expected, $actual );
     6799    }
     6800
     6801    /**
     6802     * Test that non-whitelisted pseudo selectors are ignored for blocks.
     6803     */
     6804    public function test_block_pseudo_selectors_ignores_non_whitelisted() {
     6805        $theme_json = new WP_Theme_JSON(
     6806            array(
     6807                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     6808                'styles'  => array(
     6809                    'blocks' => array(
     6810                        'core/button' => array(
     6811                            'color'     => array(
     6812                                'text'       => 'white',
     6813                                'background' => 'blue',
     6814                            ),
     6815                            ':hover'    => array(
     6816                                'color' => array(
     6817                                    'text'       => 'blue',
     6818                                    'background' => 'white',
     6819                                ),
     6820                            ),
     6821                            ':levitate' => array(
     6822                                'color' => array(
     6823                                    'text'       => 'yellow',
     6824                                    'background' => 'black',
     6825                                ),
     6826                            ),
     6827                        ),
     6828                    ),
     6829                ),
     6830            )
     6831        );
     6832
     6833        $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}';
     6834        $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
     6835        $this->assertStringNotContainsString( '.wp-block-button .wp-block-button__link:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) );
     6836    }
     6837
     6838    /**
     6839     * Test that blocks without pseudo selector support ignore pseudo selectors.
     6840     */
     6841    public function test_blocks_without_pseudo_support_ignore_pseudo_selectors() {
     6842        $theme_json = new WP_Theme_JSON(
     6843            array(
     6844                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     6845                'styles'  => array(
     6846                    'blocks' => array(
     6847                        'core/paragraph' => array(
     6848                            'color'  => array(
     6849                                'text' => 'black',
     6850                            ),
     6851                            ':hover' => array(
     6852                                'color' => array(
     6853                                    'text' => 'red',
     6854                                ),
     6855                            ),
     6856                        ),
     6857                    ),
     6858                ),
     6859            )
     6860        );
     6861
     6862        $expected = ':root :where(p){color: black;}';
     6863        $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
     6864        $this->assertStringNotContainsString( 'p:hover{', $theme_json->get_stylesheet( array( 'styles' ) ) );
     6865    }
     6866
     6867    /**
     6868     * Test that block pseudo selectors work with elements within blocks.
     6869     */
     6870    public function test_block_pseudo_selectors_with_elements() {
     6871        $theme_json = new WP_Theme_JSON(
     6872            array(
     6873                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     6874                'styles'  => array(
     6875                    'blocks' => array(
     6876                        'core/button' => array(
     6877                            'color'    => array(
     6878                                'text'       => 'white',
     6879                                'background' => 'blue',
     6880                            ),
     6881                            ':hover'   => array(
     6882                                'color' => array(
     6883                                    'text'       => 'blue',
     6884                                    'background' => 'white',
     6885                                ),
     6886                            ),
     6887                            'elements' => array(
     6888                                'button' => array(
     6889                                    'color'  => array(
     6890                                        'text' => 'green',
     6891                                    ),
     6892                                    ':hover' => array(
     6893                                        'color' => array(
     6894                                            'text' => 'orange',
     6895                                        ),
     6896                                    ),
     6897                                ),
     6898                            ),
     6899                        ),
     6900                    ),
     6901                ),
     6902            )
     6903        );
     6904
     6905        $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}:root :where(.wp-block-button .wp-block-button__link .wp-element-button,.wp-block-button .wp-block-button__link  .wp-block-button__link){color: green;}:root :where(.wp-block-button .wp-block-button__link .wp-element-button:hover,.wp-block-button .wp-block-button__link  .wp-block-button__link:hover){color: orange;}';
     6906        $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
     6907    }
    66966908}
Note: See TracChangeset for help on using the changeset viewer.