diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index d78050d79a9a1..d048cc78fede6 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -28,6 +28,12 @@ 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" + }, "supports": { "align": true }, @@ -379,6 +385,71 @@ 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` +- Optional +- 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` 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 0000000000000..6560827bc54e2 --- /dev/null +++ b/docs/reference-guides/block-api/block-selectors.md @@ -0,0 +1,145 @@ +# Selectors + +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. Each may also be overridden with editor-only selectors. + +## Root 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 +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 might be using colors on the block's wrapper +but applying 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 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. + +### 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 the earlier examples above. + +## Fallbacks + +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 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 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" + } + } +} +``` + +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`. + +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`. + +## 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 5bd06274e6efd..59a807df8a8b7 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -853,56 +853,22 @@ 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']; - } + static::$blocks_metadata[ $block_name ]['selector'] = $root_selector; + static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector ); - // 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'] - ); - } + $elements = static::get_block_element_selectors( $root_selector ); + if ( ! empty( $elements ) ) { + static::$blocks_metadata[ $block_name ]['elements'] = $elements; } - if ( ! empty( $features ) ) { - static::$blocks_metadata[ $block_name ]['features'] = $features; + // The block may or may not have a duotone selector. + $duotone_selector = wp_get_block_css_selector( $block_type, 'filters.duotone' ); + if ( null !== $duotone_selector ) { + static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector; } - // 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 ); - } // If the block has style variations, append their selectors to the block metadata. if ( ! empty( $block_type->styles ) ) { $style_selectors = array(); @@ -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,189 @@ 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 ( ! 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; + } + + $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; + } } diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php new file mode 100644 index 0000000000000..8866edf5b95fd --- /dev/null +++ b/lib/compat/wordpress-6.3/blocks.php @@ -0,0 +1,32 @@ += 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_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_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 new file mode 100644 index 0000000000000..9f46b3159e931 --- /dev/null +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -0,0 +1,188 @@ +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 ) + : 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 ); + } + + // 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 ( $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. + $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; + + // 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 ) { + $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 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 && ! $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; + } + + // 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 0196421ebc7fb..e281aebd0d141 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'; @@ -103,6 +106,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. diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 62edb882be0c9..2342aa44d6730 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,12 @@ } } }, + "selectors": { + "border": ".wp-block-image img" + }, + "editorSelectors": { + "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area" + }, "styles": [ { "name": "default", diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 8605fa4838abe..cf27e0b2f3829 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -169,6 +169,25 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { serverSideBlockDefinitions[ blockName ].ancestor = definitions[ blockName ].ancestor; } + // 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 && + definitions[ blockName ].selectors + ) { + serverSideBlockDefinitions[ blockName ].selectors = + definitions[ blockName ].selectors; + } continue; } @@ -203,6 +222,8 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { 'attributes', 'providesContext', 'usesContext', + 'selectors', + 'editorSelectors', 'supports', 'styles', 'example', @@ -290,6 +311,8 @@ export function registerBlockType( blockNameOrMetadata, settings ) { attributes: {}, 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 58bf57726a3b9..8a3e9fad0d5b5 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -134,6 +134,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -280,6 +282,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -316,6 +320,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -348,6 +354,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -382,6 +390,8 @@ describe( 'blocks', () => { }, usesContext: [ 'textColor' ], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -418,6 +428,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -454,6 +466,46 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, + supports: {}, + styles: [], + variations: [], + } ); + } ); + + // 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' }, + editorSelectors: { root: '.editor-only-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' }, + editorSelectors: { root: '.editor-only-selector' }, supports: {}, styles: [], variations: [], @@ -522,6 +574,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -553,6 +607,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -598,6 +654,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -657,6 +715,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -683,6 +743,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -769,6 +831,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -908,6 +972,8 @@ describe( 'blocks', () => { attributes: {}, providesContext: {}, usesContext: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [ @@ -974,6 +1040,8 @@ describe( 'blocks', () => { attributes: {}, providesContext: {}, usesContext: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [ { @@ -1039,6 +1107,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1056,6 +1126,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1135,6 +1207,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1160,6 +1234,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1192,6 +1268,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], @@ -1207,6 +1285,8 @@ describe( 'blocks', () => { providesContext: {}, usesContext: [], keywords: [], + selectors: {}, + editorSelectors: {}, supports: {}, styles: [], variations: [], diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php index fac777864200a..e15509e43143e 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-get-block-css-selectors-test.php b/phpunit/class-wp-get-block-css-selectors-test.php new file mode 100644 index 0000000000000..47e42158ceea4 --- /dev/null +++ b/phpunit/class-wp-get-block-css-selectors-test.php @@ -0,0 +1,440 @@ +test_block_name = null; + } + + 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, $editor_selectors = null ) { + $this->test_block_name = $name; + + return register_block_type( + $this->test_block_name, + array( + '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', + 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_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', + 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( + 'filters' => array( 'duotone' => '.duotone-selector' ), + ), + null + ); + + $selector = wp_get_block_css_selector( $block_type, array( 'filters', '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, 'filters.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, 'filters.duotone' ); + $this->assertEquals( null, $selector ); + } + + public function test_get_feature_selector_via_selectors_api() { + $block_type = self::register_test_block( + 'test/feature-selector', + array( '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( '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_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', + 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( + 'typography' => array( + '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( + '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(), + 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 ); + } + + 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 b46838c0275b1..104d87afc3e39 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; @@ -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( diff --git a/schemas/json/block.json b/schemas/json/block.json index ad3bb45516b9f..c0da49776b3f7 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -428,6 +428,202 @@ }, "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. Custom selectors per feature or sub-feature, allow different block styles to be applied to different elements within the block.", + "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" } + } + } + ] + } + } + }, + "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",