diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php index 85dbd7d39797c..ac5fe6b03e361 100644 --- a/lib/block-supports/elements.php +++ b/lib/block-supports/elements.php @@ -135,19 +135,20 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) { } $class_name = wp_get_elements_class_name( $block ); + $class_name = ".$class_name.$class_name"; $element_types = array( 'button' => array( - 'selector' => ".$class_name .wp-element-button, .$class_name .wp-block-button__link", + 'selector' => "$class_name .wp-element-button, $class_name .wp-block-button__link", 'skip' => $skip_button_color_serialization, ), 'link' => array( - 'selector' => ".$class_name a", - 'hover_selector' => ".$class_name a:hover", + 'selector' => "$class_name a", + 'hover_selector' => "$class_name a:hover", 'skip' => $skip_link_color_serialization, ), 'heading' => array( - 'selector' => ".$class_name h1, .$class_name h2, .$class_name h3, .$class_name h4, .$class_name h5, .$class_name h6", + 'selector' => "$class_name h1, $class_name h2, $class_name h3, $class_name h4, $class_name h5, $class_name h6", 'skip' => $skip_heading_color_serialization, 'elements' => array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ), ), @@ -190,7 +191,7 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) { gutenberg_style_engine_get_styles( $element_style_object, array( - 'selector' => ".$class_name $element", + 'selector' => "$class_name $element", 'context' => 'block-supports', ) ); diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index aa8de83df9597..58d29c14e7c02 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -841,9 +841,30 @@ 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 can contain styles for inner `blocks`. + // - Variations definitions cannot be nested. + // - Variations inner block styles cannot contain `elements`. + // + // As each variation needs a `blocks` schema but without `elements` and + // 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; + } + + // Generate block style variations schema including nested block styles + // schema as generated above. + $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(); + if ( ! empty( $input['styles']['blocks'][ $block ]['variations'] ) && is_array( $input['styles']['blocks'][ $block ]['variations'] ) && @@ -857,13 +878,14 @@ 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; + + // The element styles schema can now be added for this block to the + // styles.blocks.$block schema. + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; } $schema['styles'] = static::VALID_STYLES; @@ -980,12 +1002,34 @@ protected static function prepend_to_selector( $selector, $to_prepend ) { */ 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::append_to_selector( + '.is-style-' . $block_style['name'], + $block_metadata['selector'] + ); + } + } + + static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; + } + } + return static::$blocks_metadata; } @@ -1009,7 +1053,7 @@ protected static function get_blocks_metadata() { if ( $duotone_support ) { $root_selector = wp_get_block_css_selector( $block_type ); - $duotone_selector = WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $duotone_support ); + $duotone_selector = static::scope_selector( $root_selector, $duotone_support ); } } @@ -1018,11 +1062,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::append_to_selector( '.is-style-' . $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::append_to_selector( '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + } + + if ( ! empty( $style_selectors ) ) { static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; } } @@ -1748,6 +1801,10 @@ protected static function compute_preset_classes( $settings, $selector, $origins * @return string Scoped selector. */ public static function scope_selector( $scope, $selector ) { + if ( ! $selector || ! $scope ) { + return $selector; + } + $scopes = explode( ',', $scope ); $selectors = explode( ',', $selector ); @@ -2380,38 +2437,80 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { } foreach ( $theme_json['styles']['blocks'] as $name => $node ) { - $selector = null; - if ( isset( $selectors[ $name ]['selector'] ) ) { - $selector = $selectors[ $name ]['selector']; - } + $selector = $selectors[ $name ]['selector'] ?? null; + $duotone_selector = $selectors[ $name ]['duotone'] ?? null; + $feature_selectors = $selectors[ $name ]['selectors'] ?? null; + $variations = $node['variations'] ?? array(); + $variation_selectors = array(); + $variation_nodes = array(); - $duotone_selector = null; - if ( isset( $selectors[ $name ]['duotone'] ) ) { - $duotone_selector = $selectors[ $name ]['duotone']; - } + foreach ( $variations as $variation => $variation_node ) { + $variation_selector = $selectors[ $name ]['styleVariations'][ $variation ]; + $variation_selectors[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), + 'selector' => $variation_selector, + ); - $feature_selectors = null; - if ( isset( $selectors[ $name ]['selectors'] ) ) { - $feature_selectors = $selectors[ $name ]['selectors']; - } + $variation_blocks = $variation_node['blocks'] ?? array(); + $variation_elements = $variation_node['elements'] ?? array(); - $variation_selectors = array(); - if ( isset( $node['variations'] ) ) { - foreach ( $node['variations'] as $variation => $node ) { - $variation_selectors[] = array( - 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), - 'selector' => $selectors[ $name ]['styleVariations'][ $variation ], + foreach ( $variation_blocks as $variation_block => $variation_block_node ) { + $variation_block_selector = static::scope_selector( $variation_selector, $selectors[ $variation_block ]['selector'] ?? null ); + $variation_duotone_selector = static::scope_selector( $variation_selector, $selectors[ $variation_block ]['duotone'] ?? null ); + $variation_feature_selectors = $selectors[ $variation_block ]['selectors'] ?? null; + + if ( $variation_feature_selectors ) { + foreach ( $variation_feature_selectors as $feature => $feature_selector ) { + if ( is_string( $feature_selector ) ) { + $variation_feature_selectors[ $feature ] = static::scope_selector( $variation_selector, $feature_selector ); + } + + if ( is_array( $feature_selector ) ) { + foreach ( $feature_selector as $subfeature => $subfeature_selector ) { + $variation_feature_selectors[ $feature ][ $subfeature ] = static::scope_selector( $variation_selector, $subfeature_selector ); + } + } + } + } + + $variation_nodes[] = array( + 'name' => $variation_block, + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation, 'blocks', $variation_block ), + 'selector' => $variation_block_selector, + 'selectors' => $variation_feature_selectors, + 'duotone' => $variation_duotone_selector, + ); + } + + foreach ( $variation_elements as $variation_element => $variation_element_node ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation, 'elements', $variation_element ), + 'selector' => static::scope_selector( $variation_selector, static::ELEMENTS[ $variation_element ] ), ); + + // Handle any pseudo selectors for the element. + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $variation_element ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $variation_element ] as $pseudo_selector ) { + if ( isset( $variation_element_node[ $pseudo_selector ] ) ) { + $pseudo_element_selector = static::append_to_selector( static::ELEMENTS[ $variation_element ], $pseudo_selector ); + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation, 'elements', $variation_element ), + 'selector' => static::scope_selector( $variation_selector, $pseudo_element_selector ), + ); + } + } + } } } $nodes[] = array( - 'name' => $name, - 'path' => array( 'styles', 'blocks', $name ), - 'selector' => $selector, - 'selectors' => $feature_selectors, - 'duotone' => $duotone_selector, - 'variations' => $variation_selectors, + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name ), + 'selector' => $selector, + 'selectors' => $feature_selectors, + 'duotone' => $duotone_selector, + 'variations' => $variation_selectors, + 'variation_nodes' => $variation_nodes, ); if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { @@ -2490,6 +2589,25 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) { } } + $style_variation_block_declarations = array(); + if ( ! empty( $block_metadata['variation_nodes'] ) ) { + foreach ( $block_metadata['variation_nodes'] as $variation_node ) { + $style_variation_block_node = _wp_array_get( $this->theme_json, $variation_node['path'], array() ); + $variation_block_declarations = static::get_feature_declarations_for_node( $variation_node, $style_variation_block_node ); + + foreach ( $variation_block_declarations as $current_selector => $new_declarations ) { + $style_variation_block_declarations[ $current_selector ] = $new_declarations; + } + + $style_variation_block_declarations[ $variation_node['selector'] ] = static::compute_style_properties( + $style_variation_block_node, + $settings, + null, + $this->theme_json + ); + } + } + /* * Get a reference to element name from path. * $block_metadata['path'] = array( 'styles','elements','link' ); @@ -2591,6 +2709,11 @@ static function ( $pseudo_selector ) use ( $selector ) { $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations ); } + // 7. Generate and append the block style variations for inner blocks and elements. + foreach ( $style_variation_block_declarations as $style_variation_block_selector => $individual_style_variation_block_declaration ) { + $block_rules .= static::to_ruleset( $style_variation_block_selector, $individual_style_variation_block_declaration ); + } + return $block_rules; } @@ -3050,6 +3173,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 ); } @@ -3229,6 +3375,35 @@ public function get_raw_data() { return $this->theme_json; } + /** + * Converts block styles registered through the `WP_Block_Styles_Registry` + * with a style object, into theme.json format. + * + * @since 6.5.0 + * + * @return array Styles configuration adhering to the theme.json schema. + */ + public static function get_from_block_styles_registry() { + $variations_data = array(); + $registry = WP_Block_Styles_Registry::get_instance(); + $styles = $registry->get_all_registered(); + + foreach ( $styles as $block_name => $variations ) { + foreach ( $variations as $variation_name => $variation ) { + if ( ! empty( $variation['style_data'] ) ) { + $variations_data[ $block_name ]['variations'][ $variation_name ] = $variation['style_data']; + } + } + } + + return array( + 'version' => static::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => $variations_data, + ), + ); + } + /** * Transforms the given editor settings according the * add_theme_support format to the theme.json format. diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 189d411db2257..0ad947957215f 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -217,19 +217,22 @@ protected static function has_same_registered_blocks( $origin ) { * Returns the theme's data. * * Data from theme.json will be backfilled from existing - * theme supports, if any. Note that if the same data - * is present in theme.json and in theme supports, - * the theme.json takes precedence. + * theme supports and block style variations, if any. + * + * Note that if the same data is present in theme.json and in theme supports + * or registered block styles, the theme.json takes precedence. * * @since 5.8.0 * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed. * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports. + * @since 6.5.0 Theme data will now also include block style variations that were registered with a style object. * * @param array $deprecated Deprecated. Not used. * @param array $options { * Options arguments. * - * @type bool $with_supports Whether to include theme supports in the data. Default true. + * @type bool $with_supports Whether to include theme supports in the data. Default true. + * @type bool $with_style_variations Whether to include block style variations in the data. Default true. * } * @return WP_Theme_JSON Entity that holds theme data. */ @@ -238,7 +241,13 @@ public static function get_theme_data( $deprecated = array(), $options = array() _deprecated_argument( __METHOD__, '5.9.0' ); } - $options = wp_parse_args( $options, array( 'with_supports' => true ) ); + $options = wp_parse_args( + $options, + array( + 'with_supports' => true, + 'with_block_style_variations' => true, + ) + ); if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { $wp_theme = wp_get_theme(); @@ -286,74 +295,91 @@ public static function get_theme_data( $deprecated = array(), $options = array() } - if ( ! $options['with_supports'] ) { + if ( ! $options['with_supports'] && ! $options['with_block_style_variations'] ) { return static::$theme; } - /* - * We want the presets and settings declared in theme.json - * to override the ones declared via theme supports. - * So we take theme supports, transform it to theme.json shape - * and merge the static::$theme upon that. - */ - $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_classic_theme_supports_block_editor_settings() ); - if ( ! wp_theme_has_theme_json() ) { - if ( ! isset( $theme_support_data['settings']['color'] ) ) { - $theme_support_data['settings']['color'] = array(); - } + $theme_support_data = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'settings' => array(), + ); - $default_palette = false; - if ( current_theme_supports( 'default-color-palette' ) ) { - $default_palette = true; - } - if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) { - // If the theme does not have any palette, we still want to show the core one. - $default_palette = true; - } - $theme_support_data['settings']['color']['defaultPalette'] = $default_palette; + if ( $options['with_supports'] ) { + /* + * We want the presets and settings declared in theme.json + * to override the ones declared via theme supports. + * So we take theme supports, transform it to theme.json shape + * and merge any block style variations from WP_Block_Styles_Registry + * before merging the static::$theme upon that. + */ + $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_classic_theme_supports_block_editor_settings() ); + if ( ! wp_theme_has_theme_json() ) { + if ( ! isset( $theme_support_data['settings']['color'] ) ) { + $theme_support_data['settings']['color'] = array(); + } - $default_gradients = false; - if ( current_theme_supports( 'default-gradient-presets' ) ) { - $default_gradients = true; - } - if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) { - // If the theme does not have any gradients, we still want to show the core ones. - $default_gradients = true; - } - $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; - - // Allow themes to enable all border settings via theme_support. - if ( current_theme_supports( 'border' ) ) { - $theme_support_data['settings']['border']['color'] = true; - $theme_support_data['settings']['border']['radius'] = true; - $theme_support_data['settings']['border']['style'] = true; - $theme_support_data['settings']['border']['width'] = true; - } + $default_palette = false; + if ( current_theme_supports( 'default-color-palette' ) ) { + $default_palette = true; + } + if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) { + // If the theme does not have any palette, we still want to show the core one. + $default_palette = true; + } + $theme_support_data['settings']['color']['defaultPalette'] = $default_palette; - // Allow themes to enable link colors via theme_support. - if ( current_theme_supports( 'link-color' ) ) { - $theme_support_data['settings']['color']['link'] = true; - } - if ( current_theme_supports( 'experimental-link-color' ) ) { - _doing_it_wrong( - current_theme_supports( 'experimental-link-color' ), - __( '`experimental-link-color` is no longer supported. Use `link-color` instead.', 'gutenberg' ), - '6.3.0' - ); - } + $default_gradients = false; + if ( current_theme_supports( 'default-gradient-presets' ) ) { + $default_gradients = true; + } + if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) { + // If the theme does not have any gradients, we still want to show the core ones. + $default_gradients = true; + } + $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; + + // Allow themes to enable all border settings via theme_support. + if ( current_theme_supports( 'border' ) ) { + $theme_support_data['settings']['border']['color'] = true; + $theme_support_data['settings']['border']['radius'] = true; + $theme_support_data['settings']['border']['style'] = true; + $theme_support_data['settings']['border']['width'] = true; + } + + // Allow themes to enable link colors via theme_support. + if ( current_theme_supports( 'link-color' ) ) { + $theme_support_data['settings']['color']['link'] = true; + } + if ( current_theme_supports( 'experimental-link-color' ) ) { + _doing_it_wrong( + current_theme_supports( 'experimental-link-color' ), + __( '`experimental-link-color` is no longer supported. Use `link-color` instead.', 'gutenberg' ), + '6.3.0' + ); + } - // BEGIN EXPERIMENTAL. - // Allow themes to enable appearance tools via theme_support. - // This feature was backported for WordPress 6.2 as of https://core.trac.wordpress.org/ticket/56487 - // and then reverted as of https://core.trac.wordpress.org/ticket/57649 - // Not to backport until the issues are resolved. - if ( current_theme_supports( 'appearance-tools' ) ) { - $theme_support_data['settings']['appearanceTools'] = true; + // BEGIN EXPERIMENTAL. + // Allow themes to enable appearance tools via theme_support. + // This feature was backported for WordPress 6.2 as of https://core.trac.wordpress.org/ticket/56487 + // and then reverted as of https://core.trac.wordpress.org/ticket/57649 + // Not to backport until the issues are resolved. + if ( current_theme_supports( 'appearance-tools' ) ) { + $theme_support_data['settings']['appearanceTools'] = true; + } + // END EXPERIMENTAL. } - // END EXPERIMENTAL. } + $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data ); + + if ( $options['with_block_style_variations'] ) { + $block_style_variations_data = WP_Theme_JSON_Gutenberg::get_from_block_styles_registry(); + $with_block_style_variations = new WP_Theme_JSON_Gutenberg( $block_style_variations_data ); + $with_theme_supports->merge( $with_block_style_variations ); + } + $with_theme_supports->merge( static::$theme ); + return $with_theme_supports; } diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php new file mode 100644 index 0000000000000..b1a8b4762986e --- /dev/null +++ b/lib/compat/wordpress-6.5/blocks.php @@ -0,0 +1,41 @@ +register( $name, $style_properties ) ) { + $result = false; + } + } + + return $result; +} diff --git a/lib/load.php b/lib/load.php index d413334227ee7..e5788e924f9d8 100644 --- a/lib/load.php +++ b/lib/load.php @@ -101,6 +101,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.4/kses.php'; // WordPress 6.5 compat. +require __DIR__ . '/compat/wordpress-6.5/blocks.php'; require __DIR__ . '/compat/wordpress-6.5/block-patterns.php'; require __DIR__ . '/compat/wordpress-6.5/class-wp-navigation-block-renderer.php'; require __DIR__ . '/compat/wordpress-6.5/kses.php'; diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index b05381a8325b0..bfdf48eb3aaa6 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -28,6 +28,35 @@ describe( 'global styles renderer', () => { text: 'red', }, blocks: { + 'core/group': { + color: { + background: 'linen', + }, + variations: { + foo: { + color: 'aliceblue', + blocks: { + 'core/heading': { + typography: { + fontSize: '3em', + }, + }, + }, + elements: { + link: { + color: { + text: 'darkcyan', + }, + ':hover': { + color: { + text: 'darkturqoise', + }, + }, + }, + }, + }, + }, + }, 'core/heading': { color: { background: 'blue', @@ -89,6 +118,12 @@ describe( 'global styles renderer', () => { }, }; const blockSelectors = { + 'core/group': { + selector: '.my-group', + styleVariationSelectors: { + foo: '.is-style-foo.my-group', + }, + }, 'core/heading': { selector: '.my-heading1, .my-heading2', }, @@ -129,6 +164,44 @@ describe( 'global styles renderer', () => { }, selector: ELEMENTS.link, }, + { + selector: + '.is-style-foo.my-group .my-heading1, .is-style-foo.my-group .my-heading2', + styles: { + typography: { + fontSize: '3em', + }, + }, + }, + { + selector: '.is-style-foo.my-group a', + styles: { + color: { + text: 'darkcyan', + }, + ':hover': { + color: { + text: 'darkturqoise', + }, + }, + }, + }, + { + selector: '.my-group', + styles: { + color: { + background: 'linen', + }, + variations: { + foo: { + color: 'aliceblue', + }, + }, + }, + styleVariationSelectors: { + foo: '.is-style-foo.my-group', + }, + }, { styles: { color: { @@ -549,6 +622,44 @@ describe( 'global styles renderer', () => { }, }, }, + 'core/group': { + variations: { + bar: { + color: { + background: 'midnightblue', + text: 'lightskyblue', + }, + blocks: { + 'core/heading': { + color: { + text: 'royalblue', + }, + }, + 'core/image': { + border: { + color: 'darkcyan', + style: 'dashed', + width: '5px', + }, + }, + }, + elements: { + h2: { + color: { + text: 'turquoise', + }, + }, + button: { + color: { + background: 'midnightblue', + text: 'powderblue', + }, + ':hover': {}, + }, + }, + }, + }, + }, }, }, }; @@ -567,11 +678,21 @@ describe( 'global styles renderer', () => { foo: '.is-style-foo.wp-image', }, }, + 'core/group': { + selector: '.wp-group', + styleVariationSelectors: { + bar: '.is-style-bar.wp-group', + }, + }, + 'core/heading': { + selector: '.wp-heading', + }, }; expect( toStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( 'body {margin: 0;}body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-constrained > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-constrained > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { max-width: var(--wp--style--global--content-size); margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > .alignwide { max-width: var(--wp--style--global--wide-size); }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }body .is-layout-grid { display:grid; }body .is-layout-grid > * { margin: 0; }' + '.is-style-foo.wp-image.wp-image-spacing{padding-top: 2px;}.is-style-foo.wp-image.wp-image-border-color{border-color: blue;}.is-style-foo.wp-image{color: blue;}' + + '.is-style-bar.wp-group .wp-heading{color: royalblue;}.is-style-bar.wp-group .wp-image-border-color{border-color: darkcyan;}.is-style-bar.wp-group .wp-image-border{border-style: dashed;border-width: 5px;}.is-style-bar.wp-group h2{color: turquoise;}.is-style-bar.wp-group .wp-element-button, .is-style-bar.wp-group .wp-block-button__link{color: powderblue;background-color: midnightblue;}.is-style-bar.wp-group{color: lightskyblue;background-color: midnightblue;}' + '.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; }' ); } ); 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 1cd63ef4d03f0..b1f6f395d9861 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 @@ -611,6 +611,33 @@ function pickStyleKeys( treeToPickFrom ) { return Object.fromEntries( clonedEntries ); } +function scopeFeatureSelectors( scope, selectors ) { + if ( ! scope || ! selectors ) { + return; + } + + const featureSelectors = JSON.parse( JSON.stringify( selectors ) ); + + Object.entries( selectors ).forEach( ( [ feature, selector ] ) => { + if ( typeof selector === 'string' ) { + featureSelectors[ feature ] = scopeSelector( scope, selector ); + } + + if ( typeof selector === 'object' ) { + Object.entries( selector ).forEach( + ( [ subfeature, subfeatureSelector ] ) => { + featureSelectors[ feature ][ subfeature ] = scopeSelector( + scope, + subfeatureSelector + ); + } + ); + } + } ); + + return featureSelectors; +} + export const getNodesWithStyles = ( tree, blockSelectors ) => { const nodes = []; @@ -643,14 +670,77 @@ 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 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 the variations inner element styles. + Object.entries( variation.elements ?? {} ).forEach( + ( [ element, elementStyles ] ) => { + if ( elementStyles && ELEMENTS[ element ] ) { + nodes.push( { + styles: elementStyles, + selector: scopeSelector( + variationSelector, + ELEMENTS[ element ] + ), + } ); + } + } + ); + } + ); + blockStyles.variations = variations; } - if ( blockStyles && blockSelectors?.[ blockName ]?.selector ) { + + if ( blockSelectors?.[ blockName ]?.selector ) { nodes.push( { duotoneSelector: blockSelectors[ blockName ].duotoneSelector, @@ -843,6 +933,7 @@ export const toStyles = ( ( [ styleVariationName, styleVariationSelector ] ) => { const styleVariations = styles?.variations?.[ styleVariationName ]; + if ( styleVariations ) { // If the block uses any custom selectors for block support, add those first. if ( featureSelectors ) { @@ -860,6 +951,7 @@ export const toStyles = ( baseSelector, styleVariationSelector ); + const rules = declarations.join( ';' ); ruleset += `${ cssSelector }{${ rules };}`; @@ -876,6 +968,7 @@ export const toStyles = ( useRootPaddingAlign, tree ); + if ( styleVariationDeclarations.length ) { ruleset += `${ styleVariationSelector }{${ styleVariationDeclarations.join( ';' @@ -1069,13 +1162,12 @@ export const getBlockSelectors = ( blockTypes, getBlockStyles ) => { const blockStyleVariations = getBlockStyles( name ); const styleVariationSelectors = {}; - if ( blockStyleVariations?.length ) { - blockStyleVariations.forEach( ( variation ) => { - const styleVariationSelector = `.is-style-${ variation.name }${ selector }`; - styleVariationSelectors[ variation.name ] = - styleVariationSelector; - } ); - } + + blockStyleVariations?.forEach( ( variation ) => { + const styleVariationSelector = `.is-style-${ variation.name }${ selector }`; + styleVariationSelectors[ variation.name ] = styleVariationSelector; + } ); + // For each block support feature add any custom selectors. const featureSelectors = getSelectorsConfig( blockType, selector ); @@ -1088,8 +1180,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/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index 34964d3c92905..08e38592cfa86 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -387,6 +387,10 @@ export function getValueFromVariable( features, blockName, variable ) { * @return {string} Scoped selector. */ export function scopeSelector( scope, selector ) { + if ( ! scope || ! selector ) { + return selector; + } + const scopes = scope.split( ',' ); const selectors = selector.split( ',' ); diff --git a/packages/edit-site/src/components/global-styles/variations-panel.js b/packages/edit-site/src/components/global-styles/variations-panel.js index 823e27038defb..3b2e475064805 100644 --- a/packages/edit-site/src/components/global-styles/variations-panel.js +++ b/packages/edit-site/src/components/global-styles/variations-panel.js @@ -2,16 +2,26 @@ * 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 +32,10 @@ export function useBlockVariations( name ) { }, [ name ] ); - const coreBlockStyles = getCoreBlockStyles( blockStyles ); - return coreBlockStyles; + const [ variations ] = useGlobalStyle( 'variations', name, 'base' ); + const variationNames = Object.keys( variations ?? {} ); + + return getFilteredBlockStyles( blockStyles, variationNames ); } export function VariationsPanel( { name } ) { diff --git a/phpunit/block-supports/elements-test.php b/phpunit/block-supports/elements-test.php index efea11887b620..ced216e96ef28 100644 --- a/phpunit/block-supports/elements-test.php +++ b/phpunit/block-supports/elements-test.php @@ -261,7 +261,7 @@ public function data_elements_block_support_styles() { 'elements_styles' => array( 'button' => array( 'color' => $color_styles ), ), - 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} .wp-element-button, .wp-elements-[a-f0-9]{32} .wp-block-button__link' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} .wp-element-button, .wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} .wp-block-button__link' . $color_css_rules . '$/', ), 'link element styles are applied' => array( 'color_settings' => array( 'link' => true ), @@ -273,15 +273,15 @@ public function data_elements_block_support_styles() { ), ), ), - 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} a' . $color_css_rules . - '.wp-elements-[a-f0-9]{32} a:hover' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} a' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} a:hover' . $color_css_rules . '$/', ), 'generic heading element styles are applied' => array( 'color_settings' => array( 'heading' => true ), 'elements_styles' => array( 'heading' => array( 'color' => $color_styles ), ), - 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} h1, .wp-elements-[a-f0-9]{32} h2, .wp-elements-[a-f0-9]{32} h3, .wp-elements-[a-f0-9]{32} h4, .wp-elements-[a-f0-9]{32} h5, .wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h1, .wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h2, .wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h3, .wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h4, .wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h5, .wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', ), 'individual heading element styles are applied' => array( 'color_settings' => array( 'heading' => true ), @@ -293,12 +293,12 @@ public function data_elements_block_support_styles() { 'h5' => array( 'color' => $color_styles ), 'h6' => array( 'color' => $color_styles ), ), - 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} h1' . $color_css_rules . - '.wp-elements-[a-f0-9]{32} h2' . $color_css_rules . - '.wp-elements-[a-f0-9]{32} h3' . $color_css_rules . - '.wp-elements-[a-f0-9]{32} h4' . $color_css_rules . - '.wp-elements-[a-f0-9]{32} h5' . $color_css_rules . - '.wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h1' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h2' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h3' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h4' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h5' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}.wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', ), ); } diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index eda8caede7bfe..aff82bc145b81 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -226,6 +226,83 @@ public function test_add_theme_supports_are_loaded_for_themes_without_theme_json $this->assertSame( $color_palette, $settings['color']['palette']['theme'] ); } + 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, + ), + ); + + unregister_block_style( 'core/group', 'my-variation' ); + + $this->assertSameSetsWithIndex( $group_styles, $expected ); + } + + public function test_registered_block_styles_not_added_to_theme_data_when_option_is_false() { + switch_theme( 'block-theme' ); + + $variation_styles_data = array( + 'color' => array( + 'background' => 'darkslateblue', + 'text' => 'lavender', + ), + ); + + register_block_style( + 'core/group', + array( + 'name' => 'my-variation', + 'style_data' => $variation_styles_data, + ) + ); + + $options = array( 'with_block_style_variations' => false ); + $theme_json = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( null, $options )->get_raw_data(); + $group_styles = $theme_json['styles']['blocks']['core/group'] ?? array(); + + unregister_block_style( 'core/group', 'my-variation' ); + + $this->assertArrayNotHasKey( 'variations', $group_styles ); + } + /** * Recursively applies ksort to an array. */ diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 89900d45893d9..e6627fbb4aea4 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -35,6 +35,19 @@ public static function set_up_before_class() { } static::$user_id = self::factory()->user->create(); + + // Register some block styles to test block style variations that have + // been registered through `WP_Block_Styles_Registry` as opposed to + // through block.json. + register_block_style( 'core/quote', array( 'name' => 'custom' ) ); + register_block_style( 'core/group', array( 'name' => 'custom' ) ); + } + + public static function tear_down_after_class() { + unregister_block_style( 'core/quote', 'custom' ); + unregister_block_style( 'core/group', 'custom' ); + + parent::tear_down_after_class(); } public function test_get_stylesheet_generates_layout_styles() { @@ -282,6 +295,46 @@ public function test_get_stylesheet() { 'spacing' => array( 'padding' => '24px', ), + 'variations' => array( + 'custom' => array( + 'color' => array( + 'background' => 'darkslateblue', + 'text' => 'white', + ), + 'blocks' => array( + 'core/heading' => array( + 'color' => array( + 'text' => 'fuchsia', + ), + ), + 'core/image' => array( + 'border' => array( + 'color' => 'darkorange', + 'style' => 'solid', + 'width' => '5px', + ), + ), + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => 'deeppink', + 'text' => '#f0f0f0', + ), + ':hover' => array( + 'color' => array( + 'background' => 'fuchsia', + ), + ), + ), + 'h2' => array( + 'color' => array( + 'text' => 'deepskyblue', + ), + ), + ), + ), + ), ), 'core/heading' => array( 'color' => array( @@ -337,7 +390,7 @@ public function test_get_stylesheet() { ); $variables = 'body{--wp--preset--color--grey: grey;--wp--preset--font-size--small: 14px;--wp--preset--font-size--big: 41px;--wp--preset--font-family--arial: Arial, serif;}.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; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}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, .wp-block-image .components-placeholder{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; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}body{color: var(--wp--preset--color--grey);}a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.is-style-custom.wp-block-group .wp-element-button, .is-style-custom.wp-block-group .wp-block-button__link{background-color: deeppink;color: #f0f0f0;}.is-style-custom.wp-block-group .wp-element-button:hover, .is-style-custom.wp-block-group .wp-block-button__link:hover{background-color: fuchsia;}.is-style-custom.wp-block-group h2{color: deepskyblue;}.wp-block-group{border-radius: 10px;min-height: 50vh;padding: 24px;}.is-style-custom.wp-block-group{background-color: darkslateblue;color: white;}.is-style-custom.wp-block-group .wp-block-heading{color: fuchsia;}.is-style-custom.wp-block-group .wp-block-image img, .is-style-custom.wp-block-group .wp-block-image .wp-block-image__crop-area, .is-style-custom.wp-block-group .wp-block-image .components-placeholder{border-color: darkorange;border-width: 5px;border-style: solid;}.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, .wp-block-image .components-placeholder{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-size{font-size: var(--wp--preset--font-size--small) !important;}.has-big-font-size{font-size: var(--wp--preset--font-size--big) !important;}.has-arial-font-family{font-family: var(--wp--preset--font-family--arial) !important;}'; $all = $variables . $styles . $presets; @@ -1534,6 +1587,11 @@ public function test_sanitize_for_unregistered_style_variations() { 'background' => 'hotpink', ), ), + 'custom' => array( + 'color' => array( + 'background' => 'magenta', + ), + ), ), ), ), @@ -1548,11 +1606,16 @@ public function test_sanitize_for_unregistered_style_variations() { 'blocks' => array( 'core/quote' => array( 'variations' => array( - 'plain' => array( + 'plain' => array( 'color' => array( 'background' => 'hotpink', ), ), + 'custom' => array( + 'color' => array( + 'background' => 'magenta', + ), + ), ), ), ), @@ -1643,6 +1706,32 @@ public function data_sanitize_for_block_with_style_variations() { ), ), ), + '1 variation via registry with invalid properties' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'custom' => array( + 'color' => array( + 'background' => 'magenta', + ), + 'invalidProperty1' => 'value1', + 'invalidProperty2' => 'value2', + ), + ), + ), + 'expected_sanitized' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'custom' => array( + 'color' => array( + 'background' => 'magenta', + ), + ), + ), + ), + ), + ), + ), ); } @@ -1857,13 +1946,21 @@ public function data_get_styles_for_block_with_style_variations() { $plain = array( 'metadata' => array( 'path' => array( 'styles', 'blocks', 'core/quote', 'variations', 'plain' ), - 'selector' => '.is-style-plain.is-style-plain.wp-block-quote', + 'selector' => '.is-style-plain.wp-block-quote', ), - 'styles' => '.is-style-plain.is-style-plain.wp-block-quote{background-color: hotpink;}', + 'styles' => '.is-style-plain.wp-block-quote{background-color: hotpink;}', + ); + + $custom = array( + 'metadata' => array( + 'path' => array( 'styles', 'blocks', 'core/quote', 'variations', 'custom' ), + 'selector' => '.is-style-custom.wp-block-quote', + ), + 'styles' => '.is-style-custom.wp-block-quote{background-color: magenta;}', ); return array( - '1 variation with 1 invalid property' => array( + 'variation with 1 valid property' => array( 'theme_json_variations' => array( 'variations' => array( 'plain' => array( @@ -1876,7 +1973,7 @@ public function data_get_styles_for_block_with_style_variations() { 'metadata_variation' => array( $plain['metadata'] ), 'expected' => $plain['styles'], ), - '1 variation with 2 invalid properties' => array( + 'variation with 2 invalid properties' => array( 'theme_json_variations' => array( 'variations' => array( 'plain' => array( @@ -1891,6 +1988,19 @@ public function data_get_styles_for_block_with_style_variations() { 'metadata_variation' => array( $plain['metadata'] ), 'expected' => $plain['styles'], ), + 'variation via WP_Block_Styles_Registry' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'custom' => array( + 'color' => array( + 'background' => 'magenta', + ), + ), + ), + ), + 'metadata_variation' => array( $custom['metadata'] ), + 'expected' => $custom['styles'], + ), ); } @@ -1922,6 +2032,60 @@ public function test_block_style_variations() { $this->assertSameSetsWithIndex( $expected, $actual ); } + public function test_block_style_variations_from_block_styles_registry() { + $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, + ) + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'my-variation' => $variation_styles_data, + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Gutenberg::get_from_block_styles_registry(); + + unregister_block_style( 'core/group', 'my-variation' ); + $this->assertSameSetsWithIndex( $expected, $actual ); + } + public function test_block_style_variations_with_invalid_properties() { wp_set_current_user( static::$administrator_id ); @@ -1973,6 +2137,135 @@ public function test_block_style_variations_with_invalid_properties() { $this->assertSameSetsWithIndex( $expected, $actual ); } + public function test_block_styles_with_invalid_elements() { + wp_set_current_user( static::$administrator_id ); + + $partially_invalid_elements = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'deepskyblue', + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ':hover' => array( + 'color' => array( + 'text' => 'cyan', + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ), + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ), + ), + ), + ), + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'deepskyblue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'cyan', + ), + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $partially_invalid_elements ); + + $this->assertSameSetsWithIndex( $expected, $actual ); + } + public function test_block_style_variations_with_elements_and_invalid_properties() { + wp_set_current_user( static::$administrator_id ); + + $partially_invalid_variation = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'deepskyblue', + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ':hover' => array( + 'color' => array( + 'text' => 'cyan', + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ), + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ), + ), + ), + ), + ), + ), + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'deepskyblue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'cyan', + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $partially_invalid_variation ); + + $this->assertSameSetsWithIndex( $expected, $actual ); + } + public function test_update_separator_declarations() { // If only background is defined, test that includes border-color to the style so it is applied on the front end. $theme_json = new WP_Theme_JSON_Gutenberg( diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 10695f493c40d..30016a16c38f9 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -2194,20 +2194,375 @@ "$ref": "#/definitions/stylesElementsPropertiesComplete" }, "variations": { - "$ref": "#/definitions/stylesVariationPropertiesComplete" + "$ref": "#/definitions/stylesVariationsPropertiesComplete" } }, "additionalProperties": false } ] }, - "stylesVariationPropertiesComplete": { + "stylesVariationsPropertiesComplete": { "type": "object", "patternProperties": { "^[a-z][a-z0-9-]*$": { - "$ref": "#/definitions/stylesPropertiesComplete" + "$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": {} + }, + "additionalProperties": false + } + ] } }, "type": "object",