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 0c852324ba59d8..033724c8216259 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -115,6 +115,8 @@ Settings related to dimensions. | Property | Type | Default | Props | | --- | --- | --- |--- | | aspectRatio | boolean | false | | +| defaultAspectRatios | boolean | true | | +| aspectRatios | array | | name, ratio, slug | | minHeight | boolean | false | | --- diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 72c9057aa2af01..bd2b780e55a45c 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -123,9 +123,19 @@ 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 Added `aspectRatios`. * @var array */ const PRESETS_METADATA = array( + array( + 'path' => array( 'dimensions', 'aspectRatios' ), + 'prevent_override' => array( 'dimensions', 'defaultAspectRatios' ), + 'use_default_names' => false, + 'value_key' => 'ratio', + 'css_vars' => '--wp--preset--aspect-ratio--$slug', + 'classes' => array(), + 'properties' => array( 'aspect-ratio' ), + ), array( 'path' => array( 'color', 'palette' ), 'prevent_override' => array( 'color', 'defaultPalette' ), @@ -397,8 +407,10 @@ class WP_Theme_JSON_Gutenberg { ), 'custom' => null, 'dimensions' => array( - 'aspectRatio' => null, - 'minHeight' => null, + 'aspectRatio' => null, + 'aspectRatios' => null, + 'defaultAspectRatios' => null, + 'minHeight' => null, ), 'layout' => array( 'contentSize' => null, @@ -483,7 +495,7 @@ class WP_Theme_JSON_Gutenberg { * updated `blockGap` to be allowed at any level. * @since 6.2.0 Added `outline`, and `minHeight` properties. * @since 6.6.0 Added `background` sub properties to top-level only. - * + * @since 6.6.0 Added `dimensions.aspectRatio`. * @var array */ const VALID_STYLES = array( diff --git a/lib/theme.json b/lib/theme.json index 90a5d975e68d65..49aa2abb08005b 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -190,6 +190,46 @@ ], "text": true }, + "dimensions": { + "defaultAspectRatios": true, + "aspectRatios": [ + { + "name": "Square - 1:1", + "slug": "square", + "ratio": "1" + }, + { + "name": "Standard - 4:3", + "slug": "4-3", + "ratio": "4/3" + }, + { + "name": "Portrait - 3:4", + "slug": "3-4", + "ratio": "3/4" + }, + { + "name": "Classic - 3:2", + "slug": "3-2", + "ratio": "3/2" + }, + { + "name": "Classic Portrait - 2:3", + "slug": "2-3", + "ratio": "2/3" + }, + { + "name": "Wide - 16:9", + "slug": "16-9", + "ratio": "16/9" + }, + { + "name": "Tall - 9:16", + "slug": "9-16", + "ratio": "9/16" + } + ] + }, "shadow": { "defaultPresets": true, "presets": [ diff --git a/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js b/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js index 5ff35ae0e0c888..e38a01e199b792 100644 --- a/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js +++ b/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js @@ -6,75 +6,14 @@ import { __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; - /** - * @typedef {import('@wordpress/components/build-types/select-control/types').SelectControlProps} SelectControlProps + * Internal dependencies */ +import { useSettings } from '../use-settings'; /** - * @type {SelectControlProps[]} + * @typedef {import('@wordpress/components/build-types/select-control/types').SelectControlProps} SelectControlProps */ -export const DEFAULT_ASPECT_RATIO_OPTIONS = [ - { - label: _x( 'Original', 'Aspect ratio option for dimensions control' ), - value: 'auto', - }, - { - label: _x( - 'Square - 1:1', - 'Aspect ratio option for dimensions control' - ), - value: '1', - }, - { - label: _x( - 'Standard - 4:3', - 'Aspect ratio option for dimensions control' - ), - value: '4/3', - }, - { - label: _x( - 'Portrait - 3:4', - 'Aspect ratio option for dimensions control' - ), - value: '3/4', - }, - { - label: _x( - 'Classic - 3:2', - 'Aspect ratio option for dimensions control' - ), - value: '3/2', - }, - { - label: _x( - 'Classic Portrait - 2:3', - 'Aspect ratio option for dimensions control' - ), - value: '2/3', - }, - { - label: _x( - 'Wide - 16:9', - 'Aspect ratio option for dimensions control' - ), - value: '16/9', - }, - { - label: _x( - 'Tall - 9:16', - 'Aspect ratio option for dimensions control' - ), - value: '9/16', - }, - { - label: _x( 'Custom', 'Aspect ratio option for dimensions control' ), - value: 'custom', - disabled: true, - hidden: true, - }, -]; /** * @callback AspectRatioToolPropsOnChange @@ -96,14 +35,48 @@ export default function AspectRatioTool( { panelId, value, onChange = () => {}, - options = DEFAULT_ASPECT_RATIO_OPTIONS, - defaultValue = DEFAULT_ASPECT_RATIO_OPTIONS[ 0 ].value, + options, + defaultValue = 'auto', hasValue, isShownByDefault = true, } ) { // Match the CSS default so if the value is used directly in CSS it will look correct in the control. const displayValue = value ?? 'auto'; + const [ defaultRatios, themeRatios, showDefaultRatios ] = useSettings( + 'dimensions.aspectRatios.default', + 'dimensions.aspectRatios.theme', + 'dimensions.defaultAspectRatios' + ); + + const themeOptions = themeRatios?.map( ( { name, ratio } ) => ( { + label: name, + value: ratio, + } ) ); + + const defaultOptions = defaultRatios?.map( ( { name, ratio } ) => ( { + label: name, + value: ratio, + } ) ); + + const aspectRatioOptions = [ + { + label: _x( + 'Original', + 'Aspect ratio option for dimensions control' + ), + value: 'auto', + }, + ...( showDefaultRatios ? defaultOptions : [] ), + ...( themeOptions ? themeOptions : [] ), + { + label: _x( 'Custom', 'Aspect ratio option for dimensions control' ), + value: 'custom', + disabled: true, + hidden: true, + }, + ]; + return ( diff --git a/packages/block-editor/src/components/image-editor/aspect-ratio-dropdown.js b/packages/block-editor/src/components/image-editor/aspect-ratio-dropdown.js index a15c9f42b9e8ed..3f48a634bdbcea 100644 --- a/packages/block-editor/src/components/image-editor/aspect-ratio-dropdown.js +++ b/packages/block-editor/src/components/image-editor/aspect-ratio-dropdown.js @@ -8,34 +8,70 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import { useSettings } from '../use-settings'; import { POPOVER_PROPS } from './constants'; import { useImageEditingContext } from './context'; -function AspectGroup( { aspectRatios, isDisabled, label, onClick, value } ) { +function AspectRatioGroup( { + aspectRatios, + isDisabled, + label, + onClick, + value, +} ) { return ( - { aspectRatios.map( ( { title, aspect } ) => ( + { aspectRatios.map( ( { name, slug, ratio } ) => ( { - onClick( aspect ); + onClick( ratio ); } } role="menuitemradio" - isSelected={ aspect === value } - icon={ aspect === value ? check : undefined } + isSelected={ ratio === value } + icon={ ratio === value ? check : undefined } > - { title } + { name } ) ) } ); } +export function ratioToNumber( str ) { + // TODO: support two-value aspect ratio? + // https://css-tricks.com/almanac/properties/a/aspect-ratio/#aa-it-can-take-two-values + const [ a, b, ...rest ] = str.split( '/' ).map( Number ); + if ( + a <= 0 || + b <= 0 || + Number.isNaN( a ) || + Number.isNaN( b ) || + rest.length + ) { + return NaN; + } + return b ? a / b : a; +} + +function presetRatioAsNumber( { ratio, ...rest } ) { + return { + ratio: ratioToNumber( ratio ), + ...rest, + }; +} + export default function AspectRatioDropdown( { toggleProps } ) { const { isInProgress, aspect, setAspect, defaultAspect } = useImageEditingContext(); + const [ defaultRatios, themeRatios, showDefaultRatios ] = useSettings( + 'dimensions.aspectRatios.default', + 'dimensions.aspectRatios.theme', + 'dimensions.defaultAspectRatios' + ); + return ( { ( { onClose } ) => ( <> - { setAspect( newAspect ); @@ -56,61 +92,57 @@ export default function AspectRatioDropdown( { toggleProps } ) { aspectRatios={ [ // All ratios should be mirrored in AspectRatioTool in @wordpress/block-editor. { - title: __( 'Original' ), + slug: 'original', + name: __( 'Original' ), aspect: defaultAspect, }, - { - title: __( 'Square' ), - aspect: 1, - }, - ] } - /> - { - setAspect( newAspect ); - onClose(); - } } - value={ aspect } - aspectRatios={ [ - { - title: __( '16:9' ), - aspect: 16 / 9, - }, - { - title: __( '4:3' ), - aspect: 4 / 3, - }, - { - title: __( '3:2' ), - aspect: 3 / 2, - }, - ] } - /> - { - setAspect( newAspect ); - onClose(); - } } - value={ aspect } - aspectRatios={ [ - { - title: __( '9:16' ), - aspect: 9 / 16, - }, - { - title: __( '3:4' ), - aspect: 3 / 4, - }, - { - title: __( '2:3' ), - aspect: 2 / 3, - }, + ...( showDefaultRatios + ? defaultRatios + .map( presetRatioAsNumber ) + .filter( ( { ratio } ) => ratio === 1 ) + : [] ), ] } /> + { themeRatios?.length > 0 && ( + { + setAspect( newAspect ); + onClose(); + } } + value={ aspect } + aspectRatios={ themeRatios } + /> + ) } + { showDefaultRatios && ( + { + setAspect( newAspect ); + onClose(); + } } + value={ aspect } + aspectRatios={ defaultRatios + .map( presetRatioAsNumber ) + .filter( ( { ratio } ) => ratio > 1 ) } + /> + ) } + { showDefaultRatios && ( + { + setAspect( newAspect ); + onClose(); + } } + value={ aspect } + aspectRatios={ defaultRatios + .map( presetRatioAsNumber ) + .filter( ( { ratio } ) => ratio < 1 ) } + /> + ) } ) } diff --git a/packages/block-editor/src/components/image-editor/index.js b/packages/block-editor/src/components/image-editor/index.js index cfd912bb2827ca..133f79732bdbd5 100644 --- a/packages/block-editor/src/components/image-editor/index.js +++ b/packages/block-editor/src/components/image-editor/index.js @@ -6,11 +6,11 @@ import { ToolbarGroup, ToolbarItem } from '@wordpress/components'; /** * Internal dependencies */ +import AspectRatioDropdown from './aspect-ratio-dropdown'; import BlockControls from '../block-controls'; import ImageEditingProvider from './context'; import Cropper from './cropper'; import ZoomDropdown from './zoom-dropdown'; -import AspectRatioDropdown from './aspect-ratio-dropdown'; import RotationButton from './rotation-button'; import FormControls from './form-controls'; diff --git a/packages/block-editor/src/components/image-editor/test/index.js b/packages/block-editor/src/components/image-editor/test/index.js new file mode 100644 index 00000000000000..9f0f3491667a8d --- /dev/null +++ b/packages/block-editor/src/components/image-editor/test/index.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import { ratioToNumber } from '../aspect-ratio-dropdown'; + +test( 'ratioToNumber', () => { + expect( ratioToNumber( '1/1' ) ).toBe( 1 ); + expect( ratioToNumber( '1' ) ).toBe( 1 ); + expect( ratioToNumber( '11/11' ) ).toBe( 1 ); + expect( ratioToNumber( '16/9' ) ).toBe( 16 / 9 ); + expect( ratioToNumber( '4/3' ) ).toBe( 4 / 3 ); + expect( ratioToNumber( '3/2' ) ).toBe( 3 / 2 ); + expect( ratioToNumber( '2/1' ) ).toBe( 2 ); + expect( ratioToNumber( '1/2' ) ).toBe( 1 / 2 ); + expect( ratioToNumber( '2/3' ) ).toBe( 2 / 3 ); + expect( ratioToNumber( '3/4' ) ).toBe( 3 / 4 ); + expect( ratioToNumber( '9/16' ) ).toBe( 9 / 16 ); + expect( ratioToNumber( '1/16' ) ).toBe( 1 / 16 ); + expect( ratioToNumber( '16/1' ) ).toBe( 16 ); + expect( ratioToNumber( '1/9' ) ).toBe( 1 / 9 ); + expect( ratioToNumber( 'auto' ) ).toBe( NaN ); +} ); diff --git a/packages/block-library/src/post-featured-image/dimension-controls.js b/packages/block-library/src/post-featured-image/dimension-controls.js index b64b3299fc96ba..c8e8c0005cfef5 100644 --- a/packages/block-library/src/post-featured-image/dimension-controls.js +++ b/packages/block-library/src/post-featured-image/dimension-controls.js @@ -57,7 +57,13 @@ const DimensionControls = ( { setAttributes, media, } ) => { - const [ availableUnits ] = useSettings( 'spacing.units' ); + const [ availableUnits, defaultRatios, themeRatios, showDefaultRatios ] = + useSettings( + 'spacing.units', + 'dimensions.aspectRatios.default', + 'dimensions.aspectRatios.theme', + 'dimensions.defaultAspectRatios' + ); const units = useCustomUnits( { availableUnits: availableUnits || [ 'px', '%', 'vw', 'em', 'rem' ], } ); @@ -93,6 +99,28 @@ const DimensionControls = ( { const showScaleControl = height || ( aspectRatio && aspectRatio !== 'auto' ); + const themeOptions = themeRatios?.map( ( { name, ratio } ) => ( { + label: name, + value: ratio, + } ) ); + + const defaultOptions = defaultRatios?.map( ( { name, ratio } ) => ( { + label: name, + value: ratio, + } ) ); + + const aspectRatioOptions = [ + { + label: _x( + 'Original', + 'Aspect ratio option for dimensions control' + ), + value: 'auto', + }, + ...( showDefaultRatios ? defaultOptions : [] ), + ...( themeOptions ? themeOptions : [] ), + ]; + return ( <> setAttributes( { aspectRatio: nextAspectRatio } ) } diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 08b4e084e2ec9b..68877c280d4dcf 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -288,6 +288,7 @@ export const __EXPERIMENTAL_PATHS_WITH_OVERRIDE = { 'color.duotone': true, 'color.gradients': true, 'color.palette': true, + 'dimensions.aspectRatios': true, 'typography.fontSizes': true, 'spacing.spacingSizes': true, }; diff --git a/schemas/json/theme.json b/schemas/json/theme.json index ccd2ee7f2ce53e..949b16c8b9d6ab 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -286,6 +286,32 @@ "type": "boolean", "default": false }, + "defaultAspectRatios": { + "description": "Allow users to choose aspect ratios from the default set of aspect ratios.", + "type": "boolean", + "default": true + }, + "aspectRatios": { + "description": "Allow users to define aspect ratios for some blocks.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "Name of the aspect ratio preset.", + "type": "string" + }, + "slug": { + "description": "Kebab-case unique identifier for the aspect ratio preset.", + "type": "string" + }, + "ratio": { + "description": "Aspect ratio expressed as a division or decimal.", + "type": "string" + } + } + } + }, "minHeight": { "description": "Allow users to set custom minimum height.", "type": "boolean", @@ -2274,6 +2300,7 @@ }, "background": {}, "color": {}, + "dimensions": {}, "layout": {}, "lightbox": {}, "spacing": {}, diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 24fff3e579f682..314834816388b2 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -327,7 +327,7 @@ test.describe( 'Image', () => { await editor.clickBlockToolbarButton( 'Crop' ); await editor.clickBlockToolbarButton( 'Aspect Ratio' ); await page.click( - 'role=menu[name="Aspect Ratio"i] >> role=menuitemradio[name="16:9"i]' + 'role=menu[name="Aspect Ratio"i] >> role=menuitemradio[name="Wide - 16:9"i]' ); await editor.clickBlockToolbarButton( 'Apply' );