Skip to content

Commit

Permalink
Add image block aspect ratio control (WordPress#51545)
Browse files Browse the repository at this point in the history
* Simplify ImageSizeControl by using Auto as a placeholder

* Rename imageWidth and imageHeight props to naturalWidth and naturalHeight

* Convert NumberControl onChange values to Numbers

* Simplify LatestPostsEdit to use updated ImageSizeControl

* Add JSDoc types for debugging

* Remove unnecessary noop

* Fix possible undefined values in NumberControl onChange

* Fix onChangeImage param type which may be undefined

* Rename OnChange callback prop

* Inline JSDoc props instead of new object

* Simplify handing undefined and NaN in onChange

* Revert prop name change since this isn't a private API

* Add a privateApis export for experimental ImageSizeControl

* Use the privateApis version of ImageSizeControl

* Add deprecation notice to the original component

* Revert image-size-control and create image-dimensions-control instead

* Re-add deprecation notice to image-size-control

* Try making a whole new component

* Revert changes to image, latest-posts, and media-text blocks

* Organize and update the dimensions tool panel item

* Reword size help text

* Reorganize into reusable components

* Add stories for other individual tools

* Update stories path

* Remove SelectControl __next prop

* Pass through isShownByDefault to ResolutionTool

* Remove unused scss

* Deprecate experimental ImageSizeControl

* Simplify ScaleTool onChange

* Add better defaults for value and onChange

* Fix circular dependency

* Update comment about auto and custom aspect ratios

* Add JSDoc types for ScaleTool

* Add JSDoc types for WidthHeightTool

* Add default value and onChange for WidthHeightTool

* Remove unused import

* Add aspectRatio to image block attributes

* Add scale to image block attributes

* Update JSDoc comment

* Add dimensions tool to image block

* Rename naturalAspectRatio for clarity

* Fix aspect-ratio-tool lint

* Fix scale-tool lint

* Fix width-height-tool lint

* Fix dimensions-tool lint

* Fix resolution-tool lint

* Add @emption/styled to block-editor

* Fix image block lint

* Update components changelog

* Fix AspectRatioTool reference

* Support 'auto' in width-height-tool

* Make null/undefined values mean 'auto' instead of defaultValue in aspectRatioTool

* Add deprecation for image block

* Fix ResizableBox interactions

* Add comments for default values

* Fix ResizableBox with auto w/h

* Clear aspect-ratio on resize

* Add TODO comment for ResolutionTool defaultValue

* Move the scale hide/show into dimensions controls

* Add first test

* Fix scale being set after it was deleted

* WIP writing tests

* Update test

* UI tweaks

* Move alt text as ToolsPanelItem

* Tweak default scale option help text

* Only use contain and cover for image scale options

* Update test

* Test the remaining callback values

* Add comment about toStrictEqual

* Add test for setting custom aspect ratio and then resetting

* Move custom scaleOptions to the image block

* Remember last aspect ratio so it can be restored when with/height are unset then set

* Remove unused import

* Format code

* Remove image w/h reset when a new image is added

* Use UnitControl's default units instead of spacing.units

* Provide the complete set of object-fit options by default

* Update TODO that will be committed

* Clean up evalAspectRatio and add docs

* Someone can file a bug report if offsetWidth/offsetHeight causes issues

* I couldn't figure out why height depended on having a custom border, but things seem to work without that

* Update docs for image block

* Update comment about default value

* Fix redundant wording

* I think the img width and height attributes can be removed if they're specified in the style attribute

* Update package-lock.json with @emotion/styled dependency

* Update mock calls for test example

* Simplify test values

* Consolidate mock calls expect

* Require defaultScale and defaultAspectRatio for DimensionsTool

* Add DimensionsTool tests for all custom transitions

* Remove comment about matching aspect ratio options

* Remove redundant check in tests

* Add comments to defaultAspectRatio and defaultScale

* Organize tests by which field is being updated

* Fix type conversion

* Add state diagram for last two tests

* Refactor and fix some tests

* Fix and simplify WidthHeightTool onChange

* Remove default scale option in image block.json

* Simplify DimensionsTool onChange logic

* Update block deprecations with width and height

* Revert image block width and height attributes to numbers since we only support px units for now

* Revert "Update block deprecations with width and height"

This reverts commit 941a81149ed4bc344ac2c0e183624069e33d75ad.

* Prevent NaN width/height

* Fix DimensionTool width/height units

* Fix JSDoc Dimenstions width/height types

* No default needed for ResolutionTool

* Fix drag handle aspect ratio reset

* Simplify null checks

* Stop using pxWidth and pxHeight

* Remove e2e tests that reference the scale button that was removed

* Fix image scaling for small images

* Try fixing aspectRatio only images

* Update test to respect the new aspect ratio behavior

---------

Co-authored-by: Alex Lende <alex@lende.xyz>
Co-authored-by: Rich Tabor <hi@richtabor.com>
Co-authored-by: Jerry Jones <jones.jeremydavid@gmail.com>
  • Loading branch information
4 people authored and sethrubenstein committed Jul 13, 2023
1 parent 53305e2 commit 8d6a64f
Show file tree
Hide file tree
Showing 29 changed files with 1,708 additions and 238 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre
- **Name:** core/image
- **Category:** media
- **Supports:** anchor, behaviors (lightbox), color (~~background~~, ~~text~~), filter (duotone)
- **Attributes:** align, alt, caption, height, href, id, linkClass, linkDestination, linkTarget, rel, sizeSlug, title, url, width
- **Attributes:** align, alt, aspectRatio, caption, height, href, id, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width

## Latest Comments

Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
],
"dependencies": {
"@babel/runtime": "^7.16.0",
"@emotion/styled": "^11.6.0",
"@react-spring/web": "^9.4.5",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* WordPress dependencies
*/
import {
SelectControl,
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
import { __, _x } from '@wordpress/i18n';

/**
* @typedef {import('@wordpress/components/build-types/select-control/types').SelectControlProps} SelectControlProps
*/

/**
* @type {SelectControlProps[]}
*/
export const DEFAULT_ASPECT_RATIO_OPTIONS = [
{
label: _x( 'Original', 'Aspect ratio option for dimensions control' ),
value: 'auto',
},
{
label: _x(
'Square - 1:1',
'Aspect ratio option for dimensions control'
),
value: '1',
},
{
label: _x(
'Standard - 4:3',
'Aspect ratio option for dimensions control'
),
value: '4/3',
},
{
label: _x(
'Portrait - 3:4',
'Aspect ratio option for dimensions control'
),
value: '3/4',
},
{
label: _x(
'Classic - 3:2',
'Aspect ratio option for dimensions control'
),
value: '3/2',
},
{
label: _x(
'Classic Portrait - 2:3',
'Aspect ratio option for dimensions control'
),
value: '2/3',
},
{
label: _x(
'Wide - 16:9',
'Aspect ratio option for dimensions control'
),
value: '16/9',
},
{
label: _x(
'Tall - 9:16',
'Aspect ratio option for dimensions control'
),
value: '9/16',
},
{
label: _x( 'Custom', 'Aspect ratio option for dimensions control' ),
value: 'custom',
disabled: true,
hidden: true,
},
];

/**
* @callback AspectRatioToolPropsOnChange
* @param {string} [value] New aspect ratio value.
* @return {void} No return.
*/

/**
* @typedef {Object} AspectRatioToolProps
* @property {string} [panelId] ID of the panel this tool is associated with.
* @property {string} [value] Current aspect ratio value.
* @property {AspectRatioToolPropsOnChange} [onChange] Callback to update the aspect ratio value.
* @property {SelectControlProps[]} [options] Aspect ratio options.
* @property {string} [defaultValue] Default aspect ratio value.
* @property {boolean} [isShownByDefault] Whether the tool is shown by default.
*/

export default function AspectRatioTool( {
panelId,
value,
onChange = () => {},
options = DEFAULT_ASPECT_RATIO_OPTIONS,
defaultValue = DEFAULT_ASPECT_RATIO_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 ?? 'auto';

return (
<ToolsPanelItem
hasValue={ () => displayValue !== defaultValue }
label={ __( 'Aspect ratio' ) }
onDeselect={ () => onChange( undefined ) }
isShownByDefault={ isShownByDefault }
panelId={ panelId }
>
<SelectControl
label={ __( 'Aspect ratio' ) }
value={ displayValue }
options={ options }
onChange={ onChange }
size={ '__unstable-large' }
__nextHasNoMarginBottom
/>
</ToolsPanelItem>
);
}
212 changes: 212 additions & 0 deletions packages/block-editor/src/components/dimensions-tool/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import AspectRatioTool from './aspect-ratio-tool';
import ScaleTool from './scale-tool';
import WidthHeightTool from './width-height-tool';

/**
* @typedef {import('@wordpress/components/build-types/select-control/types').SelectControlProps} SelectControlProps
*/

/**
* @typedef {import('@wordpress/components/build-types/unit-control/types').WPUnitControlUnit} WPUnitControlUnit
*/

/**
* @typedef {Object} Dimensions
* @property {string} [width] CSS width property.
* @property {string} [height] CSS height property.
* @property {string} [scale] CSS object-fit property.
* @property {string} [aspectRatio] CSS aspect-ratio property.
*/

/**
* @callback DimensionsControlsOnChange
* @param {Dimensions} nextValue
* @return {void}
*/

/**
* @typedef {Object} DimensionsControlsProps
* @property {string} [panelId] ID of the panel that contains the controls.
* @property {Dimensions} [value] Current dimensions values.
* @property {DimensionsControlsOnChange} [onChange] Callback to update the dimensions values.
* @property {SelectControlProps[]} [aspectRatioOptions] Aspect ratio options.
* @property {SelectControlProps[]} [scaleOptions] Scale options.
* @property {WPUnitControlUnit[]} [unitsOptions] Units options.
*/

/**
* Component that renders controls to edit the dimensions of an image or container.
*
* @param {DimensionsControlsProps} props The component props.
*
* @return {WPElement} The dimensions controls.
*/
function DimensionsTool( {
panelId,
value = {},
onChange = () => {},
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.
unitsOptions, // Default options handled by UnitControl.
} ) {
// Coerce undefined and CSS default values to be null.
const width =
value.width === undefined || value.width === 'auto'
? null
: value.width;
const height =
value.height === undefined || value.height === 'auto'
? null
: value.height;
const aspectRatio =
value.aspectRatio === undefined || value.aspectRatio === 'auto'
? null
: value.aspectRatio;
const scale =
value.scale === undefined || value.scale === 'fill'
? null
: value.scale;

// Keep track of state internally, so when the value is cleared by means
// other than directly editing that field, it's easier to restore the
// previous value.
const [ lastScale, setLastScale ] = useState( scale );
const [ lastAspectRatio, setLastAspectRatio ] = useState( aspectRatio );

// '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 showScaleControl = aspectRatio || ( width && height );

return (
<>
<AspectRatioTool
panelId={ panelId }
options={ aspectRatioOptions }
defaultValue={ defaultAspectRatio }
value={ aspectRatioValue }
onChange={ ( nextAspectRatio ) => {
const nextValue = { ...value };

// 'auto' is CSS default, so it gets treated as null.
nextAspectRatio =
nextAspectRatio === 'auto' ? null : nextAspectRatio;

setLastAspectRatio( nextAspectRatio );

// Update aspectRatio.
if ( ! nextAspectRatio ) {
delete nextValue.aspectRatio;
} else {
nextValue.aspectRatio = nextAspectRatio;
}

// Auto-update scale.
if ( ! nextAspectRatio ) {
delete nextValue.scale;
} else if ( lastScale ) {
nextValue.scale = lastScale;
} else {
nextValue.scale = defaultScale;
setLastScale( defaultScale );
}

// Auto-update width and height.
if ( nextAspectRatio && width && height ) {
delete nextValue.height;
}

onChange( nextValue );
} }
/>
{ showScaleControl && (
<ScaleTool
panelId={ panelId }
options={ scaleOptions }
defaultValue={ defaultScale }
value={ lastScale }
onChange={ ( nextScale ) => {
const nextValue = { ...value };

// 'fill' is CSS default, so it gets treated as null.
nextScale = nextScale === 'fill' ? null : nextScale;

setLastScale( nextScale );

// Update scale.
if ( ! nextScale ) {
delete nextValue.scale;
} else {
nextValue.scale = nextScale;
}

onChange( nextValue );
} }
/>
) }
<WidthHeightTool
panelId={ panelId }
units={ unitsOptions }
value={ { width, height } }
onChange={ ( { width: nextWidth, height: nextHeight } ) => {
const nextValue = { ...value };

// 'auto' is CSS default, so it gets treated as null.
nextWidth = nextWidth === 'auto' ? null : nextWidth;
nextHeight = nextHeight === 'auto' ? null : nextHeight;

// Update width.
if ( ! nextWidth ) {
delete nextValue.width;
} else {
nextValue.width = nextWidth;
}

// Update height.
if ( ! nextHeight ) {
delete nextValue.height;
} else {
nextValue.height = nextHeight;
}

// Auto-update aspectRatio.
if ( nextWidth && nextHeight ) {
delete nextValue.aspectRatio;
} else if ( lastAspectRatio ) {
nextValue.aspectRatio = lastAspectRatio;
} else {
// No setting defaultAspectRatio here, because
// aspectRatio is optional in this scenario,
// unlike scale.
}

// Auto-update scale.
if ( ! lastAspectRatio && !! nextWidth !== !! nextHeight ) {
delete nextValue.scale;
} else if ( lastScale ) {
nextValue.scale = lastScale;
} else {
nextValue.scale = defaultScale;
setLastScale( defaultScale );
}

onChange( nextValue );
} }
/>
</>
);
}

export default DimensionsTool;
Loading

0 comments on commit 8d6a64f

Please sign in to comment.