From 0239107db1c341989267d14b296fe20ae8a28fc8 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 9 Dec 2022 16:55:15 +0800 Subject: [PATCH 01/26] Allow selectors when registering blocks --- packages/blocks/src/api/registration.js | 11 ++++++ packages/blocks/src/api/test/registration.js | 36 ++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 8605fa4838abee..717e88223b9749 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -169,6 +169,16 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { serverSideBlockDefinitions[ blockName ].ancestor = definitions[ blockName ].ancestor; } + // The `selectors` prop is not yet included in the server provided + // definitions. Polyfill it as well. + if ( + serverSideBlockDefinitions[ blockName ].selectors === + undefined && + definitions[ blockName ].selectors + ) { + serverSideBlockDefinitions[ blockName ].selectors = + definitions[ blockName ].selectors; + } continue; } @@ -203,6 +213,7 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { 'attributes', 'providesContext', 'usesContext', + 'selectors', 'supports', 'styles', 'example', diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 58bf57726a3b9b..1c91654ece6c18 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -460,6 +460,42 @@ describe( 'blocks', () => { } ); } ); + // This can be removed once polyfill adding selectors has been removed. + it( 'should apply selectors on the client when not set on the server', () => { + const blockName = 'core/test-block-with-selectors'; + unstable__bootstrapServerSideBlockDefinitions( { + [ blockName ]: { + category: 'widgets', + }, + } ); + unstable__bootstrapServerSideBlockDefinitions( { + [ blockName ]: { + selectors: { root: '.wp-block-custom-selector' }, + category: 'ignored', + }, + } ); + + const blockType = { + title: 'block title', + }; + registerBlockType( blockName, blockType ); + expect( getBlockType( blockName ) ).toEqual( { + name: blockName, + save: expect.any( Function ), + title: 'block title', + category: 'widgets', + icon: { src: BLOCK_ICON_DEFAULT }, + attributes: {}, + providesContext: {}, + usesContext: [], + keywords: [], + selectors: { root: '.wp-block-custom-selector' }, + supports: {}, + styles: [], + variations: [], + } ); + } ); + it( 'should validate the icon', () => { const blockType = { save: noop, From 8d221735bda683e9e46c1bc4f0b84f961d4623f9 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:49:52 +0800 Subject: [PATCH 02/26] Add tests for updated selectors API --- phpunit/bootstrap.php | 41 +++++++++++++++++- phpunit/class-wp-theme-json-test.php | 64 +++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php index fac777864200a8..e15509e43143eb 100644 --- a/phpunit/bootstrap.php +++ b/phpunit/bootstrap.php @@ -134,6 +134,46 @@ function gutenberg_register_test_block_for_feature_selectors() { ), ) ); + + WP_Block_Type_Registry::get_instance()->register( + 'my/block-with-selectors', + array( + 'api_version' => 2, + 'attributes' => array( + 'textColor' => array( + 'type' => 'string', + ), + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + '__experimentalBorder' => array( + 'radius' => true, + ), + 'color' => array( + 'background' => true, + 'text' => true, + ), + 'spacing' => array( + 'padding' => true, + ), + 'typography' => array( + 'fontSize' => true, + ), + ), + 'selectors' => array( + 'root' => '.custom-root-selector', + 'border' => array( + 'root' => '.custom-root-selector img', + ), + 'color' => array( + 'text' => '.custom-root-selector > figcaption', + ), + 'typography' => '.custom-root-selector > figcaption', + ), + ) + ); } tests_add_filter( 'init', 'gutenberg_register_test_block_for_feature_selectors' ); @@ -142,4 +182,3 @@ function gutenberg_register_test_block_for_feature_selectors() { // Use existing behavior for wp_die during actual test execution. remove_filter( 'wp_die_handler', 'fail_if_died' ); - diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index b46838c0275b1a..eeb5940f44cd35 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -803,7 +803,7 @@ public function test_get_stylesheet_handles_whitelisted_block_level_element_pseu * bootstrap. After a core block adopts feature level selectors we could * remove that filter and instead use the core block for the following test. */ - public function test_get_stylesheet_with_block_support_feature_level_selectors() { + public function test_get_stylesheet_with_deprecated_feature_level_selectors() { $theme_json = new WP_Theme_JSON_Gutenberg( array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, @@ -856,6 +856,68 @@ public function test_get_stylesheet_with_block_support_feature_level_selectors() $this->assertEquals( $expected, $theme_json->get_stylesheet() ); } + /** + * This test relies on a block having already been registered prior to + * theme.json generating block metadata. Until a core block adopts the + * new selectors API, we need to register a test block. + * This is achieved via `tests_add_filter()` in Gutenberg's phpunit + * bootstrap. After a core block adopts feature level selectors we could + * remove that filter and instead use the core block for the following test. + */ + public function test_get_stylesheet_with_block_json_selectors() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'settings' => array( + 'border' => array( + 'radius' => true, + ), + 'color' => array( + 'custom' => false, + 'palette' => array( + array( + 'slug' => 'green', + 'color' => 'green', + ), + ), + ), + 'spacing' => array( + 'padding' => true, + ), + 'typography' => array( + 'fontSize' => true, + ), + ), + 'styles' => array( + 'blocks' => array( + 'my/block-with-selectors' => array( + 'border' => array( + 'radius' => '9999px', + ), + 'color' => array( + 'background' => 'grey', + 'text' => 'navy', + ), + 'spacing' => array( + 'padding' => '20px', + ), + 'typography' => array( + 'fontSize' => '3em', + ), + ), + ), + ), + ) + ); + + $base_styles = 'body{--wp--preset--color--green: green;}body { margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + $block_styles = '.custom-root-selector{background-color: grey;padding: 20px;}.custom-root-selector img{border-radius: 9999px;}.custom-root-selector > figcaption{color: navy;font-size: 3em;}'; + $preset_styles = '.has-green-color{color: var(--wp--preset--color--green) !important;}.has-green-background-color{background-color: var(--wp--preset--color--green) !important;}.has-green-border-color{border-color: var(--wp--preset--color--green) !important;}'; + $expected = $base_styles . $block_styles . $preset_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + } + public function test_allow_indirect_properties() { $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( array( From 13e743e73e0082b68c949c50a34e1b5310638bb2 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:51:53 +0800 Subject: [PATCH 03/26] Add selectors to block.json schema --- schemas/json/block.json | 98 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/schemas/json/block.json b/schemas/json/block.json index ad3bb45516b9ff..f4b8df11690afa 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -428,6 +428,104 @@ }, "additionalProperties": true }, + "selectors": { + "type": "object", + "description": "Provides custom CSS selectors and mappings for the block. Selectors may be set for the block itself or per-feature e.g. typography.", + "properties": { + "root": { + "type": "string", + "description": "The primary CSS class to apply to the block. This replaces the `.wp-block-name` class if set. " + }, + "border": { + "description": "Custom CSS selector used to generate rules for the block's theme.json border styles.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "color": { "type": "string" }, + "radius": { "type": "string" }, + "style": { "type": "string" }, + "width": { "type": "string" } + } + } + ] + }, + "color": { + "description": "Custom CSS selector used to generate rules for the block's theme.json color styles.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "text": { "type": "string" }, + "background": { "type": "string" } + } + } + ] + }, + "dimensions": { + "description": "Custom CSS selector used to generate rules for the block's theme.json dimensions styles.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "minHeight": { "type": "string" } + } + } + ] + }, + "spacing": { + "description": "Custom CSS selector used to generate rules for the block's theme.json spacing styles.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "blockGap": { "type": "string" }, + "padding": { "type": "string" }, + "margin": { "type": "string" } + } + } + ] + }, + "typography": { + "description": "Custom CSS selector used to generate rules for the block's theme.json typography styles.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "fontFamily": { "type": "string" }, + "fontSize": { "type": "string" }, + "fontStyle": { "type": "string" }, + "fontWeight": { "type": "string" }, + "lineHeight": { "type": "string" }, + "letterSpacing": { "type": "string" }, + "textDecoration": { "type": "string" }, + "textTransform": { "type": "string" } + } + } + ] + } + } + }, "styles": { "type": "array", "description": "Block styles can be used to provide alternative styles to block. It works by adding a class name to the block’s wrapper. Using CSS, a theme developer can target the class name for the block style if it is selected.\n\nPlugins and Themes can also register custom block style for existing blocks.\n\nhttps://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles", From 974f1889630b03354e0843442f7bbeae86b03466 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:52:06 +0800 Subject: [PATCH 04/26] Update image block to use selectors API --- packages/block-library/src/image/block.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 62edb882be0c93..ddb33649d77ef9 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -93,7 +93,6 @@ "color": true, "radius": true, "width": true, - "__experimentalSelector": "img, .wp-block-image__crop-area", "__experimentalSkipSerialization": true, "__experimentalDefaultControls": { "color": true, @@ -102,6 +101,9 @@ } } }, + "selectors": { + "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area" + }, "styles": [ { "name": "default", From 3858633d08779ea2d6aa96a9d5b93a28da1f134c Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Sun, 15 Jan 2023 13:56:57 +1000 Subject: [PATCH 05/26] Add global function to get block CSS selectors --- .../get-global-styles-and-settings.php | 135 ++++++++ .../class-wp-get-block-css-selectors-test.php | 319 ++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 phpunit/class-wp-get-block-css-selectors-test.php diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index e02a0466a0b98f..186e38406d1bad 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -255,3 +255,138 @@ function _gutenberg_add_non_persistent_theme_json_cache_group() { wp_cache_add_non_persistent_groups( 'theme_json' ); } add_action( 'plugins_loaded', '_gutenberg_add_non_persistent_theme_json_cache_group' ); + +if ( ! function_exists( 'wp_get_block_css_selector' ) ) { + /** + * Determine the CSS selector for the block type and property provided, + * returning it if available. + * + * @param WP_Block_Type $block_type The block's type. + * @param string|array $target The desired selector's target, `root` or array path. + * @param boolean $fallback Whether or not to fallback to broader selector. + * + * @return string|null CSS selector or `null` if no selector available. + */ + function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = false ) { + if ( empty( $target ) ) { + return null; + } + + $has_selectors = isset( $block_type->selectors ) && ! empty( $block_type->selectors ); + + // Duotone (No fallback selectors for Duotone). + if ( 'duotone' === $target ) { + // If selectors API in use, only use it's value or null. + if ( $has_selectors ) { + return _wp_array_get( $block_type->selectors, array( 'color', 'duotone' ), null ); + } + + // Selectors API, not available, check for old experimental selector. + return _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null ); + } + + // Root Selector. + + // Calculated before returning as it can be used as fallback for + // feature selectors later on. + $root_selector = null; + + if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { + // Prefer the selectors API if available. + $root_selector = $block_type->selectors['root']; + } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { + // Use the old experimental selector supports property if set. + $root_selector = $block_type->supports['__experimentalSelector']; + } else { + // If no root selector found, generate default block class selector. + $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); + $root_selector = ".wp-block-{$block_name}"; + } + + // Return selector if it's the root target we are looking for. + if ( 'root' === $target ) { + return $root_selector; + } + + // If target is not `root` or `duotone` we have a feature or subfeature + // as the target. If the target is a string convert to an array. + if ( is_string( $target ) ) { + $target = explode( '.', $target ); + } + + // Feature Selectors ( May fallback to root selector ). + if ( 1 === count( $target ) ) { + $fallback_selector = $fallback ? $root_selector : null; + + // Prefer the selectors API if available. + if ( $has_selectors ) { + // Look for selector under `feature.root`. + $path = array_merge( $target, array( 'root' ) ); + $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); + + if ( $feature_selector ) { + return $feature_selector; + } + + // Check if feature selector set via shorthand. + $feature_selector = _wp_array_get( $block_type->selectors, $target, null ); + + return is_string( $feature_selector ) ? $feature_selector : $fallback_selector; + } + + // Try getting old experimental supports selector value. + $path = array_merge( $target, array( '__experimentalSelector' ) ); + $feature_selector = _wp_array_get( $block_type->supports, $path, null ); + + // Nothing to work with, provide fallback or null. + if ( null === $feature_selector ) { + return $fallback_selector; + } + + // Scope the feature selector by the block's root selector. + // TODO: Following is boilerplate from theme.json class. Is there a util? + $scopes = explode( ',', $root_selector ); + $selectors = explode( ',', $feature_selector ); + + $selectors_scoped = array(); + foreach ( $scopes as $outer ) { + foreach ( $selectors as $inner ) { + $outer = trim( $outer ); + $inner = trim( $inner ); + if ( ! empty( $outer ) && ! empty( $inner ) ) { + $selectors_scoped[] = $outer . ' ' . $inner; + } elseif ( empty( $outer ) ) { + $selectors_scoped[] = $inner; + } elseif ( empty( $inner ) ) { + $selectors_scoped[] = $outer; + } + } + } + + return implode( ', ', $selectors_scoped ); + } + + // Subfeature selector + // This may fallback either to parent feature or root selector. + $subfeature_selector = null; + // Use selectors API if available. + if ( $has_selectors ) { + $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); + + // Only return if we have a subfeature selector. + if ( $subfeature_selector ) { + return $subfeature_selector; + } + } + + // To this point we don't have a subfeature selector. If a fallback + // has been requested, remove subfeature from target path and return + // results of a call for the parent feature's selector. + if ( $fallback ) { + return wp_get_block_css_selector( $block_type, $target[0], $fallback ); + } + + // We tried... + return null; + } +} diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php new file mode 100644 index 00000000000000..6ca7b3f0eeddff --- /dev/null +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -0,0 +1,319 @@ +test_block_name = null; + } + + public function tear_down() { + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + private function register_test_block( $name, $selectors = null, $supports = null ) { + $this->test_block_name = $name; + + return register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array(), + 'selectors' => $selectors, + 'supports' => $supports, + ) + ); + } + + public function test_get_root_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/block-with-selectors', + array( 'root' => '.wp-custom-block-class' ) + ); + + $selector = wp_get_block_css_selector( $block_type ); + $this->assertEquals( '.wp-custom-block-class', $selector ); + } + + public function test_get_root_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/block-without-selectors', + null, + array( '__experimentalSelector' => '.experimental-selector' ) + ); + + $selector = wp_get_block_css_selector( $block_type ); + $this->assertEquals( '.experimental-selector', $selector ); + } + + public function test_default_root_selector_generation() { + $block_type = self::register_test_block( + 'test/without-selectors-or-supports', + null, + null + ); + + $selector = wp_get_block_css_selector( $block_type ); + $this->assertEquals( '.wp-block-test-without-selectors-or-supports', $selector ); + } + + public function test_get_duotone_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/duotone-selector', + array( + 'color' => array( 'duotone' => '.duotone-selector' ), + ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'duotone' ); + $this->assertEquals( '.duotone-selector', $selector ); + } + + public function test_get_duotone_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/experimental-duotone-selector', + null, + array( + 'color' => array( + '__experimentalDuotone' => '.experimental-duotone', + ), + ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'duotone' ); + $this->assertEquals( '.experimental-duotone', $selector ); + } + + public function test_no_duotone_selector_set() { + $block_type = self::register_test_block( + 'test/null-duotone-selector', + null, + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'duotone' ); + $this->assertEquals( '', $selector ); + } + + public function test_get_feature_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/feature-selector', + array( + 'root' => '.root', + 'typography' => array( 'root' => '.typography' ), + ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( '.typography', $selector ); + } + + public function test_get_feature_selector_via_selectors_api_shorthand_property() { + $block_type = self::register_test_block( + 'test/shorthand-feature-selector', + array( + 'root' => '.root', + 'typography' => '.typography', + ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( '.typography', $selector ); + } + + public function test_no_feature_level_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/null-feature-selector', + array( 'root' => '.fallback-root-selector' ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( null, $selector ); + } + + public function test_fallback_feature_level_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/fallback-feature-selector', + array( 'root' => '.fallback-root-selector' ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography', true ); + $this->assertEquals( '.fallback-root-selector', $selector ); + } + + public function test_get_feature_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/experimental-feature-selector', + null, + array( + 'typography' => array( + '__experimentalSelector' => '.experimental-typography', + ), + ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( '.wp-block-test-experimental-feature-selector .experimental-typography', $selector ); + } + + public function test_fallback_feature_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/fallback-feature-selector', + null, + array() + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography', true ); + $this->assertEquals( '.wp-block-test-fallback-feature-selector', $selector ); + } + + public function test_no_feature_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/null-experimental-feature-selector', + null, + array() + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( null, $selector ); + } + + public function test_get_subfeature_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/subfeature-selector', + array( + 'root' => '.root', + 'typography' => array( + 'root' => '.root .typography', + 'textDecoration' => '.root .typography .text-decoration', + ), + ), + null + ); + + $selector = wp_get_block_css_selector( + $block_type, + array( 'typography', 'textDecoration' ) + ); + + $this->assertEquals( '.root .typography .text-decoration', $selector ); + } + + public function test_fallback_subfeature_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/subfeature-selector', + array( + 'root' => '.root', + 'typography' => array( + 'root' => '.root .typography', + ), + ), + null + ); + + $selector = wp_get_block_css_selector( + $block_type, + array( 'typography', 'textDecoration' ), + true + ); + + $this->assertEquals( '.root .typography', $selector ); + } + + public function test_no_subfeature_level_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/null-subfeature-selector', + array( 'root' => '.root' ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, array( 'typography', 'fontSize' ) ); + $this->assertEquals( null, $selector ); + } + + public function test_fallback_subfeature_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/fallback-subfeature-selector', + null, + array() + ); + + $selector = wp_get_block_css_selector( + $block_type, + array( 'typography', 'fontSize' ), + true + ); + $this->assertEquals( '.wp-block-test-fallback-subfeature-selector', $selector ); + } + + public function test_no_subfeature_selector_via_experimental_property() { + $block_type = self::register_test_block( + 'test/null-experimental-subfeature-selector', + null, + array() + ); + + $selector = wp_get_block_css_selector( + $block_type, + array( 'typography', 'fontSize' ) + ); + $this->assertEquals( null, $selector ); + } + + public function test_empty_target_returns_null() { + $block_type = self::register_test_block( + 'test/null-experimental-subfeature-selector', + null, + array() + ); + + $selector = wp_get_block_css_selector( $block_type, array() ); + $this->assertEquals( null, $selector ); + + $selector = wp_get_block_css_selector( $block_type, '' ); + $this->assertEquals( null, $selector ); + } + + public function test_string_targets_for_features() { + $block_type = self::register_test_block( + 'test/target-types-for-features', + array( 'typography' => '.found' ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( '.found', $selector ); + + $selector = wp_get_block_css_selector( $block_type, array( 'typography' ) ); + $this->assertEquals( '.found', $selector ); + } + + public function test_string_targets_for_subfeatures() { + $block_type = self::register_test_block( + 'test/target-types-for-features', + array( + 'typography' => array( 'fontSize' => '.found' ), + ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography.fontSize' ); + $this->assertEquals( '.found', $selector ); + + $selector = wp_get_block_css_selector( $block_type, array( 'typography', 'fontSize' ) ); + $this->assertEquals( '.found', $selector ); + } +} From b19da22ce0bf56408a436d1af4b0b38ec9d06e60 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:59:21 +0800 Subject: [PATCH 06/26] Update theme.json class to support selectors API Leverage global block css selector function in theme.json --- lib/class-wp-theme-json-gutenberg.php | 271 ++++++++++++++++++-------- 1 file changed, 189 insertions(+), 82 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 5bd06274e6efd2..e50836381a4595 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -853,55 +853,21 @@ protected static function get_blocks_metadata() { } foreach ( $blocks as $block_name => $block_type ) { - if ( - isset( $block_type->supports['__experimentalSelector'] ) && - is_string( $block_type->supports['__experimentalSelector'] ) - ) { - static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; - } else { - static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); - } + $root_selector = wp_get_block_css_selector( $block_type ); - if ( - isset( $block_type->supports['color']['__experimentalDuotone'] ) && - is_string( $block_type->supports['color']['__experimentalDuotone'] ) - ) { - static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone']; - } - - // Generate block support feature level selectors if opted into - // for the current block. - $features = array(); - foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { - if ( - isset( $block_type->supports[ $key ]['__experimentalSelector'] ) && - $block_type->supports[ $key ]['__experimentalSelector'] - ) { - $features[ $feature ] = static::scope_selector( - static::$blocks_metadata[ $block_name ]['selector'], - $block_type->supports[ $key ]['__experimentalSelector'] - ); - } - } + static::$blocks_metadata[ $block_name ]['selector'] = $root_selector; + static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector ); - if ( ! empty( $features ) ) { - static::$blocks_metadata[ $block_name ]['features'] = $features; + $elements = static::get_block_element_selectors( $root_selector ); + if ( ! empty( $elements ) ) { + static::$blocks_metadata[ $block_name ]['elements'] = $elements; } - // Assign defaults, then overwrite those that the block sets by itself. - // If the block selector is compounded, will append the element to each - // individual block selector. - $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); - foreach ( static::ELEMENTS as $el_name => $el_selector ) { - $element_selector = array(); - foreach ( $block_selectors as $selector ) { - if ( $selector === $el_selector ) { - $element_selector = array( $el_selector ); - break; - } - $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' ); - } - static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); + // The block may or may not have a duotone selector. + // TODO: Should this target be `color.duotone` not `duotone`? + $duotone_selector = wp_get_block_css_selector( $block_type, 'duotone' ); + if ( null !== $duotone_selector ) { + static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector; } // If the block has style variations, append their selectors to the block metadata. if ( ! empty( $block_type->styles ) ) { @@ -2232,8 +2198,8 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { } $feature_selectors = null; - if ( isset( $selectors[ $name ]['features'] ) ) { - $feature_selectors = $selectors[ $name ]['features']; + if ( isset( $selectors[ $name ]['selectors'] ) ) { + $feature_selectors = $selectors[ $name ]['selectors']; } $variation_selectors = array(); @@ -2250,8 +2216,8 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { 'name' => $name, 'path' => array( 'styles', 'blocks', $name ), 'selector' => $selector, + 'selectors' => $feature_selectors, 'duotone' => $duotone_selector, - 'features' => $feature_selectors, 'variations' => $variation_selectors, ); @@ -2297,40 +2263,7 @@ public function get_styles_for_block( $block_metadata ) { $selector = $block_metadata['selector']; $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - /* - * Process style declarations for block support features the current - * block contains selectors for. Values for a feature with a custom - * selector are filtered from the theme.json node before it is - * processed as normal. - */ - $feature_declarations = array(); - - if ( ! empty( $block_metadata['features'] ) ) { - foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { - if ( ! empty( $node[ $feature_name ] ) ) { - // Create temporary node containing only the feature data - // to leverage existing `compute_style_properties` function. - $feature = array( $feature_name => $node[ $feature_name ] ); - // Generate the feature's declarations only. - $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); - - // Merge new declarations with any that already exist for - // the feature selector. This may occur when multiple block - // support features use the same custom selector. - if ( isset( $feature_declarations[ $feature_selector ] ) ) { - foreach ( $new_feature_declarations as $new_feature_declaration ) { - $feature_declarations[ $feature_selector ][] = $new_feature_declaration; - } - } else { - $feature_declarations[ $feature_selector ] = $new_feature_declarations; - } - - // Remove the feature from the block's node now the - // styles will be included under the feature level selector. - unset( $node[ $feature_name ] ); - } - } - } + $feature_declarations = static::get_feature_declarations_for_block( $block_metadata, $node, $settings, $this->theme_json ); // If there are style variations, generate the declarations for them, including any feature selectors the block may have. $style_variation_declarations = array(); @@ -3480,4 +3413,178 @@ public function set_spacing_sizes() { _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); } + + /** + * Generates the root selector for a block. + * + * @param object $block_type The block type. + * @return string + */ + protected static function get_root_block_selector( $block_type ) { + // Prefer the selectors API if available. + if ( isset( $block_type->selectors ) && + isset( $block_type->selectors['root'] ) + ) { + return $block_type->selectors['root']; + } + + // Use the old experimental selector supports property if set. + if ( isset( $block_type->supports['__experimentalSelector'] ) && + is_string( $block_type->supports['__experimentalSelector'] ) ) { + return $block_type->supports['__experimentalSelector']; + } + + // Generate default block class selector. + $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); + + return ".wp-block-{$block_name}"; + } + + /** + * Returns the selectors metadata for a block. + * + * @param object $block_type The block type. + * @param string $root_selector The block's root selector. + * + * @return object The custom selectors set by the block. + */ + protected static function get_block_selectors( $block_type, $root_selector ) { + if ( isset( $block_type->selectors ) ) { + return $block_type->selectors; + } + + $selectors = array( 'root' => $root_selector ); + 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 ); + } + } + + return $selectors; + } + + /** + * Generates all the element selectors for a block. + * + * @param string $root_selector The block's root CSS selector. + * @return array The block's element selectors. + */ + protected static function get_block_element_selectors( $root_selector ) { + // Assign defaults, then override those that the block sets by itself. + // If the block selector is compounded, will append the element to each + // individual block selector. + $block_selectors = explode( ',', $root_selector ); + $element_selectors = array(); + + foreach ( static::ELEMENTS as $el_name => $el_selector ) { + $element_selector = array(); + foreach ( $block_selectors as $selector ) { + if ( $selector === $el_selector ) { + $element_selector = array( $el_selector ); + break; + } + $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' ); + } + $element_selectors[ $el_name ] = implode( ',', $element_selector ); + } + + return $element_selectors; + } + + + /** + * Generates style declarations for the block's features e.g. color, border, + * typography etc, that have custom selectors in their block metadata. + * + * @param object $block_metadata The block's metadata containing selectors for + * features. + * @param object $block_node The merged theme.json node for the block. + * @param object $settings The theme.json settings for the node. + * @param object $theme_json The current theme.json config. + * + * @return array The style declarations for the block's features with custom + * selectors. + */ + protected static function get_feature_declarations_for_block( $block_metadata, &$block_node, $settings, $theme_json ) { + $declarations = array(); + + if ( ! isset( $block_metadata['selectors'] ) ) { + return $declarations; + } + + foreach ( $block_metadata['selectors'] as $feature => $feature_selectors ) { + // Skip if this is the block's root selector or the block doesn't + // have any styles for the feature. + if ( 'root' === $feature || empty( $block_node[ $feature ] ) ) { + continue; + } + + if ( is_array( $feature_selectors ) ) { + foreach ( $feature_selectors as $subfeature => $subfeature_selector ) { + if ( 'root' === $subfeature || empty( $block_node[ $feature ][ $subfeature ] ) ) { + continue; + } + + // Create temporary node containing only the subfeature data + // to leverage existing `compute_style_properties` function. + $subfeature_node = array( + $feature => array( + $subfeature => $block_node[ $feature ][ $subfeature ], + ), + ); + + // Generate style declarations. + $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $theme_json ); + + // Merge subfeature declarations into feature declarations. + if ( isset( $declarations[ $subfeature_selector ] ) ) { + foreach ( $new_declarations as $new_declaration ) { + $declarations[ $subfeature_selector ][] = $new_declaration; + } + } else { + $declarations[ $subfeature_selector ] = $new_declarations; + } + + // Remove the subfeature from the block's node now its + // styles will be included under its own selector not the + // block's. + unset( $block_node[ $feature ][ $subfeature ] ); + } + } + + // Now subfeatures have been processed and removed we can process + // feature root selector or simple string selector. + if ( + is_string( $feature_selectors ) || + ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] ) + ) { + $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root']; + + // Create temporary node containing only the feature data + // to leverage existing `compute_style_properties` function. + $feature_node = array( $feature => $block_node[ $feature ] ); + + // Generate the style declarations. + $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $theme_json ); + + // Merge new declarations with any that already exist for + // the feature selector. This may occur when multiple block + // support features use the same custom selector. + if ( isset( $declarations[ $feature_selector ] ) ) { + foreach ( $new_declarations as $new_declaration ) { + $declarations[ $feature_selector ][] = $new_declaration; + } + } else { + $declarations[ $feature_selector ] = $new_declarations; + } + + // Remove the feature from the block's node now its styles + // will be included under its own selector not the block's. + unset( $block_node[ $feature ] ); + } + } + + return $declarations; + } } From bb990930e8e0a19442c535f029a209fce39c2599 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:49:44 +1000 Subject: [PATCH 07/26] Add filter ensuring selectors metadata is available --- lib/compat/wordpress-6.3/blocks.php | 28 ++++++++++++++++++++++++++++ lib/load.php | 1 + 2 files changed, 29 insertions(+) create mode 100644 lib/compat/wordpress-6.3/blocks.php diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php new file mode 100644 index 00000000000000..ac56b4814ebfc6 --- /dev/null +++ b/lib/compat/wordpress-6.3/blocks.php @@ -0,0 +1,28 @@ += 6.2. + * + * @see https://github.com/WordPress/gutenberg/pull/46496 + * + * @param array $settings Current block type settings. + * @param array $metadata Block metadata as read in via block.json. + * + * @return array Filtered block type settings. + */ +function gutenberg_add_selectors_to_block_type_settings( $settings, $metadata ) { + if ( ! isset( $settings['selectors'] ) && isset( $metadata['selectors'] ) ) { + $settings['selectors'] = $metadata['selectors']; + } + + return $settings; +} +add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_to_block_type_settings', 10, 2 ); diff --git a/lib/load.php b/lib/load.php index 0196421ebc7fbc..a499073754b17f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -103,6 +103,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.3 compat. require __DIR__ . '/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php'; require __DIR__ . '/compat/wordpress-6.3/script-loader.php'; +require __DIR__ . '/compat/wordpress-6.3/blocks.php'; // Experimental features. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WP 6.0's stopgap handler for Webfonts API. From 4776e71620f74309b5514ec1d7ff664ddf62e9b1 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 30 Jan 2023 20:53:36 +1000 Subject: [PATCH 08/26] Draft block selectors API documentation --- .../block-api/block-metadata.md | 15 +++ .../block-api/block-selectors.md | 117 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/reference-guides/block-api/block-selectors.md diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index d78050d79a9a1e..8cd20a730be1f9 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -28,6 +28,9 @@ Starting in WordPress 5.8 release, we encourage using the `block.json` metadata "my-plugin/message": "message" }, "usesContext": [ "groupId" ], + "selectors": { + "root": ".wp-block-my-plugin-notice" + }, "supports": { "align": true }, @@ -379,6 +382,18 @@ See [the block context documentation](/docs/reference-guides/block-api/block-con } ``` +### Selectors + +- Type: `object` +- Optional +- Localized: No +- Property: `selectors` +- Default: `{}` + +Any custom CSS selectors, keyed by `root`, feature, or sub-feature, to be used +when generating block styles for theme.json (global styles) stylesheets. +See the [the selectors documentation](/docs/reference-guides/block-api/block-selectors.md) for more details. + ### Supports - Type: `object` diff --git a/docs/reference-guides/block-api/block-selectors.md b/docs/reference-guides/block-api/block-selectors.md new file mode 100644 index 00000000000000..3405ee46b0b8b5 --- /dev/null +++ b/docs/reference-guides/block-api/block-selectors.md @@ -0,0 +1,117 @@ +# Selectors + +Block Selectors is the API that allows blocks to customize the CSS selector used +when their styles generated. + +A block may customize its CSS selectors at three levels: root, feature, and +subfeature. + +## Root Selector + +The block's primary CSS selector. + +All blocks require a primary CSS selector for their style declarations to be +included under. If one is not provided through the Block Selectors API, a +default is generated in the form of `.wp-block-`. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector" + } +} +``` + +## Feature Selectors + +Feature selectors relate to styles for a block support e.g. border, color, +typography etc. + +A block may wish to apply the styles for specific features to different +elements within a block. An example of this might be to apply colors on the +block's wrapper but apply the typography styles to an inner heading only. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector", + "color": ".my-custom-block-selector", + "typography": ".my-custom-block-selector > h2" + } +} +``` + +## Subfeature Selectors + +These selectors relate to individual styles provided by a block support e.g. +`background-color` + +A subfeature can have styles generated under its own unique selector. This is +especially useful where one block support subfeature can't be applied to the +same element as the support's other subfeatures. + +A great example of this is `text-decoration`. Web browsers render this style +differently making it difficult to override if applied to a wrapper element. By +assigning `text-decoration` a custom selector, its style can target only the +elements to which it should be applied. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector", + "color": ".my-custom-block-selector", + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + +## Shorthand + +Rather than specify a CSS selector for every subfeature, you can set a single +selector as a string value for the relevant feature. This is the approach +demonstrated for the `color` feature in earlier examples above. + +## Fallbacks + +If a selector hasn't been configured for a specific feature, it will fallback to +the block's root selector. Similarly, if a subfeature hasn't had a custom +selector set, it will fallback to its parent feature's selector, and if not +available, fallback further to the block's root selector. + +Rather than repeating selectors for multiple subfeatures, you can simply set the +common selector as the parent feature's `root` selector and only define the +unique selectors for the subfeatures that differ. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector", + "color": { + "text": ".my-custom-block-selector p" + }, + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + +In the above example, the `color.background-color` subfeature isn't explicitly +set. As the `color` feature also doesn't define a `root` selector, +`color.background-color` would be included under the block's primary root +selector; `.my-custom-block-selector`. + +For a subfeature such as `typography.font-size`, it would fallback to its parent +feature's selector given that is present i.e. `.my-custom-block-selector > h2`. \ No newline at end of file From a7d49bbf46a58297563ecb117ca57532f74925c6 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:43:18 +1000 Subject: [PATCH 09/26] Test root block selector generation for core block --- phpunit/class-wp-get-block-css-selectors-test.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 6ca7b3f0eeddff..93027acaade53f 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -54,6 +54,17 @@ public function test_get_root_selector_via_experimental_property() { $this->assertEquals( '.experimental-selector', $selector ); } + public function test_default_root_selector_generation_for_core_block() { + $block_type = self::register_test_block( + 'core/without-selectors-or-supports', + null, + null + ); + + $selector = wp_get_block_css_selector( $block_type ); + $this->assertEquals( '.wp-block-without-selectors-or-supports', $selector ); + } + public function test_default_root_selector_generation() { $block_type = self::register_test_block( 'test/without-selectors-or-supports', From e354e2ef18880d0a277367e97fdb39749ba9296d Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 31 Jan 2023 18:08:17 +1000 Subject: [PATCH 10/26] Fix typo in null duotone selector test --- phpunit/class-wp-get-block-css-selectors-test.php | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 93027acaade53f..58f9d3fcadab6a 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -113,6 +113,7 @@ public function test_no_duotone_selector_set() { $selector = wp_get_block_css_selector( $block_type, 'duotone' ); $this->assertEquals( '', $selector ); + $this->assertEquals( null, $selector ); } public function test_get_feature_selector_via_selectors_api() { From e35ca2229de4e9fd2b9e160d27c93c6f3cb60047 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 31 Jan 2023 18:10:02 +1000 Subject: [PATCH 11/26] Make duotone use color.duotone path --- .../wordpress-6.2/get-global-styles-and-settings.php | 2 +- phpunit/class-wp-get-block-css-selectors-test.php | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index 186e38406d1bad..9168e7977620f1 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -275,7 +275,7 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f $has_selectors = isset( $block_type->selectors ) && ! empty( $block_type->selectors ); // Duotone (No fallback selectors for Duotone). - if ( 'duotone' === $target ) { + if ( 'color.duotone' === $target || array( 'color', 'duotone' ) === $target ) { // If selectors API in use, only use it's value or null. if ( $has_selectors ) { return _wp_array_get( $block_type->selectors, array( 'color', 'duotone' ), null ); diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 58f9d3fcadab6a..6be015b1148657 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -85,7 +85,7 @@ public function test_get_duotone_selector_via_selectors_api() { null ); - $selector = wp_get_block_css_selector( $block_type, 'duotone' ); + $selector = wp_get_block_css_selector( $block_type, array( 'color', 'duotone' ) ); $this->assertEquals( '.duotone-selector', $selector ); } @@ -100,7 +100,7 @@ public function test_get_duotone_selector_via_experimental_property() { ) ); - $selector = wp_get_block_css_selector( $block_type, 'duotone' ); + $selector = wp_get_block_css_selector( $block_type, 'color.duotone' ); $this->assertEquals( '.experimental-duotone', $selector ); } @@ -111,8 +111,7 @@ public function test_no_duotone_selector_set() { null ); - $selector = wp_get_block_css_selector( $block_type, 'duotone' ); - $this->assertEquals( '', $selector ); + $selector = wp_get_block_css_selector( $block_type, 'color.duotone' ); $this->assertEquals( null, $selector ); } From 045e2562cc2ab202d9293b59fd89c0822fca7bc9 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 31 Jan 2023 18:19:27 +1000 Subject: [PATCH 12/26] Remove unnecessary selectors properties for fallback tests --- .../class-wp-get-block-css-selectors-test.php | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 6be015b1148657..7282283820f9e8 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -118,10 +118,7 @@ public function test_no_duotone_selector_set() { public function test_get_feature_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/feature-selector', - array( - 'root' => '.root', - 'typography' => array( 'root' => '.typography' ), - ), + array( 'typography' => array( 'root' => '.typography' ) ), null ); @@ -132,10 +129,7 @@ public function test_get_feature_selector_via_selectors_api() { public function test_get_feature_selector_via_selectors_api_shorthand_property() { $block_type = self::register_test_block( 'test/shorthand-feature-selector', - array( - 'root' => '.root', - 'typography' => '.typography', - ), + array( 'typography' => '.typography' ), null ); @@ -206,9 +200,7 @@ public function test_get_subfeature_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/subfeature-selector', array( - 'root' => '.root', 'typography' => array( - 'root' => '.root .typography', 'textDecoration' => '.root .typography .text-decoration', ), ), @@ -227,10 +219,7 @@ public function test_fallback_subfeature_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/subfeature-selector', array( - 'root' => '.root', - 'typography' => array( - 'root' => '.root .typography', - ), + 'typography' => array( 'root' => '.root .typography' ), ), null ); @@ -247,7 +236,7 @@ public function test_fallback_subfeature_selector_via_selectors_api() { public function test_no_subfeature_level_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/null-subfeature-selector', - array( 'root' => '.root' ), + array(), null ); From fd51b64e9ce55f1e29eed1c69760ab5067f35246 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 31 Jan 2023 18:31:02 +1000 Subject: [PATCH 13/26] Add extra test to cover feature fallback to generated default selector --- phpunit/class-wp-get-block-css-selectors-test.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 7282283820f9e8..2725f6e63d20ce 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -148,6 +148,18 @@ public function test_no_feature_level_selector_via_selectors_api() { $this->assertEquals( null, $selector ); } + public function test_fallback_feature_level_selector_via_selectors_api_to_generated_class() { + $block_type = self::register_test_block( + 'test/fallback-feature-selector', + array(), + null + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography', true ); + $this->assertEquals( '.wp-block-test-fallback-feature-selector', $selector ); + } + + public function test_fallback_feature_level_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/fallback-feature-selector', From b0c53a8cd9663dbd3136807c436ee584db437c39 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 13 Feb 2023 20:40:02 +1000 Subject: [PATCH 14/26] Fix typos and improve wording for docs --- .../block-api/block-selectors.md | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/reference-guides/block-api/block-selectors.md b/docs/reference-guides/block-api/block-selectors.md index 3405ee46b0b8b5..9ce605782c628e 100644 --- a/docs/reference-guides/block-api/block-selectors.md +++ b/docs/reference-guides/block-api/block-selectors.md @@ -1,14 +1,14 @@ # Selectors Block Selectors is the API that allows blocks to customize the CSS selector used -when their styles generated. +when their styles are generated. A block may customize its CSS selectors at three levels: root, feature, and subfeature. ## Root Selector -The block's primary CSS selector. +The root selector is the block's primary CSS selector. All blocks require a primary CSS selector for their style declarations to be included under. If one is not provided through the Block Selectors API, a @@ -26,12 +26,12 @@ default is generated in the form of `.wp-block-`. ## Feature Selectors -Feature selectors relate to styles for a block support e.g. border, color, -typography etc. +Feature selectors relate to styles for a block support, e.g. border, color, +typography, etc. A block may wish to apply the styles for specific features to different -elements within a block. An example of this might be to apply colors on the -block's wrapper but apply the typography styles to an inner heading only. +elements within a block. An example might be using colors on the block's wrapper +but applying the typography styles to an inner heading only. ### Example ```json @@ -55,7 +55,7 @@ especially useful where one block support subfeature can't be applied to the same element as the support's other subfeatures. A great example of this is `text-decoration`. Web browsers render this style -differently making it difficult to override if applied to a wrapper element. By +differently, making it difficult to override if added to a wrapper element. By assigning `text-decoration` a custom selector, its style can target only the elements to which it should be applied. @@ -78,16 +78,15 @@ elements to which it should be applied. Rather than specify a CSS selector for every subfeature, you can set a single selector as a string value for the relevant feature. This is the approach -demonstrated for the `color` feature in earlier examples above. +demonstrated for the `color` feature in the earlier examples above. ## Fallbacks -If a selector hasn't been configured for a specific feature, it will fallback to +A selector that hasn't been configured for a specific feature will fall back to the block's root selector. Similarly, if a subfeature hasn't had a custom -selector set, it will fallback to its parent feature's selector, and if not -available, fallback further to the block's root selector. +selector set, it will fall back to its parent feature's selector and, if unavailable, fall back further to the block's root selector. -Rather than repeating selectors for multiple subfeatures, you can simply set the +Rather than repeating selectors for multiple subfeatures, you can set the common selector as the parent feature's `root` selector and only define the unique selectors for the subfeatures that differ. @@ -108,10 +107,10 @@ unique selectors for the subfeatures that differ. } ``` -In the above example, the `color.background-color` subfeature isn't explicitly -set. As the `color` feature also doesn't define a `root` selector, +The `color.background-color` subfeature isn't explicitly set in the above +example. As the `color` feature also doesn't define a `root` selector, `color.background-color` would be included under the block's primary root -selector; `.my-custom-block-selector`. +selector, `.my-custom-block-selector`. For a subfeature such as `typography.font-size`, it would fallback to its parent -feature's selector given that is present i.e. `.my-custom-block-selector > h2`. \ No newline at end of file +feature's selector given that is present, i.e. `.my-custom-block-selector > h2`. \ No newline at end of file From a67f109c4285741f69238a87ffd2d589899cec44 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:50:51 +1000 Subject: [PATCH 15/26] Fix up checks for whether selectors API in use --- lib/class-wp-theme-json-gutenberg.php | 2 +- lib/compat/wordpress-6.2/get-global-styles-and-settings.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index e50836381a4595..3662436424ffaf 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -3449,7 +3449,7 @@ protected static function get_root_block_selector( $block_type ) { * @return object The custom selectors set by the block. */ protected static function get_block_selectors( $block_type, $root_selector ) { - if ( isset( $block_type->selectors ) ) { + if ( ! empty( $block_type->selectors ) ) { return $block_type->selectors; } diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index 9168e7977620f1..02c7d91047b906 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -272,7 +272,7 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f return null; } - $has_selectors = isset( $block_type->selectors ) && ! empty( $block_type->selectors ); + $has_selectors = ! empty( $block_type->selectors ); // Duotone (No fallback selectors for Duotone). if ( 'color.duotone' === $target || array( 'color', 'duotone' ) === $target ) { From 5561ed7034e49a89ce6014ea65a8184e6089bf9e Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:09:21 +1000 Subject: [PATCH 16/26] Add selectors default value in block type api --- packages/blocks/src/api/registration.js | 1 + packages/blocks/src/api/test/registration.js | 21 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 717e88223b9749..c36b87e6cd9fce 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -301,6 +301,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { attributes: {}, providesContext: {}, usesContext: [], + selectors: {}, supports: {}, styles: [], variations: [], diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 1c91654ece6c18..dbb11e4d23001a 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -134,6 +134,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -280,6 +281,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -316,6 +318,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -348,6 +351,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -382,6 +386,7 @@ describe( 'blocks', () => { }, usesContext: [ 'textColor' ], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -418,6 +423,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -454,6 +460,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -558,6 +565,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -589,6 +597,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -634,6 +643,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -693,6 +703,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -719,6 +730,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -805,6 +817,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -944,6 +957,7 @@ describe( 'blocks', () => { attributes: {}, providesContext: {}, usesContext: [], + selectors: {}, supports: {}, styles: [], variations: [ @@ -1010,6 +1024,7 @@ describe( 'blocks', () => { attributes: {}, providesContext: {}, usesContext: [], + selectors: {}, supports: {}, styles: [ { @@ -1075,6 +1090,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -1092,6 +1108,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -1171,6 +1188,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -1196,6 +1214,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -1228,6 +1247,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], @@ -1243,6 +1263,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, supports: {}, styles: [], variations: [], From e133f6a35710694a903724b40b07d69a49d33a4c Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:25:19 +1000 Subject: [PATCH 17/26] Improve block metadata selectors docs --- .../block-api/block-metadata.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index 8cd20a730be1f9..63219182dd1c2c 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -389,11 +389,32 @@ See [the block context documentation](/docs/reference-guides/block-api/block-con - Localized: No - Property: `selectors` - Default: `{}` +- Since: `WordPress 6.3.0` Any custom CSS selectors, keyed by `root`, feature, or sub-feature, to be used when generating block styles for theme.json (global styles) stylesheets. +Providing custom selectors allows more fine grained control over which styles +apply to what block elements, e.g. applying typography styles only to an inner +heading while colors are still applied on the outer block wrapper etc. + + See the [the selectors documentation](/docs/reference-guides/block-api/block-selectors.md) for more details. +```json +{ + "selectors": { + "root": ".my-custom-block-selector", + "color": { + "text": ".my-custom-block-selector p" + }, + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + ### Supports - Type: `object` From 9edfa551e18c219ea73f0e3030a29ed52f90ff8e Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:25:49 +1000 Subject: [PATCH 18/26] Note when selectors polyfill can be removed --- packages/blocks/src/api/registration.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index c36b87e6cd9fce..bc09f7e58a6439 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -170,7 +170,8 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { definitions[ blockName ].ancestor; } // The `selectors` prop is not yet included in the server provided - // definitions. Polyfill it as well. + // definitions. Polyfill it as well. This can be removed when + // the minimum supported WordPress is >= 6.3. if ( serverSideBlockDefinitions[ blockName ].selectors === undefined && From a42484d30782c884be6c218303f376d38f10f945 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:26:52 +1000 Subject: [PATCH 19/26] Add note on selectors API purpose to block.json schema --- schemas/json/block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/json/block.json b/schemas/json/block.json index f4b8df11690afa..7643be3fa8dde1 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -430,7 +430,7 @@ }, "selectors": { "type": "object", - "description": "Provides custom CSS selectors and mappings for the block. Selectors may be set for the block itself or per-feature e.g. typography.", + "description": "Provides custom CSS selectors and mappings for the block. Selectors may be set for the block itself or per-feature e.g. typography. Custom selectors per feature or sub-feature, allow different block styles to be applied to different elements within the block.", "properties": { "root": { "type": "string", From f11ccd774069a178231dc83467f372ebafe1cf75 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:37:18 +1000 Subject: [PATCH 20/26] Relocate new global function to 6.3 compat file --- .../get-global-styles-and-settings.php | 135 ----------------- .../get-global-styles-and-settings.php | 142 ++++++++++++++++++ lib/load.php | 3 + 3 files changed, 145 insertions(+), 135 deletions(-) create mode 100644 lib/compat/wordpress-6.3/get-global-styles-and-settings.php diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index 02c7d91047b906..e02a0466a0b98f 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -255,138 +255,3 @@ function _gutenberg_add_non_persistent_theme_json_cache_group() { wp_cache_add_non_persistent_groups( 'theme_json' ); } add_action( 'plugins_loaded', '_gutenberg_add_non_persistent_theme_json_cache_group' ); - -if ( ! function_exists( 'wp_get_block_css_selector' ) ) { - /** - * Determine the CSS selector for the block type and property provided, - * returning it if available. - * - * @param WP_Block_Type $block_type The block's type. - * @param string|array $target The desired selector's target, `root` or array path. - * @param boolean $fallback Whether or not to fallback to broader selector. - * - * @return string|null CSS selector or `null` if no selector available. - */ - function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = false ) { - if ( empty( $target ) ) { - return null; - } - - $has_selectors = ! empty( $block_type->selectors ); - - // Duotone (No fallback selectors for Duotone). - if ( 'color.duotone' === $target || array( 'color', 'duotone' ) === $target ) { - // If selectors API in use, only use it's value or null. - if ( $has_selectors ) { - return _wp_array_get( $block_type->selectors, array( 'color', 'duotone' ), null ); - } - - // Selectors API, not available, check for old experimental selector. - return _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null ); - } - - // Root Selector. - - // Calculated before returning as it can be used as fallback for - // feature selectors later on. - $root_selector = null; - - if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { - // Prefer the selectors API if available. - $root_selector = $block_type->selectors['root']; - } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { - // Use the old experimental selector supports property if set. - $root_selector = $block_type->supports['__experimentalSelector']; - } else { - // If no root selector found, generate default block class selector. - $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); - $root_selector = ".wp-block-{$block_name}"; - } - - // Return selector if it's the root target we are looking for. - if ( 'root' === $target ) { - return $root_selector; - } - - // If target is not `root` or `duotone` we have a feature or subfeature - // as the target. If the target is a string convert to an array. - if ( is_string( $target ) ) { - $target = explode( '.', $target ); - } - - // Feature Selectors ( May fallback to root selector ). - if ( 1 === count( $target ) ) { - $fallback_selector = $fallback ? $root_selector : null; - - // Prefer the selectors API if available. - if ( $has_selectors ) { - // Look for selector under `feature.root`. - $path = array_merge( $target, array( 'root' ) ); - $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); - - if ( $feature_selector ) { - return $feature_selector; - } - - // Check if feature selector set via shorthand. - $feature_selector = _wp_array_get( $block_type->selectors, $target, null ); - - return is_string( $feature_selector ) ? $feature_selector : $fallback_selector; - } - - // Try getting old experimental supports selector value. - $path = array_merge( $target, array( '__experimentalSelector' ) ); - $feature_selector = _wp_array_get( $block_type->supports, $path, null ); - - // Nothing to work with, provide fallback or null. - if ( null === $feature_selector ) { - return $fallback_selector; - } - - // Scope the feature selector by the block's root selector. - // TODO: Following is boilerplate from theme.json class. Is there a util? - $scopes = explode( ',', $root_selector ); - $selectors = explode( ',', $feature_selector ); - - $selectors_scoped = array(); - foreach ( $scopes as $outer ) { - foreach ( $selectors as $inner ) { - $outer = trim( $outer ); - $inner = trim( $inner ); - if ( ! empty( $outer ) && ! empty( $inner ) ) { - $selectors_scoped[] = $outer . ' ' . $inner; - } elseif ( empty( $outer ) ) { - $selectors_scoped[] = $inner; - } elseif ( empty( $inner ) ) { - $selectors_scoped[] = $outer; - } - } - } - - return implode( ', ', $selectors_scoped ); - } - - // Subfeature selector - // This may fallback either to parent feature or root selector. - $subfeature_selector = null; - // Use selectors API if available. - if ( $has_selectors ) { - $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); - - // Only return if we have a subfeature selector. - if ( $subfeature_selector ) { - return $subfeature_selector; - } - } - - // To this point we don't have a subfeature selector. If a fallback - // has been requested, remove subfeature from target path and return - // results of a call for the parent feature's selector. - if ( $fallback ) { - return wp_get_block_css_selector( $block_type, $target[0], $fallback ); - } - - // We tried... - return null; - } -} diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php new file mode 100644 index 00000000000000..80f932490cf11e --- /dev/null +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -0,0 +1,142 @@ +selectors ); + + // Duotone (No fallback selectors for Duotone). + if ( 'color.duotone' === $target || array( 'color', 'duotone' ) === $target ) { + // If selectors API in use, only use it's value or null. + if ( $has_selectors ) { + return _wp_array_get( $block_type->selectors, array( 'color', 'duotone' ), null ); + } + + // Selectors API, not available, check for old experimental selector. + return _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null ); + } + + // Root Selector. + + // Calculated before returning as it can be used as fallback for + // feature selectors later on. + $root_selector = null; + + if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { + // Prefer the selectors API if available. + $root_selector = $block_type->selectors['root']; + } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { + // Use the old experimental selector supports property if set. + $root_selector = $block_type->supports['__experimentalSelector']; + } else { + // If no root selector found, generate default block class selector. + $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); + $root_selector = ".wp-block-{$block_name}"; + } + + // Return selector if it's the root target we are looking for. + if ( 'root' === $target ) { + return $root_selector; + } + + // If target is not `root` or `duotone` we have a feature or subfeature + // as the target. If the target is a string convert to an array. + if ( is_string( $target ) ) { + $target = explode( '.', $target ); + } + + // Feature Selectors ( May fallback to root selector ). + if ( 1 === count( $target ) ) { + $fallback_selector = $fallback ? $root_selector : null; + + // Prefer the selectors API if available. + if ( $has_selectors ) { + // Look for selector under `feature.root`. + $path = array_merge( $target, array( 'root' ) ); + $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); + + if ( $feature_selector ) { + return $feature_selector; + } + + // Check if feature selector set via shorthand. + $feature_selector = _wp_array_get( $block_type->selectors, $target, null ); + + return is_string( $feature_selector ) ? $feature_selector : $fallback_selector; + } + + // Try getting old experimental supports selector value. + $path = array_merge( $target, array( '__experimentalSelector' ) ); + $feature_selector = _wp_array_get( $block_type->supports, $path, null ); + + // Nothing to work with, provide fallback or null. + if ( null === $feature_selector ) { + return $fallback_selector; + } + + // Scope the feature selector by the block's root selector. + $scopes = explode( ',', $root_selector ); + $selectors = explode( ',', $feature_selector ); + + $selectors_scoped = array(); + foreach ( $scopes as $outer ) { + foreach ( $selectors as $inner ) { + $outer = trim( $outer ); + $inner = trim( $inner ); + if ( ! empty( $outer ) && ! empty( $inner ) ) { + $selectors_scoped[] = $outer . ' ' . $inner; + } elseif ( empty( $outer ) ) { + $selectors_scoped[] = $inner; + } elseif ( empty( $inner ) ) { + $selectors_scoped[] = $outer; + } + } + } + + return implode( ', ', $selectors_scoped ); + } + + // Subfeature selector + // This may fallback either to parent feature or root selector. + $subfeature_selector = null; + // Use selectors API if available. + if ( $has_selectors ) { + $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); + + // Only return if we have a subfeature selector. + if ( $subfeature_selector ) { + return $subfeature_selector; + } + } + + // To this point we don't have a subfeature selector. If a fallback + // has been requested, remove subfeature from target path and return + // results of a call for the parent feature's selector. + if ( $fallback ) { + return wp_get_block_css_selector( $block_type, $target[0], $fallback ); + } + + // We tried... + return null; + } +} diff --git a/lib/load.php b/lib/load.php index a499073754b17f..e281aebd0d1411 100644 --- a/lib/load.php +++ b/lib/load.php @@ -93,6 +93,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.2/widgets.php'; require __DIR__ . '/compat/wordpress-6.2/menu.php'; +// WordPress 6.3 compat. +require __DIR__ . '/compat/wordpress-6.3/get-global-styles-and-settings.php'; + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-attribute-token.php'; require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-span.php'; From efaef5e6d56db56c960e0b53b81496ef58d44621 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 28 Feb 2023 16:57:48 +1000 Subject: [PATCH 21/26] Remove unnecessary space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- schemas/json/block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/json/block.json b/schemas/json/block.json index 7643be3fa8dde1..6023d829c5ecd0 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -434,7 +434,7 @@ "properties": { "root": { "type": "string", - "description": "The primary CSS class to apply to the block. This replaces the `.wp-block-name` class if set. " + "description": "The primary CSS class to apply to the block. This replaces the `.wp-block-name` class if set." }, "border": { "description": "Custom CSS selector used to generate rules for the block's theme.json border styles.", From 75a1f6a217e691748ef64aae995fc317f47e7ae2 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 13 Mar 2023 16:43:41 +1000 Subject: [PATCH 22/26] Move duotone under the filters feature --- lib/class-wp-theme-json-gutenberg.php | 3 +-- lib/compat/wordpress-6.3/get-global-styles-and-settings.php | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 3662436424ffaf..efa32732046451 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -864,8 +864,7 @@ protected static function get_blocks_metadata() { } // The block may or may not have a duotone selector. - // TODO: Should this target be `color.duotone` not `duotone`? - $duotone_selector = wp_get_block_css_selector( $block_type, 'duotone' ); + $duotone_selector = wp_get_block_css_selector( $block_type, 'filters.duotone' ); if ( null !== $duotone_selector ) { static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector; } diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php index 80f932490cf11e..d4694f8ca0d03c 100644 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -26,14 +26,14 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f $has_selectors = ! empty( $block_type->selectors ); // Duotone (No fallback selectors for Duotone). - if ( 'color.duotone' === $target || array( 'color', 'duotone' ) === $target ) { + if ( 'filters.duotone' === $target || array( 'filters', 'duotone' ) === $target ) { // If selectors API in use, only use it's value or null. if ( $has_selectors ) { - return _wp_array_get( $block_type->selectors, array( 'color', 'duotone' ), null ); + return _wp_array_get( $block_type->selectors, array( 'filters', 'duotone' ), null ); } // Selectors API, not available, check for old experimental selector. - return _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null ); + return _wp_array_get( $block_type->supports, array( 'filters', '__experimentalDuotone' ), null ); } // Root Selector. From 08b2604e6b6ea40a1f9897e42a5d1e0ace969790 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 13 Mar 2023 17:56:20 +1000 Subject: [PATCH 23/26] Fix duotone fallback and selector function tests --- lib/compat/wordpress-6.3/get-global-styles-and-settings.php | 2 +- phpunit/class-wp-get-block-css-selectors-test.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php index d4694f8ca0d03c..1ebda980c2bd21 100644 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -33,7 +33,7 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f } // Selectors API, not available, check for old experimental selector. - return _wp_array_get( $block_type->supports, array( 'filters', '__experimentalDuotone' ), null ); + return _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null ); } // Root Selector. diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 2725f6e63d20ce..41aafd1bd2db42 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -100,7 +100,7 @@ public function test_get_duotone_selector_via_experimental_property() { ) ); - $selector = wp_get_block_css_selector( $block_type, 'color.duotone' ); + $selector = wp_get_block_css_selector( $block_type, 'filters.duotone' ); $this->assertEquals( '.experimental-duotone', $selector ); } @@ -111,7 +111,7 @@ public function test_no_duotone_selector_set() { null ); - $selector = wp_get_block_css_selector( $block_type, 'color.duotone' ); + $selector = wp_get_block_css_selector( $block_type, 'filters.duotone' ); $this->assertEquals( null, $selector ); } From 991dae9b6dc8e689f440c4ce3fd728e5259fb3ed Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 14 Mar 2023 17:56:26 +1000 Subject: [PATCH 24/26] Add editorSelectors API --- .../block-api/block-metadata.md | 35 +++++ .../block-api/block-selectors.md | 33 ++++- lib/class-wp-theme-json-gutenberg.php | 12 ++ lib/compat/wordpress-6.3/blocks.php | 12 +- .../get-global-styles-and-settings.php | 63 +++++++-- packages/block-library/src/image/block.json | 3 + packages/blocks/src/api/registration.js | 16 ++- packages/blocks/src/api/test/registration.js | 23 ++++ .../class-wp-get-block-css-selectors-test.php | 123 +++++++++++++++++- phpunit/class-wp-theme-json-test.php | 2 +- schemas/json/block.json | 98 ++++++++++++++ 11 files changed, 393 insertions(+), 27 deletions(-) diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index 63219182dd1c2c..b00bc533290745 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -28,6 +28,9 @@ Starting in WordPress 5.8 release, we encourage using the `block.json` metadata "my-plugin/message": "message" }, "usesContext": [ "groupId" ], + "editorSelectors": { + "root": ".editor-styles-wrapper wp-block-my-plugin-notice" + }, "selectors": { "root": ".wp-block-my-plugin-notice" }, @@ -382,6 +385,38 @@ See [the block context documentation](/docs/reference-guides/block-api/block-con } ``` +### Editor Selectors + +- Type: `object` +- Optional +- Localized: No +- Property: `editorSelectors` +- Default: `{}` +- Since: `WordPress 6.3.0` + +Any editor specific custom CSS selectors, keyed by `root`, feature, or +sub-feature, to be used when generating block styles for theme.json +(global styles) stylesheets in the editor. + +Editor only selectors override those defined within the `selectors` property. + +See the [the selectors documentation](/docs/reference-guides/block-api/block-selectors.md) for more details. + +```json +{ + "editorSelectors": { + "root": ".my-custom-block-selector", + "color": { + "text": ".my-custom-block-selector p" + }, + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + ### Selectors - Type: `object` diff --git a/docs/reference-guides/block-api/block-selectors.md b/docs/reference-guides/block-api/block-selectors.md index 9ce605782c628e..6560827bc54e29 100644 --- a/docs/reference-guides/block-api/block-selectors.md +++ b/docs/reference-guides/block-api/block-selectors.md @@ -4,7 +4,7 @@ Block Selectors is the API that allows blocks to customize the CSS selector used when their styles are generated. A block may customize its CSS selectors at three levels: root, feature, and -subfeature. +subfeature. Each may also be overridden with editor-only selectors. ## Root Selector @@ -113,4 +113,33 @@ example. As the `color` feature also doesn't define a `root` selector, selector, `.my-custom-block-selector`. For a subfeature such as `typography.font-size`, it would fallback to its parent -feature's selector given that is present, i.e. `.my-custom-block-selector > h2`. \ No newline at end of file +feature's selector given that is present, i.e. `.my-custom-block-selector > h2`. + +## Editor-only Selectors + +There are scenarios in which a block might need different markup within the +editor compared to the frontend e.g. inline cropping of the Image block. Some +generated styles may then need to be applied to different, or multiple, +elements. + +Continuing with the Image cropping example, the image border styles need to also +be applied to the cropping area. If the selector for the cropping area is added +to the normal `selectors` config for the block, it would be output unnecessarily +on the frontend. + +To avoid this, and include the selector for the editor only, the selectors for the border feature can be +overridden via the `editorSelectors` config. + +### Example +```json +{ + ... + "selectors": { + "root": ".wp-block-image", + "border": ".wp-block-image img" + }, + "editorSelectors": { + "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area" + }, +} +``` diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index efa32732046451..59a807df8a8b79 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -868,6 +868,7 @@ protected static function get_blocks_metadata() { if ( null !== $duotone_selector ) { static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector; } + // If the block has style variations, append their selectors to the block metadata. if ( ! empty( $block_type->styles ) ) { $style_selectors = array(); @@ -3449,6 +3450,17 @@ protected static function get_root_block_selector( $block_type ) { */ protected static function get_block_selectors( $block_type, $root_selector ) { if ( ! empty( $block_type->selectors ) ) { + $in_editor = false; + + if ( function_exists( 'get_current_screen' ) ) { + $current_screen = get_current_screen(); + $in_editor = $current_screen && $current_screen->is_block_editor; + } + + if ( $in_editor && ! empty( $block_type->editor_selectors ) ) { + return array_merge( $block_type->selectors, $block_type->editor_selectors ); + } + return $block_type->selectors; } diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php index ac56b4814ebfc6..8866edf5b95fda 100644 --- a/lib/compat/wordpress-6.3/blocks.php +++ b/lib/compat/wordpress-6.3/blocks.php @@ -6,8 +6,8 @@ */ /** - * Ensure the selectors property, set via block.json metadata, is included - * within the block type's settings. + * Ensure the selectors, and editorSelectors properties, set via block.json + * metadata, are included within the block type's settings. * * Note: This should be removed when the minimum required WP version is >= 6.2. * @@ -18,11 +18,15 @@ * * @return array Filtered block type settings. */ -function gutenberg_add_selectors_to_block_type_settings( $settings, $metadata ) { +function gutenberg_add_selectors_properties_to_block_type_settings( $settings, $metadata ) { if ( ! isset( $settings['selectors'] ) && isset( $metadata['selectors'] ) ) { $settings['selectors'] = $metadata['selectors']; } + if ( ! isset( $settings['editor_selectors'] ) && isset( $metadata['editorSelectors'] ) ) { + $settings['editor_selectors'] = $metadata['editorSelectors']; + } + return $settings; } -add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_to_block_type_settings', 10, 2 ); +add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_properties_to_block_type_settings', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php index 1ebda980c2bd21..5fddde2efaefdb 100644 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -23,10 +23,24 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f return null; } - $has_selectors = ! empty( $block_type->selectors ); + $has_selectors = ! empty( $block_type->selectors ); + $use_editor_selectors = false; + + // Determine if we are in the editor and require editor selectors + // if they are available. + if ( function_exists( 'get_current_screen' ) ) { + $current_screen = get_current_screen(); + $use_editor_selectors = ! empty( $block_type->editor_selectors ) && $current_screen && $current_screen->is_block_editor; + } // Duotone (No fallback selectors for Duotone). if ( 'filters.duotone' === $target || array( 'filters', 'duotone' ) === $target ) { + // Prefer editor selector if available. + $duotone_editor_selector = $use_editor_selectors && _wp_array_get( $block_type->editor_selectors, array( 'filters', 'duotone' ), null ); + if ( $duotone_editor_selector ) { + return $duotone_editor_selector; + } + // If selectors API in use, only use it's value or null. if ( $has_selectors ) { return _wp_array_get( $block_type->selectors, array( 'filters', 'duotone' ), null ); @@ -42,8 +56,11 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f // feature selectors later on. $root_selector = null; - if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { - // Prefer the selectors API if available. + if ( $use_editor_selectors && isset( $block_type->editor_selectors['root'] ) ) { + // Prefer editor selectors if specified. + $root_selector = $block_type->editor_selectors['root']; + } elseif ( $has_selectors && isset( $block_type->selectors['root'] ) ) { + // Use the selectors API if available. $root_selector = $block_type->selectors['root']; } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { // Use the old experimental selector supports property if set. @@ -69,10 +86,28 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f if ( 1 === count( $target ) ) { $fallback_selector = $fallback ? $root_selector : null; + // Look for selector under `feature.root`. + $path = array_merge( $target, array( 'root' ) ); + + // Use editor specific selector if available. + if ( $use_editor_selectors ) { + $feature_selector = _wp_array_get( $block_type->editor_selectors, $path, null ); + + if ( $feature_selector ) { + return $feature_selector; + } + + // Check if feature selector set via shorthand. + $feature_selector = _wp_array_get( $block_type->editor_selectors, $target, null ); + + // Only return if a selector was found. + if ( is_string( $feature_selector ) ) { + return $feature_selector; + } + } + // Prefer the selectors API if available. if ( $has_selectors ) { - // Look for selector under `feature.root`. - $path = array_merge( $target, array( 'root' ) ); $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); if ( $feature_selector ) { @@ -119,14 +154,22 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f // Subfeature selector // This may fallback either to parent feature or root selector. $subfeature_selector = null; + + // Use any explicit editor selector. Subfeature editor-only selectors + // will not fall back to the feature's editor specific selector if + // the normal selectors object contains a selector for the subfeature. + if ( $use_editor_selectors ) { + $subfeature_selector = _wp_array_get( $block_type->editor_selectors, $target, null ); + } + // Use selectors API if available. - if ( $has_selectors ) { + if ( $has_selectors && ! $subfeature_selector ) { $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); + } - // Only return if we have a subfeature selector. - if ( $subfeature_selector ) { - return $subfeature_selector; - } + // Only return if we have a subfeature selector. + if ( $subfeature_selector ) { + return $subfeature_selector; } // To this point we don't have a subfeature selector. If a fallback diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index ddb33649d77ef9..2342aa44d67302 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -102,6 +102,9 @@ } }, "selectors": { + "border": ".wp-block-image img" + }, + "editorSelectors": { "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area" }, "styles": [ diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index bc09f7e58a6439..cf27e0b2f38299 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -169,9 +169,17 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { serverSideBlockDefinitions[ blockName ].ancestor = definitions[ blockName ].ancestor; } - // The `selectors` prop is not yet included in the server provided - // definitions. Polyfill it as well. This can be removed when - // the minimum supported WordPress is >= 6.3. + // The `selectors` and `editorSelectors` props are not yet included + // in the server provided definitions. Polyfill it as well. This can + // be removed when the minimum supported WordPress is >= 6.3. + if ( + serverSideBlockDefinitions[ blockName ].editorSelectors === + undefined && + definitions[ blockName ].editorSelectors + ) { + serverSideBlockDefinitions[ blockName ].editorSelectors = + definitions[ blockName ].editorSelectors; + } if ( serverSideBlockDefinitions[ blockName ].selectors === undefined && @@ -215,6 +223,7 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { 'providesContext', 'usesContext', 'selectors', + 'editorSelectors', 'supports', 'styles', 'example', @@ -303,6 +312,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { providesContext: {}, usesContext: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index dbb11e4d23001a..8a3e9fad0d5b5b 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -135,6 +135,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -282,6 +283,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -319,6 +321,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -352,6 +355,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -387,6 +391,7 @@ describe( 'blocks', () => { usesContext: [ 'textColor' ], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -424,6 +429,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -461,6 +467,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -478,6 +485,7 @@ describe( 'blocks', () => { unstable__bootstrapServerSideBlockDefinitions( { [ blockName ]: { selectors: { root: '.wp-block-custom-selector' }, + editorSelectors: { root: '.editor-only-selector' }, category: 'ignored', }, } ); @@ -497,6 +505,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: { root: '.wp-block-custom-selector' }, + editorSelectors: { root: '.editor-only-selector' }, supports: {}, styles: [], variations: [], @@ -566,6 +575,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -598,6 +608,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -644,6 +655,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -704,6 +716,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -731,6 +744,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -818,6 +832,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -958,6 +973,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [ @@ -1025,6 +1041,7 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [ { @@ -1091,6 +1108,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1109,6 +1127,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1189,6 +1208,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1215,6 +1235,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1248,6 +1269,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1264,6 +1286,7 @@ describe( 'blocks', () => { usesContext: [], keywords: [], selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], diff --git a/phpunit/class-wp-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php index 41aafd1bd2db42..47e42158ceea45 100644 --- a/phpunit/class-wp-get-block-css-selectors-test.php +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -16,23 +16,30 @@ public function set_up() { public function tear_down() { unregister_block_type( $this->test_block_name ); $this->test_block_name = null; + set_current_screen( '' ); parent::tear_down(); } - private function register_test_block( $name, $selectors = null, $supports = null ) { + private function register_test_block( $name, $selectors = null, $supports = null, $editor_selectors = null ) { $this->test_block_name = $name; return register_block_type( $this->test_block_name, array( - 'api_version' => 2, - 'attributes' => array(), - 'selectors' => $selectors, - 'supports' => $supports, + 'api_version' => 2, + 'attributes' => array(), + 'selectors' => $selectors, + 'editor_selectors' => $editor_selectors, + 'supports' => $supports, ) ); } + private function set_screen_to_block_editor() { + set_current_screen( 'edit-post' ); + get_current_screen()->is_block_editor( true ); + } + public function test_get_root_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/block-with-selectors', @@ -80,12 +87,12 @@ public function test_get_duotone_selector_via_selectors_api() { $block_type = self::register_test_block( 'test/duotone-selector', array( - 'color' => array( 'duotone' => '.duotone-selector' ), + 'filters' => array( 'duotone' => '.duotone-selector' ), ), null ); - $selector = wp_get_block_css_selector( $block_type, array( 'color', 'duotone' ) ); + $selector = wp_get_block_css_selector( $block_type, array( 'filters', 'duotone' ) ); $this->assertEquals( '.duotone-selector', $selector ); } @@ -328,4 +335,106 @@ public function test_string_targets_for_subfeatures() { $selector = wp_get_block_css_selector( $block_type, array( 'typography', 'fontSize' ) ); $this->assertEquals( '.found', $selector ); } + + public function test_editor_only_root_selector() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-only-selectors', + array( 'root' => '.wp-custom-block-class' ), + null, + array( 'root' => '.editor-only.wp-custom-block-class' ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'root' ); + $this->assertEquals( '.editor-only.wp-custom-block-class', $selector ); + } + + public function test_editor_only_duotone_selector() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-duotone-selector', + array( + 'filters' => array( 'duotone' => '.duotone-selector' ), + ), + null, + array( + 'filters' => array( 'duotone' => '.editor-duotone-selector' ), + ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'filters.duotone' ); + $this->assertEquals( '.editor-duotone-selector', $selector ); + } + + public function test_editor_only_feature_selector() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-feature-selector', + array( 'typography' => array( 'root' => '.typography' ) ), + null, + array( 'typography' => array( 'root' => '.editor-typography' ) ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( '.editor-typography', $selector ); + } + + public function test_editor_only_feature_selector_shorthand() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-feature-selector', + array( 'typography' => '.typography' ), + null, + array( 'typography' => '.editor-typography' ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography' ); + $this->assertEquals( '.editor-typography', $selector ); + } + + public function test_editor_only_subfeature_selector() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-subfeature-selector', + array( 'typography' => array( 'fontSize' => '.font-size' ) ), + null, + array( 'typography' => array( 'fontSize' => '.editor-font-size' ) ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography.fontSize' ); + $this->assertEquals( '.editor-font-size', $selector ); + } + + public function test_non_editor_subfeature_does_not_fall_back_to_editor_only_feature_selector() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-subfeature-selector', + array( 'typography' => array( 'fontSize' => '.font-size' ) ), + null, + array( 'typography' => '.editor-font-size' ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography.fontSize', true ); + $this->assertEquals( '.font-size', $selector ); + } + + public function test_unspecified_subfeature_falls_back_to_editor_only_feature_selector() { + self::set_screen_to_block_editor(); + + $block_type = self::register_test_block( + 'test/editor-subfeature-selector', + array( 'typography' => '.typography' ), + null, + array( 'typography' => '.editor-typography' ) + ); + + $selector = wp_get_block_css_selector( $block_type, 'typography.fontSize', true ); + $this->assertEquals( '.editor-typography', $selector ); + } } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index eeb5940f44cd35..104d87afc3e39b 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -460,7 +460,7 @@ public function test_get_stylesheet() { ); $variables = 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}'; - $styles = 'body { margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }body{color: var(--wp--preset--color--grey);}a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;min-height: 50vh;padding: 24px;}.wp-block-group a:where(:not(.wp-element-button)){color: #111;}.wp-block-heading{color: #123456;}.wp-block-heading a:where(:not(.wp-element-button)){background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a:where(:not(.wp-element-button)){background-color: #777;color: #555;}.wp-block-post-excerpt{column-count: 2;}.wp-block-image{margin-bottom: 30px;}.wp-block-image img, .wp-block-image .wp-block-image__crop-area{border-top-left-radius: 10px;border-bottom-right-radius: 1em;}'; + $styles = 'body { margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }body{color: var(--wp--preset--color--grey);}a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;min-height: 50vh;padding: 24px;}.wp-block-group a:where(:not(.wp-element-button)){color: #111;}.wp-block-heading{color: #123456;}.wp-block-heading a:where(:not(.wp-element-button)){background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a:where(:not(.wp-element-button)){background-color: #777;color: #555;}.wp-block-post-excerpt{column-count: 2;}.wp-block-image{margin-bottom: 30px;}.wp-block-image img{border-top-left-radius: 10px;border-bottom-right-radius: 1em;}'; $presets = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-small-font-family{font-family: var(--wp--preset--font-family--small) !important;}.has-big-font-family{font-family: var(--wp--preset--font-family--big) !important;}'; $all = $variables . $styles . $presets; diff --git a/schemas/json/block.json b/schemas/json/block.json index 6023d829c5ecd0..c0da49776b3f77 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -526,6 +526,104 @@ } } }, + "editorSelectors": { + "type": "object", + "description": "Provides editor specific overrides to the custom CSS selectors defined within the block's selectors config.", + "properties": { + "root": { + "type": "string", + "description": "The primary CSS class to apply to the block within the editor. This replaces the `.wp-block-name` class if set." + }, + "border": { + "description": "Custom CSS selector used to generate rules for the block's theme.json border styles within the editor.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "color": { "type": "string" }, + "radius": { "type": "string" }, + "style": { "type": "string" }, + "width": { "type": "string" } + } + } + ] + }, + "color": { + "description": "Custom CSS selector used to generate rules for the block's theme.json color styles within the editor.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "text": { "type": "string" }, + "background": { "type": "string" } + } + } + ] + }, + "dimensions": { + "description": "Custom CSS selector used to generate rules for the block's theme.json dimensions styles within the editor.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "minHeight": { "type": "string" } + } + } + ] + }, + "spacing": { + "description": "Custom CSS selector used to generate rules for the block's theme.json spacing styles within the editor.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "blockGap": { "type": "string" }, + "padding": { "type": "string" }, + "margin": { "type": "string" } + } + } + ] + }, + "typography": { + "description": "Custom CSS selector used to generate rules for the block's theme.json typography styles within the editor.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "root": { "type": "string" }, + "fontFamily": { "type": "string" }, + "fontSize": { "type": "string" }, + "fontStyle": { "type": "string" }, + "fontWeight": { "type": "string" }, + "lineHeight": { "type": "string" }, + "letterSpacing": { "type": "string" }, + "textDecoration": { "type": "string" }, + "textTransform": { "type": "string" } + } + } + ] + } + } + }, "styles": { "type": "array", "description": "Block styles can be used to provide alternative styles to block. It works by adding a class name to the block’s wrapper. Using CSS, a theme developer can target the class name for the block style if it is selected.\n\nPlugins and Themes can also register custom block style for existing blocks.\n\nhttps://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles", From 28d39fa4263cf3434dff5e123d95d74513348114 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 15 Mar 2023 18:15:39 +1000 Subject: [PATCH 25/26] Switch to ternary for editor duotone selector check --- lib/compat/wordpress-6.3/get-global-styles-and-settings.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php index 5fddde2efaefdb..9f46b3159e9310 100644 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -36,7 +36,10 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f // Duotone (No fallback selectors for Duotone). if ( 'filters.duotone' === $target || array( 'filters', 'duotone' ) === $target ) { // Prefer editor selector if available. - $duotone_editor_selector = $use_editor_selectors && _wp_array_get( $block_type->editor_selectors, array( 'filters', 'duotone' ), null ); + $duotone_editor_selector = $use_editor_selectors + ? _wp_array_get( $block_type->editor_selectors, array( 'filters', 'duotone' ), null ) + : null; + if ( $duotone_editor_selector ) { return $duotone_editor_selector; } From 8aa8f2405f994fdfc134f0dc9a12d301066f9b98 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 17 Mar 2023 15:06:03 +1000 Subject: [PATCH 26/26] Fix typo Co-authored-by: Alex Lende --- docs/reference-guides/block-api/block-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index b00bc533290745..d048cc78fede68 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -29,7 +29,7 @@ Starting in WordPress 5.8 release, we encourage using the `block.json` metadata }, "usesContext": [ "groupId" ], "editorSelectors": { - "root": ".editor-styles-wrapper wp-block-my-plugin-notice" + "root": ".editor-styles-wrapper .wp-block-my-plugin-notice" }, "selectors": { "root": ".wp-block-my-plugin-notice"