diff --git a/backport-changelog/7.1/12077.md b/backport-changelog/7.1/12077.md
new file mode 100644
index 00000000000000..849dc1167bac18
--- /dev/null
+++ b/backport-changelog/7.1/12077.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/12077
+
+* https://github.com/WordPress/gutenberg/pull/78795
\ No newline at end of file
diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php
index df37b612294c0a..f1ddd78522e6d3 100644
--- a/lib/block-supports/dimensions.php
+++ b/lib/block-supports/dimensions.php
@@ -79,6 +79,22 @@ function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) {
return $attributes;
}
+/**
+ * Checks whether an aspectRatio block-support value is explicitly set.
+ *
+ * @param mixed $aspect_ratio Aspect-ratio value.
+ * @return bool Whether the value is an explicit aspect ratio.
+ */
+function gutenberg_is_explicit_aspect_ratio_value( $aspect_ratio ) {
+ if ( ! is_string( $aspect_ratio ) && ! is_numeric( $aspect_ratio ) ) {
+ return false;
+ }
+
+ $aspect_ratio = strtolower( trim( (string) $aspect_ratio ) );
+
+ return '' !== $aspect_ratio && 'auto' !== $aspect_ratio;
+}
+
/**
* Renders server-side dimensions styles to the block wrapper.
* This block support uses the `render_block` hook to ensure that
@@ -105,7 +121,7 @@ function gutenberg_render_dimensions_support( $block_content, $block ) {
// To ensure the aspect ratio does not get overridden by `minHeight` or `height` unset any existing rule.
if (
- isset( $dimensions_block_styles['aspectRatio'] )
+ gutenberg_is_explicit_aspect_ratio_value( $dimensions_block_styles['aspectRatio'] )
) {
$dimensions_block_styles['minHeight'] = 'unset';
$dimensions_block_styles['height'] = 'unset';
@@ -142,7 +158,7 @@ function gutenberg_render_dimensions_support( $block_content, $block ) {
foreach ( explode( ' ', $styles['classnames'] ) as $class_name ) {
if (
str_contains( $class_name, 'aspect-ratio' ) &&
- ! isset( $block_attributes['style']['dimensions']['aspectRatio'] )
+ ! gutenberg_is_explicit_aspect_ratio_value( $block_attributes['style']['dimensions']['aspectRatio'] ?? null )
) {
continue;
}
diff --git a/lib/block-supports/states.php b/lib/block-supports/states.php
index 27d641b2df0549..27e5d3550c6c6f 100644
--- a/lib/block-supports/states.php
+++ b/lib/block-supports/states.php
@@ -90,6 +90,54 @@ function gutenberg_get_state_declarations_with_fallback_border_styles( $declarat
return $declarations;
}
+/**
+ * Adds fallback dimension styles for aspectRatio and height block-support values.
+ *
+ * @param array $state_style State style object.
+ * @return array State style object with fallback dimension styles applied where needed.
+ */
+function gutenberg_get_state_style_with_fallback_dimension_styles( $state_style ) {
+ if ( ! is_array( $state_style ) ) {
+ return $state_style;
+ }
+
+ $dimensions = isset( $state_style['dimensions'] ) && is_array( $state_style['dimensions'] )
+ ? $state_style['dimensions']
+ : array();
+
+ if ( empty( $dimensions ) ) {
+ return $state_style;
+ }
+
+ if ( gutenberg_is_explicit_aspect_ratio_value( $dimensions['aspectRatio'] ?? null ) ) {
+ return array_replace_recursive(
+ $state_style,
+ array(
+ 'dimensions' => array(
+ 'minHeight' => 'unset',
+ 'height' => 'unset',
+ ),
+ )
+ );
+ }
+
+ $has_min_height = isset( $dimensions['minHeight'] ) && ( is_string( $dimensions['minHeight'] ) || is_numeric( $dimensions['minHeight'] ) ) && '' !== trim( (string) $dimensions['minHeight'] );
+ $has_height = isset( $dimensions['height'] ) && ( is_string( $dimensions['height'] ) || is_numeric( $dimensions['height'] ) ) && '' !== trim( (string) $dimensions['height'] );
+
+ if ( $has_min_height || $has_height ) {
+ return array_replace_recursive(
+ $state_style,
+ array(
+ 'dimensions' => array(
+ 'aspectRatio' => 'unset',
+ ),
+ )
+ );
+ }
+
+ return $state_style;
+}
+
/**
* Adds a style fragment to a selector-keyed state style group.
*
@@ -216,8 +264,9 @@ function gutenberg_get_block_state_style_rules( $state_styles, $block_type, $rul
}
foreach ( gutenberg_get_state_style_groups( $state_style, $block_selectors ) as $group ) {
+ $style = gutenberg_get_state_style_with_fallback_dimension_styles( $group['style'] );
$compiled = gutenberg_style_engine_get_styles(
- gutenberg_normalize_state_style_for_css_output( $group['style'] )
+ gutenberg_normalize_state_style_for_css_output( $style )
);
if ( ! empty( $compiled['declarations'] ) ) {
@@ -431,8 +480,8 @@ function gutenberg_render_block_states_support( $block_content, $block ) {
*/
$style_rules = array();
foreach ( $css_rules as $rule ) {
- $declarations = array();
- foreach ( $rule['declarations'] as $property => $value ) {
+ $declarations = $rule['declarations'];
+ foreach ( $declarations as $property => $value ) {
$declarations[ $property ] = is_string( $value ) && str_contains( $value, '!important' )
? $value
: $value . ' !important';
diff --git a/packages/block-editor/src/components/dimensions-tool/index.js b/packages/block-editor/src/components/dimensions-tool/index.js
index 49d0fe03b4cb48..2527f1b67ec36c 100644
--- a/packages/block-editor/src/components/dimensions-tool/index.js
+++ b/packages/block-editor/src/components/dimensions-tool/index.js
@@ -56,7 +56,7 @@ function DimensionsTool( {
aspectRatioOptions, // Default options handled by AspectRatioTool.
defaultAspectRatio = 'auto', // Match CSS default value for aspect-ratio.
scaleOptions, // Default options handled by ScaleTool.
- defaultScale = 'fill', // Match CSS default value for object-fit.
+ defaultScale = 'cover',
unitsOptions, // Default options handled by UnitControl.
tools = [ 'aspectRatio', 'widthHeight', 'scale' ],
} ) {
@@ -73,21 +73,19 @@ function DimensionsTool( {
value.aspectRatio === undefined || value.aspectRatio === 'auto'
? null
: value.aspectRatio;
- const scale =
- value.scale === undefined || value.scale === 'fill'
- ? null
- : value.scale;
+ const scale = value.scale === undefined ? null : value.scale;
// Keep track of state internally, so when the value is cleared by means
// other than directly editing that field, it's easier to restore the
// previous value.
const [ lastScale, setLastScale ] = useState( scale );
const [ lastAspectRatio, setLastAspectRatio ] = useState( aspectRatio );
+ const hasCustomAspectRatio = !! ( width && height );
// 'custom' is not a valid value for CSS aspect-ratio, but it is used in the
// dropdown to indicate that setting both the width and height is the same
// as a custom aspect ratio.
- const aspectRatioValue = width && height ? 'custom' : lastAspectRatio;
+ const aspectRatioValue = hasCustomAspectRatio ? 'custom' : aspectRatio;
const showScaleControl = aspectRatio || ( width && height );
@@ -197,9 +195,6 @@ function DimensionsTool( {
onChange={ ( nextScale ) => {
const nextValue = { ...value };
- // 'fill' is CSS default, so it gets treated as null.
- nextScale = nextScale === 'fill' ? null : nextScale;
-
setLastScale( nextScale );
// Update scale.
diff --git a/packages/block-editor/src/components/dimensions-tool/scale-tool.js b/packages/block-editor/src/components/dimensions-tool/scale-tool.js
index 212bd487d530e9..0c7254e9a99e46 100644
--- a/packages/block-editor/src/components/dimensions-tool/scale-tool.js
+++ b/packages/block-editor/src/components/dimensions-tool/scale-tool.js
@@ -86,8 +86,7 @@ export default function ScaleTool( {
defaultValue = DEFAULT_SCALE_OPTIONS[ 0 ].value,
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 ?? 'fill';
+ const displayValue = value ?? defaultValue;
const scaleHelp = useMemo( () => {
return options.reduce( ( acc, option ) => {
diff --git a/packages/block-editor/src/components/dimensions-tool/test/index.js b/packages/block-editor/src/components/dimensions-tool/test/index.js
index f6f7d71eb2dcbf..8b0b6e47d1465e 100644
--- a/packages/block-editor/src/components/dimensions-tool/test/index.js
+++ b/packages/block-editor/src/components/dimensions-tool/test/index.js
@@ -16,6 +16,17 @@ import { useState } from '@wordpress/element';
import DimensionsTool from '../';
const EMPTY_OBJECT = {};
+const ASPECT_RATIO_OPTIONS = [
+ { label: 'Original', value: 'auto' },
+ { label: '16/9', value: '16/9' },
+ { label: '4/3', value: '4/3' },
+ {
+ label: 'Custom',
+ value: 'custom',
+ disabled: true,
+ hidden: true,
+ },
+];
function Example( { initialValue, onChange, ...props } ) {
const [ value, setValue ] = useState( initialValue );
@@ -33,16 +44,27 @@ function Example( { initialValue, onChange, ...props } ) {
} }
defaultScale="cover"
defaultAspectRatio="auto"
- aspectRatioOptions={ [
- { label: 'Original', value: 'auto' },
- { label: '16/9', value: '16/9' },
- {
- label: 'Custom',
- value: 'custom',
- disabled: true,
- hidden: true,
- },
- ] }
+ aspectRatioOptions={ ASPECT_RATIO_OPTIONS }
+ value={ value }
+ { ...props }
+ />
+
+ );
+}
+
+function ControlledExample( { value, onChange, ...props } ) {
+ return (
+ onChange( EMPTY_OBJECT ) }
+ >
+
@@ -60,6 +82,39 @@ function Example( { initialValue, onChange, ...props } ) {
// properties are treated differently from missing properties.
describe( 'DimensionsTool', () => {
+ describe( 'controlled values', () => {
+ it( 'updates the aspect ratio control when the value prop changes', () => {
+ const onChange = jest.fn();
+ const { rerender } = render(
+
+ );
+ const aspectRatioSelect = screen.getByRole( 'combobox', {
+ name: 'Aspect ratio',
+ } );
+
+ expect( aspectRatioSelect ).toHaveValue( '16/9' );
+
+ rerender(
+
+ );
+ expect( aspectRatioSelect ).toHaveValue( '4/3' );
+
+ rerender(
+
+ );
+ expect( aspectRatioSelect ).toHaveValue( 'auto' );
+ } );
+ } );
+
describe( 'updating aspectRatio', () => {
it( 'when starting with empty initial state, setting aspectRatio also sets scale (0000) -> (1100)', async () => {
const user = userEvent.setup();
@@ -323,7 +378,30 @@ describe( 'DimensionsTool', () => {
} );
describe( 'updating scale', () => {
- // No custom interactions here. Things should just update normally.
+ it( 'when default scale is cover, setting scale to fill preserves the fill value', async () => {
+ const user = userEvent.setup();
+ const onChange = jest.fn();
+
+ const initialValue = {
+ aspectRatio: '16/9',
+ scale: 'cover',
+ };
+
+ render(
+
+ );
+
+ const scaleFillRadio = screen.getByRole( 'radio', {
+ name: 'Fill',
+ } );
+
+ await user.click( scaleFillRadio );
+ expect( scaleFillRadio ).toBeChecked();
+
+ expect( onChange.mock.calls ).toStrictEqual( [
+ [ { aspectRatio: '16/9', scale: 'fill' } ],
+ ] );
+ } );
} );
describe( 'updating dimensions', () => {
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 05b1b93f5e49a8..cf1a718760c2d3 100644
--- a/packages/block-editor/src/components/global-styles/dimensions-panel.js
+++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js
@@ -32,7 +32,6 @@ import { setImmutably } from '../../utils/object';
import {
DEFAULT_BLOCK_STYLE_STATE,
hasPseudoBlockStyleState,
- isDefaultBlockStyleState,
hasViewportBlockStyleState,
} from '../../hooks/block-style-state';
@@ -96,7 +95,7 @@ function hasWidth( settings ) {
function hasAspectRatio( settings, styleState = DEFAULT_BLOCK_STYLE_STATE ) {
return (
- isDefaultBlockStyleState( styleState ) &&
+ ! hasPseudoBlockStyleState( styleState ) &&
settings?.dimensions?.aspectRatio
);
}
diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js
index b968e356b146b3..61555b5aa5256a 100644
--- a/packages/block-editor/src/hooks/dimensions.js
+++ b/packages/block-editor/src/hooks/dimensions.js
@@ -194,6 +194,14 @@ export function hasDimensionsSupport( blockName, feature = 'any' ) {
return !! support?.[ feature ];
}
+export function isExplicitAspectRatio( aspectRatio ) {
+ if ( ! aspectRatio ) {
+ return false;
+ }
+
+ return `${ aspectRatio }`.trim().toLowerCase() !== 'auto';
+}
+
export default {
useBlockProps,
attributeKeys: [ 'height', 'minHeight', 'width', 'style' ],
@@ -210,8 +218,11 @@ function useBlockProps( { name, height, minHeight, style } ) {
return {};
}
+ const hasExplicitAspectRatio = isExplicitAspectRatio(
+ style?.dimensions?.aspectRatio
+ );
const className = clsx( {
- 'has-aspect-ratio': !! style?.dimensions?.aspectRatio,
+ 'has-aspect-ratio': hasExplicitAspectRatio,
} );
// Allow dimensions-based inline style overrides to override any global styles rules that
@@ -219,12 +230,12 @@ function useBlockProps( { name, height, minHeight, style } ) {
const inlineStyleOverrides = {};
// Apply rules to unset incompatible styles.
- // Note that a set `aspectRatio` will win out if both an aspect ratio and height-related properties are set.
+ // Note that an explicit `aspectRatio` will win out if both an aspect ratio and height-related properties are set.
// This is because the aspect ratio is a newer block support, so (in theory) any aspect ratio
// that is set should be intentional and should override any existing height properties. The Cover block
// and dimensions controls have logic that will manually clear the aspect ratio if height properties
// are set.
- if ( style?.dimensions?.aspectRatio ) {
+ if ( hasExplicitAspectRatio ) {
// To ensure the aspect ratio does not get overridden by `minHeight` or `height` unset any existing rule.
inlineStyleOverrides.minHeight = 'unset';
inlineStyleOverrides.height = 'unset';
diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js
index 8d1300065cacc0..87d68e780e46c8 100644
--- a/packages/block-editor/src/hooks/style.js
+++ b/packages/block-editor/src/hooks/style.js
@@ -29,6 +29,7 @@ import {
DIMENSIONS_SUPPORT_KEY,
SPACING_SUPPORT_KEY,
DimensionsPanel,
+ isExplicitAspectRatio,
} from './dimensions';
import {
cleanEmptyObject,
@@ -134,6 +135,37 @@ function getStateFallbackBorderStyles( stateStyles ) {
return cleanEmptyObject( { border: cleanEmptyObject( fallbackBorder ) } );
}
+/**
+ * Returns fallback dimension styles that keep state styles aligned with the
+ * default dimensions block-support output.
+ *
+ * @param {Object} stateStyles State style object.
+ * @return {Object|undefined} Style object containing fallback dimension styles.
+ */
+function getStateFallbackDimensionStyles( stateStyles ) {
+ const dimensions = stateStyles?.dimensions;
+ if ( ! dimensions ) {
+ return undefined;
+ }
+
+ if ( isExplicitAspectRatio( dimensions.aspectRatio ) ) {
+ return {
+ dimensions: {
+ minHeight: 'unset',
+ height: 'unset',
+ },
+ };
+ }
+
+ if ( dimensions.minHeight || dimensions.height ) {
+ return {
+ dimensions: {
+ aspectRatio: 'unset',
+ },
+ };
+ }
+}
+
/**
* Generates CSS for a block instance state style object.
*
@@ -146,7 +178,12 @@ function getStateFallbackBorderStyles( stateStyles ) {
* @return {string} Generated stylesheet.
*/
export function getStateStylesCSS( stateStyles, selector ) {
- const css = compileCSS( stateStyles, { selector } );
+ const fallbackDimensionStyles =
+ getStateFallbackDimensionStyles( stateStyles );
+ const stylesWithDimensionFallbacks = fallbackDimensionStyles
+ ? mergeStyleObjects( stateStyles, fallbackDimensionStyles )
+ : stateStyles;
+ const css = compileCSS( stylesWithDimensionFallbacks, { selector } );
const importantCSS = css ? css.replace( /;/g, ' !important;' ) : undefined;
const fallbackBorderStyles = getStateFallbackBorderStyles( stateStyles );
const fallbackCSS = fallbackBorderStyles
diff --git a/packages/block-editor/src/hooks/test/dimensions.js b/packages/block-editor/src/hooks/test/dimensions.js
index 304ad13a02e51d..3b7e49142e4663 100644
--- a/packages/block-editor/src/hooks/test/dimensions.js
+++ b/packages/block-editor/src/hooks/test/dimensions.js
@@ -1,6 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ getBlockType,
+ registerBlockType,
+ unregisterBlockType,
+} from '@wordpress/blocks';
+
/**
* Internal dependencies
*/
+import dimensions from '../dimensions';
import { getDimensionsClassesAndStyles } from '../use-dimensions-props';
describe( 'getDimensionsClassesAndStyles', () => {
@@ -126,3 +136,64 @@ describe( 'getDimensionsClassesAndStyles', () => {
} );
} );
} );
+
+describe( 'useBlockProps', () => {
+ const blockName = 'test/dimensions-with-aspect-ratio';
+
+ afterEach( () => {
+ if ( getBlockType( blockName ) ) {
+ unregisterBlockType( blockName );
+ }
+ } );
+
+ const registerDimensionsBlock = () =>
+ registerBlockType( blockName, {
+ apiVersion: 3,
+ title: 'Dimensions with aspect ratio',
+ category: 'text',
+ supports: {
+ dimensions: {
+ aspectRatio: true,
+ },
+ },
+ } );
+
+ it( 'unsets height styles when aspect ratio is explicit', () => {
+ registerDimensionsBlock();
+
+ expect(
+ dimensions.useBlockProps( {
+ name: blockName,
+ style: {
+ dimensions: {
+ aspectRatio: '16/9',
+ },
+ },
+ } )
+ ).toEqual( {
+ className: 'has-aspect-ratio',
+ style: {
+ minHeight: 'unset',
+ height: 'unset',
+ },
+ } );
+ } );
+
+ it( 'does not unset height styles when aspect ratio is the default', () => {
+ registerDimensionsBlock();
+
+ expect(
+ dimensions.useBlockProps( {
+ name: blockName,
+ style: {
+ dimensions: {
+ aspectRatio: 'auto',
+ },
+ },
+ } )
+ ).toEqual( {
+ className: '',
+ style: {},
+ } );
+ } );
+} );
diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js
index 4b09375f07e6ee..d43b02ae242ce9 100644
--- a/packages/block-editor/src/hooks/test/style.js
+++ b/packages/block-editor/src/hooks/test/style.js
@@ -208,6 +208,49 @@ describe( 'getStateStylesCSS', () => {
'.wp-block-test:hover { border-top-color: #0000ff !important; }\n.wp-block-test:hover { border-top-style: solid; }'
);
} );
+
+ it( 'adds important fallback dimensions when aspect ratio is set', () => {
+ expect(
+ getStateStylesCSS(
+ {
+ dimensions: {
+ aspectRatio: '16/9',
+ },
+ },
+ '.wp-block-test'
+ )
+ ).toBe(
+ '.wp-block-test { height: unset !important; min-height: unset !important; aspect-ratio: 16/9 !important; }'
+ );
+ } );
+
+ it( 'does not add fallback dimensions when aspect ratio is the default', () => {
+ expect(
+ getStateStylesCSS(
+ {
+ dimensions: {
+ aspectRatio: 'auto',
+ },
+ },
+ '.wp-block-test'
+ )
+ ).toBe( '.wp-block-test { aspect-ratio: auto !important; }' );
+ } );
+
+ it( 'adds important fallback aspect ratio when height is set', () => {
+ expect(
+ getStateStylesCSS(
+ {
+ dimensions: {
+ height: '20rem',
+ },
+ },
+ '.wp-block-test'
+ )
+ ).toBe(
+ '.wp-block-test { height: 20rem !important; aspect-ratio: unset !important; }'
+ );
+ } );
} );
describe( 'getBlockStateStylesCSS', () => {
@@ -286,10 +329,24 @@ describe( 'getResponsiveStateCSSRules', () => {
},
},
} );
+
+ registerBlockType( 'test/state-image', {
+ apiVersion: 3,
+ title: 'State Image',
+ category: 'media',
+ attributes: {},
+ edit: () => null,
+ save: () => null,
+ selectors: {
+ root: '.wp-block-test-state-image',
+ dimensions: '.wp-block-test-state-image img',
+ },
+ } );
} );
afterEach( () => {
unregisterBlockType( 'test/state-button' );
+ unregisterBlockType( 'test/state-image' );
} );
it( 'generates media-query scoped root styles for viewport states', () => {
@@ -325,6 +382,22 @@ describe( 'getResponsiveStateCSSRules', () => {
] );
} );
+ it( 'outputs explicit fill object fit for viewport states', () => {
+ expect(
+ getResponsiveStateCSSRules(
+ {
+ mobile: {
+ dimensions: { objectFit: 'fill' },
+ },
+ },
+ 'test/state-image',
+ '.wp-elements-1'
+ )
+ ).toEqual( [
+ '@media (width <= 480px){.wp-elements-1 img { object-fit: fill !important; }}',
+ ] );
+ } );
+
it( 'generates media-query scoped pseudo styles for viewport states', () => {
expect(
getResponsiveStateCSSRules(
diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index 4dbd42bd34c251..7bccf12d758386 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -16,6 +16,11 @@ import { PrivateListView } from './components/list-view';
import InspectorControlsLastItem from './components/inspector-controls/last-item';
import { useHasBlockToolbar } from './components/block-toolbar/use-has-block-toolbar';
import { cleanEmptyObject, usePrivateStyleOverride } from './hooks/utils';
+import {
+ getStyleForState,
+ isDefaultBlockStyleState,
+ setStyleForState,
+} from './hooks/block-style-state';
import BlockQuickNavigation from './components/block-quick-navigation';
import { LayoutStyle } from './components/block-list/layout';
import BlockManager from './components/block-manager';
@@ -98,6 +103,9 @@ lock( privateApis, {
InspectorControlsLastItem,
useHasBlockToolbar,
cleanEmptyObject,
+ getStyleForState,
+ isDefaultBlockStyleState,
+ setStyleForState,
usePrivateStyleOverride,
BlockQuickNavigation,
LayoutStyle,
diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js
index 4d554c14fa2788..786437984b0628 100644
--- a/packages/block-library/src/cover/edit/inspector-controls.js
+++ b/packages/block-library/src/cover/edit/inspector-controls.js
@@ -34,12 +34,21 @@ import { Link } from '@wordpress/ui';
import { COVER_MIN_HEIGHT, mediaPosition } from '../shared';
import { unlock } from '../../lock-unlock';
import { useToolsPanelDropdownMenuProps } from '../../utils/hooks';
+import {
+ getActiveDimensionValue,
+ getDimensionResetAttributes,
+ getDimensionUpdateAttributes,
+ getStyleStateKey,
+} from '../../utils/style-state';
import { DEFAULT_MEDIA_SIZE_SLUG } from '../constants';
import PosterImage from '../../utils/poster-image';
-const { cleanEmptyObject, ResolutionTool, HTMLElementControl } = unlock(
- blockEditorPrivateApis
-);
+const {
+ cleanEmptyObject,
+ isDefaultBlockStyleState,
+ ResolutionTool,
+ HTMLElementControl,
+} = unlock( blockEditorPrivateApis );
function CoverHeightInput( {
onChange,
@@ -124,20 +133,45 @@ export default function CoverInspectorControls( {
const sizeSlug = attributes.sizeSlug || DEFAULT_MEDIA_SIZE_SLUG;
const { gradientValue, setGradient } = __experimentalUseGradient();
- const { imageSizes, hasSelectedStyleState } = useSelect(
+ const { imageSizes, selectedStyleState } = useSelect(
( select ) => {
- const {
- getSettings,
- hasSelectedStyleState: hasSelectedBlockStyleState,
- } = unlock( select( blockEditorStore ) );
+ const { getSettings, getSelectedBlockStyleState } = unlock(
+ select( blockEditorStore )
+ );
return {
imageSizes: getSettings()?.imageSizes,
- hasSelectedStyleState: hasSelectedBlockStyleState( clientId ),
+ selectedStyleState: getSelectedBlockStyleState( clientId ),
};
},
[ clientId ]
);
+ const hasSelectedStyleState =
+ ! isDefaultBlockStyleState( selectedStyleState );
+ const selectedStyleStateKey = getStyleStateKey( selectedStyleState );
+ const stateMinHeight = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'minHeight',
+ styleKey: 'minHeight',
+ rootValue: undefined,
+ } );
+ const [ stateMinHeightValue, stateMinHeightUnit ] =
+ parseQuantityAndUnitFromRawValue( stateMinHeight || '' );
+ const activeMinHeight = hasSelectedStyleState
+ ? stateMinHeightValue
+ : minHeight;
+ const activeMinHeightUnit = hasSelectedStyleState
+ ? stateMinHeightUnit || minHeightUnit
+ : minHeightUnit;
+ const activeAspectRatio = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'aspectRatio',
+ rootValue: attributes?.style?.dimensions?.aspectRatio,
+ } );
const image = useSelect(
( select ) =>
@@ -203,6 +237,52 @@ export default function CoverInspectorControls( {
const showOverlayControls =
colorGradientSettings.hasColorsOrGradients && ! hasSelectedStyleState;
+ const setMinHeightAttributes = ( nextMinHeight, nextUnit ) => {
+ if ( hasSelectedStyleState ) {
+ setAttributes(
+ getDimensionUpdateAttributes( {
+ style: attributes.style,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ nextDimensions: {
+ minHeight:
+ nextMinHeight === undefined
+ ? undefined
+ : `${ nextMinHeight }${
+ nextUnit || activeMinHeightUnit || 'px'
+ }`,
+ aspectRatio: undefined,
+ },
+ } )
+ );
+ return;
+ }
+
+ setAttributes( {
+ minHeight: nextMinHeight,
+ style: cleanEmptyObject( {
+ ...attributes?.style,
+ dimensions: {
+ ...attributes?.style?.dimensions,
+ aspectRatio: undefined, // Reset aspect ratio when minHeight is set.
+ },
+ } ),
+ } );
+ };
+
+ const getResetMinHeightAttributes = ( attrs = attributes ) => {
+ return getDimensionResetAttributes( {
+ style: attrs.style,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ keys: [ 'minHeight' ],
+ defaultAttributes: {
+ minHeight: undefined,
+ minHeightUnit: undefined,
+ },
+ } );
+ };
+
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
return (
@@ -401,53 +481,43 @@ export default function CoverInspectorControls( {
) }
- { ! hasSelectedStyleState && (
-
- !! minHeight }
- label={ __( 'Minimum height' ) }
- onDeselect={ () =>
- setAttributes( {
- minHeight: undefined,
- minHeightUnit: undefined,
- } )
+
+ !! activeMinHeight }
+ label={ __( 'Minimum height' ) }
+ onDeselect={ () =>
+ setAttributes( getResetMinHeightAttributes() )
+ }
+ resetAllFilter={ getResetMinHeightAttributes }
+ isShownByDefault
+ panelId={ clientId }
+ >
+
+ setMinHeightAttributes( newMinHeight )
}
- resetAllFilter={ () => ( {
- minHeight: undefined,
- minHeightUnit: undefined,
- } ) }
- isShownByDefault
- panelId={ clientId }
- >
-
- setAttributes( {
- minHeight: newMinHeight,
- style: cleanEmptyObject( {
- ...attributes?.style,
- dimensions: {
- ...attributes?.style?.dimensions,
- aspectRatio: undefined, // Reset aspect ratio when minHeight is set.
- },
- } ),
- } )
- }
- onUnitChange={ ( nextUnit ) =>
- setAttributes( {
- minHeightUnit: nextUnit,
- } )
+ onUnitChange={ ( nextUnit ) => {
+ if ( hasSelectedStyleState ) {
+ if ( activeMinHeight !== undefined ) {
+ setMinHeightAttributes(
+ activeMinHeight,
+ nextUnit
+ );
+ }
+ return;
}
- />
-
-
- ) }
+
+ setAttributes( {
+ minHeightUnit: nextUnit,
+ } );
+ } }
+ />
+
+
{
+ if ( ! isSingleSelected ) {
+ return undefined;
+ }
+ const { getSelectedBlockStyleState } = unlock(
+ select( blockEditorStore )
+ );
+ return getSelectedBlockStyleState( clientId );
+ },
+ [ clientId, isSingleSelected ]
+ );
+ const hasSelectedStyleState =
+ ! isDefaultBlockStyleState( selectedStyleState );
+ const selectedStyleStateKey = getStyleStateKey( selectedStyleState );
+ const activeWidth = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'width',
+ } );
+ const activeHeight = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'height',
+ } );
+ const activeAspectRatio = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'aspectRatio',
+ } );
+ const activeScale = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'scale',
+ styleKey: 'objectFit',
+ } );
+ const setDimensionAttributes = ( nextDimensions ) => {
+ setAttributes(
+ getDimensionUpdateAttributes( {
+ style: attributes.style,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ nextDimensions,
+ dimensionKeyMap: { scale: 'objectFit' },
+ } )
+ );
+ };
+
const dimensionsControl =
showDimensionsControls &&
( SIZED_LAYOUTS.includes( parentLayoutType ) ? (
{
- setAttributes( {
+ setDimensionAttributes( {
aspectRatio: newAspectRatio,
scale: 'cover',
} );
@@ -690,26 +752,29 @@ export default function Image( {
/>
) : (
{
- // Rebuilding the object forces setting `undefined`
- // for values that are removed since setAttributes
- // doesn't do anything with keys that aren't set.
- setAttributes( {
+ setDimensionAttributes( {
// CSS includes `height: auto`, but we need
// `width: auto` to fix the aspect ratio when
// only height is set due to the width and
// height attributes set via the server.
width: ! newWidth && newHeight ? 'auto' : newWidth,
height: newHeight,
- scale: newScale,
aspectRatio: newAspectRatio,
+ scale: newScale,
} );
} }
defaultScale="cover"
@@ -742,14 +807,11 @@ export default function Image( {
lockTitleControls = false,
lockTitleControlsMessage,
hideCaptionControls = false,
- hasSelectedStyleState = false,
} = useSelect(
( select ) => {
if ( ! isSingleSelected ) {
return {};
}
- const { hasSelectedStyleState: hasSelectedBlockStyleState } =
- unlock( select( blockEditorStore ) );
const {
url: urlBinding,
alt: altBinding,
@@ -767,7 +829,6 @@ export default function Image( {
titleBinding?.source
);
return {
- hasSelectedStyleState: hasSelectedBlockStyleState( clientId ),
lockUrlControls:
!! urlBinding &&
! urlBindingSource?.canUserEditValue?.( {
@@ -812,7 +873,6 @@ export default function Image( {
},
[
arePatternOverridesEnabled,
- clientId,
context,
isSingleSelected,
metadata?.bindings,
@@ -1018,47 +1078,52 @@ export default function Image( {
) }
- { ! hasSelectedStyleState && (
- ( {
- ...attrs,
- aspectRatio: undefined,
- width: undefined,
- height: undefined,
- scale: undefined,
- focalPoint: undefined,
- } ) }
- >
- { dimensionsControl }
- { url && scale && (
- {
+ return getDimensionResetAttributes( {
+ attributes: attrs,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ keys: [ 'aspectRatio', 'height', 'objectFit', 'width' ],
+ defaultAttributes: {
+ aspectRatio: undefined,
+ width: undefined,
+ height: undefined,
+ scale: undefined,
+ focalPoint: undefined,
+ },
+ } );
+ } }
+ >
+ { dimensionsControl }
+ { ! hasSelectedStyleState && url && scale && (
+ !! focalPoint }
+ onDeselect={ () =>
+ setAttributes( {
+ focalPoint: undefined,
+ } )
+ }
+ panelId={ clientId }
+ >
+ !! focalPoint }
- onDeselect={ () =>
+ url={ url }
+ value={ focalPoint }
+ onDragStart={ imperativeFocalPointPreview }
+ onDrag={ imperativeFocalPointPreview }
+ onChange={ ( newFocalPoint ) =>
setAttributes( {
- focalPoint: undefined,
+ focalPoint: newFocalPoint,
} )
}
- panelId={ clientId }
- >
-
- setAttributes( {
- focalPoint: newFocalPoint,
- } )
- }
- />
-
- ) }
-
- ) }
+ />
+
+ ) }
+
{ !! imageSizeOptions.length && (
-
-
-
- >
-);
+/**
+ * Internal dependencies
+ */
+import {
+ getActiveDimensionValue,
+ getDimensionUpdateAttributes,
+ getStyleStateKey,
+} from '../utils/style-state';
+import { unlock } from '../lock-unlock';
+
+const { DimensionsTool } = unlock( blockEditorPrivateApis );
const DEFAULT_SCALE = 'cover';
+const DIMENSION_KEYS = [ 'aspectRatio', 'width', 'height', 'scale' ];
-const scaleHelp = {
- cover: __(
- 'Image is scaled and cropped to fill the entire space without being distorted.'
- ),
- contain: __(
- 'Image is scaled to fill the space without clipping nor distorting.'
- ),
- fill: __(
- 'Image will be stretched and distorted to completely fill the space.'
- ),
-};
+const scaleOptions = [
+ {
+ value: 'cover',
+ label: _x( 'Cover', 'Scale option for Image dimension control' ),
+ help: __(
+ 'Image is scaled and cropped to fill the entire space without being distorted.'
+ ),
+ },
+ {
+ value: 'contain',
+ label: _x( 'Contain', 'Scale option for Image dimension control' ),
+ help: __(
+ 'Image is scaled to fill the space without clipping nor distorting.'
+ ),
+ },
+ {
+ value: 'fill',
+ label: _x( 'Fill', 'Scale option for Image dimension control' ),
+ help: __(
+ 'Image will be stretched and distorted to completely fill the space.'
+ ),
+ },
+];
const DimensionControls = ( {
clientId,
- attributes: { aspectRatio, width, height, scale },
+ attributes,
setAttributes,
+ selectedStyleState,
+ hasSelectedStyleState = false,
} ) => {
- const [ availableUnits, defaultRatios, themeRatios, showDefaultRatios ] =
- useSettings(
- 'spacing.units',
- 'dimensions.aspectRatios.default',
- 'dimensions.aspectRatios.theme',
- 'dimensions.defaultAspectRatios'
- );
+ const { style } = attributes;
+ const selectedStyleStateKey = getStyleStateKey( selectedStyleState );
+ const activeAspectRatio = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'aspectRatio',
+ } );
+ const activeWidth = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'width',
+ } );
+ const activeHeight = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'height',
+ } );
+ const activeScale = getActiveDimensionValue( {
+ attributes,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ attributeKey: 'scale',
+ styleKey: 'objectFit',
+ } );
+
+ const [ availableUnits ] = useSettings( 'spacing.units' );
const units = useCustomUnits( {
availableUnits: availableUnits || [ 'px', '%', 'vw', 'em', 'rem' ],
} );
- const onDimensionChange = ( dimension, nextValue ) => {
- const parsedValue = parseFloat( nextValue );
- /**
- * If we have no value set and we change the unit,
- * we don't want to set the attribute, as it would
- * end up having the unit as value without any number.
- */
- if ( isNaN( parsedValue ) && nextValue ) {
- return;
- }
- setAttributes( {
- [ dimension ]: parsedValue < 0 ? '0' : nextValue,
- } );
- };
- const scaleLabel = _x( 'Scale', 'Image scaling options' );
-
- const showScaleControl =
- height || ( aspectRatio && aspectRatio !== 'auto' );
+ const setDimensionAttributes = ( nextDimensions ) => {
+ const nextImageDimensions = {
+ ...nextDimensions,
+ width:
+ ! nextDimensions.width && nextDimensions.height
+ ? 'auto'
+ : nextDimensions.width,
+ };
- 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 : [] ),
- ];
+ setAttributes(
+ getDimensionUpdateAttributes( {
+ style,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ nextDimensions: nextImageDimensions,
+ dimensionKeyMap: { scale: 'objectFit' },
+ dimensionKeys: DIMENSION_KEYS,
+ } )
+ );
+ };
return (
- <>
- !! aspectRatio }
- label={ __( 'Aspect ratio' ) }
- onDeselect={ () => setAttributes( { aspectRatio: undefined } ) }
- resetAllFilter={ () => ( {
- aspectRatio: undefined,
- } ) }
- isShownByDefault
- panelId={ clientId }
- >
-
- setAttributes( { aspectRatio: nextAspectRatio } )
- }
- />
-
- !! height }
- label={ __( 'Height' ) }
- onDeselect={ () => setAttributes( { height: undefined } ) }
- resetAllFilter={ () => ( {
- height: undefined,
- } ) }
- isShownByDefault
- panelId={ clientId }
- >
-
- onDimensionChange( 'height', nextHeight )
- }
- units={ units }
- />
-
- !! width }
- label={ __( 'Width' ) }
- onDeselect={ () => setAttributes( { width: undefined } ) }
- resetAllFilter={ () => ( {
- width: undefined,
- } ) }
- isShownByDefault
- panelId={ clientId }
- >
-
- onDimensionChange( 'width', nextWidth )
- }
- units={ units }
- />
-
- { showScaleControl && (
- !! scale && scale !== DEFAULT_SCALE }
- label={ scaleLabel }
- onDeselect={ () =>
- setAttributes( {
- scale: DEFAULT_SCALE,
- } )
- }
- resetAllFilter={ () => ( {
- scale: DEFAULT_SCALE,
- } ) }
- isShownByDefault
- panelId={ clientId }
- >
-
- setAttributes( {
- scale: value,
- } )
- }
- isBlock
- >
- { SCALE_OPTIONS }
-
-
- ) }
- >
+
);
};
diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js
index 71ffde9e591aa6..514a25c463d2ad 100644
--- a/packages/block-library/src/post-featured-image/edit.js
+++ b/packages/block-library/src/post-featured-image/edit.js
@@ -49,9 +49,15 @@ import OverlayControls from './overlay-controls';
import Overlay from './overlay';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
import { unlock } from '../lock-unlock';
+import { getDimensionResetAttributes } from '../utils/style-state';
const ALLOWED_MEDIA_TYPES = [ 'image' ];
-const { ResolutionTool } = unlock( blockEditorPrivateApis );
+const { isDefaultBlockStyleState, ResolutionTool } = unlock(
+ blockEditorPrivateApis
+);
+
+const hasDimensionValue = ( value ) =>
+ value !== undefined && value !== null && value !== '';
const DEFAULT_MEDIA_SIZE_SLUG = 'full';
function FeaturedImageResolutionTool( { image, value, onChange } ) {
@@ -137,12 +143,13 @@ export default function PostFeaturedImageEdit( {
return imageId;
}, [ storedFeaturedImage, useFirstImageFromPost, postContent ] );
- const { media, postType, postPermalink, hasSelectedStyleState } = useSelect(
+ const { media, postType, postPermalink, selectedStyleState } = useSelect(
( select ) => {
const { getEntityRecord, getPostType, getEditedEntityRecord } =
select( coreStore );
- const { hasSelectedStyleState: hasSelectedBlockStyleState } =
- unlock( select( blockEditorStore ) );
+ const { getSelectedBlockStyleState } = unlock(
+ select( blockEditorStore )
+ );
return {
media:
featuredImage &&
@@ -155,18 +162,19 @@ export default function PostFeaturedImageEdit( {
postTypeSlug,
postId
)?.link,
- hasSelectedStyleState: hasSelectedBlockStyleState( clientId ),
+ selectedStyleState: getSelectedBlockStyleState( clientId ),
};
},
[ clientId, featuredImage, postTypeSlug, postId ]
);
+ const hasSelectedStyleState =
+ ! isDefaultBlockStyleState( selectedStyleState );
const mediaUrl =
media?.media_details?.sizes?.[ sizeSlug ]?.source_url ||
media?.source_url;
const blockProps = useBlockProps( {
- style: { width, height, aspectRatio },
className: clsx( {
'is-transient': temporaryURL,
} ),
@@ -184,8 +192,13 @@ export default function PostFeaturedImageEdit( {
) }
withIllustration
style={ {
- height: !! aspectRatio && '100%',
- width: !! aspectRatio && '100%',
+ aspectRatio,
+ height: hasDimensionValue( height )
+ ? height
+ : hasDimensionValue( width ) && 'auto',
+ width: hasDimensionValue( width )
+ ? width
+ : !! aspectRatio && '100%',
...borderProps.style,
...shadowProps.style,
} }
@@ -240,16 +253,32 @@ export default function PostFeaturedImageEdit( {
clientId={ clientId }
/>
- { ! hasSelectedStyleState && (
-
-
-
- ) }
+ {
+ return getDimensionResetAttributes( {
+ attributes: attrs,
+ selectedState: selectedStyleState,
+ hasSelectedStyleState,
+ keys: [ 'aspectRatio', 'height', 'objectFit', 'width' ],
+ defaultAttributes: {
+ aspectRatio: undefined,
+ height: undefined,
+ scale: undefined,
+ width: undefined,
+ },
+ } );
+ } }
+ >
+
+
{ ( featuredImage || isDescendentOfQueryLoop || ! postId ) && (
$attributes['style']['shadow'] ) );
@@ -90,6 +100,13 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
foreach ( $processor->get_attribute_names_with_prefix( '' ) as $name ) {
$tag_html->set_attribute( $name, $processor->get_attribute( $name ) );
}
+ if ( ! empty( $attr['style'] ) ) {
+ $existing_style = $tag_html->get_attribute( 'style' );
+ $style = is_string( $existing_style ) && '' !== $existing_style
+ ? rtrim( $existing_style, ';' ) . ';' . $attr['style']
+ : $attr['style'];
+ $tag_html->set_attribute( 'style', $style );
+ }
$featured_image = $tag_html->get_updated_html();
}
}
@@ -101,13 +118,11 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
if ( $is_link ) {
$link_target = $attributes['linkTarget'];
$rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : '';
- $height = ! empty( $attributes['height'] ) ? 'style="' . esc_attr( safecss_filter_attr( 'height:' . $attributes['height'] ) ) . '"' : '';
$featured_image = sprintf(
- '%5$s%6$s',
+ '%4$s%5$s',
esc_url( get_the_permalink( $post_ID ) ),
esc_attr( $link_target ),
$rel,
- $height,
$featured_image,
$overlay_markup
);
@@ -115,20 +130,7 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
$featured_image = $featured_image . $overlay_markup;
}
- $aspect_ratio = ! empty( $attributes['aspectRatio'] )
- ? esc_attr( safecss_filter_attr( 'aspect-ratio:' . $attributes['aspectRatio'] ) ) . ';'
- : '';
- $width = ! empty( $attributes['width'] )
- ? esc_attr( safecss_filter_attr( 'width:' . $attributes['width'] ) ) . ';'
- : '';
- $height = ! empty( $attributes['height'] )
- ? esc_attr( safecss_filter_attr( 'height:' . $attributes['height'] ) ) . ';'
- : '';
- if ( ! $height && ! $width && ! $aspect_ratio ) {
- $wrapper_attributes = get_block_wrapper_attributes();
- } else {
- $wrapper_attributes = get_block_wrapper_attributes( array( 'style' => $aspect_ratio . $width . $height ) );
- }
+ $wrapper_attributes = get_block_wrapper_attributes();
return "{$featured_image}";
}
diff --git a/packages/block-library/src/utils/style-state.js b/packages/block-library/src/utils/style-state.js
new file mode 100644
index 00000000000000..56584b26144726
--- /dev/null
+++ b/packages/block-library/src/utils/style-state.js
@@ -0,0 +1,151 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../lock-unlock';
+
+const { cleanEmptyObject, getStyleForState, setStyleForState } = unlock(
+ blockEditorPrivateApis
+);
+
+function getStateStyle( style, selectedState ) {
+ return getStyleForState( style, selectedState ) || {};
+}
+
+function getMappedDimensions( dimensions, dimensionKeyMap = {} ) {
+ return Object.fromEntries(
+ Object.entries( dimensions ).map( ( [ key, value ] ) => [
+ dimensionKeyMap[ key ] || key,
+ value,
+ ] )
+ );
+}
+
+function getControlledDimensions( dimensions, dimensionKeys ) {
+ if ( ! dimensionKeys ) {
+ return dimensions;
+ }
+
+ return Object.fromEntries(
+ dimensionKeys.map( ( key ) => [ key, dimensions[ key ] ] )
+ );
+}
+
+export function getStyleStateKey( selectedState ) {
+ return [
+ selectedState?.viewport || 'default',
+ selectedState?.pseudo || 'default',
+ ].join( ':' );
+}
+
+export function getStateDimensions( style, selectedState ) {
+ return getStateStyle( style, selectedState )?.dimensions || {};
+}
+
+export function getActiveDimensionValue( options = {} ) {
+ const {
+ attributes = {},
+ style = attributes?.style,
+ selectedState,
+ hasSelectedStyleState,
+ attributeKey,
+ styleKey = attributeKey,
+ rootValue,
+ } = options;
+
+ if ( hasSelectedStyleState ) {
+ return getStateDimensions( style, selectedState )?.[ styleKey ];
+ }
+
+ if ( Object.hasOwn( options, 'rootValue' ) ) {
+ return rootValue;
+ }
+
+ return attributes?.[ attributeKey ];
+}
+
+export function setStateDimensions( style, selectedState, nextDimensions ) {
+ const stateStyle = getStateStyle( style, selectedState );
+
+ return setStyleForState(
+ style,
+ selectedState,
+ cleanEmptyObject( {
+ ...stateStyle,
+ dimensions: cleanEmptyObject( {
+ ...stateStyle?.dimensions,
+ ...nextDimensions,
+ } ),
+ } )
+ );
+}
+
+export function getDimensionUpdateAttributes( {
+ style,
+ selectedState,
+ hasSelectedStyleState,
+ nextDimensions,
+ dimensionKeyMap,
+ dimensionKeys,
+} ) {
+ const controlledDimensions = getControlledDimensions(
+ nextDimensions,
+ dimensionKeys
+ );
+
+ if ( ! hasSelectedStyleState ) {
+ return controlledDimensions;
+ }
+
+ return {
+ style: setStateDimensions(
+ style,
+ selectedState,
+ getMappedDimensions( controlledDimensions, dimensionKeyMap )
+ ),
+ };
+}
+
+export function resetDimensions( style, keys ) {
+ const dimensionsReset = Object.fromEntries(
+ keys.map( ( key ) => [ key, undefined ] )
+ );
+
+ return cleanEmptyObject( {
+ ...style,
+ dimensions: cleanEmptyObject( {
+ ...style?.dimensions,
+ ...dimensionsReset,
+ } ),
+ } );
+}
+
+export function resetStateDimensions( style, selectedState, keys ) {
+ return setStyleForState(
+ style,
+ selectedState,
+ resetDimensions( getStateStyle( style, selectedState ), keys )
+ );
+}
+
+export function getDimensionResetAttributes( {
+ attributes = {},
+ style = attributes?.style,
+ selectedState,
+ hasSelectedStyleState,
+ keys,
+ defaultAttributes = {},
+} ) {
+ return {
+ ...( hasSelectedStyleState
+ ? {}
+ : { ...attributes, ...defaultAttributes } ),
+ style: hasSelectedStyleState
+ ? resetStateDimensions( style, selectedState, keys )
+ : resetDimensions( style, keys ),
+ };
+}
diff --git a/packages/block-library/src/utils/test/style-state.js b/packages/block-library/src/utils/test/style-state.js
new file mode 100644
index 00000000000000..f9625e85cced13
--- /dev/null
+++ b/packages/block-library/src/utils/test/style-state.js
@@ -0,0 +1,307 @@
+/**
+ * Internal dependencies
+ */
+import {
+ getActiveDimensionValue,
+ getDimensionResetAttributes,
+ getDimensionUpdateAttributes,
+ getStyleStateKey,
+ resetDimensions,
+ resetStateDimensions,
+ setStateDimensions,
+} from '../style-state';
+
+describe( 'style state dimension utilities', () => {
+ it( 'resets root dimensions without changing viewport dimensions', () => {
+ const style = {
+ dimensions: {
+ aspectRatio: '1',
+ minHeight: '40px',
+ },
+ mobile: {
+ dimensions: {
+ aspectRatio: '2',
+ },
+ },
+ };
+
+ expect( resetDimensions( style, [ 'aspectRatio' ] ) ).toEqual( {
+ dimensions: {
+ minHeight: '40px',
+ },
+ mobile: {
+ dimensions: {
+ aspectRatio: '2',
+ },
+ },
+ } );
+ } );
+
+ it( 'resets dimensions only for the selected viewport state', () => {
+ const style = {
+ dimensions: {
+ aspectRatio: '1',
+ },
+ mobile: {
+ dimensions: {
+ aspectRatio: '2',
+ width: '200px',
+ },
+ },
+ tablet: {
+ dimensions: {
+ aspectRatio: '3',
+ },
+ },
+ };
+
+ expect(
+ resetStateDimensions(
+ style,
+ { viewport: 'mobile', pseudo: 'default' },
+ [ 'aspectRatio' ]
+ )
+ ).toEqual( {
+ dimensions: {
+ aspectRatio: '1',
+ },
+ mobile: {
+ dimensions: {
+ width: '200px',
+ },
+ },
+ tablet: {
+ dimensions: {
+ aspectRatio: '3',
+ },
+ },
+ } );
+ } );
+
+ it( 'sets dimensions only for the selected viewport state', () => {
+ const style = {
+ mobile: {
+ dimensions: {
+ width: '200px',
+ },
+ },
+ tablet: {
+ dimensions: {
+ width: '300px',
+ },
+ },
+ };
+
+ expect(
+ setStateDimensions(
+ style,
+ { viewport: 'mobile', pseudo: 'default' },
+ { height: '100px' }
+ )
+ ).toEqual( {
+ mobile: {
+ dimensions: {
+ height: '100px',
+ width: '200px',
+ },
+ },
+ tablet: {
+ dimensions: {
+ width: '300px',
+ },
+ },
+ } );
+ } );
+
+ it( 'generates a stable key for selected style states', () => {
+ expect(
+ getStyleStateKey( { viewport: 'mobile', pseudo: ':hover' } )
+ ).toBe( 'mobile::hover' );
+ expect( getStyleStateKey( undefined ) ).toBe( 'default:default' );
+ } );
+
+ it( 'reads root attribute dimensions for the default state', () => {
+ expect(
+ getActiveDimensionValue( {
+ attributes: {
+ width: '200px',
+ },
+ attributeKey: 'width',
+ hasSelectedStyleState: false,
+ } )
+ ).toBe( '200px' );
+ } );
+
+ it( 'reads mapped dimensions for selected style states', () => {
+ expect(
+ getActiveDimensionValue( {
+ attributes: {
+ scale: 'cover',
+ style: {
+ mobile: {
+ dimensions: {
+ objectFit: 'contain',
+ },
+ },
+ },
+ },
+ selectedState: { viewport: 'mobile', pseudo: 'default' },
+ hasSelectedStyleState: true,
+ attributeKey: 'scale',
+ styleKey: 'objectFit',
+ } )
+ ).toBe( 'contain' );
+ } );
+
+ it( 'maps root dimension attributes to selected style state dimensions', () => {
+ expect(
+ getDimensionUpdateAttributes( {
+ style: {
+ mobile: {
+ dimensions: {
+ width: '200px',
+ },
+ },
+ },
+ selectedState: { viewport: 'mobile', pseudo: 'default' },
+ hasSelectedStyleState: true,
+ nextDimensions: {
+ scale: 'contain',
+ },
+ dimensionKeyMap: {
+ scale: 'objectFit',
+ },
+ } )
+ ).toEqual( {
+ style: {
+ mobile: {
+ dimensions: {
+ objectFit: 'contain',
+ width: '200px',
+ },
+ },
+ },
+ } );
+ } );
+
+ it( 'clears omitted controlled root dimension attributes', () => {
+ expect(
+ getDimensionUpdateAttributes( {
+ hasSelectedStyleState: false,
+ nextDimensions: {
+ aspectRatio: '16/9',
+ width: '200px',
+ scale: 'cover',
+ },
+ dimensionKeys: [ 'aspectRatio', 'width', 'height', 'scale' ],
+ } )
+ ).toEqual( {
+ aspectRatio: '16/9',
+ width: '200px',
+ height: undefined,
+ scale: 'cover',
+ } );
+ } );
+
+ it( 'clears omitted controlled selected style state dimensions', () => {
+ expect(
+ getDimensionUpdateAttributes( {
+ style: {
+ mobile: {
+ dimensions: {
+ height: '100px',
+ width: '200px',
+ },
+ },
+ },
+ selectedState: { viewport: 'mobile', pseudo: 'default' },
+ hasSelectedStyleState: true,
+ nextDimensions: {
+ aspectRatio: '16/9',
+ width: '200px',
+ scale: 'cover',
+ },
+ dimensionKeyMap: {
+ scale: 'objectFit',
+ },
+ dimensionKeys: [ 'aspectRatio', 'width', 'height', 'scale' ],
+ } )
+ ).toEqual( {
+ style: {
+ mobile: {
+ dimensions: {
+ aspectRatio: '16/9',
+ objectFit: 'cover',
+ width: '200px',
+ },
+ },
+ },
+ } );
+ } );
+
+ it( 'resets selected style state dimensions without root attributes', () => {
+ expect(
+ getDimensionResetAttributes( {
+ attributes: {
+ width: '200px',
+ style: {
+ dimensions: {
+ width: '300px',
+ },
+ mobile: {
+ dimensions: {
+ width: '400px',
+ },
+ },
+ },
+ },
+ selectedState: { viewport: 'mobile', pseudo: 'default' },
+ hasSelectedStyleState: true,
+ keys: [ 'width' ],
+ defaultAttributes: {
+ width: undefined,
+ },
+ } )
+ ).toEqual( {
+ style: {
+ dimensions: {
+ width: '300px',
+ },
+ },
+ } );
+ } );
+
+ it( 'resets default dimensions and root attributes', () => {
+ expect(
+ getDimensionResetAttributes( {
+ attributes: {
+ width: '200px',
+ style: {
+ dimensions: {
+ width: '300px',
+ },
+ mobile: {
+ dimensions: {
+ width: '400px',
+ },
+ },
+ },
+ },
+ hasSelectedStyleState: false,
+ keys: [ 'width' ],
+ defaultAttributes: {
+ width: undefined,
+ },
+ } )
+ ).toEqual( {
+ width: undefined,
+ style: {
+ mobile: {
+ dimensions: {
+ width: '400px',
+ },
+ },
+ },
+ } );
+ } );
+} );
diff --git a/packages/style-engine/src/class-wp-style-engine.php b/packages/style-engine/src/class-wp-style-engine.php
index 447bc24140848b..f46099cf5cfd84 100644
--- a/packages/style-engine/src/class-wp-style-engine.php
+++ b/packages/style-engine/src/class-wp-style-engine.php
@@ -237,6 +237,12 @@ final class WP_Style_Engine {
'dimension' => '--wp--preset--dimension--$slug',
),
),
+ 'objectFit' => array(
+ 'property_keys' => array(
+ 'default' => 'object-fit',
+ ),
+ 'path' => array( 'dimensions', 'objectFit' ),
+ ),
'width' => array(
'property_keys' => array(
'default' => 'width',
diff --git a/packages/style-engine/src/styles/dimensions/index.ts b/packages/style-engine/src/styles/dimensions/index.ts
index 3be3e318892c58..29be1e8e54580a 100644
--- a/packages/style-engine/src/styles/dimensions/index.ts
+++ b/packages/style-engine/src/styles/dimensions/index.ts
@@ -64,4 +64,16 @@ const width = {
},
};
-export default [ height, minHeight, minWidth, aspectRatio, width ];
+const objectFit = {
+ name: 'objectFit',
+ generate: ( style: Style, options: StyleOptions ) => {
+ return generateRule(
+ style,
+ options,
+ [ 'dimensions', 'objectFit' ],
+ 'objectFit'
+ );
+ },
+};
+
+export default [ height, minHeight, minWidth, aspectRatio, width, objectFit ];
diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js
index 5135b06d7c2330..4b15c81ed303e9 100644
--- a/packages/style-engine/src/test/index.js
+++ b/packages/style-engine/src/test/index.js
@@ -57,6 +57,7 @@ describe( 'generate', () => {
dimensions: {
minHeight: '50vh',
minWidth: '25vw',
+ objectFit: 'cover',
},
spacing: {
padding: { top: '10px', bottom: '5px' },
@@ -90,7 +91,7 @@ describe( 'generate', () => {
}
)
).toEqual(
- ".some-selector { color: #cccccc; background: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(33,32,33) 42%,rgb(65,88,208) 100%); background-color: #111111; min-height: 50vh; min-width: 25vw; outline-color: red; outline-style: dashed; outline-offset: 2px; outline-width: 4px; margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; font-family: 'Helvetica Neue',sans-serif; font-size: 2.2rem; font-style: italic; font-weight: 800; letter-spacing: 12px; line-height: 3.3; column-count: 2; text-decoration: line-through; text-transform: uppercase; }"
+ ".some-selector { color: #cccccc; background: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(33,32,33) 42%,rgb(65,88,208) 100%); background-color: #111111; min-height: 50vh; min-width: 25vw; object-fit: cover; outline-color: red; outline-style: dashed; outline-offset: 2px; outline-width: 4px; margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; font-family: 'Helvetica Neue',sans-serif; font-size: 2.2rem; font-style: italic; font-weight: 800; letter-spacing: 12px; line-height: 3.3; column-count: 2; text-decoration: line-through; text-transform: uppercase; }"
);
} );
diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts
index e434053ba0820b..45428d5d2d088a 100644
--- a/packages/style-engine/src/types.ts
+++ b/packages/style-engine/src/types.ts
@@ -52,6 +52,7 @@ export interface Style {
height?: CSSProperties[ 'height' ];
minHeight?: CSSProperties[ 'minHeight' ];
minWidth?: CSSProperties[ 'minWidth' ];
+ objectFit?: CSSProperties[ 'objectFit' ];
width?: CSSProperties[ 'width' ];
};
spacing?: {
diff --git a/phpunit/block-supports/dimensions-test.php b/phpunit/block-supports/dimensions-test.php
index 9620f94bdf9384..8816747cfaf0f2 100644
--- a/phpunit/block-supports/dimensions-test.php
+++ b/phpunit/block-supports/dimensions-test.php
@@ -358,4 +358,41 @@ public function test_min_width_with_individual_skipped_serialization_block_suppo
$this->assertSame( $expected, $actual );
}
+
+ public function test_default_aspect_ratio_does_not_unset_height_styles() {
+ $this->test_block_name = 'test/default-aspect-ratio-does-not-unset-height-styles';
+ register_block_type(
+ $this->test_block_name,
+ array(
+ 'api_version' => 3,
+ 'attributes' => array(
+ 'style' => array(
+ 'type' => 'object',
+ ),
+ ),
+ 'supports' => array(
+ 'dimensions' => array(
+ 'aspectRatio' => true,
+ ),
+ ),
+ )
+ );
+
+ $actual = gutenberg_render_dimensions_support(
+ 'Hello
',
+ array(
+ 'blockName' => $this->test_block_name,
+ 'attrs' => array(
+ 'style' => array(
+ 'dimensions' => array(
+ 'aspectRatio' => 'auto',
+ ),
+ ),
+ ),
+ )
+ );
+
+ $this->assertStringNotContainsString( 'height:unset', $actual );
+ $this->assertStringNotContainsString( 'min-height:unset', $actual );
+ }
}
diff --git a/phpunit/block-supports/states-test.php b/phpunit/block-supports/states-test.php
index ece71c396669a6..fea2e6395d4576 100644
--- a/phpunit/block-supports/states-test.php
+++ b/phpunit/block-supports/states-test.php
@@ -124,6 +124,81 @@ public function test_preserves_authored_border_style_declarations() {
);
}
+ /**
+ * Tests that fallback dimension styles are added for aspect ratio.
+ *
+ * @covers ::gutenberg_get_state_style_with_fallback_dimension_styles
+ */
+ public function test_adds_fallback_dimension_styles_for_aspect_ratio() {
+ $actual = gutenberg_get_state_style_with_fallback_dimension_styles(
+ array(
+ 'dimensions' => array(
+ 'aspectRatio' => '16/9',
+ ),
+ )
+ );
+
+ $this->assertSame(
+ array(
+ 'dimensions' => array(
+ 'aspectRatio' => '16/9',
+ 'minHeight' => 'unset',
+ 'height' => 'unset',
+ ),
+ ),
+ $actual
+ );
+ }
+
+ /**
+ * Tests that fallback dimension styles are not added for the default aspect ratio.
+ *
+ * @covers ::gutenberg_get_state_style_with_fallback_dimension_styles
+ */
+ public function test_does_not_add_fallback_dimension_styles_for_default_aspect_ratio() {
+ $actual = gutenberg_get_state_style_with_fallback_dimension_styles(
+ array(
+ 'dimensions' => array(
+ 'aspectRatio' => 'auto',
+ ),
+ )
+ );
+
+ $this->assertSame(
+ array(
+ 'dimensions' => array(
+ 'aspectRatio' => 'auto',
+ ),
+ ),
+ $actual
+ );
+ }
+
+ /**
+ * Tests that fallback aspectRatio styles are added for height.
+ *
+ * @covers ::gutenberg_get_state_style_with_fallback_dimension_styles
+ */
+ public function test_adds_fallback_aspect_ratio_style_for_height() {
+ $actual = gutenberg_get_state_style_with_fallback_dimension_styles(
+ array(
+ 'dimensions' => array(
+ 'height' => '20rem',
+ ),
+ )
+ );
+
+ $this->assertSame(
+ array(
+ 'dimensions' => array(
+ 'height' => '20rem',
+ 'aspectRatio' => 'unset',
+ ),
+ ),
+ $actual
+ );
+ }
+
/**
* Tests that modifier classes on the first compound selector are preserved
* when state selectors are scoped to the block wrapper.