diff --git a/src/wp-includes/block-supports/states.php b/src/wp-includes/block-supports/states.php index 2a80c26db420c..809072fd5612e 100644 --- a/src/wp-includes/block-supports/states.php +++ b/src/wp-includes/block-supports/states.php @@ -314,9 +314,11 @@ function wp_add_state_style_group( &$groups, $selector, $style ) { * * @since 7.1.0 * - * @param array $state_style State style object. - * @param array $block_selectors Block selectors metadata. + * @param array $state_style State style object. + * @param array> $block_selectors Block selectors metadata. * @return array[] Selector/style groups. + * + * @phpstan-param array{ root?: string, ...> } $block_selectors */ function wp_get_state_style_groups( $state_style, $block_selectors ) { $groups = array(); diff --git a/src/wp-includes/class-wp-block-type.php b/src/wp-includes/class-wp-block-type.php index 811e65c6ab390..921b83c94d0a9 100644 --- a/src/wp-includes/class-wp-block-type.php +++ b/src/wp-includes/class-wp-block-type.php @@ -137,8 +137,15 @@ class WP_Block_Type { /** * Custom CSS selectors for theme.json style generation. * + * The `root` key holds the selector for the block itself. Each other key is a block support + * feature (such as `color` or `typography`) whose value is either a selector string, or an + * array of selectors keyed by subfeature (such as `root` or `text`). The exception is the + * `states` key, which holds the selectors for the block's custom states, keyed by state + * name (such as `-current`). + * * @since 6.3.0 - * @var array + * @var array> + * @phpstan-var array{ root?: string, ...> } */ public $selectors = array(); @@ -311,37 +318,37 @@ class WP_Block_Type { * Optional. Array or string of arguments for registering a block type. Any arguments may be defined, * however the ones described below are supported by default. Default empty array. * - * @type string $api_version Block API version. - * @type string $title Human-readable block type label. - * @type string|null $category Block type category classification, used in - * search interfaces to arrange block types by category. - * @type string[]|null $parent Setting parent lets a block require that it is only - * available when nested within the specified blocks. - * @type string[]|null $ancestor Setting ancestor makes a block available only inside the specified - * block types at any position of the ancestor's block subtree. - * @type string[]|null $allowed_blocks Limits which block types can be inserted as children of this block type. - * @type string|null $icon Block type icon. - * @type string $description A detailed block type description. - * @type string[] $keywords Additional keywords to produce block type as - * result in search interfaces. - * @type string|null $textdomain The translation textdomain. - * @type array[] $styles Alternative block styles. - * @type array[] $variations Block variations. - * @type array $selectors Custom CSS selectors for theme.json style generation. - * @type array|null $supports Supported features. - * @type array|null $example Structured data for the block preview. - * @type callable|null $render_callback Block type render callback. - * @type callable|null $variation_callback Block type variations callback. - * @type array|null $attributes Block type attributes property schemas. - * @type string[] $uses_context Context values inherited by blocks of this type. - * @type string[]|null $provides_context Context provided by blocks of this type. - * @type string[] $block_hooks Block hooks. - * @type string[] $editor_script_handles Block type editor only script handles. - * @type string[] $script_handles Block type front end and editor script handles. - * @type string[] $view_script_handles Block type front end only script handles. - * @type string[] $editor_style_handles Block type editor only style handles. - * @type string[] $style_handles Block type front end and editor style handles. - * @type string[] $view_style_handles Block type front end only style handles. + * @type string $api_version Block API version. + * @type string $title Human-readable block type label. + * @type string|null $category Block type category classification, used in + * search interfaces to arrange block types by category. + * @type string[]|null $parent Setting parent lets a block require that it is only + * available when nested within the specified blocks. + * @type string[]|null $ancestor Setting ancestor makes a block available only inside the specified + * block types at any position of the ancestor's block subtree. + * @type string[]|null $allowed_blocks Limits which block types can be inserted as children of this block type. + * @type string|null $icon Block type icon. + * @type string $description A detailed block type description. + * @type string[] $keywords Additional keywords to produce block type as + * result in search interfaces. + * @type string|null $textdomain The translation textdomain. + * @type array[] $styles Alternative block styles. + * @type array[] $variations Block variations. + * @type array> $selectors Custom CSS selectors for theme.json style generation. + * @type array|null $supports Supported features. + * @type array|null $example Structured data for the block preview. + * @type callable|null $render_callback Block type render callback. + * @type callable|null $variation_callback Block type variations callback. + * @type array|null $attributes Block type attributes property schemas. + * @type string[] $uses_context Context values inherited by blocks of this type. + * @type string[]|null $provides_context Context provided by blocks of this type. + * @type string[] $block_hooks Block hooks. + * @type string[] $editor_script_handles Block type editor only script handles. + * @type string[] $script_handles Block type front end and editor script handles. + * @type string[] $view_script_handles Block type front end only script handles. + * @type string[] $editor_style_handles Block type editor only style handles. + * @type string[] $style_handles Block type front end and editor style handles. + * @type string[] $view_style_handles Block type front end only style handles. * } */ public function __construct( $block_type, $args = array() ) { diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 9027c779a7eb3..45660e3ea4e23 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -742,7 +742,13 @@ class WP_Theme_JSON { * * @type bool $include_desktop Whether to include the desktop media query. Default false. * } - * @return array Responsive media queries. + * @return array Responsive media queries, keyed by style-state name. + * + * @phpstan-return array{ + * '@mobile'?: non-falsy-string, + * '@tablet'?: non-falsy-string, + * '@desktop'?: non-falsy-string, + * } */ public static function get_viewport_media_queries( $viewport_settings = null, $options = array() ) { $breakpoints = self::sanitize_viewport_settings( $viewport_settings ); @@ -5993,24 +5999,26 @@ private static function resolve_custom_css_format( $tree ) { * * @since 6.3.0 * - * @param object $block_type The block type. - * @param string $root_selector The block's root selector. - * @return array The custom selectors set by the block. + * @param WP_Block_Type $block_type The block type. + * @param string $root_selector The block's root selector. + * @return array> The custom selectors set by the block. + * + * @phpstan-return array{ root?: string, ...> } */ protected static function get_block_selectors( $block_type, $root_selector ) { if ( ! empty( $block_type->selectors ) ) { return $block_type->selectors; } - $selectors = array( 'root' => $root_selector ); + $feature_selectors = array(); foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { $feature_selector = wp_get_block_css_selector( $block_type, $key ); if ( null !== $feature_selector ) { - $selectors[ $feature ] = array( 'root' => $feature_selector ); + $feature_selectors[ $feature ] = array( 'root' => $feature_selector ); } } - return $selectors; + return array( 'root' => $root_selector ) + $feature_selectors; } /** @@ -6057,7 +6065,7 @@ protected static function get_block_element_selectors( $root_selector ) { * @return array The style declarations for the node's features with custom * selectors, keyed by selector. * - * @phpstan-param array{ selectors?: array> } $metadata + * @phpstan-param array{ selectors?: array{ root?: string, ...> } } $metadata * @phpstan-return array> */ protected function get_feature_declarations_for_node( $metadata, &$node ) { diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 56c448ee1c43a..70d2838984e3b 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -10584,4 +10584,122 @@ public function test_get_styles_for_block_variation_custom_css_uses_css_feature_ $this->assertStringContainsString( '{:root :where(.custom-p.is-style-fancy){color:blue;}}', $actual ); $this->assertStringNotContainsString( ':root :where(p.is-style-fancy){color:', $actual ); } + + /** + * Tests that the default selectors for a block without custom selectors list the root + * selector first, followed by the feature selectors in the order of + * {@see WP_Theme_JSON::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS}. + * + * @ticket 65817 + * + * @covers WP_Theme_JSON::get_block_selectors + */ + public function test_get_block_selectors_builds_default_selectors_with_root_first() { + $block_type = new WP_Block_Type( + 'test/selectors', + array( + 'supports' => array( + 'typography' => array( '__experimentalSelector' => '.t' ), + 'color' => array( '__experimentalSelector' => '.c' ), + ), + ) + ); + + $this->assertSame( + array( + 'root' => '.wp-block-test-selectors', + 'color' => array( 'root' => '.wp-block-test-selectors .c' ), + 'typography' => array( 'root' => '.wp-block-test-selectors .t' ), + ), + $this->get_block_selectors( WP_Theme_JSON::class, $block_type, '.wp-block-test-selectors' ) + ); + } + + /** + * Tests that custom selectors set by the block are returned as they are. + * + * @ticket 65817 + * + * @covers WP_Theme_JSON::get_block_selectors + */ + public function test_get_block_selectors_returns_custom_selectors() { + $selectors = array( + 'color' => array( 'text' => '.custom-text' ), + 'root' => '.custom-root', + 'states' => array( '-current' => '.custom-root.is-current' ), + ); + $block_type = new WP_Block_Type( + 'test/selectors', + array( + 'selectors' => $selectors, + 'supports' => array( + 'color' => array( '__experimentalSelector' => '.c' ), + ), + ) + ); + + $this->assertSame( + $selectors, + $this->get_block_selectors( WP_Theme_JSON::class, $block_type, '.custom-root' ) + ); + } + + /** + * Tests that a feature named `root` does not replace the root selector string in the + * default selectors. + * + * {@see WP_Theme_JSON::get_block_selectors()} reads the feature level selectors with + * `static::`, so a subclass can override them. The subclass has an empty constructor, so + * that creating it does not fill the shared blocks metadata cache with its feature level + * selectors, which would affect later tests. + * + * @ticket 65817 + * + * @covers WP_Theme_JSON::get_block_selectors + */ + public function test_get_block_selectors_keeps_root_selector_string() { + $subclass = get_class( + new class() extends WP_Theme_JSON { + const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( + 'color' => 'root', + 'typography' => 'typography', + ); + + public function __construct() {} + } + ); + $block_type = new WP_Block_Type( + 'test/selectors', + array( + 'supports' => array( + 'color' => array( '__experimentalSelector' => '.c' ), + 'typography' => array( '__experimentalSelector' => '.t' ), + ), + ) + ); + + $this->assertSame( + array( + 'root' => '.wp-block-test-selectors', + 'typography' => array( 'root' => '.wp-block-test-selectors .t' ), + ), + $this->get_block_selectors( $subclass, $block_type, '.wp-block-test-selectors' ) + ); + } + + /** + * Calls the protected {@see WP_Theme_JSON::get_block_selectors()} method. + * + * @param class-string $class_name The name of WP_Theme_JSON or a subclass of it. + * @param WP_Block_Type $block_type The block type. + * @param string $root_selector The block's root selector. + * @return mixed The selectors for the block, as returned through reflection. + */ + private function get_block_selectors( string $class_name, WP_Block_Type $block_type, string $root_selector ) { + $reflection = new ReflectionMethod( $class_name, 'get_block_selectors' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + return $reflection->invoke( null, $block_type, $root_selector ); + } }