@wordpress/block-library 9.19.2 → 9.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/build/archives/edit.js +2 -2
  3. package/build/archives/edit.js.map +1 -1
  4. package/build/audio/edit.js +66 -33
  5. package/build/audio/edit.js.map +1 -1
  6. package/build/avatar/index.js +8 -3
  7. package/build/avatar/index.js.map +1 -1
  8. package/build/button/edit.js +43 -16
  9. package/build/button/edit.js.map +1 -1
  10. package/build/categories/edit.js +3 -3
  11. package/build/categories/edit.js.map +1 -1
  12. package/build/comment-template/hooks.js +6 -0
  13. package/build/comment-template/hooks.js.map +1 -1
  14. package/build/cover/index.js +8 -1
  15. package/build/cover/index.js.map +1 -1
  16. package/build/embed/edit.js +4 -1
  17. package/build/embed/edit.js.map +1 -1
  18. package/build/image/constants.js +2 -1
  19. package/build/image/constants.js.map +1 -1
  20. package/build/image/edit.js +3 -2
  21. package/build/image/edit.js.map +1 -1
  22. package/build/image/image.js +98 -80
  23. package/build/image/image.js.map +1 -1
  24. package/build/navigation/edit/index.js +8 -4
  25. package/build/navigation/edit/index.js.map +1 -1
  26. package/build/navigation-link/edit.js +27 -8
  27. package/build/navigation-link/edit.js.map +1 -1
  28. package/build/post-author/index.js +8 -1
  29. package/build/post-author/index.js.map +1 -1
  30. package/build/post-featured-image/edit.js +2 -1
  31. package/build/post-featured-image/edit.js.map +1 -1
  32. package/build/rss/edit.js +21 -1
  33. package/build/rss/edit.js.map +1 -1
  34. package/build/rss/index.js +7 -0
  35. package/build/rss/index.js.map +1 -1
  36. package/build/site-logo/index.js +8 -1
  37. package/build/site-logo/index.js.map +1 -1
  38. package/build/site-title/edit.js +1 -1
  39. package/build/site-title/edit.js.map +1 -1
  40. package/build/social-links/index.js +1 -0
  41. package/build/social-links/index.js.map +1 -1
  42. package/build/table-of-contents/edit.js +50 -8
  43. package/build/table-of-contents/edit.js.map +1 -1
  44. package/build/table-of-contents/hooks.js +13 -4
  45. package/build/table-of-contents/hooks.js.map +1 -1
  46. package/build/table-of-contents/index.js +3 -0
  47. package/build/table-of-contents/index.js.map +1 -1
  48. package/build-module/archives/edit.js +2 -2
  49. package/build-module/archives/edit.js.map +1 -1
  50. package/build-module/audio/edit.js +68 -35
  51. package/build-module/audio/edit.js.map +1 -1
  52. package/build-module/avatar/index.js +8 -3
  53. package/build-module/avatar/index.js.map +1 -1
  54. package/build-module/button/edit.js +44 -17
  55. package/build-module/button/edit.js.map +1 -1
  56. package/build-module/categories/edit.js +3 -3
  57. package/build-module/categories/edit.js.map +1 -1
  58. package/build-module/comment-template/hooks.js +6 -0
  59. package/build-module/comment-template/hooks.js.map +1 -1
  60. package/build-module/cover/index.js +8 -1
  61. package/build-module/cover/index.js.map +1 -1
  62. package/build-module/embed/edit.js +4 -1
  63. package/build-module/embed/edit.js.map +1 -1
  64. package/build-module/image/constants.js +1 -0
  65. package/build-module/image/constants.js.map +1 -1
  66. package/build-module/image/edit.js +3 -2
  67. package/build-module/image/edit.js.map +1 -1
  68. package/build-module/image/image.js +102 -84
  69. package/build-module/image/image.js.map +1 -1
  70. package/build-module/navigation/edit/index.js +8 -4
  71. package/build-module/navigation/edit/index.js.map +1 -1
  72. package/build-module/navigation-link/edit.js +28 -9
  73. package/build-module/navigation-link/edit.js.map +1 -1
  74. package/build-module/post-author/index.js +8 -1
  75. package/build-module/post-author/index.js.map +1 -1
  76. package/build-module/post-featured-image/edit.js +2 -1
  77. package/build-module/post-featured-image/edit.js.map +1 -1
  78. package/build-module/rss/edit.js +22 -2
  79. package/build-module/rss/edit.js.map +1 -1
  80. package/build-module/rss/index.js +7 -0
  81. package/build-module/rss/index.js.map +1 -1
  82. package/build-module/site-logo/index.js +8 -1
  83. package/build-module/site-logo/index.js.map +1 -1
  84. package/build-module/site-title/edit.js +1 -1
  85. package/build-module/site-title/edit.js.map +1 -1
  86. package/build-module/social-links/index.js +1 -0
  87. package/build-module/social-links/index.js.map +1 -1
  88. package/build-module/table-of-contents/edit.js +52 -10
  89. package/build-module/table-of-contents/edit.js.map +1 -1
  90. package/build-module/table-of-contents/hooks.js +13 -4
  91. package/build-module/table-of-contents/hooks.js.map +1 -1
  92. package/build-module/table-of-contents/index.js +3 -0
  93. package/build-module/table-of-contents/index.js.map +1 -1
  94. package/build-style/editor-rtl.css +0 -9
  95. package/build-style/editor.css +0 -9
  96. package/build-style/image/editor-rtl.css +0 -9
  97. package/build-style/image/editor.css +0 -9
  98. package/package.json +35 -35
  99. package/src/archives/edit.js +2 -2
  100. package/src/audio/edit.js +84 -33
  101. package/src/avatar/block.json +8 -3
  102. package/src/button/edit.js +69 -24
  103. package/src/categories/edit.js +3 -3
  104. package/src/comment-template/hooks.js +14 -6
  105. package/src/cover/block.json +8 -1
  106. package/src/embed/edit.js +7 -1
  107. package/src/image/constants.js +1 -0
  108. package/src/image/edit.js +3 -3
  109. package/src/image/editor.scss +0 -13
  110. package/src/image/image.js +124 -134
  111. package/src/navigation/edit/index.js +4 -0
  112. package/src/navigation-link/edit.js +45 -11
  113. package/src/post-author/block.json +8 -1
  114. package/src/post-featured-image/edit.js +2 -1
  115. package/src/rss/block.json +7 -0
  116. package/src/rss/edit.js +21 -0
  117. package/src/rss/index.php +27 -9
  118. package/src/site-logo/block.json +8 -1
  119. package/src/site-title/edit.js +1 -1
  120. package/src/site-title/index.php +1 -1
  121. package/src/social-links/block.json +1 -0
  122. package/src/table-of-contents/block.json +3 -0
  123. package/src/table-of-contents/edit.js +45 -4
  124. package/src/table-of-contents/hooks.js +12 -3
@@ -15,7 +15,13 @@ import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
15
15
  * WordPress dependencies
16
16
  */
17
17
  import { __, sprintf } from '@wordpress/i18n';
18
- import { useEffect, useState, useRef, useMemo } from '@wordpress/element';
18
+ import {
19
+ useEffect,
20
+ useState,
21
+ useRef,
22
+ useMemo,
23
+ createInterpolateElement,
24
+ } from '@wordpress/element';
19
25
  import {
20
26
  TextControl,
21
27
  ToolbarButton,
@@ -219,6 +225,63 @@ function ButtonEdit( props ) {
219
225
  const nofollow = !! rel?.includes( NOFOLLOW_REL );
220
226
  const isLinkTag = 'a' === TagName;
221
227
 
228
+ const {
229
+ createPageEntity,
230
+ userCanCreatePages,
231
+ lockUrlControls = false,
232
+ } = useSelect(
233
+ ( select ) => {
234
+ if ( ! isSelected ) {
235
+ return {};
236
+ }
237
+
238
+ const _settings = select( blockEditorStore ).getSettings();
239
+
240
+ const blockBindingsSource = getBlockBindingsSource(
241
+ metadata?.bindings?.url?.source
242
+ );
243
+
244
+ return {
245
+ createPageEntity: _settings.__experimentalCreatePageEntity,
246
+ userCanCreatePages: _settings.__experimentalUserCanCreatePages,
247
+ lockUrlControls:
248
+ !! metadata?.bindings?.url &&
249
+ ! blockBindingsSource?.canUserEditValue?.( {
250
+ select,
251
+ context,
252
+ args: metadata?.bindings?.url?.args,
253
+ } ),
254
+ };
255
+ },
256
+ [ context, isSelected, metadata?.bindings?.url ]
257
+ );
258
+
259
+ async function handleCreate( pageTitle ) {
260
+ const page = await createPageEntity( {
261
+ title: pageTitle,
262
+ status: 'draft',
263
+ } );
264
+
265
+ return {
266
+ id: page.id,
267
+ type: page.type,
268
+ title: page.title.rendered,
269
+ url: page.link,
270
+ kind: 'post-type',
271
+ };
272
+ }
273
+
274
+ function createButtonText( searchTerm ) {
275
+ return createInterpolateElement(
276
+ sprintf(
277
+ /* translators: %s: search term. */
278
+ __( 'Create page: <mark>%s</mark>' ),
279
+ searchTerm
280
+ ),
281
+ { mark: <mark /> }
282
+ );
283
+ }
284
+
222
285
  function startEditing( event ) {
223
286
  event.preventDefault();
224
287
  setIsEditingURL( true );
@@ -249,29 +312,6 @@ function ButtonEdit( props ) {
249
312
  const useEnterRef = useEnter( { content: text, clientId } );
250
313
  const mergedRef = useMergeRefs( [ useEnterRef, richTextRef ] );
251
314
 
252
- const { lockUrlControls = false } = useSelect(
253
- ( select ) => {
254
- if ( ! isSelected ) {
255
- return {};
256
- }
257
-
258
- const blockBindingsSource = getBlockBindingsSource(
259
- metadata?.bindings?.url?.source
260
- );
261
-
262
- return {
263
- lockUrlControls:
264
- !! metadata?.bindings?.url &&
265
- ! blockBindingsSource?.canUserEditValue?.( {
266
- select,
267
- context,
268
- args: metadata?.bindings?.url?.args,
269
- } ),
270
- };
271
- },
272
- [ context, isSelected, metadata?.bindings?.url ]
273
- );
274
-
275
315
  const [ fluidTypographySettings, layout ] = useSettings(
276
316
  'typography.fluid',
277
317
  'layout'
@@ -400,6 +440,11 @@ function ButtonEdit( props ) {
400
440
  } }
401
441
  forceIsEditingLink={ isEditingURL }
402
442
  settings={ LINK_SETTINGS }
443
+ createSuggestion={
444
+ createPageEntity && handleCreate
445
+ }
446
+ withCreateSuggestion={ userCanCreatePages }
447
+ createSuggestionButtonText={ createButtonText }
403
448
  />
404
449
  </Popover>
405
450
  ) }
@@ -125,7 +125,7 @@ export default function CategoriesEdit( {
125
125
  <RichText
126
126
  className="wp-block-categories__label"
127
127
  aria-label={ __( 'Label text' ) }
128
- placeholder={ taxonomy.name }
128
+ placeholder={ taxonomy?.name }
129
129
  withoutInteractiveFormatting
130
130
  value={ label }
131
131
  onChange={ ( html ) =>
@@ -134,7 +134,7 @@ export default function CategoriesEdit( {
134
134
  />
135
135
  ) : (
136
136
  <VisuallyHidden as="label" htmlFor={ selectId }>
137
- { label ? label : taxonomy.name }
137
+ { label ? label : taxonomy?.name }
138
138
  </VisuallyHidden>
139
139
  ) }
140
140
  <select id={ selectId }>
@@ -142,7 +142,7 @@ export default function CategoriesEdit( {
142
142
  { sprintf(
143
143
  /* translators: %s: taxonomy's singular name */
144
144
  __( 'Select %s' ),
145
- taxonomy.labels.singular_name
145
+ taxonomy?.labels?.singular_name
146
146
  ) }
147
147
  </option>
148
148
  { categoriesList.map( ( category ) =>
@@ -107,13 +107,21 @@ const useDefaultPageIndex = ( { defaultPage, postId, perPage, queryArgs } ) => {
107
107
  } ),
108
108
  method: 'HEAD',
109
109
  parse: false,
110
- } ).then( ( res ) => {
111
- const pages = parseInt( res.headers.get( 'X-WP-TotalPages' ) );
112
- setDefaultPages( {
113
- ...defaultPages,
114
- [ key ]: pages <= 1 ? 1 : pages, // If there are 0 pages, it means that there are no comments, but there is no 0th page.
110
+ } )
111
+ .then( ( res ) => {
112
+ const pages = parseInt( res.headers.get( 'X-WP-TotalPages' ) );
113
+ setDefaultPages( {
114
+ ...defaultPages,
115
+ [ key ]: pages <= 1 ? 1 : pages, // If there are 0 pages, it means that there are no comments, but there is no 0th page.
116
+ } );
117
+ } )
118
+ .catch( () => {
119
+ // There's no 0th page, but we can't know the number of pages, fallback to 1.
120
+ setDefaultPages( {
121
+ ...defaultPages,
122
+ [ key ]: 1,
123
+ } );
115
124
  } );
116
- } );
117
125
  }, [ defaultPage, postId, perPage, setDefaultPages ] );
118
126
 
119
127
  // The oldest one is always the first one.
@@ -111,7 +111,6 @@
111
111
  }
112
112
  },
113
113
  "color": {
114
- "__experimentalDuotone": "> .wp-block-cover__image-background, > .wp-block-cover__video-background",
115
114
  "heading": true,
116
115
  "text": true,
117
116
  "background": false,
@@ -139,6 +138,14 @@
139
138
  },
140
139
  "interactivity": {
141
140
  "clientNavigation": true
141
+ },
142
+ "filter": {
143
+ "duotone": true
144
+ }
145
+ },
146
+ "selectors": {
147
+ "filter": {
148
+ "duotone": ".wp-block-cover > .wp-block-cover__image-background, .wp-block-cover > .wp-block-cover__video-background"
142
149
  }
143
150
  },
144
151
  "editorStyle": "wp-block-cover-editor",
package/src/embed/edit.js CHANGED
@@ -172,7 +172,13 @@ const EmbedEdit = ( props ) => {
172
172
  // When obtaining an incoming preview,
173
173
  // we set the attributes derived from the preview data.
174
174
  const mergedAttributes = getMergedAttributes();
175
- setAttributes( mergedAttributes );
175
+ const hasChanges = Object.keys( mergedAttributes ).some(
176
+ ( key ) => mergedAttributes[ key ] !== attributes[ key ]
177
+ );
178
+
179
+ if ( hasChanges ) {
180
+ setAttributes( mergedAttributes );
181
+ }
176
182
 
177
183
  if ( onReplace ) {
178
184
  const upgradedBlock = createUpgradedEmbedBlock(
@@ -6,3 +6,4 @@ export const LINK_DESTINATION_CUSTOM = 'custom';
6
6
  export const NEW_TAB_REL = [ 'noreferrer', 'noopener' ];
7
7
  export const ALLOWED_MEDIA_TYPES = [ 'image' ];
8
8
  export const MEDIA_ID_NO_FEATURED_IMAGE_SET = 0;
9
+ export const SIZED_LAYOUTS = [ 'flex', 'grid' ];
package/src/image/edit.js CHANGED
@@ -116,9 +116,9 @@ export function ImageEdit( {
116
116
  // Only observe the max width from the parent container when the parent layout is not flex nor grid.
117
117
  // This won't work for them because the container width changes with the image.
118
118
  // TODO: Find a way to observe the container width for flex and grid layouts.
119
+ const layoutType = parentLayout?.type || parentLayout?.default?.type;
119
120
  const isMaxWidthContainerWidth =
120
- ! parentLayout ||
121
- ( parentLayout.type !== 'flex' && parentLayout.type !== 'grid' );
121
+ ! layoutType || ( layoutType !== 'flex' && layoutType !== 'grid' );
122
122
  const [ maxWidthObserver, maxContentWidth ] = useMaxWidthObserver();
123
123
 
124
124
  const [ placeholderResizeListener, { width: placeholderWidth } ] =
@@ -452,7 +452,7 @@ export function ImageEdit( {
452
452
  context={ context }
453
453
  clientId={ clientId }
454
454
  blockEditingMode={ blockEditingMode }
455
- parentLayoutType={ parentLayout?.type }
455
+ parentLayoutType={ layoutType }
456
456
  maxContentWidth={ maxContentWidth }
457
457
  />
458
458
  <MediaPlaceholder
@@ -48,19 +48,6 @@ figure.wp-block-image:not(.wp-block) {
48
48
  }
49
49
  }
50
50
 
51
- // This is necessary for the editor resize handles to accurately work on a non-floated, non-resized, small image.
52
- .wp-block-image .components-resizable-box__container {
53
- // Using "display: table" because:
54
- // - it visually hides empty white space in between elements
55
- // - it allows the element to be as wide as its contents (instead of 100% width, as it would be with `display: block`)
56
- display: table;
57
- img {
58
- display: block;
59
- width: inherit;
60
- height: inherit;
61
- }
62
- }
63
-
64
51
  .block-editor-block-list__block[data-type="core/image"] .block-editor-block-toolbar .block-editor-url-input__button-modal {
65
52
  position: absolute;
66
53
  left: 0;
@@ -19,7 +19,11 @@ import {
19
19
  DropdownMenu,
20
20
  Popover,
21
21
  } from '@wordpress/components';
22
- import { useViewportMatch } from '@wordpress/compose';
22
+ import {
23
+ useMergeRefs,
24
+ useResizeObserver,
25
+ useViewportMatch,
26
+ } from '@wordpress/compose';
23
27
  import { useSelect, useDispatch } from '@wordpress/data';
24
28
  import {
25
29
  BlockControls,
@@ -34,13 +38,13 @@ import {
34
38
  privateApis as blockEditorPrivateApis,
35
39
  BlockSettingsMenuControls,
36
40
  } from '@wordpress/block-editor';
37
- import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
41
+ import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
38
42
  import { __, _x, sprintf, isRTL } from '@wordpress/i18n';
39
43
  import { getFilename } from '@wordpress/url';
40
44
  import { getBlockBindingsSource, switchToBlockType } from '@wordpress/blocks';
41
45
  import { crop, overlayText, upload, chevronDown } from '@wordpress/icons';
42
46
  import { store as noticesStore } from '@wordpress/notices';
43
- import { store as coreStore, useEntityProp } from '@wordpress/core-data';
47
+ import { store as coreStore } from '@wordpress/core-data';
44
48
 
45
49
  /**
46
50
  * Internal dependencies
@@ -54,7 +58,7 @@ import { Caption } from '../utils/caption';
54
58
  * Module constants
55
59
  */
56
60
  import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
57
- import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants';
61
+ import { MIN_SIZE, ALLOWED_MEDIA_TYPES, SIZED_LAYOUTS } from './constants';
58
62
  import { evalAspectRatio } from './utils';
59
63
 
60
64
  const { DimensionsTool, ResolutionTool } = unlock( blockEditorPrivateApis );
@@ -280,14 +284,23 @@ export default function Image( {
280
284
  lightbox,
281
285
  metadata,
282
286
  } = attributes;
283
-
284
- // The only supported unit is px, so we can parseInt to strip the px here.
285
- const numericWidth = width ? parseInt( width, 10 ) : undefined;
286
- const numericHeight = height ? parseInt( height, 10 ) : undefined;
287
-
288
- const imageRef = useRef();
287
+ const [ imageElement, setImageElement ] = useState();
288
+ const [ resizeDelta, setResizeDelta ] = useState( null );
289
+ const [ pixelSize, setPixelSize ] = useState( {} );
290
+ const [ offsetTop, setOffsetTop ] = useState( 0 );
291
+ const setResizeObserved = useResizeObserver( ( [ entry ] ) => {
292
+ if ( ! resizeDelta ) {
293
+ const [ box ] = entry.borderBoxSize;
294
+ setPixelSize( { width: box.inlineSize, height: box.blockSize } );
295
+ }
296
+ // This is usually 0 unless the image height is less than the line-height.
297
+ setOffsetTop( entry.target.offsetTop );
298
+ } );
299
+ const effectResizeableBoxPlacement = useCallback( () => {
300
+ setOffsetTop( imageElement?.offsetTop ?? 0 );
301
+ }, [ imageElement ] );
302
+ const setRefs = useMergeRefs( [ setImageElement, setResizeObserved ] );
289
303
  const { allowResize = true } = context;
290
- const { getBlock, getSettings } = useSelect( blockEditorStore );
291
304
 
292
305
  const image = useSelect(
293
306
  ( select ) =>
@@ -299,7 +312,7 @@ export default function Image( {
299
312
 
300
313
  const { canInsertCover, imageEditing, imageSizes, maxWidth } = useSelect(
301
314
  ( select ) => {
302
- const { getBlockRootClientId, canInsertBlockType } =
315
+ const { getBlockRootClientId, canInsertBlockType, getSettings } =
303
316
  select( blockEditorStore );
304
317
 
305
318
  const rootClientId = getBlockRootClientId( clientId );
@@ -317,10 +330,13 @@ export default function Image( {
317
330
  },
318
331
  [ clientId ]
319
332
  );
333
+ const { getBlock, getSettings } = useSelect( blockEditorStore );
320
334
 
321
335
  const { replaceBlocks, toggleSelection } = useDispatch( blockEditorStore );
322
336
  const { createErrorNotice, createSuccessNotice } =
323
337
  useDispatch( noticesStore );
338
+ const { editEntityRecord } = useDispatch( coreStore );
339
+
324
340
  const isLargeViewport = useViewportMatch( 'medium' );
325
341
  const isWideAligned = [ 'wide', 'full' ].includes( align );
326
342
  const [
@@ -367,36 +383,20 @@ export default function Image( {
367
383
  .then( ( blob ) => setExternalBlob( blob ) )
368
384
  // Do nothing, cannot upload.
369
385
  .catch( () => {} );
370
- }, [ id, url, isSingleSelected, externalBlob ] );
386
+ }, [ id, url, isSingleSelected, externalBlob, getSettings ] );
371
387
 
372
- // Get naturalWidth and naturalHeight from image ref, and fall back to loaded natural
388
+ // Get naturalWidth and naturalHeight from image, and fall back to loaded natural
373
389
  // width and height. This resolves an issue in Safari where the loaded natural
374
390
  // width and height is otherwise lost when switching between alignments.
375
391
  // See: https://github.com/WordPress/gutenberg/pull/37210.
376
392
  const { naturalWidth, naturalHeight } = useMemo( () => {
377
393
  return {
378
394
  naturalWidth:
379
- imageRef.current?.naturalWidth ||
380
- loadedNaturalWidth ||
381
- undefined,
395
+ imageElement?.naturalWidth || loadedNaturalWidth || undefined,
382
396
  naturalHeight:
383
- imageRef.current?.naturalHeight ||
384
- loadedNaturalHeight ||
385
- undefined,
397
+ imageElement?.naturalHeight || loadedNaturalHeight || undefined,
386
398
  };
387
- }, [
388
- loadedNaturalWidth,
389
- loadedNaturalHeight,
390
- imageRef.current?.complete,
391
- ] );
392
-
393
- function onResizeStart() {
394
- toggleSelection( false );
395
- }
396
-
397
- function onResizeStop() {
398
- toggleSelection( true );
399
- }
399
+ }, [ loadedNaturalWidth, loadedNaturalHeight, imageElement?.complete ] );
400
400
 
401
401
  function onImageError() {
402
402
  setHasImageErrored( true );
@@ -541,49 +541,49 @@ export default function Image( {
541
541
 
542
542
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
543
543
 
544
- const dimensionsControl = (
545
- <DimensionsTool
546
- value={ { width, height, scale, aspectRatio } }
547
- onChange={ ( {
548
- width: newWidth,
549
- height: newHeight,
550
- scale: newScale,
551
- aspectRatio: newAspectRatio,
552
- } ) => {
553
- // Rebuilding the object forces setting `undefined`
554
- // for values that are removed since setAttributes
555
- // doesn't do anything with keys that aren't set.
556
- setAttributes( {
557
- // CSS includes `height: auto`, but we need
558
- // `width: auto` to fix the aspect ratio when
559
- // only height is set due to the width and
560
- // height attributes set via the server.
561
- width: ! newWidth && newHeight ? 'auto' : newWidth,
544
+ const dimensionsControl =
545
+ isResizable &&
546
+ ( SIZED_LAYOUTS.includes( parentLayoutType ) ? (
547
+ <DimensionsTool
548
+ value={ { aspectRatio } }
549
+ onChange={ ( { aspectRatio: newAspectRatio } ) => {
550
+ setAttributes( {
551
+ aspectRatio: newAspectRatio,
552
+ scale: 'cover',
553
+ } );
554
+ } }
555
+ defaultAspectRatio="auto"
556
+ tools={ [ 'aspectRatio' ] }
557
+ />
558
+ ) : (
559
+ <DimensionsTool
560
+ value={ { width, height, scale, aspectRatio } }
561
+ onChange={ ( {
562
+ width: newWidth,
562
563
  height: newHeight,
563
564
  scale: newScale,
564
565
  aspectRatio: newAspectRatio,
565
- } );
566
- } }
567
- defaultScale="cover"
568
- defaultAspectRatio="auto"
569
- scaleOptions={ scaleOptions }
570
- unitsOptions={ dimensionsUnitsOptions }
571
- />
572
- );
573
-
574
- const aspectRatioControl = (
575
- <DimensionsTool
576
- value={ { aspectRatio } }
577
- onChange={ ( { aspectRatio: newAspectRatio } ) => {
578
- setAttributes( {
579
- aspectRatio: newAspectRatio,
580
- scale: 'cover',
581
- } );
582
- } }
583
- defaultAspectRatio="auto"
584
- tools={ [ 'aspectRatio' ] }
585
- />
586
- );
566
+ } ) => {
567
+ // Rebuilding the object forces setting `undefined`
568
+ // for values that are removed since setAttributes
569
+ // doesn't do anything with keys that aren't set.
570
+ setAttributes( {
571
+ // CSS includes `height: auto`, but we need
572
+ // `width: auto` to fix the aspect ratio when
573
+ // only height is set due to the width and
574
+ // height attributes set via the server.
575
+ width: ! newWidth && newHeight ? 'auto' : newWidth,
576
+ height: newHeight,
577
+ scale: newScale,
578
+ aspectRatio: newAspectRatio,
579
+ } );
580
+ } }
581
+ defaultScale="cover"
582
+ defaultAspectRatio="auto"
583
+ scaleOptions={ scaleOptions }
584
+ unitsOptions={ dimensionsUnitsOptions }
585
+ />
586
+ ) );
587
587
 
588
588
  const resetAll = () => {
589
589
  setAttributes( {
@@ -603,10 +603,7 @@ export default function Image( {
603
603
  resetAll={ resetAll }
604
604
  dropdownMenuProps={ dropdownMenuProps }
605
605
  >
606
- { isResizable &&
607
- ( parentLayoutType === 'grid'
608
- ? aspectRatioControl
609
- : dimensionsControl ) }
606
+ { dimensionsControl }
610
607
  </ToolsPanel>
611
608
  </InspectorControls>
612
609
  );
@@ -835,10 +832,7 @@ export default function Image( {
835
832
  />
836
833
  </ToolsPanelItem>
837
834
  ) }
838
- { isResizable &&
839
- ( parentLayoutType === 'grid'
840
- ? aspectRatioControl
841
- : dimensionsControl ) }
835
+ { dimensionsControl }
842
836
  { !! imageSizeOptions.length && (
843
837
  <ResolutionTool
844
838
  value={ sizeSlug }
@@ -899,13 +893,6 @@ export default function Image( {
899
893
  const { postType, postId, queryId } = context;
900
894
  const isDescendentOfQueryLoop = Number.isFinite( queryId );
901
895
 
902
- const [ , setFeaturedImage ] = useEntityProp(
903
- 'postType',
904
- postType,
905
- 'featured_media',
906
- postId
907
- );
908
-
909
896
  let img =
910
897
  temporaryURL && hasImageErrored ? (
911
898
  // Show a placeholder during upload when the blob URL can't be loaded. This can
@@ -926,17 +913,19 @@ export default function Image( {
926
913
  alt={ defaultedAlt }
927
914
  onError={ onImageError }
928
915
  onLoad={ onImageLoad }
929
- ref={ imageRef }
916
+ ref={ setRefs }
930
917
  className={ borderProps.className }
918
+ width={ naturalWidth }
919
+ height={ naturalHeight }
931
920
  style={ {
932
- width:
933
- ( width && height ) || aspectRatio
934
- ? '100%'
935
- : undefined,
936
- height:
937
- ( width && height ) || aspectRatio
938
- ? '100%'
939
- : undefined,
921
+ aspectRatio,
922
+ ...( resizeDelta
923
+ ? {
924
+ width: pixelSize.width + resizeDelta.width,
925
+ height:
926
+ pixelSize.height + resizeDelta.height,
927
+ }
928
+ : { width, height } ),
940
929
  objectFit: scale,
941
930
  ...borderProps.style,
942
931
  ...shadowProps.style,
@@ -953,8 +942,7 @@ export default function Image( {
953
942
  <ImageEditor
954
943
  id={ id }
955
944
  url={ url }
956
- width={ numericWidth }
957
- height={ numericHeight }
945
+ { ...pixelSize }
958
946
  naturalHeight={ naturalHeight }
959
947
  naturalWidth={ naturalWidth }
960
948
  onSaveImage={ ( imageAttributes ) =>
@@ -967,26 +955,21 @@ export default function Image( {
967
955
  />
968
956
  </ImageWrapper>
969
957
  );
970
- } else if ( ! isResizable || parentLayoutType === 'grid' ) {
971
- img = (
972
- <div style={ { width, height, aspectRatio } }>
973
- <ImageWrapper href={ href }>{ img }</ImageWrapper>
974
- </div>
975
- );
976
958
  } else {
959
+ img = <ImageWrapper href={ href }>{ img }</ImageWrapper>;
960
+ }
961
+
962
+ let resizableBox;
963
+ if (
964
+ isResizable &&
965
+ isSingleSelected &&
966
+ ! isEditingImage &&
967
+ ! SIZED_LAYOUTS.includes( parentLayoutType )
968
+ ) {
977
969
  const numericRatio = aspectRatio && evalAspectRatio( aspectRatio );
978
- const customRatio = numericWidth / numericHeight;
970
+ const customRatio = pixelSize.width / pixelSize.height;
979
971
  const naturalRatio = naturalWidth / naturalHeight;
980
972
  const ratio = numericRatio || customRatio || naturalRatio || 1;
981
- const currentWidth =
982
- ! numericWidth && numericHeight
983
- ? numericHeight * ratio
984
- : numericWidth;
985
- const currentHeight =
986
- ! numericHeight && numericWidth
987
- ? numericWidth / ratio
988
- : numericHeight;
989
-
990
973
  const minWidth =
991
974
  naturalWidth < naturalHeight ? MIN_SIZE : MIN_SIZE * ratio;
992
975
  const minHeight =
@@ -1032,21 +1015,17 @@ export default function Image( {
1032
1015
  }
1033
1016
  }
1034
1017
  /* eslint-enable no-lonely-if */
1035
- img = (
1018
+ resizableBox = (
1036
1019
  <ResizableBox
1020
+ ref={ effectResizeableBoxPlacement }
1037
1021
  style={ {
1038
- display: 'block',
1039
- objectFit: scale,
1040
- aspectRatio:
1041
- ! width && ! height && aspectRatio
1042
- ? aspectRatio
1043
- : undefined,
1044
- } }
1045
- size={ {
1046
- width: currentWidth ?? 'auto',
1047
- height: currentHeight ?? 'auto',
1022
+ position: 'absolute',
1023
+ // To match the vertical-align: bottom of the img (from style.scss)
1024
+ // syncs the top with the img. This matters when the img height is
1025
+ // less than the line-height.
1026
+ inset: `${ offsetTop }px 0 0 0`,
1048
1027
  } }
1049
- showHandle={ isSingleSelected }
1028
+ size={ pixelSize }
1050
1029
  minWidth={ minWidth }
1051
1030
  maxWidth={ maxResizeWidth }
1052
1031
  minHeight={ minHeight }
@@ -1058,9 +1037,19 @@ export default function Image( {
1058
1037
  bottom: true,
1059
1038
  left: showLeftHandle,
1060
1039
  } }
1061
- onResizeStart={ onResizeStart }
1062
- onResizeStop={ ( event, direction, elt ) => {
1063
- onResizeStop();
1040
+ onResizeStart={ () => {
1041
+ toggleSelection( false );
1042
+ } }
1043
+ onResize={ ( event, direction, elt, delta ) => {
1044
+ setResizeDelta( delta );
1045
+ } }
1046
+ onResizeStop={ ( event, direction, elt, delta ) => {
1047
+ toggleSelection( true );
1048
+ setResizeDelta( null );
1049
+ setPixelSize( ( current ) => ( {
1050
+ width: current.width + delta.width,
1051
+ height: current.height + delta.height,
1052
+ } ) );
1064
1053
 
1065
1054
  // Clear hardcoded width if the resized width is close to the max-content width.
1066
1055
  if (
@@ -1091,9 +1080,7 @@ export default function Image( {
1091
1080
  } );
1092
1081
  } }
1093
1082
  resizeRatio={ align === 'center' ? 2 : 1 }
1094
- >
1095
- <ImageWrapper href={ href }>{ img }</ImageWrapper>
1096
- </ResizableBox>
1083
+ />
1097
1084
  );
1098
1085
  }
1099
1086
 
@@ -1111,7 +1098,9 @@ export default function Image( {
1111
1098
  * Set the post's featured image with the current image.
1112
1099
  */
1113
1100
  const setPostFeatureImage = () => {
1114
- setFeaturedImage( id );
1101
+ editEntityRecord( 'postType', postType, postId, {
1102
+ featured_media: id,
1103
+ } );
1115
1104
  createSuccessNotice( __( 'Post featured image updated.' ), {
1116
1105
  type: 'snackbar',
1117
1106
  } );
@@ -1139,6 +1128,7 @@ export default function Image( {
1139
1128
  { controls }
1140
1129
  { featuredImageControl }
1141
1130
  { img }
1131
+ { resizableBox }
1142
1132
 
1143
1133
  <Caption
1144
1134
  attributes={ attributes }