diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index 2977afbeb8cb9..3cde142c641cb 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -488,7 +488,7 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex unset( $editor_settings['__experimentalFeatures']['spacing']['padding'] ); } if ( isset( $editor_settings['__experimentalFeatures']['spacing']['customSpacingSize'] ) ) { - $editor_settings['disableCustomSpacingSizes'] = ! $editor_ettings['__experimentalFeatures']['spacing']['customSpacingSize']; + $editor_settings['disableCustomSpacingSizes'] = ! $editor_settings['__experimentalFeatures']['spacing']['customSpacingSize']; unset( $editor_settings['__experimentalFeatures']['spacing']['customSpacingSize'] ); } diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 5d0dd099c20e5..74a0cd1daa0d9 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -418,7 +418,7 @@ public static function get_user_data() { * @since 5.8.0 * @since 5.9.0 Added user data, removed the `$settings` parameter, * added the `$origin` parameter. - * @since 6.1.0 Added block data. + * @since 6.1.0 Added block data and generation of spacingSizes array. * * @param string $origin Optional. To what level should we merge data. * Valid values are 'theme' or 'custom'. Default 'custom'. @@ -438,6 +438,9 @@ public static function get_merged_data( $origin = 'custom' ) { $result->merge( static::get_user_data() ); } + // Generate the default spacingSizes array based on the merged spacingScale settings. + $result->set_spacing_sizes(); + return $result; } diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 4e7c704cce16b..752cbee00d96a 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -164,6 +164,24 @@ class WP_Theme_JSON { 'classes' => array( '.has-$slug-font-family' => 'font-family' ), 'properties' => array( 'font-family' ), ), + array( + 'path' => array( 'spacing', 'spacingSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--spacing--$slug', + 'classes' => array(), + 'properties' => array( 'padding', 'margin' ), + ), + array( + 'path' => array( 'spacing', 'spacingScale' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--spacing--$slug', + 'classes' => array(), + 'properties' => array( 'padding', 'margin' ), + ), ); /** @@ -307,10 +325,13 @@ class WP_Theme_JSON { 'wideSize' => null, ), 'spacing' => array( - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, + 'customSpacingSize' => null, + 'spacingSizes' => null, + 'spacingScale' => null, + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, ), 'typography' => array( 'customFontSize' => null, @@ -2886,4 +2907,122 @@ public function get_data() { return $output; } + /** + * Sets the spacingSizes array based on the spacingScale values from theme.json. + * + * @since 6.1.0 + * + * @return null|void + */ + public function set_spacing_sizes() { + $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() ); + + if ( ! is_numeric( $spacing_scale['steps'] ) + || ! isset( $spacing_scale['mediumStep'] ) + || ! isset( $spacing_scale['unit'] ) + || ! isset( $spacing_scale['operator'] ) + || ! isset( $spacing_scale['increment'] ) + || ! isset( $spacing_scale['steps'] ) + || ! is_numeric( $spacing_scale['increment'] ) + || ! is_numeric( $spacing_scale['mediumStep'] ) + || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { + if ( ! empty( $spacing_scale ) ) { + trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ), E_USER_NOTICE ); + } + return null; + } + + // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0. + if ( 0 === $spacing_scale['steps'] ) { + return null; + } + + $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); + $current_step = $spacing_scale['mediumStep']; + $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); + $x_small_count = null; + $below_sizes = array(); + $slug = 40; + $remainder = 0; + + for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) { + if ( '+' === $spacing_scale['operator'] ) { + $current_step -= $spacing_scale['increment']; + } elseif ( $spacing_scale['increment'] > 1 ) { + $current_step /= $spacing_scale['increment']; + } else { + $current_step *= $spacing_scale['increment']; + } + + if ( $current_step <= 0 ) { + $remainder = $below_midpoint_count; + break; + } + + $below_sizes[] = array( + /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ + 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ), + 'slug' => (string) $slug, + 'size' => round( $current_step, 2 ) . $unit, + ); + + if ( $below_midpoint_count === $steps_mid_point - 2 ) { + $x_small_count = 2; + } + + if ( $below_midpoint_count < $steps_mid_point - 2 ) { + $x_small_count++; + } + + $slug -= 10; + } + + $below_sizes = array_reverse( $below_sizes ); + + $below_sizes[] = array( + 'name' => __( 'Medium' ), + 'slug' => '50', + 'size' => $spacing_scale['mediumStep'] . $unit, + ); + + $current_step = $spacing_scale['mediumStep']; + $x_large_count = null; + $above_sizes = array(); + $slug = 60; + $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder; + + for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) { + $current_step = '+' === $spacing_scale['operator'] + ? $current_step + $spacing_scale['increment'] + : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] ); + + $above_sizes[] = array( + /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ + 'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ), + 'slug' => (string) $slug, + 'size' => round( $current_step, 2 ) . $unit, + ); + + if ( 1 === $above_midpoint_count ) { + $x_large_count = 2; + } + + if ( $above_midpoint_count > 1 ) { + $x_large_count++; + } + + $slug += 10; + } + + $spacing_sizes = array_merge( $below_sizes, $above_sizes ); + + // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes. + if ( $spacing_scale['steps'] <= 7 ) { + for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { + $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); + } + } + + _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); + } } diff --git a/src/wp-includes/theme-i18n.json b/src/wp-includes/theme-i18n.json index f5a65dbddc37d..82f89bd3d282f 100644 --- a/src/wp-includes/theme-i18n.json +++ b/src/wp-includes/theme-i18n.json @@ -30,6 +30,13 @@ } ] }, + "spacing": { + "spacingSizes": [ + { + "name": "Space size name" + } + ] + }, "blocks": { "*": { "typography": { @@ -55,6 +62,13 @@ "name": "Gradient name" } ] + }, + "spacing": { + "spacingSizes": [ + { + "name": "Space size name" + } + ] } } } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index f1e3e1ae8b9c1..d67b5872b91c2 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -3493,4 +3493,385 @@ function test_get_styles_for_block_with_content_width() { $style_rules = $theme_json->get_styles_for_block( $metadata ); $this->assertSame( $expected, $root_rules . $style_rules ); } + + /** + * Tests generating the spacing presets array based on the spacing scale provided. + * + * @ticket 56467 + * + * @dataProvider data_generate_spacing_scale_fixtures + * + * @param array $spacing_scale Example spacing scale definitions from the data provider. + * @param array $expected_output Expected output from data provider. + */ + function test_should_set_spacing_sizes( $spacing_scale, $expected_output ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'settings' => array( + 'spacing' => array( + 'spacingScale' => $spacing_scale, + ), + ), + ) + ); + + $theme_json->set_spacing_sizes(); + $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); + } + + /** + * Data provider for spacing scale tests. + * + * @ticket 56467 + * + * @return array + */ + function data_generate_spacing_scale_fixtures() { + return array( + 'only one value when single step in spacing scale' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '50', + 'size' => '4rem', + ), + ), + ), + 'one step above medium when two steps in spacing scale' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 2, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '50', + 'size' => '4rem', + ), + array( + 'name' => '2', + 'slug' => '60', + 'size' => '5.5rem', + ), + ), + ), + 'one step above medium and one below when three steps in spacing scale' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 3, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '40', + 'size' => '2.5rem', + ), + array( + 'name' => '2', + 'slug' => '50', + 'size' => '4rem', + ), + array( + 'name' => '3', + 'slug' => '60', + 'size' => '5.5rem', + ), + ), + ), + 'extra step added above medium when an even number of steps > 2 specified' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 4, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '40', + 'size' => '2.5rem', + ), + array( + 'name' => '2', + 'slug' => '50', + 'size' => '4rem', + ), + array( + 'name' => '3', + 'slug' => '60', + 'size' => '5.5rem', + ), + array( + 'name' => '4', + 'slug' => '70', + 'size' => '7rem', + ), + ), + ), + 'extra steps above medium if bottom end will go below zero' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 2.5, + 'steps' => 5, + 'mediumStep' => 5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '40', + 'size' => '2.5rem', + ), + array( + 'name' => '2', + 'slug' => '50', + 'size' => '5rem', + ), + array( + 'name' => '3', + 'slug' => '60', + 'size' => '7.5rem', + ), + array( + 'name' => '4', + 'slug' => '70', + 'size' => '10rem', + ), + array( + 'name' => '5', + 'slug' => '80', + 'size' => '12.5rem', + ), + ), + ), + 'multiplier correctly calculated above and below medium' => array( + 'spacing_scale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '30', + 'size' => '0.67rem', + ), + array( + 'name' => '2', + 'slug' => '40', + 'size' => '1rem', + ), + array( + 'name' => '3', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => '4', + 'slug' => '60', + 'size' => '2.25rem', + ), + array( + 'name' => '5', + 'slug' => '70', + 'size' => '3.38rem', + ), + ), + ), + 'increment < 1 combined showing * operator acting as divisor above and below medium' => array( + 'spacing_scale' => array( + 'operator' => '*', + 'increment' => 0.25, + 'steps' => 5, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '1', + 'slug' => '30', + 'size' => '0.09rem', + ), + array( + 'name' => '2', + 'slug' => '40', + 'size' => '0.38rem', + ), + array( + 'name' => '3', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => '4', + 'slug' => '60', + 'size' => '6rem', + ), + array( + 'name' => '5', + 'slug' => '70', + 'size' => '24rem', + ), + ), + ), + 't-shirt sizing used if more than 7 steps in scale' => array( + 'spacing_scale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 8, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '2X-Small', + 'slug' => '20', + 'size' => '0.44rem', + ), + array( + 'name' => 'X-Small', + 'slug' => '30', + 'size' => '0.67rem', + ), + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '1rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '2.25rem', + ), + array( + 'name' => 'X-Large', + 'slug' => '70', + 'size' => '3.38rem', + ), + array( + 'name' => '2X-Large', + 'slug' => '80', + 'size' => '5.06rem', + ), + array( + 'name' => '3X-Large', + 'slug' => '90', + 'size' => '7.59rem', + ), + ), + ), + ); + } + + /** + * Tests generating the spacing presets array based on the spacing scale provided. + * + * @ticket 56467 + * + * @dataProvider data_set_spacing_sizes_when_invalid + * + * @param array $spacing_scale Example spacing scale definitions from the data provider. + * @param array $expected_output Expected output from data provider. + */ + public function test_set_spacing_sizes_should_detect_invalid_spacing_scale( $spacing_scale, $expected_output ) { + $this->expectNotice(); + $this->expectNoticeMessage( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'settings' => array( + 'spacing' => array( + 'spacingScale' => $spacing_scale, + ), + ), + ) + ); + + $theme_json->set_spacing_sizes(); + $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); + } + + /** + * Data provider for spacing scale tests. + * + * @ticket 56467 + * + * @return array + */ + function data_set_spacing_sizes_when_invalid() { + return array( + 'missing operator value' => array( + 'spacing_scale' => array( + 'operator' => '', + 'increment' => 1.5, + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + 'non numeric increment' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 'add two to previous value', + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + 'non numeric steps' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 'spiral staircase preferred', + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + 'non numeric medium step' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 'That which is just right', + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + 'missing unit value' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 4, + ), + 'expected_output' => null, + ), + ); + } }