From 2f28641831fa8488745571090e97317334ea3174 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 29 May 2026 15:34:25 +1000 Subject: [PATCH 1/8] Add support for aspect ratio and related controls in viewport states --- lib/block-supports/states.php | 28 ++- .../global-styles/dimensions-panel.js | 3 +- packages/block-editor/src/hooks/style.js | 38 +++- packages/block-editor/src/hooks/test/style.js | 30 +++ packages/block-editor/src/private-apis.js | 12 ++ .../src/cover/edit/inspector-controls.js | 172 ++++++++++++------ packages/block-library/src/image/block.json | 1 + packages/block-library/src/image/image.js | 163 +++++++++++------ .../src/post-featured-image/block.json | 6 + .../post-featured-image/dimension-controls.js | 153 +++++++++++++--- .../src/post-featured-image/edit.js | 59 ++++-- .../src/post-featured-image/index.php | 35 ++-- .../block-library/src/utils/style-state.js | 59 ++++++ .../src/class-wp-style-engine.php | 6 + .../src/styles/dimensions/index.ts | 14 +- packages/style-engine/src/test/index.js | 3 +- packages/style-engine/src/types.ts | 1 + phpunit/block-supports/states-test.php | 43 +++++ 18 files changed, 655 insertions(+), 171 deletions(-) create mode 100644 packages/block-library/src/utils/style-state.js diff --git a/lib/block-supports/states.php b/lib/block-supports/states.php index 27d641b2df0549..234c8a2bf90389 100644 --- a/lib/block-supports/states.php +++ b/lib/block-supports/states.php @@ -90,6 +90,30 @@ function gutenberg_get_state_declarations_with_fallback_border_styles( $declarat return $declarations; } +/** + * Adds fallback dimension declarations for aspect-ratio and height declarations. + * + * @param array $declarations CSS declarations generated by the style engine. + * @return array CSS declarations with fallback dimension styles applied where needed. + */ +function gutenberg_get_state_declarations_with_fallback_dimension_styles( $declarations ) { + if ( ! is_array( $declarations ) ) { + return $declarations; + } + + if ( isset( $declarations['aspect-ratio'] ) && '' !== $declarations['aspect-ratio'] ) { + $declarations['min-height'] = 'unset'; + $declarations['height'] = 'unset'; + } elseif ( + ( isset( $declarations['min-height'] ) && '' !== $declarations['min-height'] ) || + ( isset( $declarations['height'] ) && '' !== $declarations['height'] ) + ) { + $declarations['aspect-ratio'] = 'unset'; + } + + return $declarations; +} + /** * Adds a style fragment to a selector-keyed state style group. * @@ -431,8 +455,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 = gutenberg_get_state_declarations_with_fallback_dimension_styles( $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/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/style.js b/packages/block-editor/src/hooks/style.js index 8d1300065cacc0..4ffe72f764f84c 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -134,6 +134,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 ( 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 +177,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/style.js b/packages/block-editor/src/hooks/test/style.js index 4b09375f07e6ee..22f658718a63d6 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -208,6 +208,36 @@ 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( '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', () => { diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 4dbd42bd34c251..1a5bf34b2efdc1 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -16,6 +16,13 @@ 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, + hasPseudoBlockStyleState, + hasViewportBlockStyleState, + 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 +105,11 @@ lock( privateApis, { InspectorControlsLastItem, useHasBlockToolbar, cleanEmptyObject, + getStyleForState, + hasPseudoBlockStyleState, + hasViewportBlockStyleState, + 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..927cb7b5280fb5 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 { + getStateDimensions, + resetDimensions, + resetStateDimensions, + setStateDimensions, +} 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,36 @@ 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 stateDimensions = hasSelectedStyleState + ? getStateDimensions( attributes.style, selectedStyleState ) + : {}; + const stateMinHeight = stateDimensions.minHeight; + const [ stateMinHeightValue, stateMinHeightUnit ] = + parseQuantityAndUnitFromRawValue( stateMinHeight || '' ); + const activeMinHeight = hasSelectedStyleState + ? stateMinHeightValue + : minHeight; + const activeMinHeightUnit = hasSelectedStyleState + ? stateMinHeightUnit || minHeightUnit + : minHeightUnit; + const activeAspectRatio = hasSelectedStyleState + ? stateDimensions.aspectRatio + : attributes?.style?.dimensions?.aspectRatio; const image = useSelect( ( select ) => @@ -203,6 +228,56 @@ export default function CoverInspectorControls( { const showOverlayControls = colorGradientSettings.hasColorsOrGradients && ! hasSelectedStyleState; + const setMinHeightAttributes = ( nextMinHeight, nextUnit ) => { + if ( hasSelectedStyleState ) { + setAttributes( { + style: setStateDimensions( + attributes.style, + selectedStyleState, + { + 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 = () => { + if ( hasSelectedStyleState ) { + return { + style: resetStateDimensions( + attributes.style, + selectedStyleState, + [ 'minHeight' ] + ), + }; + } + + return { + minHeight: undefined, + minHeightUnit: undefined, + style: resetDimensions( attributes.style, [ 'minHeight' ] ), + }; + }; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); return ( @@ -401,53 +476,42 @@ 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 stateDimensions = hasSelectedStyleState + ? getStateDimensions( attributes.style, selectedStyleState ) + : {}; + const activeWidth = hasSelectedStyleState ? stateDimensions.width : width; + const activeHeight = hasSelectedStyleState + ? stateDimensions.height + : height; + const activeAspectRatio = hasSelectedStyleState + ? stateDimensions.aspectRatio + : aspectRatio; + const activeScale = hasSelectedStyleState + ? stateDimensions.objectFit + : scale; + const setDimensionAttributes = ( nextDimensions ) => { + if ( hasSelectedStyleState ) { + setAttributes( { + style: setStateDimensions( + attributes.style, + selectedStyleState, + nextDimensions + ), + } ); + return; + } + + setAttributes( nextDimensions ); + }; + const dimensionsControl = showDimensionsControls && ( SIZED_LAYOUTS.includes( parentLayoutType ) ? ( { - setAttributes( { + setDimensionAttributes( { aspectRatio: newAspectRatio, - scale: 'cover', + ...( hasSelectedStyleState + ? { objectFit: 'cover' } + : { scale: 'cover' } ), } ); } } defaultAspectRatio="auto" @@ -691,25 +743,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, + ...( hasSelectedStyleState + ? { objectFit: newScale } + : { scale: newScale } ), } ); } } defaultScale="cover" @@ -742,14 +798,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 +820,6 @@ export default function Image( { titleBinding?.source ); return { - hasSelectedStyleState: hasSelectedBlockStyleState( clientId ), lockUrlControls: !! urlBinding && ! urlBindingSource?.canUserEditValue?.( { @@ -812,7 +864,6 @@ export default function Image( { }, [ arePatternOverridesEnabled, - clientId, context, isSingleSelected, metadata?.bindings, @@ -1018,47 +1069,51 @@ export default function Image( { ) } - { ! hasSelectedStyleState && ( - ( { - ...attrs, - aspectRatio: undefined, - width: undefined, - height: undefined, - scale: undefined, - focalPoint: undefined, - } ) } - > - { dimensionsControl } - { url && scale && ( - ( { + ...attrs, + aspectRatio: undefined, + width: undefined, + height: undefined, + scale: undefined, + focalPoint: undefined, + style: resetDimensions( attrs.style, [ + 'aspectRatio', + 'height', + 'objectFit', + 'width', + ] ), + } ) } + > + { 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 && ( + value !== undefined && value !== null && value !== ''; + const scaleHelp = { cover: __( 'Image is scaled and cropped to fill the entire space without being distorted.' @@ -48,9 +61,27 @@ const scaleHelp = { const DimensionControls = ( { clientId, - attributes: { aspectRatio, width, height, scale }, + attributes, setAttributes, + selectedStyleState, + hasSelectedStyleState = false, } ) => { + const { aspectRatio, width, height, scale, style } = attributes; + const stateDimensions = hasSelectedStyleState + ? getStateDimensions( style, selectedStyleState ) + : {}; + const activeAspectRatio = hasSelectedStyleState + ? stateDimensions.aspectRatio + : aspectRatio; + const activeWidth = hasSelectedStyleState ? stateDimensions.width : width; + const activeHeight = hasSelectedStyleState + ? stateDimensions.height + : height; + const activeScale = hasSelectedStyleState + ? stateDimensions.objectFit + : scale; + const displayScale = activeScale || DEFAULT_SCALE; + const [ availableUnits, defaultRatios, themeRatios, showDefaultRatios ] = useSettings( 'spacing.units', @@ -62,6 +93,54 @@ const DimensionControls = ( { availableUnits: availableUnits || [ 'px', '%', 'vw', 'em', 'rem' ], } ); + const setDimensionAttributes = ( nextDimensions ) => { + const dimensions = { ...nextDimensions }; + const isSettingAspectRatio = + Object.hasOwn( dimensions, 'aspectRatio' ) && + hasDimensionValue( dimensions.aspectRatio ) && + dimensions.aspectRatio !== 'auto'; + const isSettingHeight = + Object.hasOwn( dimensions, 'height' ) && + hasDimensionValue( dimensions.height ); + + if ( isSettingAspectRatio ) { + dimensions.height = undefined; + } + if ( isSettingHeight ) { + dimensions.aspectRatio = undefined; + } + + if ( hasSelectedStyleState ) { + const nextStateDimensions = {}; + if ( Object.hasOwn( dimensions, 'aspectRatio' ) ) { + nextStateDimensions.aspectRatio = dimensions.aspectRatio; + } + if ( Object.hasOwn( dimensions, 'width' ) ) { + nextStateDimensions.width = dimensions.width; + } + if ( Object.hasOwn( dimensions, 'height' ) ) { + nextStateDimensions.height = dimensions.height; + } + if ( Object.hasOwn( dimensions, 'scale' ) ) { + nextStateDimensions.objectFit = dimensions.scale; + } + + setAttributes( { + style: setStateDimensions( style, selectedStyleState, { + ...nextStateDimensions, + } ), + } ); + return; + } + + setAttributes( dimensions ); + }; + const getResetDimensionAttributes = ( keys ) => ( { + style: hasSelectedStyleState + ? resetStateDimensions( style, selectedStyleState, keys ) + : resetDimensions( style, keys ), + } ); + const onDimensionChange = ( dimension, nextValue ) => { const parsedValue = parseFloat( nextValue ); /** @@ -72,14 +151,20 @@ const DimensionControls = ( { if ( isNaN( parsedValue ) && nextValue ) { return; } - setAttributes( { + const nextDimensions = { [ dimension ]: parsedValue < 0 ? '0' : nextValue, - } ); + }; + if ( dimension === 'height' ) { + nextDimensions.scale = nextValue + ? activeScale || DEFAULT_SCALE + : undefined; + } + setDimensionAttributes( nextDimensions ); }; const scaleLabel = _x( 'Scale', 'Image scaling options' ); const showScaleControl = - height || ( aspectRatio && aspectRatio !== 'auto' ); + activeHeight || ( activeAspectRatio && activeAspectRatio !== 'auto' ); const themeOptions = themeRatios?.map( ( { name, ratio } ) => ( { label: name, @@ -106,11 +191,14 @@ const DimensionControls = ( { return ( <> !! aspectRatio } + hasValue={ () => !! activeAspectRatio } label={ __( 'Aspect ratio' ) } - onDeselect={ () => setAttributes( { aspectRatio: undefined } ) } + onDeselect={ () => + setDimensionAttributes( { aspectRatio: undefined } ) + } resetAllFilter={ () => ( { aspectRatio: undefined, + ...getResetDimensionAttributes( [ 'aspectRatio' ] ), } ) } isShownByDefault panelId={ clientId } @@ -118,20 +206,37 @@ const DimensionControls = ( { - setAttributes( { aspectRatio: nextAspectRatio } ) - } + onChange={ ( nextAspectRatio ) => { + nextAspectRatio = + nextAspectRatio === 'auto' + ? undefined + : nextAspectRatio; + setDimensionAttributes( { + aspectRatio: nextAspectRatio, + scale: nextAspectRatio + ? activeScale || DEFAULT_SCALE + : undefined, + } ); + } } /> !! height } + hasValue={ () => !! activeHeight } label={ __( 'Height' ) } - onDeselect={ () => setAttributes( { height: undefined } ) } + onDeselect={ () => + setDimensionAttributes( { + height: undefined, + scale: activeAspectRatio + ? activeScale || DEFAULT_SCALE + : undefined, + } ) + } resetAllFilter={ () => ( { height: undefined, + ...getResetDimensionAttributes( [ 'height' ] ), } ) } isShownByDefault panelId={ clientId } @@ -140,7 +245,7 @@ const DimensionControls = ( { __next40pxDefaultSize label={ __( 'Height' ) } labelPosition="top" - value={ height || '' } + value={ activeHeight || '' } min={ 0 } onChange={ ( nextHeight ) => onDimensionChange( 'height', nextHeight ) @@ -150,11 +255,14 @@ const DimensionControls = ( { !! width } + hasValue={ () => !! activeWidth } label={ __( 'Width' ) } - onDeselect={ () => setAttributes( { width: undefined } ) } + onDeselect={ () => + setDimensionAttributes( { width: undefined } ) + } resetAllFilter={ () => ( { width: undefined, + ...getResetDimensionAttributes( [ 'width' ] ), } ) } isShownByDefault panelId={ clientId } @@ -163,7 +271,7 @@ const DimensionControls = ( { __next40pxDefaultSize label={ __( 'Width' ) } labelPosition="top" - value={ width || '' } + value={ activeWidth || '' } min={ 0 } onChange={ ( nextWidth ) => onDimensionChange( 'width', nextWidth ) @@ -173,15 +281,18 @@ const DimensionControls = ( { { showScaleControl && ( !! scale && scale !== DEFAULT_SCALE } + hasValue={ () => + !! activeScale && activeScale !== DEFAULT_SCALE + } label={ scaleLabel } onDeselect={ () => - setAttributes( { + setDimensionAttributes( { scale: DEFAULT_SCALE, } ) } resetAllFilter={ () => ( { scale: DEFAULT_SCALE, + ...getResetDimensionAttributes( [ 'objectFit' ] ), } ) } isShownByDefault panelId={ clientId } @@ -189,10 +300,10 @@ const DimensionControls = ( { - setAttributes( { + setDimensionAttributes( { scale: value, } ) } diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 71ffde9e591aa6..5e5d573a125cba 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -49,9 +49,12 @@ import OverlayControls from './overlay-controls'; import Overlay from './overlay'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; import { unlock } from '../lock-unlock'; +import { resetDimensions } from '../utils/style-state'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; -const { ResolutionTool } = unlock( blockEditorPrivateApis ); +const { isDefaultBlockStyleState, ResolutionTool } = unlock( + blockEditorPrivateApis +); const DEFAULT_MEDIA_SIZE_SLUG = 'full'; function FeaturedImageResolutionTool( { image, value, onChange } ) { @@ -137,12 +140,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 +159,20 @@ 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 }, + style: { width }, className: clsx( { 'is-transient': temporaryURL, } ), @@ -184,7 +190,8 @@ export default function PostFeaturedImageEdit( { ) } withIllustration style={ { - height: !! aspectRatio && '100%', + aspectRatio, + height: aspectRatio ? undefined : height, width: !! aspectRatio && '100%', ...borderProps.style, ...shadowProps.style, @@ -240,16 +247,31 @@ export default function PostFeaturedImageEdit( { clientId={ clientId } /> - { ! hasSelectedStyleState && ( - - - - ) } + ( { + ...attrs, + aspectRatio: undefined, + height: undefined, + scale: undefined, + width: undefined, + style: resetDimensions( attrs.style, [ + 'aspectRatio', + 'height', + 'objectFit', + 'width', + ] ), + } ) } + > + + { ( featuredImage || isDescendentOfQueryLoop || ! postId ) && ( $attributes['style']['shadow'] ) ); @@ -90,6 +92,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 +110,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,19 +122,13 @@ 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'] ) + $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 ) { + if ( ! $width ) { $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( array( 'style' => $width ) ); } 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..d9c999383aa154 --- /dev/null +++ b/packages/block-library/src/utils/style-state.js @@ -0,0 +1,59 @@ +/** + * 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 ) || {}; +} + +export function getStateDimensions( style, selectedState ) { + return getStateStyle( style, selectedState )?.dimensions || {}; +} + +export function setStateDimensions( style, selectedState, nextDimensions ) { + const stateStyle = getStateStyle( style, selectedState ); + + return setStyleForState( + style, + selectedState, + cleanEmptyObject( { + ...stateStyle, + dimensions: cleanEmptyObject( { + ...stateStyle?.dimensions, + ...nextDimensions, + } ), + } ) + ); +} + +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 ) + ); +} 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/states-test.php b/phpunit/block-supports/states-test.php index ece71c396669a6..b99b306dad2479 100644 --- a/phpunit/block-supports/states-test.php +++ b/phpunit/block-supports/states-test.php @@ -124,6 +124,49 @@ public function test_preserves_authored_border_style_declarations() { ); } + /** + * Tests that fallback dimension declarations are added for aspect ratio. + * + * @covers ::gutenberg_get_state_declarations_with_fallback_dimension_styles + */ + public function test_adds_fallback_dimension_declarations_for_aspect_ratio() { + $actual = gutenberg_get_state_declarations_with_fallback_dimension_styles( + array( + 'aspect-ratio' => '16/9', + ) + ); + + $this->assertSame( + array( + 'aspect-ratio' => '16/9', + 'min-height' => 'unset', + 'height' => 'unset', + ), + $actual + ); + } + + /** + * Tests that fallback aspect-ratio declarations are added for height. + * + * @covers ::gutenberg_get_state_declarations_with_fallback_dimension_styles + */ + public function test_adds_fallback_aspect_ratio_declaration_for_height() { + $actual = gutenberg_get_state_declarations_with_fallback_dimension_styles( + array( + 'height' => '20rem', + ) + ); + + $this->assertSame( + array( + 'height' => '20rem', + 'aspect-ratio' => 'unset', + ), + $actual + ); + } + /** * Tests that modifier classes on the first compound selector are preserved * when state selectors are scoped to the block wrapper. From 771289deade7fb4c702fe753d358402ef950a49c Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 29 May 2026 15:59:21 +1000 Subject: [PATCH 2/8] remove unused private apis --- packages/block-editor/src/private-apis.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 1a5bf34b2efdc1..7bccf12d758386 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -18,8 +18,6 @@ import { useHasBlockToolbar } from './components/block-toolbar/use-has-block-too import { cleanEmptyObject, usePrivateStyleOverride } from './hooks/utils'; import { getStyleForState, - hasPseudoBlockStyleState, - hasViewportBlockStyleState, isDefaultBlockStyleState, setStyleForState, } from './hooks/block-style-state'; @@ -106,8 +104,6 @@ lock( privateApis, { useHasBlockToolbar, cleanEmptyObject, getStyleForState, - hasPseudoBlockStyleState, - hasViewportBlockStyleState, isDefaultBlockStyleState, setStyleForState, usePrivateStyleOverride, From ab20103d11cd944d23cdf3324fbb22c55ce6a3b8 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 3 Jun 2026 15:04:18 +1000 Subject: [PATCH 3/8] fix wonky resets --- .../src/cover/edit/inspector-controls.js | 17 +-- packages/block-library/src/image/image.js | 35 ++++-- .../post-featured-image/dimension-controls.js | 50 +++++--- .../src/post-featured-image/edit.js | 34 ++++-- .../src/utils/test/style-state.js | 111 ++++++++++++++++++ 5 files changed, 201 insertions(+), 46 deletions(-) create mode 100644 packages/block-library/src/utils/test/style-state.js diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index 927cb7b5280fb5..f3900299b10baf 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -148,6 +148,10 @@ export default function CoverInspectorControls( { ); const hasSelectedStyleState = ! isDefaultBlockStyleState( selectedStyleState ); + const selectedStyleStateKey = [ + selectedStyleState?.viewport || 'default', + selectedStyleState?.pseudo || 'default', + ].join( ':' ); const stateDimensions = hasSelectedStyleState ? getStateDimensions( attributes.style, selectedStyleState ) : {}; @@ -260,21 +264,19 @@ export default function CoverInspectorControls( { } ); }; - const getResetMinHeightAttributes = () => { + const getResetMinHeightAttributes = ( attrs = attributes ) => { if ( hasSelectedStyleState ) { return { - style: resetStateDimensions( - attributes.style, - selectedStyleState, - [ 'minHeight' ] - ), + style: resetStateDimensions( attrs.style, selectedStyleState, [ + 'minHeight', + ] ), }; } return { minHeight: undefined, minHeightUnit: undefined, - style: resetDimensions( attributes.style, [ 'minHeight' ] ), + style: resetDimensions( attrs.style, [ 'minHeight' ] ), }; }; @@ -478,6 +480,7 @@ export default function CoverInspectorControls( { ) } !! activeMinHeight } label={ __( 'Minimum height' ) } diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index bed7bea0cc1c86..59a587f7c5b6b5 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -67,6 +67,7 @@ import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; import { getStateDimensions, resetDimensions, + resetStateDimensions, setStateDimensions, } from '../utils/style-state'; import { useOpenImageMediaEditorModal } from './use-open-image-media-editor-modal'; @@ -1071,20 +1072,34 @@ export default function Image( { ) } ( { - ...attrs, - aspectRatio: undefined, - width: undefined, - height: undefined, - scale: undefined, - focalPoint: undefined, - style: resetDimensions( attrs.style, [ + resetAllFilter={ ( attrs ) => { + const resetKeys = [ 'aspectRatio', 'height', 'objectFit', 'width', - ] ), - } ) } + ]; + + if ( hasSelectedStyleState ) { + return { + style: resetStateDimensions( + attrs.style, + selectedStyleState, + resetKeys + ), + }; + } + + return { + ...attrs, + aspectRatio: undefined, + width: undefined, + height: undefined, + scale: undefined, + focalPoint: undefined, + style: resetDimensions( attrs.style, resetKeys ), + }; + } } > { dimensionsControl } { ! hasSelectedStyleState && url && scale && ( 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 d940e8d0c1b17e..1cf8a0346a61d3 100644 --- a/packages/block-library/src/post-featured-image/dimension-controls.js +++ b/packages/block-library/src/post-featured-image/dimension-controls.js @@ -81,6 +81,10 @@ const DimensionControls = ( { ? stateDimensions.objectFit : scale; const displayScale = activeScale || DEFAULT_SCALE; + const selectedStyleStateKey = [ + selectedStyleState?.viewport || 'default', + selectedStyleState?.pseudo || 'default', + ].join( ':' ); const [ availableUnits, defaultRatios, themeRatios, showDefaultRatios ] = useSettings( @@ -135,11 +139,17 @@ const DimensionControls = ( { setAttributes( dimensions ); }; - const getResetDimensionAttributes = ( keys ) => ( { + const getResetDimensionAttributes = ( keys, nextStyle = style ) => ( { style: hasSelectedStyleState - ? resetStateDimensions( style, selectedStyleState, keys ) - : resetDimensions( style, keys ), + ? resetStateDimensions( nextStyle, selectedStyleState, keys ) + : resetDimensions( nextStyle, keys ), } ); + const getResetAllFilter = + ( defaultAttributes, keys ) => + ( attrs = {} ) => ( { + ...( hasSelectedStyleState ? {} : defaultAttributes ), + ...getResetDimensionAttributes( keys, attrs.style ), + } ); const onDimensionChange = ( dimension, nextValue ) => { const parsedValue = parseFloat( nextValue ); @@ -191,15 +201,16 @@ const DimensionControls = ( { return ( <> !! activeAspectRatio } label={ __( 'Aspect ratio' ) } onDeselect={ () => setDimensionAttributes( { aspectRatio: undefined } ) } - resetAllFilter={ () => ( { - aspectRatio: undefined, - ...getResetDimensionAttributes( [ 'aspectRatio' ] ), - } ) } + resetAllFilter={ getResetAllFilter( + { aspectRatio: undefined }, + [ 'aspectRatio' ] + ) } isShownByDefault panelId={ clientId } > @@ -223,6 +234,7 @@ const DimensionControls = ( { /> !! activeHeight } label={ __( 'Height' ) } @@ -234,10 +246,9 @@ const DimensionControls = ( { : undefined, } ) } - resetAllFilter={ () => ( { - height: undefined, - ...getResetDimensionAttributes( [ 'height' ] ), - } ) } + resetAllFilter={ getResetAllFilter( { height: undefined }, [ + 'height', + ] ) } isShownByDefault panelId={ clientId } > @@ -254,16 +265,16 @@ const DimensionControls = ( { /> !! activeWidth } label={ __( 'Width' ) } onDeselect={ () => setDimensionAttributes( { width: undefined } ) } - resetAllFilter={ () => ( { - width: undefined, - ...getResetDimensionAttributes( [ 'width' ] ), - } ) } + resetAllFilter={ getResetAllFilter( { width: undefined }, [ + 'width', + ] ) } isShownByDefault panelId={ clientId } > @@ -281,6 +292,7 @@ const DimensionControls = ( { { showScaleControl && ( !! activeScale && activeScale !== DEFAULT_SCALE } @@ -290,10 +302,10 @@ const DimensionControls = ( { scale: DEFAULT_SCALE, } ) } - resetAllFilter={ () => ( { - scale: DEFAULT_SCALE, - ...getResetDimensionAttributes( [ 'objectFit' ] ), - } ) } + resetAllFilter={ getResetAllFilter( + { scale: DEFAULT_SCALE }, + [ 'objectFit' ] + ) } isShownByDefault panelId={ clientId } > diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 5e5d573a125cba..7b36695f2408d8 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -49,7 +49,7 @@ import OverlayControls from './overlay-controls'; import Overlay from './overlay'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; import { unlock } from '../lock-unlock'; -import { resetDimensions } from '../utils/style-state'; +import { resetDimensions, resetStateDimensions } from '../utils/style-state'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; const { isDefaultBlockStyleState, ResolutionTool } = unlock( @@ -249,19 +249,33 @@ export default function PostFeaturedImageEdit( { ( { - ...attrs, - aspectRatio: undefined, - height: undefined, - scale: undefined, - width: undefined, - style: resetDimensions( attrs.style, [ + resetAllFilter={ ( attrs ) => { + const resetKeys = [ 'aspectRatio', 'height', 'objectFit', 'width', - ] ), - } ) } + ]; + + if ( hasSelectedStyleState ) { + return { + style: resetStateDimensions( + attrs.style, + selectedStyleState, + resetKeys + ), + }; + } + + return { + ...attrs, + aspectRatio: undefined, + height: undefined, + scale: undefined, + width: undefined, + style: resetDimensions( attrs.style, resetKeys ), + }; + } } > { + 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', + }, + }, + } ); + } ); +} ); From 26dc6c0d0b2db8be9bc426eb59920826604d2b40 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Thu, 4 Jun 2026 11:55:06 +1000 Subject: [PATCH 4/8] consolidate custom control logic and fix featured image bugs --- .../src/cover/edit/inspector-controls.js | 75 ++-- packages/block-library/src/image/image.js | 113 +++-- .../src/post-featured-image/block.json | 7 +- .../post-featured-image/dimension-controls.js | 387 ++++-------------- .../src/post-featured-image/edit.js | 57 ++- .../src/post-featured-image/index.php | 19 +- .../block-library/src/utils/style-state.js | 92 +++++ .../src/utils/test/style-state.js | 196 +++++++++ 8 files changed, 507 insertions(+), 439 deletions(-) diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index f3900299b10baf..786437984b0628 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -35,10 +35,10 @@ import { COVER_MIN_HEIGHT, mediaPosition } from '../shared'; import { unlock } from '../../lock-unlock'; import { useToolsPanelDropdownMenuProps } from '../../utils/hooks'; import { - getStateDimensions, - resetDimensions, - resetStateDimensions, - setStateDimensions, + getActiveDimensionValue, + getDimensionResetAttributes, + getDimensionUpdateAttributes, + getStyleStateKey, } from '../../utils/style-state'; import { DEFAULT_MEDIA_SIZE_SLUG } from '../constants'; import PosterImage from '../../utils/poster-image'; @@ -148,14 +148,15 @@ export default function CoverInspectorControls( { ); const hasSelectedStyleState = ! isDefaultBlockStyleState( selectedStyleState ); - const selectedStyleStateKey = [ - selectedStyleState?.viewport || 'default', - selectedStyleState?.pseudo || 'default', - ].join( ':' ); - const stateDimensions = hasSelectedStyleState - ? getStateDimensions( attributes.style, selectedStyleState ) - : {}; - const stateMinHeight = stateDimensions.minHeight; + 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 @@ -164,9 +165,13 @@ export default function CoverInspectorControls( { const activeMinHeightUnit = hasSelectedStyleState ? stateMinHeightUnit || minHeightUnit : minHeightUnit; - const activeAspectRatio = hasSelectedStyleState - ? stateDimensions.aspectRatio - : attributes?.style?.dimensions?.aspectRatio; + const activeAspectRatio = getActiveDimensionValue( { + attributes, + selectedState: selectedStyleState, + hasSelectedStyleState, + attributeKey: 'aspectRatio', + rootValue: attributes?.style?.dimensions?.aspectRatio, + } ); const image = useSelect( ( select ) => @@ -234,11 +239,12 @@ export default function CoverInspectorControls( { const setMinHeightAttributes = ( nextMinHeight, nextUnit ) => { if ( hasSelectedStyleState ) { - setAttributes( { - style: setStateDimensions( - attributes.style, - selectedStyleState, - { + setAttributes( + getDimensionUpdateAttributes( { + style: attributes.style, + selectedState: selectedStyleState, + hasSelectedStyleState, + nextDimensions: { minHeight: nextMinHeight === undefined ? undefined @@ -246,9 +252,9 @@ export default function CoverInspectorControls( { nextUnit || activeMinHeightUnit || 'px' }`, aspectRatio: undefined, - } - ), - } ); + }, + } ) + ); return; } @@ -265,19 +271,16 @@ export default function CoverInspectorControls( { }; const getResetMinHeightAttributes = ( attrs = attributes ) => { - if ( hasSelectedStyleState ) { - return { - style: resetStateDimensions( attrs.style, selectedStyleState, [ - 'minHeight', - ] ), - }; - } - - return { - minHeight: undefined, - minHeightUnit: undefined, - style: resetDimensions( attrs.style, [ 'minHeight' ] ), - }; + return getDimensionResetAttributes( { + style: attrs.style, + selectedState: selectedStyleState, + hasSelectedStyleState, + keys: [ 'minHeight' ], + defaultAttributes: { + minHeight: undefined, + minHeightUnit: undefined, + }, + } ); }; const dropdownMenuProps = useToolsPanelDropdownMenuProps(); diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 59a587f7c5b6b5..113a52e82cf401 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -65,10 +65,9 @@ import { Caption } from '../utils/caption'; import { MediaControl } from '../utils/media-control'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; import { - getStateDimensions, - resetDimensions, - resetStateDimensions, - setStateDimensions, + getActiveDimensionValue, + getDimensionResetAttributes, + getDimensionUpdateAttributes, } from '../utils/style-state'; import { useOpenImageMediaEditorModal } from './use-open-image-media-editor-modal'; import { @@ -696,32 +695,41 @@ export default function Image( { ); const hasSelectedStyleState = ! isDefaultBlockStyleState( selectedStyleState ); - const stateDimensions = hasSelectedStyleState - ? getStateDimensions( attributes.style, selectedStyleState ) - : {}; - const activeWidth = hasSelectedStyleState ? stateDimensions.width : width; - const activeHeight = hasSelectedStyleState - ? stateDimensions.height - : height; - const activeAspectRatio = hasSelectedStyleState - ? stateDimensions.aspectRatio - : aspectRatio; - const activeScale = hasSelectedStyleState - ? stateDimensions.objectFit - : scale; + 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 ) => { - if ( hasSelectedStyleState ) { - setAttributes( { - style: setStateDimensions( - attributes.style, - selectedStyleState, - nextDimensions - ), - } ); - return; - } - - setAttributes( nextDimensions ); + setAttributes( + getDimensionUpdateAttributes( { + style: attributes.style, + selectedState: selectedStyleState, + hasSelectedStyleState, + nextDimensions, + dimensionKeyMap: { scale: 'objectFit' }, + } ) + ); }; const dimensionsControl = @@ -733,9 +741,7 @@ export default function Image( { onChange={ ( { aspectRatio: newAspectRatio } ) => { setDimensionAttributes( { aspectRatio: newAspectRatio, - ...( hasSelectedStyleState - ? { objectFit: 'cover' } - : { scale: 'cover' } ), + scale: 'cover', } ); } } defaultAspectRatio="auto" @@ -764,9 +770,7 @@ export default function Image( { width: ! newWidth && newHeight ? 'auto' : newWidth, height: newHeight, aspectRatio: newAspectRatio, - ...( hasSelectedStyleState - ? { objectFit: newScale } - : { scale: newScale } ), + scale: newScale, } ); } } defaultScale="cover" @@ -1073,32 +1077,19 @@ export default function Image( { { - const resetKeys = [ - 'aspectRatio', - 'height', - 'objectFit', - 'width', - ]; - - if ( hasSelectedStyleState ) { - return { - style: resetStateDimensions( - attrs.style, - selectedStyleState, - resetKeys - ), - }; - } - - return { - ...attrs, - aspectRatio: undefined, - width: undefined, - height: undefined, - scale: undefined, - focalPoint: undefined, - style: resetDimensions( attrs.style, resetKeys ), - }; + return getDimensionResetAttributes( { + attributes: attrs, + selectedState: selectedStyleState, + hasSelectedStyleState, + keys: [ 'aspectRatio', 'height', 'objectFit', 'width' ], + defaultAttributes: { + aspectRatio: undefined, + width: undefined, + height: undefined, + scale: undefined, + focalPoint: undefined, + }, + } ); } } > { dimensionsControl } diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 9efafb9038c6c6..72332d2d1d1e71 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -98,12 +98,7 @@ } }, "selectors": { - "dimensions": { - "root": ".wp-block-post-featured-image", - "aspectRatio": ".wp-block-post-featured-image img", - "height": ".wp-block-post-featured-image img", - "objectFit": ".wp-block-post-featured-image img" - }, + "dimensions": ".wp-block-post-featured-image img", "border": ".wp-block-post-featured-image img, .wp-block-post-featured-image .block-editor-media-placeholder, .wp-block-post-featured-image .wp-block-post-featured-image__overlay", "shadow": ".wp-block-post-featured-image img, .wp-block-post-featured-image .components-placeholder", "filter": { 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 1cf8a0346a61d3..bcb0e8950cda32 100644 --- a/packages/block-library/src/post-featured-image/dimension-controls.js +++ b/packages/block-library/src/post-featured-image/dimension-controls.js @@ -2,62 +2,49 @@ * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; +import { __experimentalUseCustomUnits as useCustomUnits } from '@wordpress/components'; import { - SelectControl, - __experimentalUnitControl as UnitControl, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - __experimentalUseCustomUnits as useCustomUnits, - __experimentalToolsPanelItem as ToolsPanelItem, -} from '@wordpress/components'; -import { useSettings } from '@wordpress/block-editor'; + privateApis as blockEditorPrivateApis, + useSettings, +} from '@wordpress/block-editor'; /** * Internal dependencies */ import { - getStateDimensions, - resetDimensions, - resetStateDimensions, - setStateDimensions, + getActiveDimensionValue, + getDimensionUpdateAttributes, } from '../utils/style-state'; +import { unlock } from '../lock-unlock'; -const SCALE_OPTIONS = ( - <> - - - - -); +const { DimensionsTool } = unlock( blockEditorPrivateApis ); const DEFAULT_SCALE = 'cover'; - -const hasDimensionValue = ( value ) => - value !== undefined && value !== null && value !== ''; - -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 DIMENSION_KEYS = [ 'aspectRatio', 'width', 'height', 'scale' ]; + +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, @@ -66,266 +53,74 @@ const DimensionControls = ( { selectedStyleState, hasSelectedStyleState = false, } ) => { - const { aspectRatio, width, height, scale, style } = attributes; - const stateDimensions = hasSelectedStyleState - ? getStateDimensions( style, selectedStyleState ) - : {}; - const activeAspectRatio = hasSelectedStyleState - ? stateDimensions.aspectRatio - : aspectRatio; - const activeWidth = hasSelectedStyleState ? stateDimensions.width : width; - const activeHeight = hasSelectedStyleState - ? stateDimensions.height - : height; - const activeScale = hasSelectedStyleState - ? stateDimensions.objectFit - : scale; - const displayScale = activeScale || DEFAULT_SCALE; - const selectedStyleStateKey = [ - selectedStyleState?.viewport || 'default', - selectedStyleState?.pseudo || 'default', - ].join( ':' ); + const { style } = attributes; + 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, defaultRatios, themeRatios, showDefaultRatios ] = - useSettings( - 'spacing.units', - 'dimensions.aspectRatios.default', - 'dimensions.aspectRatios.theme', - 'dimensions.defaultAspectRatios' - ); + const [ availableUnits ] = useSettings( 'spacing.units' ); const units = useCustomUnits( { availableUnits: availableUnits || [ 'px', '%', 'vw', 'em', 'rem' ], } ); const setDimensionAttributes = ( nextDimensions ) => { - const dimensions = { ...nextDimensions }; - const isSettingAspectRatio = - Object.hasOwn( dimensions, 'aspectRatio' ) && - hasDimensionValue( dimensions.aspectRatio ) && - dimensions.aspectRatio !== 'auto'; - const isSettingHeight = - Object.hasOwn( dimensions, 'height' ) && - hasDimensionValue( dimensions.height ); - - if ( isSettingAspectRatio ) { - dimensions.height = undefined; - } - if ( isSettingHeight ) { - dimensions.aspectRatio = undefined; - } - - if ( hasSelectedStyleState ) { - const nextStateDimensions = {}; - if ( Object.hasOwn( dimensions, 'aspectRatio' ) ) { - nextStateDimensions.aspectRatio = dimensions.aspectRatio; - } - if ( Object.hasOwn( dimensions, 'width' ) ) { - nextStateDimensions.width = dimensions.width; - } - if ( Object.hasOwn( dimensions, 'height' ) ) { - nextStateDimensions.height = dimensions.height; - } - if ( Object.hasOwn( dimensions, 'scale' ) ) { - nextStateDimensions.objectFit = dimensions.scale; - } - - setAttributes( { - style: setStateDimensions( style, selectedStyleState, { - ...nextStateDimensions, - } ), - } ); - return; - } - - setAttributes( dimensions ); - }; - const getResetDimensionAttributes = ( keys, nextStyle = style ) => ( { - style: hasSelectedStyleState - ? resetStateDimensions( nextStyle, selectedStyleState, keys ) - : resetDimensions( nextStyle, keys ), - } ); - const getResetAllFilter = - ( defaultAttributes, keys ) => - ( attrs = {} ) => ( { - ...( hasSelectedStyleState ? {} : defaultAttributes ), - ...getResetDimensionAttributes( keys, attrs.style ), - } ); - - 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; - } - const nextDimensions = { - [ dimension ]: parsedValue < 0 ? '0' : nextValue, + const nextImageDimensions = { + ...nextDimensions, + width: + ! nextDimensions.width && nextDimensions.height + ? 'auto' + : nextDimensions.width, }; - if ( dimension === 'height' ) { - nextDimensions.scale = nextValue - ? activeScale || DEFAULT_SCALE - : undefined; - } - setDimensionAttributes( nextDimensions ); - }; - const scaleLabel = _x( 'Scale', 'Image scaling options' ); - const showScaleControl = - activeHeight || ( activeAspectRatio && activeAspectRatio !== '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 : [] ), - ]; + setAttributes( + getDimensionUpdateAttributes( { + style, + selectedState: selectedStyleState, + hasSelectedStyleState, + nextDimensions: nextImageDimensions, + dimensionKeyMap: { scale: 'objectFit' }, + dimensionKeys: DIMENSION_KEYS, + } ) + ); + }; return ( - <> - !! activeAspectRatio } - label={ __( 'Aspect ratio' ) } - onDeselect={ () => - setDimensionAttributes( { aspectRatio: undefined } ) - } - resetAllFilter={ getResetAllFilter( - { aspectRatio: undefined }, - [ 'aspectRatio' ] - ) } - isShownByDefault - panelId={ clientId } - > - { - nextAspectRatio = - nextAspectRatio === 'auto' - ? undefined - : nextAspectRatio; - setDimensionAttributes( { - aspectRatio: nextAspectRatio, - scale: nextAspectRatio - ? activeScale || DEFAULT_SCALE - : undefined, - } ); - } } - /> - - !! activeHeight } - label={ __( 'Height' ) } - onDeselect={ () => - setDimensionAttributes( { - height: undefined, - scale: activeAspectRatio - ? activeScale || DEFAULT_SCALE - : undefined, - } ) - } - resetAllFilter={ getResetAllFilter( { height: undefined }, [ - 'height', - ] ) } - isShownByDefault - panelId={ clientId } - > - - onDimensionChange( 'height', nextHeight ) - } - units={ units } - /> - - !! activeWidth } - label={ __( 'Width' ) } - onDeselect={ () => - setDimensionAttributes( { width: undefined } ) - } - resetAllFilter={ getResetAllFilter( { width: undefined }, [ - 'width', - ] ) } - isShownByDefault - panelId={ clientId } - > - - onDimensionChange( 'width', nextWidth ) - } - units={ units } - /> - - { showScaleControl && ( - - !! activeScale && activeScale !== DEFAULT_SCALE - } - label={ scaleLabel } - onDeselect={ () => - setDimensionAttributes( { - scale: DEFAULT_SCALE, - } ) - } - resetAllFilter={ getResetAllFilter( - { scale: DEFAULT_SCALE }, - [ 'objectFit' ] - ) } - isShownByDefault - panelId={ clientId } - > - - setDimensionAttributes( { - 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 7b36695f2408d8..514a25c463d2ad 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -49,12 +49,15 @@ import OverlayControls from './overlay-controls'; import Overlay from './overlay'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; import { unlock } from '../lock-unlock'; -import { resetDimensions, resetStateDimensions } from '../utils/style-state'; +import { getDimensionResetAttributes } from '../utils/style-state'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; const { isDefaultBlockStyleState, ResolutionTool } = unlock( blockEditorPrivateApis ); + +const hasDimensionValue = ( value ) => + value !== undefined && value !== null && value !== ''; const DEFAULT_MEDIA_SIZE_SLUG = 'full'; function FeaturedImageResolutionTool( { image, value, onChange } ) { @@ -172,7 +175,6 @@ export default function PostFeaturedImageEdit( { media?.source_url; const blockProps = useBlockProps( { - style: { width }, className: clsx( { 'is-transient': temporaryURL, } ), @@ -191,8 +193,12 @@ export default function PostFeaturedImageEdit( { withIllustration style={ { aspectRatio, - height: aspectRatio ? undefined : height, - width: !! aspectRatio && '100%', + height: hasDimensionValue( height ) + ? height + : hasDimensionValue( width ) && 'auto', + width: hasDimensionValue( width ) + ? width + : !! aspectRatio && '100%', ...borderProps.style, ...shadowProps.style, } } @@ -250,31 +256,18 @@ export default function PostFeaturedImageEdit( { { - const resetKeys = [ - 'aspectRatio', - 'height', - 'objectFit', - 'width', - ]; - - if ( hasSelectedStyleState ) { - return { - style: resetStateDimensions( - attrs.style, - selectedStyleState, - resetKeys - ), - }; - } - - return { - ...attrs, - aspectRatio: undefined, - height: undefined, - scale: undefined, - width: undefined, - style: resetDimensions( attrs.style, resetKeys ), - }; + return getDimensionResetAttributes( { + attributes: attrs, + selectedState: selectedStyleState, + hasSelectedStyleState, + keys: [ 'aspectRatio', 'height', 'objectFit', 'width' ], + defaultAttributes: { + aspectRatio: undefined, + height: undefined, + scale: undefined, + width: undefined, + }, + } ); } } > $width ) ); - } + $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 index d9c999383aa154..56584b26144726 100644 --- a/packages/block-library/src/utils/style-state.js +++ b/packages/block-library/src/utils/style-state.js @@ -16,10 +16,58 @@ 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 ); @@ -36,6 +84,32 @@ export function setStateDimensions( style, selectedState, 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 ] ) @@ -57,3 +131,21 @@ export function resetStateDimensions( style, selectedState, keys ) { 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 index cafc84b1421549..f9625e85cced13 100644 --- a/packages/block-library/src/utils/test/style-state.js +++ b/packages/block-library/src/utils/test/style-state.js @@ -2,6 +2,10 @@ * Internal dependencies */ import { + getActiveDimensionValue, + getDimensionResetAttributes, + getDimensionUpdateAttributes, + getStyleStateKey, resetDimensions, resetStateDimensions, setStateDimensions, @@ -108,4 +112,196 @@ describe( 'style state dimension utilities', () => { }, } ); } ); + + 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', + }, + }, + }, + } ); + } ); } ); From 16fa0115bd3909ac3c4c31761180e7226324e4dc Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Thu, 4 Jun 2026 14:47:30 +1000 Subject: [PATCH 5/8] changelog --- backport-changelog/7.1/12077.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backport-changelog/7.1/12077.md 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 From 5189cda019530214b3ce3936b760d87f72de8894 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 9 Jun 2026 15:07:40 +1000 Subject: [PATCH 6/8] Fix aspect ratio values --- .../src/components/dimensions-tool/index.js | 3 +- .../components/dimensions-tool/test/index.js | 75 ++++++++++++++++--- packages/block-library/src/image/README.md | 1 + packages/block-library/src/image/image.js | 4 + .../src/post-featured-image/README.md | 1 + .../post-featured-image/dimension-controls.js | 3 + 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/dimensions-tool/index.js b/packages/block-editor/src/components/dimensions-tool/index.js index 49d0fe03b4cb48..7976c2c769d7f9 100644 --- a/packages/block-editor/src/components/dimensions-tool/index.js +++ b/packages/block-editor/src/components/dimensions-tool/index.js @@ -83,11 +83,12 @@ function DimensionsTool( { // 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 ); 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..443ea2051e8c65 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(); diff --git a/packages/block-library/src/image/README.md b/packages/block-library/src/image/README.md index 33a13c4c04db8b..cc3a67ddbc87a7 100644 --- a/packages/block-library/src/image/README.md +++ b/packages/block-library/src/image/README.md @@ -79,6 +79,7 @@ _Defined via the [`styles`](https://developer.wordpress.org/block-editor/referen _Defined via the [`selectors`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-selectors/) property in block.json._ +- **dimensions**: `.wp-block-image img` - **border**: `.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder` - **shadow**: `.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder` - **filter**: diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 113a52e82cf401..1c8a89c468a081 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -68,6 +68,7 @@ import { getActiveDimensionValue, getDimensionResetAttributes, getDimensionUpdateAttributes, + getStyleStateKey, } from '../utils/style-state'; import { useOpenImageMediaEditorModal } from './use-open-image-media-editor-modal'; import { @@ -695,6 +696,7 @@ export default function Image( { ); const hasSelectedStyleState = ! isDefaultBlockStyleState( selectedStyleState ); + const selectedStyleStateKey = getStyleStateKey( selectedStyleState ); const activeWidth = getActiveDimensionValue( { attributes, selectedState: selectedStyleState, @@ -736,6 +738,7 @@ export default function Image( { showDimensionsControls && ( SIZED_LAYOUTS.includes( parentLayoutType ) ? ( { @@ -749,6 +752,7 @@ export default function Image( { /> ) : ( { const { style } = attributes; + const selectedStyleStateKey = getStyleStateKey( selectedStyleState ); const activeAspectRatio = getActiveDimensionValue( { attributes, selectedState: selectedStyleState, @@ -108,6 +110,7 @@ const DimensionControls = ( { return ( Date: Tue, 9 Jun 2026 16:02:14 +1000 Subject: [PATCH 7/8] Fix aspect ratio unsetting min-height in cover block --- lib/block-supports/dimensions.php | 20 +++++- lib/block-supports/states.php | 59 ++++++++++----- packages/block-editor/src/hooks/dimensions.js | 17 ++++- packages/block-editor/src/hooks/style.js | 3 +- .../block-editor/src/hooks/test/dimensions.js | 71 +++++++++++++++++++ packages/block-editor/src/hooks/test/style.js | 13 ++++ phpunit/block-supports/dimensions-test.php | 37 ++++++++++ phpunit/block-supports/states-test.php | 62 ++++++++++++---- 8 files changed, 244 insertions(+), 38 deletions(-) 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 234c8a2bf90389..27e5d3550c6c6f 100644 --- a/lib/block-supports/states.php +++ b/lib/block-supports/states.php @@ -91,27 +91,51 @@ function gutenberg_get_state_declarations_with_fallback_border_styles( $declarat } /** - * Adds fallback dimension declarations for aspect-ratio and height declarations. + * Adds fallback dimension styles for aspectRatio and height block-support values. * - * @param array $declarations CSS declarations generated by the style engine. - * @return array CSS declarations with fallback dimension styles applied where needed. + * @param array $state_style State style object. + * @return array State style object with fallback dimension styles applied where needed. */ -function gutenberg_get_state_declarations_with_fallback_dimension_styles( $declarations ) { - if ( ! is_array( $declarations ) ) { - return $declarations; +function gutenberg_get_state_style_with_fallback_dimension_styles( $state_style ) { + if ( ! is_array( $state_style ) ) { + return $state_style; } - if ( isset( $declarations['aspect-ratio'] ) && '' !== $declarations['aspect-ratio'] ) { - $declarations['min-height'] = 'unset'; - $declarations['height'] = 'unset'; - } elseif ( - ( isset( $declarations['min-height'] ) && '' !== $declarations['min-height'] ) || - ( isset( $declarations['height'] ) && '' !== $declarations['height'] ) - ) { - $declarations['aspect-ratio'] = 'unset'; + $dimensions = isset( $state_style['dimensions'] ) && is_array( $state_style['dimensions'] ) + ? $state_style['dimensions'] + : array(); + + if ( empty( $dimensions ) ) { + return $state_style; } - return $declarations; + 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; } /** @@ -240,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'] ) ) { @@ -455,7 +480,7 @@ function gutenberg_render_block_states_support( $block_content, $block ) { */ $style_rules = array(); foreach ( $css_rules as $rule ) { - $declarations = gutenberg_get_state_declarations_with_fallback_dimension_styles( $rule['declarations'] ); + $declarations = $rule['declarations']; foreach ( $declarations as $property => $value ) { $declarations[ $property ] = is_string( $value ) && str_contains( $value, '!important' ) ? $value 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 4ffe72f764f84c..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, @@ -147,7 +148,7 @@ function getStateFallbackDimensionStyles( stateStyles ) { return undefined; } - if ( dimensions.aspectRatio ) { + if ( isExplicitAspectRatio( dimensions.aspectRatio ) ) { return { dimensions: { minHeight: 'unset', 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 22f658718a63d6..db031f2bb63917 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -224,6 +224,19 @@ describe( 'getStateStylesCSS', () => { ); } ); + 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( 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 b99b306dad2479..fea2e6395d4576 100644 --- a/phpunit/block-supports/states-test.php +++ b/phpunit/block-supports/states-test.php @@ -125,43 +125,75 @@ public function test_preserves_authored_border_style_declarations() { } /** - * Tests that fallback dimension declarations are added for aspect ratio. + * Tests that fallback dimension styles are added for aspect ratio. * - * @covers ::gutenberg_get_state_declarations_with_fallback_dimension_styles + * @covers ::gutenberg_get_state_style_with_fallback_dimension_styles */ - public function test_adds_fallback_dimension_declarations_for_aspect_ratio() { - $actual = gutenberg_get_state_declarations_with_fallback_dimension_styles( + public function test_adds_fallback_dimension_styles_for_aspect_ratio() { + $actual = gutenberg_get_state_style_with_fallback_dimension_styles( array( - 'aspect-ratio' => '16/9', + '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( - 'aspect-ratio' => '16/9', - 'min-height' => 'unset', - 'height' => 'unset', + 'dimensions' => array( + 'aspectRatio' => 'auto', + ), ), $actual ); } /** - * Tests that fallback aspect-ratio declarations are added for height. + * Tests that fallback aspectRatio styles are added for height. * - * @covers ::gutenberg_get_state_declarations_with_fallback_dimension_styles + * @covers ::gutenberg_get_state_style_with_fallback_dimension_styles */ - public function test_adds_fallback_aspect_ratio_declaration_for_height() { - $actual = gutenberg_get_state_declarations_with_fallback_dimension_styles( + public function test_adds_fallback_aspect_ratio_style_for_height() { + $actual = gutenberg_get_state_style_with_fallback_dimension_styles( array( - 'height' => '20rem', + 'dimensions' => array( + 'height' => '20rem', + ), ) ); $this->assertSame( array( - 'height' => '20rem', - 'aspect-ratio' => 'unset', + 'dimensions' => array( + 'height' => '20rem', + 'aspectRatio' => 'unset', + ), ), $actual ); From 0f478243dd7a8fb17e9ea8f67867d3fa21763457 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 12 Jun 2026 10:37:15 +1000 Subject: [PATCH 8/8] fix scale control defaults --- .../src/components/dimensions-tool/index.js | 10 ++----- .../components/dimensions-tool/scale-tool.js | 3 +- .../components/dimensions-tool/test/index.js | 25 +++++++++++++++- packages/block-editor/src/hooks/test/style.js | 30 +++++++++++++++++++ 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/dimensions-tool/index.js b/packages/block-editor/src/components/dimensions-tool/index.js index 7976c2c769d7f9..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,10 +73,7 @@ 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 @@ -198,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 443ea2051e8c65..8b0b6e47d1465e 100644 --- a/packages/block-editor/src/components/dimensions-tool/test/index.js +++ b/packages/block-editor/src/components/dimensions-tool/test/index.js @@ -378,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/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index db031f2bb63917..d43b02ae242ce9 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -329,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', () => { @@ -368,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(