diff --git a/backport-changelog/6.6/6616.md b/backport-changelog/6.6/6616.md new file mode 100644 index 0000000000000..91261f78fb5c7 --- /dev/null +++ b/backport-changelog/6.6/6616.md @@ -0,0 +1,5 @@ +https://github.com/WordPress/wordpress-develop/pull/6616 + +* https://github.com/WordPress/gutenberg/pull/58409 +* https://github.com/WordPress/gutenberg/pull/61328 +* https://github.com/WordPress/gutenberg/pull/61842 \ No newline at end of file diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md index 69f0606c93649..a5c3e828a2d66 100644 --- a/docs/how-to-guides/themes/global-settings-and-styles.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -336,16 +336,7 @@ The following presets can be defined via `theme.json`: - `color.palette`: - generates 3 classes per preset value: color, background-color, and border-color. - generates a single custom property per preset value. -- `spacing.spacingScale`: used to generate an array of spacing preset sizes for use with padding, margin, and gap settings. - - `operator`: specifies how to calculate the steps with either `*` for multiplier, or `+` for sum. - - `increment`: the amount to increment each step by. Core by default uses a 'perfect 5th' multiplier of `1.5`. - - `steps`: the number of steps to generate in the spacing scale. The default is 7. To prevent the generation of the spacing presets, and to disable the related UI, this can be set to `0`. - - `mediumStep`: the steps in the scale are generated descending and ascending from a medium step, so this should be the size value of the medium space, without the unit. The default medium step is `1.5rem` so the mediumStep value is `1.5`. - - `unit`: the unit the scale uses, eg. `px, rem, em, %`. The default is `rem`. -- `spacing.spacingSizes`: themes can choose to include a static `spacing.spacingSizes` array of spacing preset sizes if they have a sequence of sizes that can't be generated via an increment or multiplier. - - `name`: a human readable name for the size, eg. `Small, Medium, Large`. - - `slug`: the machine readable name. In order to provide the best cross site/theme compatibility the slugs should be in the format, "10","20","30","40","50","60", with "50" representing the `Medium` size value. - - `size`: the size, including the unit, eg. `1.5rem`. It is possible to include fluid values like `clamp(2rem, 10vw, 20rem)`. +- `spacing.spacingSizes`/`spacing.spacingScale`: generates a single custom property per preset value. - `typography.fontSizes`: generates a single class and custom property per preset value. - `typography.fontFamilies`: generates a single custom property per preset value. diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index f752fe8104a56..59a820a16697c 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -167,6 +167,7 @@ Settings related to spacing. | padding | boolean | false | | | units | array | px,em,rem,vh,vw,% | | | customSpacingSize | boolean | true | | +| defaultSpacingSizes | boolean | true | | | spacingSizes | array | | name, size, slug | | spacingScale | object | | | diff --git a/docs/reference-guides/theme-json-reference/theme-json-migrations.md b/docs/reference-guides/theme-json-reference/theme-json-migrations.md index c304bfe39493e..8e9d56ed054e6 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-migrations.md +++ b/docs/reference-guides/theme-json-reference/theme-json-migrations.md @@ -88,8 +88,27 @@ The new `defaultFontSizes` option gives control over showing default font sizes It is `true` by default when switching to v3. This is to be consistent with how other `default*` options work such as `settings.color.defaultPalette`, but differs from the behavior in v2. -In theme.json v2, the default font sizes were only shown when theme sizes were not defined. A theme providing font sizes with the same slugs as the defaults would always override the default ones. - To keep behavior similar to v2 with a v3 theme.json: * If you do not have any `fontSizes` defined, `defaultFontSizes` can be left out or set to `true`. * If you have some `fontSizes` defined, set `defaultFontSizes` to `false`. + +#### `settings.spacing.defaultSpacingSizes` + +In theme.json v2, there are two settings that could be used to set theme level spacing sizes: `settings.spacing.spacingSizes` and `settings.spacing.spacingScale`. Setting both `spacingSizes` _and_ `spacingScale` would only use the values from `spacingSizes`. And setting either of them would always replace the entire set of default spacing sizes provided by WordPress. + +The default `spacingSizes` slugs provided by WordPress are: `20`, `30`, `40`, `50`, `60`, `70`, and `80`. + +The new `defaultSpacingSizes` option gives control over showing default spacing sizes and preventing those defaults from being overridden. + +- When set to `true` it will show the default spacing sizes and prevent them from being overridden by the theme. +- When set to `false` it will hide the default spacing sizes and allow the theme to use the default slugs. + +`defaultSpacingSizes` is `true` by default when switching to v3. This is to be consistent with how other `default*` options work such as `settings.color.defaultPalette`, but differs from the behavior in v2. + +Additionally, in v3 both `spacingSizes` and `spacingScale` can be set at the same time. Presets defined in `spacingSizes` with slugs matching the generated presets from `spacingSizes` will override the generated ones. + +To keep behavior similar to v2 with a v3 theme.json: +* If you do not have any `spacingSizes` presets or `spacingScale` config defined, `defaultSpacingSizes` can be left out or set to `true`. +* If you disabled default spacing sizes by setting `spacingScale` to `{ "steps": 0 }`, remove the `spacingScale` config and set `defaultSpacingSizes` to `false`. +* If you defined only one of either `spacingScale` or `spacingSizes` for your presets, set `defaultSpacingSizes` to `false`. +* If you defined both `spacingScale` and `spacingSizes`, remove the `spacingSizes` config _and_ set `defaultSpacingSizes` to `false`. diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 938d8afa9d4fa..ad4e2fe105b0c 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -123,6 +123,7 @@ class WP_Theme_JSON_Gutenberg { * @since 6.0.0 Replaced `override` with `prevent_override` and updated the * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`. * @since 6.2.0 Added 'shadow' presets. + * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`. * @since 6.6.0 Added `aspectRatios`. * @var array */ @@ -187,7 +188,7 @@ class WP_Theme_JSON_Gutenberg { ), array( 'path' => array( 'spacing', 'spacingSizes' ), - 'prevent_override' => false, + 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ), 'use_default_names' => true, 'value_key' => 'size', 'css_vars' => '--wp--preset--spacing--$slug', @@ -427,13 +428,14 @@ class WP_Theme_JSON_Gutenberg { 'sticky' => null, ), 'spacing' => array( - 'customSpacingSize' => null, - 'spacingSizes' => null, - 'spacingScale' => null, - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, + 'customSpacingSize' => null, + 'defaultSpacingSizes' => null, + 'spacingSizes' => null, + 'spacingScale' => null, + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, ), 'shadow' => array( 'presets' => null, @@ -727,6 +729,8 @@ public static function get_element_class_name( $element ) { * Constructor. * * @since 5.8.0 + * @since 6.6.0 Key spacingScale by origin, and pre-generate the + * spacingSizes from spacingScale. * * @param array $theme_json A structure that follows the theme.json schema. * @param string $origin Optional. What source of data this object represents. @@ -742,8 +746,8 @@ public function __construct( $theme_json = array( 'version' => WP_Theme_JSON_Gut $valid_block_names = array_keys( $registry->get_all_registered() ); $valid_element_names = array_keys( static::ELEMENTS ); $valid_variations = static::get_valid_block_style_variations(); - $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations ); - $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); + $this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations ); + $this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json ); // Internally, presets are keyed by origin. $nodes = static::get_setting_nodes( $this->theme_json ); @@ -762,6 +766,27 @@ public function __construct( $theme_json = array( 'version' => WP_Theme_JSON_Gut } } } + + // In addition to presets, spacingScale (which generates presets) is also keyed by origin. + $scale_path = array( 'settings', 'spacing', 'spacingScale' ); + $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); + if ( null !== $spacing_scale ) { + // If the spacingScale is not already keyed by origin. + if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) { + _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) ); + } + } + + // Pre-generate the spacingSizes from spacingScale. + $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); + $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); + if ( isset( $spacing_scale ) ) { + $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin ); + $spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() ); + $spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale ); + $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes ); + _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes ); + } } /** @@ -2947,6 +2972,8 @@ protected static function get_metadata_boolean( $data, $path, $default_value = f * * @since 5.8.0 * @since 5.9.0 Duotone preset also has origins. + * @since 6.6.0 Use the spacingScale keyed by origin, and re-generate the + * spacingSizes from spacingScale. * * @param WP_Theme_JSON_Gutenberg $incoming Data to merge. */ @@ -2954,6 +2981,40 @@ public function merge( $incoming ) { $incoming_data = $incoming->get_raw_data(); $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); + /* + * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor + * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so + * we can allow partial spacingScale data to inherit missing data from earlier layers when + * computing the spacing sizes. + * + * This happens before the presets are merged to ensure that default spacing sizes can be + * removed from the theme origin if $prevent_override is true. + */ + $flattened_spacing_scale = array(); + foreach ( static::VALID_ORIGINS as $origin ) { + $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); + + // Apply the base spacing scale to the current layer. + $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() ); + $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale ); + + $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null ); + if ( ! isset( $spacing_scale ) ) { + continue; + } + + // Allow partial scale settings by merging with lower layers. + $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale ); + + // Generate and merge the scales for this layer. + $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin ); + $spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() ); + $spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale ); + $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes ); + + _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes ); + } + /* * The array_replace_recursive algorithm merges at the leaf level, * but we don't want leaf arrays to be merged, so we overwrite it. @@ -3733,12 +3794,19 @@ public function get_data() { /** * Sets the spacingSizes array based on the spacingScale values from theme.json. * + * No longer used since theme.json version 3 as the spacingSizes are now + * automatically generated during construction and merge instead of manually + * set in the resolver. + * * @since 6.1.0 + * @deprecated 6.6.0 * * @return null|void */ public function set_spacing_sizes() { - $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale'] ?? array(); + _deprecated_function( __METHOD__, '6.6.0' ); + + $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale']['default'] ?? array(); // Gutenberg didn't have the 1st isset check. if ( ! isset( $spacing_scale['steps'] ) @@ -3762,6 +3830,94 @@ public function set_spacing_sizes() { return null; } + $spacing_sizes = static::compute_spacing_sizes( $spacing_scale ); + + // 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 ); + } + + /** + * Merges two sets of spacing size presets. + * + * @since 6.6.0 + * + * @param array $base The base set of spacing sizes. + * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values. + * @return array The merged set of spacing sizes. + */ + private static function merge_spacing_sizes( $base, $incoming ) { + $merged = array(); + foreach ( $base as $item ) { + $merged[ $item['slug'] ] = $item; + } + foreach ( $incoming as $item ) { + $merged[ $item['slug'] ] = $item; + } + return array_values( $merged ); + } + + /** + * Generates a set of spacing sizes by starting with a medium size and + * applying an operator with an increment value to generate the rest of the + * sizes outward from the medium size. The medium slug is '50' with the rest + * of the slugs being 10 apart. The generated names use t-shirt sizing. + * + * Example: + * + * $spacing_scale = array( + * 'steps' => 4, + * 'mediumStep' => 16, + * 'unit' => 'px', + * 'operator' => '+', + * 'increment' => 2, + * ); + * $spacing_sizes = static::compute_spacing_sizes( $spacing_scale ); + * // -> array( + * // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ), + * // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ), + * // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ), + * // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ), + * // ) + * + * @since 6.6.0 + * + * @param array $spacing_scale { + * The spacing scale values. All are required. + * + * @type int $steps The number of steps in the scale. (up to 10 steps are supported.) + * @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.) + * @type string $unit The CSS unit to use for the sizes. + * @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'. + * @type float $increment The value used with the operator to generate the other sizes. + * } + * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid. + */ + private static function compute_spacing_sizes( $spacing_scale ) { + /* + * This condition is intentionally missing some checks on ranges for the values in order to + * keep backwards compatibility with the previous implementation. + */ + if ( + ! isset( $spacing_scale['steps'] ) || + ! is_numeric( $spacing_scale['steps'] ) || + 0 === $spacing_scale['steps'] || + ! isset( $spacing_scale['mediumStep'] ) || + ! is_numeric( $spacing_scale['mediumStep'] ) || + ! isset( $spacing_scale['unit'] ) || + ! isset( $spacing_scale['operator'] ) || + ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) || + ! isset( $spacing_scale['increment'] ) || + ! is_numeric( $spacing_scale['increment'] ) + ) { + return array(); + } + $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); $current_step = $spacing_scale['mediumStep']; $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); @@ -3844,14 +4000,7 @@ public function set_spacing_sizes() { $spacing_sizes[] = $above_sizes_item; } - // 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 ); + return $spacing_sizes; } /** diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index b21fb956ff8ff..84f999e4e9d02 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -600,7 +600,6 @@ public static function get_merged_data( $origin = 'custom' ) { $result = new WP_Theme_JSON_Gutenberg(); $result->merge( static::get_core_data() ); if ( 'default' === $origin ) { - $result->set_spacing_sizes(); return $result; } @@ -611,12 +610,10 @@ public static function get_merged_data( $origin = 'custom' ) { $result->merge( static::get_theme_data() ); if ( 'theme' === $origin ) { - $result->set_spacing_sizes(); return $result; } $result->merge( static::get_user_data() ); - $result->set_spacing_sizes(); return $result; } diff --git a/lib/class-wp-theme-json-schema-gutenberg.php b/lib/class-wp-theme-json-schema-gutenberg.php index 1eea7ddaa2736..0def88f86a23a 100644 --- a/lib/class-wp-theme-json-schema-gutenberg.php +++ b/lib/class-wp-theme-json-schema-gutenberg.php @@ -131,7 +131,7 @@ private static function migrate_v2_to_v3( $old ) { * affect the generated CSS. And in v2 we provided default font sizes * when the theme did not provide any. */ - if ( isset( $new['settings']['typography']['fontSizes'] ) ) { + if ( isset( $old['settings']['typography']['fontSizes'] ) ) { if ( ! isset( $new['settings'] ) ) { $new['settings'] = array(); } @@ -141,6 +141,38 @@ private static function migrate_v2_to_v3( $old ) { $new['settings']['typography']['defaultFontSizes'] = false; } + /* + * Similarly to defaultFontSizes, we need to migrate defaultSpacingSizes + * as it controls the PRESETS_METADATA prevent_override which was + * previously hardcoded to false. This only needs to happen when the + * theme provided spacing sizes via spacingSizes or spacingScale. + */ + if ( + isset( $old['settings']['spacing']['spacingSizes'] ) || + isset( $old['settings']['spacing']['spacingScale'] ) + ) { + if ( ! isset( $new['settings'] ) ) { + $new['settings'] = array(); + } + if ( ! isset( $new['settings']['spacing'] ) ) { + $new['settings']['spacing'] = array(); + } + $new['settings']['spacing']['defaultSpacingSizes'] = false; + } + + /* + * In v3 spacingSizes is merged with the generated spacingScale sizes + * instead of completely replacing them. The v3 behavior is what was + * documented for the v2 schema, but the code never actually did work + * that way. Instead of surprising users with a behavior change two + * years after the fact at the same time as a v3 update is introduced, + * we'll continue using the "bugged" behavior for v2 themes. And treat + * the "bug fix" as a breaking change for v3. + */ + if ( isset( $old['settings']['spacing']['spacingSizes'] ) ) { + unset( $new['settings']['spacing']['spacingScale'] ); + } + return $new; } diff --git a/lib/theme.json b/lib/theme.json index 2f1f723bf75f7..f638d9722ef67 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -265,6 +265,7 @@ "margin": false, "padding": false, "customSpacingSize": true, + "defaultSpacingSizes": true, "units": [ "px", "em", "rem", "vh", "vw", "%" ], "spacingScale": { "operator": "*", diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 26da27760aa5d..9718545795f7c 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -100,14 +100,13 @@ function useHasChildLayout( settings ) { } function useHasSpacingPresets( settings ) { - const { - custom, - theme, - default: defaultPresets, - } = settings?.spacing?.spacingSizes || {}; - const presets = custom ?? theme ?? defaultPresets ?? []; - - return presets.length > 0; + const { defaultSpacingSizes, spacingSizes } = settings?.spacing || {}; + return ( + ( defaultSpacingSizes !== false && + spacingSizes?.default?.length > 0 ) || + spacingSizes?.theme?.length > 0 || + spacingSizes?.custom?.length > 0 + ); } function filterValuesBySides( values, sides ) { diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 5c1e87001ca84..e5a67dce0167a 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -60,6 +60,7 @@ const VALID_SETTINGS = [ 'position.fixed', 'position.sticky', 'spacing.customSpacingSize', + 'spacing.defaultSpacingSizes', 'spacing.spacingSizes', 'spacing.spacingScale', 'spacing.blockGap', diff --git a/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js b/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js index e75f3519a1ca6..fcd4e3fb964a6 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js +++ b/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js @@ -1,28 +1,60 @@ /** * WordPress dependencies */ +import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { useSettings } from '../../use-settings'; +import { RANGE_CONTROL_MAX_SIZE } from '../utils'; + +const EMPTY_ARRAY = []; + +const compare = new Intl.Collator( 'und', { numeric: true } ).compare; export default function useSpacingSizes() { - const spacingSizes = [ { name: 0, slug: '0', size: 0 } ]; - - const [ settingsSizes ] = useSettings( 'spacing.spacingSizes' ); - if ( settingsSizes ) { - spacingSizes.push( ...settingsSizes ); - } - - if ( spacingSizes.length > 8 ) { - spacingSizes.unshift( { - name: __( 'Default' ), - slug: 'default', - size: undefined, - } ); - } - - return spacingSizes; + const [ + customSpacingSizes, + themeSpacingSizes, + defaultSpacingSizes, + defaultSpacingSizesEnabled, + ] = useSettings( + 'spacing.spacingSizes.custom', + 'spacing.spacingSizes.theme', + 'spacing.spacingSizes.default', + 'spacing.defaultSpacingSizes' + ); + + const customSizes = customSpacingSizes ?? EMPTY_ARRAY; + + const themeSizes = themeSpacingSizes ?? EMPTY_ARRAY; + + const defaultSizes = + defaultSpacingSizes && defaultSpacingSizesEnabled !== false + ? defaultSpacingSizes + : EMPTY_ARRAY; + + return useMemo( () => { + const sizes = [ + { name: __( 'None' ), slug: '0', size: 0 }, + ...customSizes, + ...themeSizes, + ...defaultSizes, + ].sort( ( a, b ) => compare( a.slug, b.slug ) ); + + return sizes.length > RANGE_CONTROL_MAX_SIZE + ? [ + { + name: __( 'Default' ), + slug: 'default', + size: undefined, + }, + ...sizes, + ] + : // See https://github.com/WordPress/gutenberg/pull/44247 for reasoning + // to use the index as the name in the range control. + sizes.map( ( { slug, size }, i ) => ( { name: i, slug, size } ) ); + }, [ customSizes, themeSizes, defaultSizes ] ); } diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js index eacdda5927f2d..58a81d8b130a3 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js +++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js @@ -23,6 +23,7 @@ import { settings } from '@wordpress/icons'; import { useSettings } from '../../use-settings'; import { store as blockEditorStore } from '../../../store'; import { + RANGE_CONTROL_MAX_SIZE, ALL_SIDES, LABELS, getSliderValueFromPreset, @@ -79,7 +80,7 @@ export default function SpacingInputControl( { value = getPresetValueFromCustomValue( value, spacingSizes ); let selectListSizes = spacingSizes; - const showRangeControl = spacingSizes.length <= 8; + const showRangeControl = spacingSizes.length <= RANGE_CONTROL_MAX_SIZE; const disableCustomSpacingSizes = useSelect( ( select ) => { const editorSettings = select( blockEditorStore ).getSettings(); diff --git a/packages/block-editor/src/components/spacing-sizes-control/utils.js b/packages/block-editor/src/components/spacing-sizes-control/utils.js index 32f0dbc59ac46..91c5a91934f6e 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/utils.js +++ b/packages/block-editor/src/components/spacing-sizes-control/utils.js @@ -12,6 +12,8 @@ import { sidesVertical, } from '@wordpress/icons'; +export const RANGE_CONTROL_MAX_SIZE = 8; + export const ALL_SIDES = [ 'top', 'right', 'bottom', 'left' ]; export const DEFAULT_VALUES = { diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index c14b6329cf2ec..4338262300c61 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -240,7 +240,11 @@ export function useBlockSettings( name, parentLayout ) { padding, margin, blockGap, - spacingSizes, + defaultSpacingSizesEnabled, + customSpacingSize, + userSpacingSizes, + defaultSpacingSizes, + themeSpacingSizes, units, aspectRatio, minHeight, @@ -293,7 +297,11 @@ export function useBlockSettings( name, parentLayout ) { 'spacing.padding', 'spacing.margin', 'spacing.blockGap', - 'spacing.spacingSizes', + 'spacing.defaultSpacingSizes', + 'spacing.customSpacingSize', + 'spacing.spacingSizes.custom', + 'spacing.spacingSizes.default', + 'spacing.spacingSizes.theme', 'spacing.units', 'dimensions.aspectRatio', 'dimensions.minHeight', @@ -384,8 +392,12 @@ export function useBlockSettings( name, parentLayout ) { }, spacing: { spacingSizes: { - custom: spacingSizes, + custom: userSpacingSizes, + default: defaultSpacingSizes, + theme: themeSpacingSizes, }, + customSpacingSize, + defaultSpacingSizes: defaultSpacingSizesEnabled, padding, margin, blockGap, @@ -428,7 +440,11 @@ export function useBlockSettings( name, parentLayout ) { padding, margin, blockGap, - spacingSizes, + defaultSpacingSizesEnabled, + customSpacingSize, + userSpacingSizes, + defaultSpacingSizes, + themeSpacingSizes, units, aspectRatio, minHeight, diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 006ec42987a01..848a3ee49251b 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -42,6 +42,7 @@ import { PrivateRichText } from './components/rich-text/'; import { PrivateBlockPopover } from './components/block-popover'; import { PrivateInserterLibrary } from './components/inserter/library'; import { PrivatePublishDateTimePicker } from './components/publish-date-time-picker'; +import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes'; /** * Private @wordpress/block-editor APIs. @@ -84,4 +85,5 @@ lock( privateApis, { reusableBlocksSelectKey, PrivateBlockPopover, PrivatePublishDateTimePicker, + useSpacingSizes, } ); diff --git a/packages/block-library/src/spacer/controls.js b/packages/block-library/src/spacer/controls.js index 91a1e79be173e..160335fcc092e 100644 --- a/packages/block-library/src/spacer/controls.js +++ b/packages/block-library/src/spacer/controls.js @@ -7,6 +7,7 @@ import { useSettings, __experimentalSpacingSizesControl as SpacingSizesControl, isValueSpacingPreset, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { BaseControl, @@ -21,14 +22,15 @@ import { View } from '@wordpress/primitives'; /** * Internal dependencies */ +import { unlock } from '../lock-unlock'; import { MIN_SPACER_SIZE } from './constants'; +const { useSpacingSizes } = unlock( blockEditorPrivateApis ); + function DimensionInput( { label, onChange, isResizing, value = '' } ) { const inputId = useInstanceId( UnitControl, 'block-spacer-height-input' ); - const [ spacingSizes, spacingUnits ] = useSettings( - 'spacing.spacingSizes', - 'spacing.units' - ); + const spacingSizes = useSpacingSizes(); + const [ spacingUnits ] = useSettings( 'spacing.units' ); // In most contexts the spacer size cannot meaningfully be set to a // percentage, since this is relative to the parent container. This // unit is disabled from the UI. diff --git a/packages/block-library/src/spacer/edit.js b/packages/block-library/src/spacer/edit.js index 03fc59e07ed17..1e0ffb9700d68 100644 --- a/packages/block-library/src/spacer/edit.js +++ b/packages/block-library/src/spacer/edit.js @@ -8,10 +8,10 @@ import clsx from 'clsx'; */ import { useBlockProps, - useSettings, getCustomValueFromPreset, getSpacingPresetCssVar, store as blockEditorStore, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { ResizableBox } from '@wordpress/components'; import { useState, useEffect } from '@wordpress/element'; @@ -21,9 +21,12 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ +import { unlock } from '../lock-unlock'; import SpacerControls from './controls'; import { MIN_SPACER_SIZE } from './constants'; +const { useSpacingSizes } = unlock( blockEditorPrivateApis ); + const ResizableSpacer = ( { orientation, onResizeStart, @@ -112,7 +115,7 @@ const SpacerEdit = ( { const { layout = {} } = blockStyle; const { selfStretch, flexSize } = layout; - const [ spacingSizes ] = useSettings( 'spacing.spacingSizes' ); + const spacingSizes = useSpacingSizes(); const [ isResizing, setIsResizing ] = useState( false ); const [ temporaryHeight, setTemporaryHeight ] = useState( null ); diff --git a/phpunit/class-wp-theme-json-schema-test.php b/phpunit/class-wp-theme-json-schema-test.php index 4259b8b5de6d4..24a7cf0e2e43d 100644 --- a/phpunit/class-wp-theme-json-schema-test.php +++ b/phpunit/class-wp-theme-json-schema-test.php @@ -223,6 +223,20 @@ public function test_migrate_v2_to_latest() { ), ), ), + 'spacing' => array( + 'spacingSizes' => array( + array( + 'name' => 'Small', + 'slug' => 20, + 'size' => '20px', + ), + array( + 'name' => 'Large', + 'slug' => 80, + 'size' => '80px', + ), + ), + ), ), ); @@ -246,6 +260,21 @@ public function test_migrate_v2_to_latest() { ), ), ), + 'spacing' => array( + 'defaultSpacingSizes' => false, + 'spacingSizes' => array( + array( + 'name' => 'Small', + 'slug' => 20, + 'size' => '20px', + ), + array( + 'name' => 'Large', + 'slug' => 80, + 'size' => '80px', + ), + ), + ), ), ); diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 28401bb20d484..f9e919e68f5a0 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -4276,10 +4276,10 @@ public function test_set_spacing_sizes( $spacing_scale, $expected_output ) { 'spacingScale' => $spacing_scale, ), ), - ) + ), + 'default' ); - $theme_json->set_spacing_sizes(); $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); } @@ -4300,7 +4300,7 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), @@ -4316,12 +4316,12 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), array( - 'name' => '2', + 'name' => 'Large', 'slug' => '60', 'size' => '5.5rem', ), @@ -4337,17 +4337,17 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Small', 'slug' => '40', 'size' => '2.5rem', ), array( - 'name' => '2', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), array( - 'name' => '3', + 'name' => 'Large', 'slug' => '60', 'size' => '5.5rem', ), @@ -4363,22 +4363,22 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Small', 'slug' => '40', 'size' => '2.5rem', ), array( - 'name' => '2', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), array( - 'name' => '3', + 'name' => 'Large', 'slug' => '60', 'size' => '5.5rem', ), array( - 'name' => '4', + 'name' => 'X-Large', 'slug' => '70', 'size' => '7rem', ), @@ -4394,27 +4394,27 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Small', 'slug' => '40', 'size' => '2.5rem', ), array( - 'name' => '2', + 'name' => 'Medium', 'slug' => '50', 'size' => '5rem', ), array( - 'name' => '3', + 'name' => 'Large', 'slug' => '60', 'size' => '7.5rem', ), array( - 'name' => '4', + 'name' => 'X-Large', 'slug' => '70', 'size' => '10rem', ), array( - 'name' => '5', + 'name' => '2X-Large', 'slug' => '80', 'size' => '12.5rem', ), @@ -4430,27 +4430,27 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'X-Small', 'slug' => '30', 'size' => '0.67rem', ), array( - 'name' => '2', + 'name' => 'Small', 'slug' => '40', 'size' => '1rem', ), array( - 'name' => '3', + 'name' => 'Medium', 'slug' => '50', 'size' => '1.5rem', ), array( - 'name' => '4', + 'name' => 'Large', 'slug' => '60', 'size' => '2.25rem', ), array( - 'name' => '5', + 'name' => 'X-Large', 'slug' => '70', 'size' => '3.38rem', ), @@ -4466,27 +4466,27 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'X-Small', 'slug' => '30', 'size' => '0.09rem', ), array( - 'name' => '2', + 'name' => 'Small', 'slug' => '40', 'size' => '0.38rem', ), array( - 'name' => '3', + 'name' => 'Medium', 'slug' => '50', 'size' => '1.5rem', ), array( - 'name' => '4', + 'name' => 'Large', 'slug' => '60', 'size' => '6rem', ), array( - 'name' => '5', + 'name' => 'X-Large', 'slug' => '70', 'size' => '24rem', ), @@ -4555,9 +4555,6 @@ public function data_set_spacing_sizes() { * @param array $expected_output Expected output from data provider. */ public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_output ) { - $this->expectException( Exception::class ); - $this->expectExceptionMessage( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ); - $theme_json = new WP_Theme_JSON_Gutenberg( array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, @@ -4566,19 +4563,10 @@ public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_o 'spacingScale' => $spacing_scale, ), ), - ) - ); - - // Ensure PHPUnit 10 compatibility. - set_error_handler( - static function ( $errno, $errstr ) { - restore_error_handler(); - throw new Exception( $errstr, $errno ); - }, - E_ALL + ), + 'default' ); - $theme_json->set_spacing_sizes(); $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); } @@ -4597,7 +4585,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 4, 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'non numeric increment' => array( 'spacing_scale' => array( @@ -4607,7 +4595,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 4, 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'non numeric steps' => array( 'spacing_scale' => array( @@ -4617,7 +4605,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 4, 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'non numeric medium step' => array( 'spacing_scale' => array( @@ -4627,7 +4615,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 'That which is just right', 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'missing unit value' => array( 'spacing_scale' => array( @@ -4636,7 +4624,7 @@ public function data_set_spacing_sizes_when_invalid() { 'steps' => 5, 'mediumStep' => 4, ), - 'expected_output' => null, + 'expected_output' => array(), ), ); } diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 6a3ec7e81d394..bdfea7279f67a 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -428,6 +428,11 @@ "type": "boolean", "default": true }, + "defaultSpacingSizes": { + "description": "Allow users to choose space sizes from the default space size presets.", + "type": "boolean", + "default": true + }, "spacingSizes": { "description": "Space size presets for the space size selector.\nGenerates a custom property (`--wp--preset--spacing--{slug}`) per preset value.", "type": "array", @@ -439,8 +444,9 @@ "type": "string" }, "slug": { - "description": "Unique identifier for the space size preset. For best cross theme compatibility these should be in the form '10','20','30','40','50','60', etc. with '50' representing the 'Medium' size step.", - "type": "string" + "description": "Unique identifier for the space size preset. Will be sorted numerically. For best cross theme compatibility these should be in the form '10','20','30','40','50','60', etc. with '50' representing the 'Medium' size step.", + "type": "string", + "pattern": "^[0-9].*$" }, "size": { "description": "CSS space-size value, including units.", @@ -463,16 +469,22 @@ "increment": { "description": "The amount to increment each step by.", "type": "number", + "exclusiveMinimum": true, + "minimum": 0, "default": 1.5 }, "steps": { "description": "Number of steps to generate in scale.", "type": "integer", + "minimum": 1, + "maximum": 10, "default": 7 }, "mediumStep": { "description": "The value to medium setting in the scale.", "type": "number", + "exclusiveMinimum": true, + "minimum": 0, "default": 1.5 }, "unit": {