From 16e2b2992fa6a0139e8aeb7a15c2e90fb295ab0c Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 29 May 2024 13:56:45 +1000 Subject: [PATCH] Block Styles: Extend block style variations as mechanism for achieving section styling (#57908) Co-authored-by: aaronrobertshaw Co-authored-by: tellthemachines Co-authored-by: ramonjd Co-authored-by: talldan Co-authored-by: youknowriad Co-authored-by: ellatrix Co-authored-by: SaxonF Co-authored-by: richtabor Co-authored-by: fabiankaegy Co-authored-by: cbirdsong Co-authored-by: bacoords Co-authored-by: getdave Co-authored-by: colorful-tones Co-authored-by: hanneslsm Co-authored-by: andrewserong Co-authored-by: kevin940726 Co-authored-by: ajlende Co-authored-by: MaggieCabrera Co-authored-by: scruffian --- backport-changelog/6.6/6662.md | 3 + lib/block-supports/block-style-variations.php | 375 ++++++++++++++++ lib/class-wp-theme-json-gutenberg.php | 102 ++++- ...class-wp-theme-json-resolver-gutenberg.php | 38 +- lib/load.php | 1 + .../src/components/global-styles/index.js | 2 + .../global-styles/use-global-styles-output.js | 137 +++++- .../src/hooks/block-style-variation.js | 156 +++++++ packages/block-editor/src/hooks/index.js | 2 + packages/block-editor/src/hooks/utils.js | 1 + packages/block-library/src/list/style.scss | 6 +- .../block-library/src/paragraph/style.scss | 3 +- .../components/global-styles/screen-block.js | 2 +- .../variations/variations-panel.js | 21 +- .../global-styles-provider/index.js | 97 +++- packages/editor/src/utils/set-nested-value.js | 39 ++ .../block-style-variations-test.php | 130 ++++++ phpunit/class-wp-theme-json-resolver-test.php | 167 ++++--- .../style.css | 8 + .../styles/block-style-variation-a.json | 10 + .../theme.json | 4 + .../styles/block-style-variation-a.json | 10 + .../styles/block-style-variation-b.json | 10 + schemas/json/theme.json | 416 +++++++++++++++++- 24 files changed, 1629 insertions(+), 111 deletions(-) create mode 100644 backport-changelog/6.6/6662.md create mode 100644 lib/block-supports/block-style-variations.php create mode 100644 packages/block-editor/src/hooks/block-style-variation.js create mode 100644 packages/editor/src/utils/set-nested-value.js create mode 100644 phpunit/block-supports/block-style-variations-test.php create mode 100644 phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css create mode 100644 phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json create mode 100644 phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json create mode 100644 phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json create mode 100644 phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json diff --git a/backport-changelog/6.6/6662.md b/backport-changelog/6.6/6662.md new file mode 100644 index 00000000000000..2dfbc68dd23e03 --- /dev/null +++ b/backport-changelog/6.6/6662.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/6662 + +* https://github.com/WordPress/gutenberg/pull/57908 diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php new file mode 100644 index 00000000000000..6fc89b01c6793d --- /dev/null +++ b/lib/block-supports/block-style-variations.php @@ -0,0 +1,375 @@ +get_raw_data(); + + // Only the first block style variation with data is supported. + $variation_data = array(); + foreach ( $variations as $variation ) { + $variation_data = $theme_json['styles']['blocks'][ $parsed_block['blockName'] ]['variations'][ $variation ] ?? array(); + + if ( ! empty( $variation_data ) ) { + break; + } + } + + if ( empty( $variation_data ) ) { + return $parsed_block; + } + + $config = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => $variation_data, + ); + + $class_name = gutenberg_get_block_style_variation_class_name( $parsed_block, $variation ); + $updated_class_name = $parsed_block['attrs']['className'] . " $class_name"; + + $class_name = ".$class_name"; + + if ( ! is_admin() ) { + remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); + } + + $variation_theme_json = new WP_Theme_JSON_Gutenberg( $config, 'blocks' ); + $variation_styles = $variation_theme_json->get_stylesheet( + array( 'styles' ), + array( 'custom' ), + array( + 'root_selector' => $class_name, + 'skip_root_layout_styles' => true, + 'scope' => $class_name, + ) + ); + + if ( ! is_admin() ) { + add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); + } + + if ( empty( $variation_styles ) ) { + return $parsed_block; + } + + wp_register_style( 'block-style-variation-styles', false, array( 'global-styles' ) ); + wp_add_inline_style( 'block-style-variation-styles', $variation_styles ); + + /* + * Add variation instance class name to block's className string so it can + * be enforced in the block markup via render_block filter. + */ + _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); + + return $parsed_block; +} + +/** + * Ensure the variation block support class name generated and added to + * block attributes in the `render_block_data` filter gets applied to the + * block's markup. + * + * @see gutenberg_render_block_style_variation_support_styles + * + * @since 6.6.0 + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * + * @return string Filtered block content. + */ +function gutenberg_render_block_style_variation_class_name( $block_content, $block ) { + if ( ! $block_content || empty( $block['attrs']['className'] ) ) { + return $block_content; + } + + /* + * Matches a class prefixed by `is-style`, followed by the + * variation slug, then `--`, and finally a hash. + * + * See `gutenberg_get_block_style_variation_class_name` for class generation. + */ + preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches ); + + if ( empty( $matches ) ) { + return $block_content; + } + + $tags = new WP_HTML_Tag_Processor( $block_content ); + + if ( $tags->next_tag() ) { + /* + * Ensure the variation instance class name set in the + * `render_block_data` filter is applied in markup. + * See `gutenberg_render_block_style_variation_support_styles`. + */ + $tags->add_class( $matches[0] ); + } + + return $tags->get_updated_html(); +} + +/** + * Collects block style variation data for merging with theme.json data. + * As each block style variation is processed it is registered if it hasn't + * been already. This registration is required for later sanitization of + * theme.json data. + * + * @since 6.6.0 + * + * @param array $variations Shared block style variations. + * + * @return array Block variations data to be merged under styles.blocks + */ +function gutenberg_resolve_and_register_block_style_variations( $variations ) { + $variations_data = array(); + + if ( empty( $variations ) ) { + return $variations_data; + } + + $registry = WP_Block_Styles_Registry::get_instance(); + $have_named_variations = ! wp_is_numeric_array( $variations ); + + foreach ( $variations as $key => $variation ) { + $supported_blocks = $variation['blockTypes'] ?? array(); + + /* + * Standalone theme.json partial files for block style variations + * will have their styles under a top-level property by the same name. + * Variations defined within an existing theme.json or theme style + * variation will themselves already be the required styles data. + */ + $variation_data = $variation['styles'] ?? $variation; + + if ( empty( $variation_data ) ) { + continue; + } + + /* + * Block style variations read in via standalone theme.json partials + * need to have their name set to the kebab case version of their title. + */ + $variation_name = $have_named_variations ? $key : _wp_to_kebab_case( $variation['title'] ); + $variation_label = $variation['title'] ?? $variation_name; + + foreach ( $supported_blocks as $block_type ) { + $registered_styles = $registry->get_registered_styles_for_block( $block_type ); + + // Register block style variation if it hasn't already been registered. + if ( ! array_key_exists( $variation_name, $registered_styles ) ) { + gutenberg_register_block_style( + $block_type, + array( + 'name' => $variation_name, + 'label' => $variation_label, + ) + ); + } + + // Add block style variation data under current block type. + $path = array( $block_type, 'variations', $variation_name ); + _wp_array_set( $variations_data, $path, $variation_data ); + } + } + + return $variations_data; +} + +/** + * Merges variations data with existing theme.json data ensuring that the + * current theme.json data values take precedence. + * + * @since 6.6.0 + * + * @param array $variations_data Block style variations data keyed by block type. + * @param WP_Theme_JSON_Data_Gutenberg $theme_json Current theme.json data. + * @param string $origin Origin for the theme.json data. + * + * @return WP_Theme_JSON_Gutenberg The merged theme.json data. + */ +function gutenberg_merge_block_style_variations_data( $variations_data, $theme_json, $origin = 'theme' ) { + if ( empty( $variations_data ) ) { + return $theme_json; + } + + $variations_theme_json_data = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( 'blocks' => $variations_data ), + ); + + $variations_theme_json = new WP_Theme_JSON_Data_Gutenberg( $variations_theme_json_data, $origin ); + + /* + * Merge the current theme.json data over shared variation data so that + * any explicit per block variation values take precedence. + */ + return $variations_theme_json->update_with( $theme_json->get_data() ); +} + +/** + * Merges any shared block style variation definitions from a theme style + * variation into their appropriate block type within theme json styles. Any + * custom user selections already made will take precedence over the shared + * style variation value. + * + * @since 6.6.0 + * + * @param WP_Theme_JSON_Data_Gutenberg $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data_Gutenberg + */ +function gutenberg_resolve_block_style_variations_from_theme_style_variation( $theme_json ) { + $theme_json_data = $theme_json->get_data(); + $shared_variations = $theme_json_data['styles']['blocks']['variations'] ?? array(); + $variations_data = gutenberg_resolve_and_register_block_style_variations( $shared_variations ); + + return gutenberg_merge_block_style_variations_data( $variations_data, $theme_json, 'user' ); +} + +/** + * Merges block style variation data sourced from standalone partial + * theme.json files. + * + * @since 6.6.0 + * + * @param WP_Theme_JSON_Data_Gutenberg $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data_Gutenberg + */ +function gutenberg_resolve_block_style_variations_from_theme_json_partials( $theme_json ) { + $block_style_variations = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations( 'block' ); + $variations_data = gutenberg_resolve_and_register_block_style_variations( $block_style_variations ); + + return gutenberg_merge_block_style_variations_data( $variations_data, $theme_json ); +} + +/** + * Merges shared block style variations registered within the + * `styles.blocks.variations` property of the primary theme.json file. + * + * @since 6.6.0 + * + * @param WP_Theme_JSON_Data_Gutenberg $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data_Gutenberg + */ +function gutenberg_resolve_block_style_variations_from_primary_theme_json( $theme_json ) { + $theme_json_data = $theme_json->get_data(); + $block_style_variations = $theme_json_data['styles']['blocks']['variations'] ?? array(); + $variations_data = gutenberg_resolve_and_register_block_style_variations( $block_style_variations ); + + return gutenberg_merge_block_style_variations_data( $variations_data, $theme_json ); +} + +/** + * Merges block style variations registered via the block styles registry with a + * style object, under their appropriate block types within theme.json styles. + * Any variation values defined within the theme.json specific to a block type + * will take precedence over these shared definitions. + * + * @since 6.6.0 + * + * @param WP_Theme_JSON_Data_Gutenberg $theme_json Current theme.json data. + * + * @return WP_Theme_JSON_Data_Gutenberg + */ +function gutenberg_resolve_block_style_variations_from_styles_registry( $theme_json ) { + $registry = WP_Block_Styles_Registry::get_instance(); + $styles = $registry->get_all_registered(); + $variations_data = array(); + + foreach ( $styles as $block_type => $variations ) { + foreach ( $variations as $variation_name => $variation ) { + if ( ! empty( $variation['style_data'] ) ) { + $path = array( $block_type, 'variations', $variation_name ); + _wp_array_set( $variations_data, $path, $variation['style_data'] ); + } + } + } + + return gutenberg_merge_block_style_variations_data( $variations_data, $theme_json ); +} + +/** + * Enqueues styles for block style variations. + * + * @since 6.6.0 + */ +function gutenberg_enqueue_block_style_variation_styles() { + wp_enqueue_style( 'block-style-variation-styles' ); +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( 'block-style-variation', array() ); + +add_filter( 'render_block_data', 'gutenberg_render_block_style_variation_support_styles', 10, 2 ); +add_filter( 'render_block', 'gutenberg_render_block_style_variation_class_name', 10, 2 ); +add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_block_style_variation_styles', 1 ); + +// Resolve block style variations from all their potential sources. The order here is deliberate. +add_filter( 'wp_theme_json_data_theme', 'gutenberg_resolve_block_style_variations_from_primary_theme_json', 10, 1 ); +add_filter( 'wp_theme_json_data_theme', 'gutenberg_resolve_block_style_variations_from_theme_json_partials', 10, 1 ); +add_filter( 'wp_theme_json_data_theme', 'gutenberg_resolve_block_style_variations_from_styles_registry', 10, 1 ); + +add_filter( 'wp_theme_json_data_user', 'gutenberg_resolve_block_style_variations_from_theme_style_variation', 10, 1 ); diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index b5636ad7c8dc57..938d8afa9d4faf 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -348,6 +348,7 @@ class WP_Theme_JSON_Gutenberg { * @var string[] */ const VALID_TOP_LEVEL_KEYS = array( + 'blockTypes', 'customTemplates', 'description', 'patterns', @@ -816,6 +817,7 @@ protected static function do_opt_in_into_settings( &$context ) { * * @since 5.8.0 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * @since 6.6.0 Extended schema definition to allow enhanced block style variations. * * @param array $input Structure to sanitize. * @param array $valid_block_names List of valid block names. @@ -874,6 +876,27 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks = array(); $schema_settings_blocks = array(); + + /* + * Generate a schema for blocks. + * - Block styles can contain `elements` & `variations` definitions. + * - Variations definitions cannot be nested. + * - Variations can contain styles for inner `blocks`. + * - Variation inner `blocks` styles can contain `elements`. + * + * As each variation needs a `blocks` schema but further nested + * inner `blocks`, the overall schema will be generated in multiple passes. + */ + foreach ( $valid_block_names as $block ) { + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + } + + $block_style_variation_styles = static::VALID_STYLES; + $block_style_variation_styles['blocks'] = $schema_styles_blocks; + $block_style_variation_styles['elements'] = $schema_styles_elements; + foreach ( $valid_block_names as $block ) { // Build the schema for each block style variation. $style_variation_names = array(); @@ -890,12 +913,9 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_variations = array(); if ( ! empty( $style_variation_names ) ) { - $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level ); + $schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles ); } - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; - $schema_styles_blocks[ $block ] = $styles_non_top_level; - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; } @@ -906,6 +926,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema['settings']['blocks'] = $schema_settings_blocks; $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA ); + /* + * Shared block style variations can be registered from the theme.json data so we can't + * validate them against pre-registered block style variations. + */ + $schema['styles']['blocks']['variations'] = null; + // Remove anything that's not present in the schema. foreach ( array( 'styles', 'settings' ) as $subtree ) { if ( ! isset( $input[ $subtree ] ) ) { @@ -1008,17 +1034,37 @@ protected static function prepend_to_selector( $selector, $to_prepend ) { * @since 5.8.0 * @since 5.9.0 Added `duotone` key with CSS selector. * @since 6.1.0 Added `features` key with block support feature level selectors. + * @since 6.6.0 Added non-core block style variations to generated metadata. * * @return array Block metadata. */ protected static function get_blocks_metadata() { // NOTE: the compat/6.1 version of this method in Gutenberg did not have these changes. - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = $registry->get_all_registered(); + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + $style_registry = WP_Block_Styles_Registry::get_instance(); // Is there metadata for all currently registered blocks? $blocks = array_diff_key( $blocks, static::$blocks_metadata ); if ( empty( $blocks ) ) { + /* + * New block styles may have been registered within WP_Block_Styles_Registry. + * Update block metadata for any new block style variations. + */ + $registered_styles = $style_registry->get_all_registered(); + foreach ( static::$blocks_metadata as $block_name => $block_metadata ) { + if ( ! empty( $registered_styles[ $block_name ] ) ) { + $style_selectors = $block_metadata['styleVariations'] ?? array(); + + foreach ( $registered_styles[ $block_name ] as $block_style ) { + if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) { + $style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] ); + } + } + + static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; + } + } return static::$blocks_metadata; } @@ -1051,11 +1097,20 @@ protected static function get_blocks_metadata() { } // If the block has style variations, append their selectors to the block metadata. + $style_selectors = array(); if ( ! empty( $block_type->styles ) ) { - $style_selectors = array(); foreach ( $block_type->styles as $style ) { $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); } + } + + // Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json. + $registered_styles = $style_registry->get_registered_styles_for_block( $block_name ); + foreach ( $registered_styles as $style ) { + $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + } + + if ( ! empty( $style_selectors ) ) { static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; } } @@ -1167,6 +1222,7 @@ public function get_settings() { * * @since 5.8.0 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters. + * @since 6.6.0 Added option to skip root layout styles. * * @param array $types Types of styles to load. Will load all by default. It accepts: * - `variables`: only the CSS Custom Properties for presets & custom ones. @@ -1174,8 +1230,10 @@ public function get_settings() { * - `presets`: only the classes for the presets. * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. * @param array $options An array of options for now used for internal purposes only (may change without notice). - * The options currently supported are 'scope' that makes sure all style are scoped to a given selector, - * and root_selector which overwrites and forces a given selector to be used on the root node. + * The options currently supported are: + * - 'scope' that makes sure all style are scoped to a given selector + * - `root_selector` which overwrites and forces a given selector to be used on the root node + * - `skip_root_layout_styles` which omits root layout styles from the generated stylesheet. * @return string The resulting stylesheet. */ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { @@ -1228,7 +1286,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' } if ( in_array( 'styles', $types, true ) ) { - if ( false !== $root_style_key ) { + if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) { $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] ); } $stylesheet .= $this->get_block_classes( $style_nodes ); @@ -3159,6 +3217,7 @@ protected static function filter_slugs( $node, $slugs ) { * Removes insecure data from theme.json. * * @since 5.9.0 + * @since 6.6.0 Added support for block style variation element styles. * * @param array $theme_json Structure to sanitize. * @return array Sanitized structure. @@ -3220,6 +3279,29 @@ public static function remove_insecure_properties( $theme_json ) { } $variation_output = static::remove_insecure_styles( $variation_input ); + + // Process a variation's elements and element pseudo selector styles. + if ( isset( $variation_input['elements'] ) ) { + foreach ( $valid_element_names as $element_name ) { + $element_input = $variation_input['elements'][ $element_name ] ?? null; + if ( $element_input ) { + $element_output = static::remove_insecure_styles( $element_input ); + + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { + if ( isset( $element_input[ $pseudo_selector ] ) ) { + $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] ); + } + } + } + + if ( ! empty( $element_output ) ) { + _wp_array_set( $variation_output, array( 'elements', $element_name ), $element_output ); + } + } + } + } + if ( ! empty( $variation_output ) ) { _wp_array_set( $sanitized, $variation['path'], $variation_output ); } diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 0d6aa7bd73fd3a..b21fb956ff8ff7 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -716,14 +716,44 @@ private static function recursively_iterate_json( $dir ) { return $nested_json_files; } + /** + * Determines if a supplied style variation matches the provided scope. + * + * For backwards compatibility, if a variation does not define any scope + * related property, e.g. `blockTypes`, it is assumed to be a theme style + * variation. + * + * @since 6.6.0 + * + * @param array $variation Theme.json shaped style variation object. + * @param string $scope Scope to check e.g. theme, block etc. + * + * @return boolean + */ + private static function style_variation_has_scope( $variation, $scope ) { + if ( 'block' === $scope ) { + return isset( $variation['blockTypes'] ); + } + + if ( 'theme' === $scope ) { + return ! isset( $variation['blockTypes'] ); + } + + return false; + } + /** * Returns the style variations defined by the theme (parent and child). * * @since 6.2.0 Returns parent theme variations if theme is a child. + * @since 6.6.0 Added configurable scope parameter to allow filtering + * theme.json partial files by the scope to which they + * can be applied e.g. theme vs block etc. * + * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc. * @return array */ - public static function get_style_variations() { + public static function get_style_variations( $scope = 'theme' ) { $variation_files = array(); $variations = array(); $base_directory = get_stylesheet_directory() . '/styles'; @@ -746,7 +776,7 @@ public static function get_style_variations() { ksort( $variation_files ); foreach ( $variation_files as $path => $file ) { $decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) ); - if ( is_array( $decoded_file ) ) { + if ( is_array( $decoded_file ) && static::style_variation_has_scope( $decoded_file, $scope ) ) { $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); $variation = ( new WP_Theme_JSON_Gutenberg( $translated ) )->get_raw_data(); if ( empty( $variation['title'] ) ) { @@ -766,7 +796,7 @@ public static function get_style_variations() { * * @since 6.6.0 * - * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance. + * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance. * @return array An array of resolved paths. */ public static function get_resolved_theme_uris( $theme_json ) { @@ -809,7 +839,7 @@ public static function get_resolved_theme_uris( $theme_json ) { * * @since 6.6.0 * - * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance. + * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance. * @return WP_Theme_JSON_Gutenberg Theme merged with resolved paths, if any found. */ public static function resolve_theme_file_uris( $theme_json ) { diff --git a/lib/load.php b/lib/load.php index 357935b4137794..6179ade9a2288e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -232,6 +232,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/duotone.php'; require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; +require __DIR__ . '/block-supports/block-style-variations.php'; // Data views. require_once __DIR__ . '/experimental/data-views.php'; diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 0e9aeb4c9c84ec..062df0a5606e90 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -8,6 +8,8 @@ export { export { getBlockCSSSelector } from './get-block-css-selector'; export { getLayoutStyles, + getBlockSelectors, + toStyles, useGlobalStylesOutput, useGlobalStylesOutputWithConfig, } from './use-global-styles-output'; diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index f2eaa50a890a9b..6ce2150b9d2031 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -21,6 +21,7 @@ import { ROOT_BLOCK_SELECTOR, ROOT_CSS_PROPERTIES_SELECTOR, scopeSelector, + scopeFeatureSelectors, appendToSelector, getBlockStyleVariationSelector, } from './utils'; @@ -646,14 +647,104 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { if ( node?.variations ) { const variations = {}; - Object.keys( node.variations ).forEach( ( variation ) => { - variations[ variation ] = pickStyleKeys( - node.variations[ variation ] - ); - } ); + Object.entries( node.variations ).forEach( + ( [ variationName, variation ] ) => { + variations[ variationName ] = + pickStyleKeys( variation ); + + const variationSelector = + blockSelectors[ blockName ] + .styleVariationSelectors?.[ variationName ]; + + // Process the variation's inner element styles. + // This comes before the inner block styles so the + // element styles within the block type styles take + // precedence over these. + Object.entries( variation?.elements ?? {} ).forEach( + ( [ element, elementStyles ] ) => { + if ( elementStyles && ELEMENTS[ element ] ) { + nodes.push( { + styles: elementStyles, + selector: scopeSelector( + variationSelector, + ELEMENTS[ element ] + ), + } ); + } + } + ); + + // Process the variations inner block type styles. + Object.entries( variation?.blocks ?? {} ).forEach( + ( [ + variationBlockName, + variationBlockStyles, + ] ) => { + const variationBlockSelector = scopeSelector( + variationSelector, + blockSelectors[ variationBlockName ] + .selector + ); + const variationDuotoneSelector = scopeSelector( + variationSelector, + blockSelectors[ variationBlockName ] + .duotoneSelector + ); + const variationFeatureSelectors = + scopeFeatureSelectors( + variationSelector, + blockSelectors[ variationBlockName ] + .featureSelectors + ); + + nodes.push( { + selector: variationBlockSelector, + duotoneSelector: variationDuotoneSelector, + featureSelectors: variationFeatureSelectors, + fallbackGapValue: + blockSelectors[ variationBlockName ] + .fallbackGapValue, + hasLayoutSupport: + blockSelectors[ variationBlockName ] + .hasLayoutSupport, + styles: pickStyleKeys( + variationBlockStyles + ), + } ); + + // Process element styles for the inner blocks + // of the variation. + Object.entries( + variationBlockStyles.elements ?? {} + ).forEach( + ( [ + variationBlockElement, + variationBlockElementStyles, + ] ) => { + if ( + variationBlockElementStyles && + ELEMENTS[ variationBlockElement ] + ) { + nodes.push( { + styles: variationBlockElementStyles, + selector: scopeSelector( + variationBlockSelector, + ELEMENTS[ + variationBlockElement + ] + ), + } ); + } + } + ); + } + ); + } + ); blockStyles.variations = variations; } - if ( blockStyles && blockSelectors?.[ blockName ]?.selector ) { + + if ( blockSelectors?.[ blockName ]?.selector ) { nodes.push( { duotoneSelector: blockSelectors[ blockName ].duotoneSelector, @@ -970,7 +1061,7 @@ export const toStyles = ( return; } - // `selector` maybe provided in a form + // `selector` may be provided in a form // where block level selectors have sub element // selectors appended to them as a comma separated // string. @@ -1074,7 +1165,11 @@ const getSelectorsConfig = ( blockType, rootSelector ) => { return config; }; -export const getBlockSelectors = ( blockTypes, getBlockStyles ) => { +export const getBlockSelectors = ( + blockTypes, + getBlockStyles, + variationInstanceId +) => { const result = {}; blockTypes.forEach( ( blockType ) => { const name = blockType.name; @@ -1104,16 +1199,19 @@ export const getBlockSelectors = ( blockTypes, getBlockStyles ) => { const blockStyleVariations = getBlockStyles( name ); const styleVariationSelectors = {}; - if ( blockStyleVariations?.length ) { - blockStyleVariations.forEach( ( variation ) => { - const styleVariationSelector = getBlockStyleVariationSelector( - variation.name, - selector - ); - styleVariationSelectors[ variation.name ] = - styleVariationSelector; - } ); - } + blockStyleVariations?.forEach( ( variation ) => { + const variationSuffix = variationInstanceId + ? `-${ variationInstanceId }` + : ''; + const variationName = `${ variation.name }${ variationSuffix }`; + const styleVariationSelector = getBlockStyleVariationSelector( + variationName, + selector + ); + + styleVariationSelectors[ variationName ] = styleVariationSelector; + } ); + // For each block support feature add any custom selectors. const featureSelectors = getSelectorsConfig( blockType, selector ); @@ -1126,8 +1224,7 @@ export const getBlockSelectors = ( blockTypes, getBlockStyles ) => { hasLayoutSupport, name, selector, - styleVariationSelectors: Object.keys( styleVariationSelectors ) - .length + styleVariationSelectors: blockStyleVariations?.length ? styleVariationSelectors : undefined, }; diff --git a/packages/block-editor/src/hooks/block-style-variation.js b/packages/block-editor/src/hooks/block-style-variation.js new file mode 100644 index 00000000000000..ee02b97d216704 --- /dev/null +++ b/packages/block-editor/src/hooks/block-style-variation.js @@ -0,0 +1,156 @@ +/** + * WordPress dependencies + */ +import { getBlockTypes, store as blocksStore } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +import { useContext, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + GlobalStylesContext, + toStyles, + getBlockSelectors, +} from '../components/global-styles'; +import { useStyleOverride } from './utils'; +import { store as blockEditorStore } from '../store'; +import { globalStylesDataKey } from '../store/private-keys'; + +/** + * Get the first block style variation that has been registered from the class string. + * + * @param {string} className CSS class string for a block. + * @param {Array} registeredStyles Currently registered block styles. + * + * @return {string|null} The name of the first registered variation. + */ +function getVariationNameFromClass( className, registeredStyles = [] ) { + // The global flag affects how capturing groups work in JS. So the regex + // below will only return full CSS classes not just the variation name. + const matches = className?.match( /\bis-style-(?!default)(\S+)\b/g ); + + if ( ! matches ) { + return null; + } + + for ( const variationClass of matches ) { + const variation = variationClass.substring( 9 ); // Remove 'is-style-' prefix. + if ( registeredStyles.some( ( style ) => style.name === variation ) ) { + return variation; + } + } + return null; +} + +function useBlockSyleVariation( name, variation, clientId ) { + // Prefer global styles data in GlobalStylesContext, which are available + // if in the site editor. Otherwise fall back to whatever is in the + // editor settings and available in the post editor. + const { merged: mergedConfig } = useContext( GlobalStylesContext ); + const { globalSettings, globalStyles } = useSelect( ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + return { + globalSettings: settings.__experimentalFeatures, + globalStyles: settings[ globalStylesDataKey ], + }; + }, [] ); + + return useMemo( () => { + const styles = mergedConfig?.styles ?? globalStyles; + const variationStyles = + styles?.blocks?.[ name ]?.variations?.[ variation ]; + + return { + settings: mergedConfig?.settings ?? globalSettings, + // The variation style data is all that is needed to generate + // the styles for the current application to a block. The variation + // name is updated to match the instance specific class name. + styles: { + blocks: { + [ name ]: { + variations: { + [ `${ variation }-${ clientId }` ]: variationStyles, + }, + }, + }, + }, + }; + }, [ + mergedConfig, + globalSettings, + globalStyles, + variation, + clientId, + name, + ] ); +} + +// Rather than leveraging `useInstanceId` here, the `clientId` is used. +// This is so that the variation style override's ID is predictable +// when the order of applied style variations changes. +function useBlockProps( { name, className, clientId } ) { + const { getBlockStyles } = useSelect( blocksStore ); + + const registeredStyles = getBlockStyles( name ); + const variation = getVariationNameFromClass( className, registeredStyles ); + const variationClass = `is-style-${ variation }-${ clientId }`; + + const { settings, styles } = useBlockSyleVariation( + name, + variation, + clientId + ); + + const variationStyles = useMemo( () => { + if ( ! variation ) { + return; + } + + const variationConfig = { settings, styles }; + const blockSelectors = getBlockSelectors( + getBlockTypes(), + getBlockStyles, + clientId + ); + const hasBlockGapSupport = false; + const hasFallbackGapSupport = true; + const disableLayoutStyles = true; + const isTemplate = true; + + return toStyles( + variationConfig, + blockSelectors, + hasBlockGapSupport, + hasFallbackGapSupport, + disableLayoutStyles, + isTemplate, + { + blockGap: false, + blockStyles: true, + layoutStyles: false, + marginReset: false, + presets: false, + rootPadding: false, + } + ); + }, [ variation, settings, styles, getBlockStyles, clientId ] ); + + useStyleOverride( { + id: `variation-${ clientId }`, + css: variationStyles, + __unstableType: 'variation', + // The clientId will be stored with the override and used to ensure + // the order of overrides matches the order of blocks so that the + // correct CSS cascade is maintained. + clientId, + } ); + + return variation ? { className: variationClass } : {}; +} + +export default { + hasSupport: () => true, + attributeKeys: [ 'className' ], + useBlockProps, +}; diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 4a59c2faa0073f..89e6819c1d0314 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -24,6 +24,7 @@ import fontSize from './font-size'; import textAlign from './text-align'; import border from './border'; import position from './position'; +import blockStyleVariation from './block-style-variation'; import layout from './layout'; import childLayout from './layout-child'; import contentLockUI from './content-lock-ui'; @@ -61,6 +62,7 @@ createBlockListBlockFilter( [ fontSize, border, position, + blockStyleVariation, childLayout, ] ); createBlockSaveFilter( [ diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 391287afb6ba09..c14b6329cf2ec3 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -595,6 +595,7 @@ export function createBlockListBlockFilter( features ) { // function reference. setAllWrapperProps={ setAllWrapperProps } name={ props.name } + clientId={ props.clientId } // This component is pure, so only pass needed // props!!! { ...neededProps } diff --git a/packages/block-library/src/list/style.scss b/packages/block-library/src/list/style.scss index badf1b9e560ebd..11f7c4888a5eff 100644 --- a/packages/block-library/src/list/style.scss +++ b/packages/block-library/src/list/style.scss @@ -1,8 +1,8 @@ ol, ul { box-sizing: border-box; +} - &.has-background { - padding: $block-bg-padding--v $block-bg-padding--h; - } +:root :where(ul.has-background, ol.has-background) { + padding: $block-bg-padding--v $block-bg-padding--h; } diff --git a/packages/block-library/src/paragraph/style.scss b/packages/block-library/src/paragraph/style.scss index 34960bdb2fd589..7bd8c77e85de83 100644 --- a/packages/block-library/src/paragraph/style.scss +++ b/packages/block-library/src/paragraph/style.scss @@ -38,7 +38,8 @@ p.has-drop-cap.has-background { overflow: hidden; } -p.has-background { +// Specificity is reduced to 0-1-0 so global styles can override this. +:root :where(p.has-background) { padding: $block-bg-padding--v $block-bg-padding--h; } diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index 9811f10b834dab..2368f7499acbf6 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -235,7 +235,7 @@ function ScreenBlock( { name, variation } ) { return ( <> { hasVariationsPanel && ( diff --git a/packages/edit-site/src/components/global-styles/variations/variations-panel.js b/packages/edit-site/src/components/global-styles/variations/variations-panel.js index 7e52498e0a4385..f98cc65e5c95b1 100644 --- a/packages/edit-site/src/components/global-styles/variations/variations-panel.js +++ b/packages/edit-site/src/components/global-styles/variations/variations-panel.js @@ -2,16 +2,25 @@ * WordPress dependencies */ import { store as blocksStore } from '@wordpress/blocks'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; + /** * Internal dependencies */ - import { NavigationButtonAsItem } from '../navigation-button'; +import { unlock } from '../../../lock-unlock'; + +const { useGlobalStyle } = unlock( blockEditorPrivateApis ); -function getCoreBlockStyles( blockStyles ) { - return blockStyles?.filter( ( style ) => style.source === 'block' ); +// Only core block styles (source === block) or block styles with a matching +// theme.json style variation will be configurable via Global Styles. +function getFilteredBlockStyles( blockStyles, variations ) { + return blockStyles?.filter( + ( style ) => + style.source === 'block' || variations.includes( style.name ) + ); } export function useBlockVariations( name ) { @@ -22,8 +31,10 @@ export function useBlockVariations( name ) { }, [ name ] ); - const coreBlockStyles = getCoreBlockStyles( blockStyles ); - return coreBlockStyles; + const [ variations ] = useGlobalStyle( 'variations', name ); + const variationNames = Object.keys( variations ?? {} ); + + return getFilteredBlockStyles( blockStyles, variationNames ); } export function VariationsPanel( { name } ) { diff --git a/packages/editor/src/components/global-styles-provider/index.js b/packages/editor/src/components/global-styles-provider/index.js index 9e4ba24e7311fe..04a42ab7af819c 100644 --- a/packages/editor/src/components/global-styles-provider/index.js +++ b/packages/editor/src/components/global-styles-provider/index.js @@ -7,15 +7,17 @@ import { isPlainObject } from 'is-plain-object'; /** * WordPress dependencies */ +import { registerBlockStyle, store as blocksStore } from '@wordpress/blocks'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useMemo, useCallback } from '@wordpress/element'; +import { useEffect, useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; +import setNestedValue from '../../utils/set-nested-value'; const { GlobalStylesContext, cleanEmptyObject } = unlock( blockEditorPrivateApis @@ -30,6 +32,85 @@ export function mergeBaseAndUserConfigs( base, user ) { } ); } +/** + * Resolves shared block style variation definitions from the user origin + * under their respective block types and registers the block style if required. + * + * @param {Object} userConfig Current user origin global styles data. + * @return {Object} Updated global styles data. + */ +function useResolvedBlockStyleVariationsConfig( userConfig ) { + const { getBlockStyles } = useSelect( blocksStore ); + const sharedVariations = userConfig?.styles?.blocks?.variations; + + // Collect block style variation definitions to merge and unregistered + // block styles for automatic registration. + const [ userConfigToMerge, unregisteredStyles ] = useMemo( () => { + if ( ! sharedVariations ) { + return []; + } + + const variationsConfigToMerge = {}; + const unregisteredBlockStyles = []; + + Object.entries( sharedVariations ).forEach( + ( [ variationName, variation ] ) => { + if ( ! variation?.blockTypes?.length ) { + return; + } + + variation.blockTypes.forEach( ( blockName ) => { + const blockStyles = getBlockStyles( blockName ); + const registeredBlockStyle = blockStyles.find( + ( { name } ) => name === variationName + ); + + if ( ! registeredBlockStyle ) { + unregisteredBlockStyles.push( [ + blockName, + { + name: variationName, + label: variationName, + }, + ] ); + } + + const path = [ + 'styles', + 'blocks', + blockName, + 'variations', + variationName, + ]; + setNestedValue( variationsConfigToMerge, path, variation ); + } ); + } + ); + + return [ variationsConfigToMerge, unregisteredBlockStyles ]; + }, [ sharedVariations, getBlockStyles ] ); + + // Automatically register missing block styles from variations. + useEffect( + () => + unregisteredStyles?.forEach( ( unregisteredStyle ) => + registerBlockStyle( ...unregisteredStyle ) + ), + [ unregisteredStyles ] + ); + + // Merge shared block style variation definitions into overall user config. + const updatedConfig = useMemo( () => { + if ( ! userConfigToMerge ) { + return userConfig; + } + + return deepmerge( userConfigToMerge, userConfig ); + }, [ userConfigToMerge, userConfig ] ); + + return updatedConfig; +} + function useGlobalStylesUserConfig() { const { globalStylesId, isReady, settings, styles, _links } = useSelect( ( select ) => { @@ -128,24 +209,28 @@ export function useGlobalStylesContext() { const [ isUserConfigReady, userConfig, setUserConfig ] = useGlobalStylesUserConfig(); const [ isBaseConfigReady, baseConfig ] = useGlobalStylesBaseConfig(); + const userConfigWithVariations = + useResolvedBlockStyleVariationsConfig( userConfig ); + const mergedConfig = useMemo( () => { - if ( ! baseConfig || ! userConfig ) { + if ( ! baseConfig || ! userConfigWithVariations ) { return {}; } - return mergeBaseAndUserConfigs( baseConfig, userConfig ); - }, [ userConfig, baseConfig ] ); + + return mergeBaseAndUserConfigs( baseConfig, userConfigWithVariations ); + }, [ userConfigWithVariations, baseConfig ] ); const context = useMemo( () => { return { isReady: isUserConfigReady && isBaseConfigReady, - user: userConfig, + user: userConfigWithVariations, base: baseConfig, merged: mergedConfig, setUserConfig, }; }, [ mergedConfig, - userConfig, + userConfigWithVariations, baseConfig, setUserConfig, isUserConfigReady, diff --git a/packages/editor/src/utils/set-nested-value.js b/packages/editor/src/utils/set-nested-value.js new file mode 100644 index 00000000000000..ec684e751cd041 --- /dev/null +++ b/packages/editor/src/utils/set-nested-value.js @@ -0,0 +1,39 @@ +/** + * Sets the value at path of object. + * If a portion of path doesn’t exist, it’s created. + * Arrays are created for missing index properties while objects are created + * for all other missing properties. + * + * This function intentionally mutates the input object. + * + * Inspired by _.set(). + * + * @see https://lodash.com/docs/4.17.15#set + * + * @todo Needs to be deduplicated with its copy in `@wordpress/core-data`. + * + * @param {Object} object Object to modify + * @param {Array} path Path of the property to set. + * @param {*} value Value to set. + */ +export default function setNestedValue( object, path, value ) { + if ( ! object || typeof object !== 'object' ) { + return object; + } + + path.reduce( ( acc, key, idx ) => { + if ( acc[ key ] === undefined ) { + if ( Number.isInteger( path[ idx + 1 ] ) ) { + acc[ key ] = []; + } else { + acc[ key ] = {}; + } + } + if ( idx === path.length - 1 ) { + acc[ key ] = value; + } + return acc[ key ]; + }, object ); + + return object; +} diff --git a/phpunit/block-supports/block-style-variations-test.php b/phpunit/block-supports/block-style-variations-test.php new file mode 100644 index 00000000000000..b84267446330cc --- /dev/null +++ b/phpunit/block-supports/block-style-variations-test.php @@ -0,0 +1,130 @@ +theme_root = realpath( dirname( __DIR__ ) . '/data/themedir1' ); + + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + public function tear_down() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + + // Reset data between tests. + _gutenberg_clean_theme_json_caches(); + parent::tear_down(); + } + + public function filter_set_theme_root() { + return $this->theme_root; + } + + /** + * Tests that block style variations registered via either + * `gutenberg_register_block_style` with a style object, or a standalone + * block style variation file within `/styles`, are added to the theme data. + */ + public function test_add_registered_block_styles_to_theme_data() { + switch_theme( 'block-theme' ); + + $variation_styles_data = array( + 'color' => array( + 'background' => 'darkslateblue', + 'text' => 'lavender', + ), + 'blocks' => array( + 'core/heading' => array( + 'color' => array( + 'text' => 'violet', + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'fuchsia', + ), + ':hover' => array( + 'color' => array( + 'text' => 'deeppink', + ), + ), + ), + ), + ); + + register_block_style( + 'core/group', + array( + 'name' => 'my-variation', + 'style_data' => $variation_styles_data, + ) + ); + + $theme_json = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_raw_data(); + $group_styles = $theme_json['styles']['blocks']['core/group'] ?? array(); + $expected = array( + 'variations' => array( + 'my-variation' => $variation_styles_data, + + /* + * The following block style variations are registered + * automatically from their respective JSON files within the + * theme's `/styles` directory. + */ + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'indigo', + 'text' => 'plum', + ), + ), + 'block-style-variation-b' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', + ), + ), + ), + ); + + unregister_block_style( 'core/group', 'my-variation' ); + + $this->assertSameSetsWithIndex( $group_styles, $expected ); + } +} diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index 9ba170cd785d22..50e1d9d846899e 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -981,95 +981,146 @@ public function data_get_merged_data_returns_origin() { } /** - * Tests that get_style_variations returns all variations, including parent theme variations if the theme is a child, - * and that the child variation overwrites the parent variation of the same name. + * Tests that `get_style_variations` returns all the appropriate variations, + * including parent variations if the theme is a child, and that the child + * variation overwrites the parent variation of the same name. * - * @covers WP_Theme_JSON_Resolver_Gutenberg::get_style_variations + * Note: This covers both theme and block style variations. + * + * @covers WP_Theme_JSON_Resolver::get_style_variations + * + * @dataProvider data_get_style_variations + * + * @since 6.6.0 Added tests for block style variations. + * + * @param string $theme Name of the theme to use. + * @param string $scope Scope to filter variations by e.g. theme vs block. + * @param array $expected_variations Collection of expected variations. */ - public function test_get_style_variations_returns_all_variations() { - // Switch to a child theme. - switch_theme( 'block-theme-child' ); + public function test_get_style_variations( $theme, $scope, $expected_variations ) { + switch_theme( $theme ); wp_set_current_user( self::$administrator_id ); - $actual_settings = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations(); - $expected_settings = array( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'title' => 'variation-a', - 'settings' => array( - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'dark', - 'name' => 'Dark', - 'color' => '#010101', + $actual_variations = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations( $scope ); + + wp_recursive_ksort( $actual_variations ); + wp_recursive_ksort( $expected_variations ); + + $this->assertSame( $expected_variations, $actual_variations ); + } + + /** + * Data provider for test_get_style_variations + * + * @since 6.6.0 Added data provider for testing theme and block style variations. + * + * @return array + */ + public function data_get_style_variations() { + return array( + 'theme_style_variations' => array( + 'theme' => 'block-theme-child', + 'scope' => 'theme', + 'expected_variations' => array( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'title' => 'variation-a', + 'settings' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#010101', + ), + ), ), ), ), ), ), ), - ), - ), - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'title' => 'variation-b', - 'settings' => array( - 'blocks' => array( - 'core/post-title' => array( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'title' => 'variation-b', + 'settings' => array( + 'blocks' => array( + 'core/post-title' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#010101', + ), + ), + ), + ), + ), + ), + ), + ), + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'title' => 'Block theme variation', + 'settings' => array( 'color' => array( 'palette' => array( 'theme' => array( array( - 'slug' => 'dark', - 'name' => 'Dark', - 'color' => '#010101', + 'slug' => 'foreground', + 'name' => 'Foreground', + 'color' => '#3F67C6', ), ), ), ), ), + 'styles' => array( + 'blocks' => array( + 'core/post-title' => array( + 'typography' => array( + 'fontWeight' => '700', + ), + ), + ), + ), ), ), ), - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'title' => 'Block theme variation', - 'settings' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'foreground', - 'name' => 'Foreground', - 'color' => '#3F67C6', - ), + 'block_style_variations' => array( + 'theme' => 'block-theme-child-with-block-style-variations', + 'scope' => 'block', + 'expected_variations' => array( + array( + 'blockTypes' => array( 'core/group', 'core/columns', 'core/media-text' ), + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'title' => 'block-style-variation-a', + 'styles' => array( + 'color' => array( + 'background' => 'darkcyan', + 'text' => 'aliceblue', ), ), ), - ), - 'styles' => array( - 'blocks' => array( - 'core/post-title' => array( - 'typography' => array( - 'fontWeight' => '700', + array( + 'blockTypes' => array( 'core/group', 'core/columns' ), + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'title' => 'block-style-variation-b', + 'styles' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', ), ), ), ), ), ); - - wp_recursive_ksort( $actual_settings ); - wp_recursive_ksort( $expected_settings ); - - $this->assertSame( - $expected_settings, - $actual_settings - ); } public function test_theme_shadow_presets_do_not_override_default_shadow_presets() { diff --git a/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css new file mode 100644 index 00000000000000..c1cc20aaf1f101 --- /dev/null +++ b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/style.css @@ -0,0 +1,8 @@ +/* +Theme Name: Block Theme Child With Block Style Variations Theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Template: block-theme +Version: 1.0.0 +Text Domain: block-theme-child-with-block-style-variations +*/ diff --git a/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json new file mode 100644 index 00000000000000..195321a33b3366 --- /dev/null +++ b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/styles/block-style-variation-a.json @@ -0,0 +1,10 @@ +{ + "version": 3, + "blockTypes": [ "core/group", "core/columns", "core/media-text" ], + "styles": { + "color": { + "background": "darkcyan", + "text": "aliceblue" + } + } +} diff --git a/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json new file mode 100644 index 00000000000000..a471d8f326a4ab --- /dev/null +++ b/phpunit/data/themedir1/block-theme-child-with-block-style-variations/theme.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 3 +} diff --git a/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json b/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json new file mode 100644 index 00000000000000..356bc4fc3de7d3 --- /dev/null +++ b/phpunit/data/themedir1/block-theme/styles/block-style-variation-a.json @@ -0,0 +1,10 @@ +{ + "version": 3, + "blockTypes": [ "core/group", "core/columns" ], + "styles": { + "color": { + "background": "indigo", + "text": "plum" + } + } +} diff --git a/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json b/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json new file mode 100644 index 00000000000000..8b79948517255f --- /dev/null +++ b/phpunit/data/themedir1/block-theme/styles/block-style-variation-b.json @@ -0,0 +1,10 @@ +{ + "version": 3, + "blockTypes": [ "core/group", "core/columns" ], + "styles": { + "color": { + "background": "midnightblue", + "text": "lightblue" + } + } +} diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 1443685ff83cb6..6a3ec7e81d3947 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -1927,6 +1927,9 @@ "stylesBlocksPropertiesComplete": { "type": "object", "properties": { + "variations": { + "$ref": "#/definitions/stylesBlocksSharedVariationProperties" + }, "core/archives": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, @@ -2250,20 +2253,420 @@ "$ref": "#/definitions/stylesElementsPropertiesComplete" }, "variations": { - "$ref": "#/definitions/stylesVariationPropertiesComplete" + "$ref": "#/definitions/stylesVariationsPropertiesComplete" } }, "additionalProperties": false } ] }, - "stylesVariationPropertiesComplete": { + "stylesBlocksSharedVariationProperties": { "type": "object", "patternProperties": { "^[a-z][a-z0-9-]*$": { - "$ref": "#/definitions/stylesPropertiesComplete" + "$ref": "#/definitions/stylesSharedVariationProperties" + } + } + }, + "stylesSharedVariationProperties": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/stylesProperties" + }, + { + "properties": { + "blockTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "border": {}, + "color": {}, + "dimensions": {}, + "spacing": {}, + "typography": {}, + "filter": {}, + "shadow": {}, + "outline": {}, + "css": {}, + "elements": { + "$ref": "#/definitions/stylesElementsPropertiesComplete" + }, + "blocks": { + "$ref": "#/definitions/stylesVariationBlocksPropertiesComplete" + } + }, + "additionalProperties": false + } + ] + }, + "stylesVariationsPropertiesComplete": { + "type": "object", + "patternProperties": { + "^[a-z][a-z0-9-]*$": { + "$ref": "#/definitions/stylesVariationPropertiesComplete" } } + }, + "stylesVariationPropertiesComplete": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/stylesProperties" + }, + { + "properties": { + "border": {}, + "color": {}, + "dimensions": {}, + "spacing": {}, + "typography": {}, + "filter": {}, + "shadow": {}, + "outline": {}, + "css": {}, + "elements": { + "$ref": "#/definitions/stylesElementsPropertiesComplete" + }, + "blocks": { + "$ref": "#/definitions/stylesVariationBlocksPropertiesComplete" + } + }, + "additionalProperties": false + } + ] + }, + "stylesVariationBlocksPropertiesComplete": { + "type": "object", + "properties": { + "core/archives": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/audio": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/avatar": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/block": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/button": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/buttons": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/calendar": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/categories": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/code": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/column": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/columns": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-author-avatar": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-author-name": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-content": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-date": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-edit-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-reply-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comments": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comments-pagination": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comments-pagination-next": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comments-pagination-numbers": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comments-pagination-previous": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comments-title": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/comment-template": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/cover": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/details": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/embed": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/file": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/freeform": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/gallery": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/group": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/heading": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/home-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/html": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/image": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/latest-comments": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/latest-posts": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/list": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/list-item": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/loginout": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/media-text": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/missing": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/more": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/navigation": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/navigation-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/navigation-submenu": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/nextpage": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/page-list": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/page-list-item": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/paragraph": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-author": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-author-biography": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-author-name": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-comment": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-comments-count": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-comments-form": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-comments-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-content": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-date": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-excerpt": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-featured-image": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-navigation-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-template": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-terms": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-time-to-read": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/post-title": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/preformatted": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/pullquote": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query-no-results": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query-pagination": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query-pagination-next": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query-pagination-numbers": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query-pagination-previous": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/query-title": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/quote": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/read-more": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/rss": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/search": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/separator": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/shortcode": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/site-logo": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/site-tagline": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/site-title": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/social-link": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/social-links": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/spacer": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/table": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/table-of-contents": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/tag-cloud": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/template-part": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/term-description": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/text-columns": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/verse": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/video": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/widget-area": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/legacy-widget": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, + "core/widget-group": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + } + }, + "patternProperties": { + "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + } + }, + "additionalProperties": false + }, + "stylesVariationBlockPropertiesComplete": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/stylesProperties" + }, + { + "properties": { + "border": {}, + "color": {}, + "dimensions": {}, + "spacing": {}, + "typography": {}, + "filter": {}, + "shadow": {}, + "outline": {}, + "css": {}, + "elements": { + "$ref": "#/definitions/stylesElementsPropertiesComplete" + } + }, + "additionalProperties": false + } + ] } }, "type": "object", @@ -2285,6 +2688,13 @@ "type": "string", "description": "Description of the global styles variation." }, + "blockTypes": { + "type": "array", + "description": "List of block types that can use the block style variation this theme.json file represents.", + "items": { + "type": "string" + } + }, "settings": { "description": "Settings for the block editor and individual blocks. These include things like:\n- Which customization options should be available to the user. \n- The default colors, font sizes... available to the user. \n- CSS custom properties and class names used in styles.\n- And the default layout of the editor (widths and available alignments).", "type": "object",