@wordpress/block-library 10.2.0 → 10.3.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 (308) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/build/accordion-panel/block.json +1 -0
  3. package/build/code/edit.cjs +3 -1
  4. package/build/code/edit.cjs.map +2 -2
  5. package/build/cover/block.json +1 -0
  6. package/build/cover/edit/block-controls.cjs +3 -1
  7. package/build/cover/edit/block-controls.cjs.map +2 -2
  8. package/build/cover/edit/index.cjs +75 -31
  9. package/build/cover/edit/index.cjs.map +2 -2
  10. package/build/embed/edit.cjs +1 -1
  11. package/build/embed/edit.cjs.map +2 -2
  12. package/build/embed/embed-placeholder.cjs +1 -1
  13. package/build/embed/embed-placeholder.cjs.map +2 -2
  14. package/build/gallery/dynamic-gallery.cjs +104 -61
  15. package/build/gallery/dynamic-gallery.cjs.map +3 -3
  16. package/build/gallery/use-dynamic-gallery.cjs +15 -8
  17. package/build/gallery/use-dynamic-gallery.cjs.map +2 -2
  18. package/build/heading/edit.cjs +1 -1
  19. package/build/heading/edit.cjs.map +2 -2
  20. package/build/html/modal.cjs +63 -54
  21. package/build/html/modal.cjs.map +3 -3
  22. package/build/icon/block.json +5 -3
  23. package/build/image/edit.cjs +1 -1
  24. package/build/image/edit.cjs.map +2 -2
  25. package/build/image/image.cjs +29 -5
  26. package/build/image/image.cjs.map +3 -3
  27. package/build/image/use-open-image-media-editor-modal.cjs +6 -2
  28. package/build/image/use-open-image-media-editor-modal.cjs.map +2 -2
  29. package/build/list-item/hooks/use-indent-list-item.cjs +10 -2
  30. package/build/list-item/hooks/use-indent-list-item.cjs.map +2 -2
  31. package/build/math/edit.cjs +39 -56
  32. package/build/math/edit.cjs.map +3 -3
  33. package/build/media-text/edit.cjs +2 -1
  34. package/build/media-text/edit.cjs.map +2 -2
  35. package/build/more/edit.cjs +2 -2
  36. package/build/more/edit.cjs.map +2 -2
  37. package/build/navigation/edit/index.cjs +30 -16
  38. package/build/navigation/edit/index.cjs.map +3 -3
  39. package/build/navigation/edit/use-layout-custom-properties.cjs +72 -0
  40. package/build/navigation/edit/use-layout-custom-properties.cjs.map +7 -0
  41. package/build/navigation/view.cjs +9 -6
  42. package/build/navigation/view.cjs.map +2 -2
  43. package/build/navigation-link/edit.cjs +3 -3
  44. package/build/navigation-link/edit.cjs.map +2 -2
  45. package/build/navigation-link/link-ui/index.cjs +2 -2
  46. package/build/navigation-link/link-ui/index.cjs.map +2 -2
  47. package/build/paragraph/block.json +0 -1
  48. package/build/paragraph/edit.cjs +5 -2
  49. package/build/paragraph/edit.cjs.map +3 -3
  50. package/build/paragraph/index.cjs +4 -1
  51. package/build/paragraph/index.cjs.map +2 -2
  52. package/build/playlist/context.cjs +2 -1
  53. package/build/playlist/context.cjs.map +2 -2
  54. package/build/playlist/edit.cjs +113 -26
  55. package/build/playlist/edit.cjs.map +2 -2
  56. package/build/playlist/utils.cjs +19 -4
  57. package/build/playlist/utils.cjs.map +2 -2
  58. package/build/playlist-track/edit.cjs +21 -6
  59. package/build/playlist-track/edit.cjs.map +2 -2
  60. package/build/post-content/edit.cjs +36 -15
  61. package/build/post-content/edit.cjs.map +2 -2
  62. package/build/post-featured-image/edit.cjs +3 -2
  63. package/build/post-featured-image/edit.cjs.map +2 -2
  64. package/build/post-terms/edit.cjs +2 -2
  65. package/build/post-terms/edit.cjs.map +2 -2
  66. package/build/post-title/edit.cjs +2 -2
  67. package/build/post-title/edit.cjs.map +2 -2
  68. package/build/preformatted/edit.cjs +3 -1
  69. package/build/preformatted/edit.cjs.map +2 -2
  70. package/build/pullquote/edit.cjs +5 -3
  71. package/build/pullquote/edit.cjs.map +2 -2
  72. package/build/query/edit/inspector-controls/index.cjs +4 -1
  73. package/build/query/edit/inspector-controls/index.cjs.map +2 -2
  74. package/build/query/edit/inspector-controls/offset-controls.cjs +1 -1
  75. package/build/query/edit/inspector-controls/offset-controls.cjs.map +2 -2
  76. package/build/read-more/edit.cjs +3 -1
  77. package/build/read-more/edit.cjs.map +2 -2
  78. package/build/site-tagline/edit.cjs +3 -1
  79. package/build/site-tagline/edit.cjs.map +2 -2
  80. package/build/site-title/edit.cjs +3 -1
  81. package/build/site-title/edit.cjs.map +2 -2
  82. package/build/social-links/edit.cjs +11 -4
  83. package/build/social-links/edit.cjs.map +2 -2
  84. package/build/tab-list/block.json +2 -1
  85. package/build/tab-list/edit.cjs +2 -0
  86. package/build/tab-list/edit.cjs.map +3 -3
  87. package/build/tab-list/tab-movers.cjs +87 -0
  88. package/build/tab-list/tab-movers.cjs.map +7 -0
  89. package/build/tab-panel/edit.cjs +1 -9
  90. package/build/tab-panel/edit.cjs.map +2 -2
  91. package/build/table/edit.cjs +1 -1
  92. package/build/table/edit.cjs.map +2 -2
  93. package/build/tabs/use-tab-actions.cjs +23 -1
  94. package/build/tabs/use-tab-actions.cjs.map +2 -2
  95. package/build/utils/caption.cjs +2 -2
  96. package/build/utils/caption.cjs.map +2 -2
  97. package/build/verse/edit.cjs +3 -1
  98. package/build/verse/edit.cjs.map +2 -2
  99. package/build/video/tracks-editor.cjs +0 -1
  100. package/build/video/tracks-editor.cjs.map +2 -2
  101. package/build-module/accordion-panel/block.json +1 -0
  102. package/build-module/code/edit.mjs +3 -1
  103. package/build-module/code/edit.mjs.map +2 -2
  104. package/build-module/cover/block.json +1 -0
  105. package/build-module/cover/edit/block-controls.mjs +3 -1
  106. package/build-module/cover/edit/block-controls.mjs.map +2 -2
  107. package/build-module/cover/edit/index.mjs +78 -34
  108. package/build-module/cover/edit/index.mjs.map +2 -2
  109. package/build-module/embed/edit.mjs +1 -1
  110. package/build-module/embed/edit.mjs.map +2 -2
  111. package/build-module/embed/embed-placeholder.mjs +1 -1
  112. package/build-module/embed/embed-placeholder.mjs.map +2 -2
  113. package/build-module/gallery/dynamic-gallery.mjs +104 -61
  114. package/build-module/gallery/dynamic-gallery.mjs.map +2 -2
  115. package/build-module/gallery/use-dynamic-gallery.mjs +16 -9
  116. package/build-module/gallery/use-dynamic-gallery.mjs.map +2 -2
  117. package/build-module/heading/edit.mjs +1 -1
  118. package/build-module/heading/edit.mjs.map +2 -2
  119. package/build-module/html/modal.mjs +68 -55
  120. package/build-module/html/modal.mjs.map +2 -2
  121. package/build-module/icon/block.json +5 -3
  122. package/build-module/image/edit.mjs +1 -1
  123. package/build-module/image/edit.mjs.map +2 -2
  124. package/build-module/image/image.mjs +19 -5
  125. package/build-module/image/image.mjs.map +2 -2
  126. package/build-module/image/use-open-image-media-editor-modal.mjs +6 -2
  127. package/build-module/image/use-open-image-media-editor-modal.mjs.map +2 -2
  128. package/build-module/list-item/hooks/use-indent-list-item.mjs +10 -2
  129. package/build-module/list-item/hooks/use-indent-list-item.mjs.map +2 -2
  130. package/build-module/math/edit.mjs +41 -60
  131. package/build-module/math/edit.mjs.map +2 -2
  132. package/build-module/media-text/edit.mjs +2 -1
  133. package/build-module/media-text/edit.mjs.map +2 -2
  134. package/build-module/more/edit.mjs +2 -2
  135. package/build-module/more/edit.mjs.map +2 -2
  136. package/build-module/navigation/edit/index.mjs +30 -16
  137. package/build-module/navigation/edit/index.mjs.map +2 -2
  138. package/build-module/navigation/edit/use-layout-custom-properties.mjs +51 -0
  139. package/build-module/navigation/edit/use-layout-custom-properties.mjs.map +7 -0
  140. package/build-module/navigation/view.mjs +9 -6
  141. package/build-module/navigation/view.mjs.map +2 -2
  142. package/build-module/navigation-link/edit.mjs +3 -3
  143. package/build-module/navigation-link/edit.mjs.map +2 -2
  144. package/build-module/navigation-link/link-ui/index.mjs +2 -2
  145. package/build-module/navigation-link/link-ui/index.mjs.map +2 -2
  146. package/build-module/paragraph/block.json +0 -1
  147. package/build-module/paragraph/edit.mjs +6 -2
  148. package/build-module/paragraph/edit.mjs.map +2 -2
  149. package/build-module/paragraph/index.mjs +4 -1
  150. package/build-module/paragraph/index.mjs.map +2 -2
  151. package/build-module/playlist/context.mjs +2 -1
  152. package/build-module/playlist/context.mjs.map +2 -2
  153. package/build-module/playlist/edit.mjs +113 -27
  154. package/build-module/playlist/edit.mjs.map +2 -2
  155. package/build-module/playlist/utils.mjs +19 -4
  156. package/build-module/playlist/utils.mjs.map +2 -2
  157. package/build-module/playlist-track/edit.mjs +21 -6
  158. package/build-module/playlist-track/edit.mjs.map +2 -2
  159. package/build-module/post-content/edit.mjs +37 -15
  160. package/build-module/post-content/edit.mjs.map +2 -2
  161. package/build-module/post-featured-image/edit.mjs +3 -2
  162. package/build-module/post-featured-image/edit.mjs.map +2 -2
  163. package/build-module/post-terms/edit.mjs +2 -2
  164. package/build-module/post-terms/edit.mjs.map +2 -2
  165. package/build-module/post-title/edit.mjs +2 -2
  166. package/build-module/post-title/edit.mjs.map +2 -2
  167. package/build-module/preformatted/edit.mjs +3 -1
  168. package/build-module/preformatted/edit.mjs.map +2 -2
  169. package/build-module/pullquote/edit.mjs +5 -3
  170. package/build-module/pullquote/edit.mjs.map +2 -2
  171. package/build-module/query/edit/inspector-controls/index.mjs +5 -2
  172. package/build-module/query/edit/inspector-controls/index.mjs.map +2 -2
  173. package/build-module/query/edit/inspector-controls/offset-controls.mjs +2 -2
  174. package/build-module/query/edit/inspector-controls/offset-controls.mjs.map +2 -2
  175. package/build-module/read-more/edit.mjs +3 -1
  176. package/build-module/read-more/edit.mjs.map +2 -2
  177. package/build-module/site-tagline/edit.mjs +3 -1
  178. package/build-module/site-tagline/edit.mjs.map +2 -2
  179. package/build-module/site-title/edit.mjs +3 -1
  180. package/build-module/site-title/edit.mjs.map +2 -2
  181. package/build-module/social-links/edit.mjs +11 -4
  182. package/build-module/social-links/edit.mjs.map +2 -2
  183. package/build-module/tab-list/block.json +2 -1
  184. package/build-module/tab-list/edit.mjs +2 -0
  185. package/build-module/tab-list/edit.mjs.map +2 -2
  186. package/build-module/tab-list/tab-movers.mjs +59 -0
  187. package/build-module/tab-list/tab-movers.mjs.map +7 -0
  188. package/build-module/tab-panel/edit.mjs +1 -9
  189. package/build-module/tab-panel/edit.mjs.map +2 -2
  190. package/build-module/table/edit.mjs +1 -1
  191. package/build-module/table/edit.mjs.map +2 -2
  192. package/build-module/tabs/use-tab-actions.mjs +23 -1
  193. package/build-module/tabs/use-tab-actions.mjs.map +2 -2
  194. package/build-module/utils/caption.mjs +2 -2
  195. package/build-module/utils/caption.mjs.map +2 -2
  196. package/build-module/verse/edit.mjs +3 -1
  197. package/build-module/verse/edit.mjs.map +2 -2
  198. package/build-module/video/tracks-editor.mjs +0 -1
  199. package/build-module/video/tracks-editor.mjs.map +2 -2
  200. package/build-style/cover/editor-rtl.css +3 -0
  201. package/build-style/cover/editor.css +3 -0
  202. package/build-style/editor-rtl.css +18 -41
  203. package/build-style/editor.css +18 -41
  204. package/build-style/icon/editor-rtl.css +1 -4
  205. package/build-style/icon/editor.css +1 -4
  206. package/build-style/image/editor-rtl.css +3 -32
  207. package/build-style/image/editor.css +3 -32
  208. package/build-style/playlist/editor-rtl.css +0 -5
  209. package/build-style/playlist/editor.css +0 -5
  210. package/build-style/playlist/style-rtl.css +44 -4
  211. package/build-style/playlist/style.css +44 -4
  212. package/build-style/pullquote/editor-rtl.css +4 -0
  213. package/build-style/pullquote/editor.css +4 -0
  214. package/build-style/style-rtl.css +44 -4
  215. package/build-style/style.css +44 -4
  216. package/build-style/tab-list/editor-rtl.css +101 -0
  217. package/build-style/tab-list/editor.css +101 -0
  218. package/package.json +43 -43
  219. package/src/accordion-panel/block.json +1 -0
  220. package/src/breadcrumbs/index.php +3 -1
  221. package/src/button/index.php +1 -1
  222. package/src/code/edit.js +7 -2
  223. package/src/cover/block.json +1 -0
  224. package/src/cover/edit/block-controls.js +5 -0
  225. package/src/cover/edit/index.js +100 -35
  226. package/src/cover/editor.scss +6 -0
  227. package/src/cover/index.php +9 -2
  228. package/src/editor.scss +1 -0
  229. package/src/embed/edit.js +3 -1
  230. package/src/embed/embed-placeholder.js +9 -7
  231. package/src/form/index.php +6 -4
  232. package/src/gallery/dynamic-gallery.js +133 -74
  233. package/src/gallery/use-dynamic-gallery.js +27 -12
  234. package/src/heading/edit.js +1 -1
  235. package/src/html/modal.js +13 -2
  236. package/src/icon/README.md +1 -3
  237. package/src/icon/block.json +5 -3
  238. package/src/icon/editor.scss +1 -4
  239. package/src/image/edit.js +1 -5
  240. package/src/image/editor.scss +6 -46
  241. package/src/image/image.js +37 -3
  242. package/src/image/test/use-open-image-media-editor-modal.js +37 -1
  243. package/src/image/use-open-image-media-editor-modal.js +8 -1
  244. package/src/latest-posts/index.php +3 -2
  245. package/src/list-item/hooks/use-indent-list-item.js +10 -1
  246. package/src/math/edit.js +42 -59
  247. package/src/media-text/edit.js +1 -0
  248. package/src/media-text/index.php +11 -2
  249. package/src/more/edit.js +7 -4
  250. package/src/navigation/edit/index.js +52 -36
  251. package/src/navigation/edit/test/layout-custom-properties.js +138 -0
  252. package/src/navigation/edit/use-layout-custom-properties.js +84 -0
  253. package/src/navigation/index.php +123 -12
  254. package/src/navigation/test/view.js +51 -0
  255. package/src/navigation/view.js +15 -14
  256. package/src/navigation-link/edit.js +11 -7
  257. package/src/navigation-link/index.php +64 -0
  258. package/src/navigation-link/link-ui/index.js +2 -1
  259. package/src/navigation-link/test/__snapshots__/hooks.js.snap +1 -1
  260. package/src/page-list/index.php +4 -3
  261. package/src/paragraph/README.md +0 -1
  262. package/src/paragraph/block.json +0 -1
  263. package/src/paragraph/edit.js +9 -2
  264. package/src/paragraph/index.js +4 -1
  265. package/src/pattern/index.php +5 -5
  266. package/src/playlist/context.js +1 -0
  267. package/src/playlist/edit.js +151 -47
  268. package/src/playlist/editor.scss +0 -7
  269. package/src/playlist/index.php +14 -9
  270. package/src/playlist/style.scss +3 -0
  271. package/src/playlist/test/edit-component.js +139 -0
  272. package/src/playlist/test/edit.js +30 -0
  273. package/src/playlist/utils.js +25 -4
  274. package/src/playlist-track/edit.js +22 -5
  275. package/src/playlist-track/index.php +8 -7
  276. package/src/playlist-track/test/edit.js +87 -3
  277. package/src/post-content/edit.js +45 -16
  278. package/src/post-excerpt/index.php +4 -1
  279. package/src/post-featured-image/edit.js +3 -2
  280. package/src/post-featured-image/index.php +4 -1
  281. package/src/post-terms/edit.js +7 -4
  282. package/src/post-title/edit.js +5 -3
  283. package/src/preformatted/edit.js +7 -2
  284. package/src/pullquote/edit.js +9 -4
  285. package/src/pullquote/editor.scss +6 -0
  286. package/src/query/edit/inspector-controls/index.js +5 -2
  287. package/src/query/edit/inspector-controls/offset-controls.js +2 -2
  288. package/src/read-more/edit.js +7 -2
  289. package/src/rss/index.php +9 -3
  290. package/src/site-tagline/edit.js +7 -2
  291. package/src/site-title/edit.js +7 -2
  292. package/src/social-link/index.php +19 -9
  293. package/src/social-links/edit.js +21 -12
  294. package/src/tab-list/block.json +2 -1
  295. package/src/tab-list/edit.js +2 -0
  296. package/src/tab-list/editor.scss +9 -0
  297. package/src/tab-list/tab-movers.js +72 -0
  298. package/src/tab-panel/edit.js +1 -9
  299. package/src/table/edit.js +1 -1
  300. package/src/table-of-contents/index.php +386 -1
  301. package/src/tabs/use-tab-actions.js +32 -3
  302. package/src/tag-cloud/index.php +3 -2
  303. package/src/template-part/index.php +2 -1
  304. package/src/utils/caption.js +7 -4
  305. package/src/utils/test/waveform-utils.js +63 -0
  306. package/src/verse/edit.js +7 -2
  307. package/src/video/index.php +2 -2
  308. package/src/video/tracks-editor.js +0 -1
@@ -2,7 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { useMemo } from '@wordpress/element';
5
- import { useDispatch, useSelect } from '@wordpress/data';
5
+ import { useDispatch, useSelect, useRegistry } from '@wordpress/data';
6
6
  import { store as blockEditorStore } from '@wordpress/block-editor';
7
7
  import { store as coreStore } from '@wordpress/core-data';
8
8
  import { createBlock } from '@wordpress/blocks';
@@ -122,7 +122,9 @@ export default function useDynamicGallery( {
122
122
  const sourceOrderby = dynamicContent?.args?.orderBy ?? DEFAULT_ORDERBY;
123
123
  const sourceOrder = dynamicContent?.args?.order ?? DEFAULT_ORDER;
124
124
 
125
- const { replaceInnerBlocks } = useDispatch( blockEditorStore );
125
+ const registry = useRegistry();
126
+ const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } =
127
+ useDispatch( blockEditorStore );
126
128
 
127
129
  // Resolve the configured source to a media query. `null` (static mode, or an
128
130
  // unresolvable source) short-circuits the select below so no request fires.
@@ -208,21 +210,34 @@ export default function useDynamicGallery( {
208
210
  // for the pre-innerBlocks format (see `deprecated.js`/`transforms.js`), empty
209
211
  // on any gallery reachable here.
210
212
  function enableDynamicMode() {
211
- setAttributes( { dynamicContent: { source: ATTACHED_MEDIA } } );
212
- replaceInnerBlocks( clientId, [] );
213
+ // Batch the attribute change and the inner-block reset into a single
214
+ // undo level: they're two halves of one mode switch, so one undo should
215
+ // revert both. Marking the second dispatch non-persistent stops it from
216
+ // opening a second undo level, which would otherwise leave the gallery
217
+ // in a half-switched state (dynamic source set, images still present).
218
+ registry.batch( () => {
219
+ setAttributes( { dynamicContent: { source: ATTACHED_MEDIA } } );
220
+ __unstableMarkNextChangeAsNotPersistent();
221
+ replaceInnerBlocks( clientId, [] );
222
+ } );
213
223
  }
214
224
 
215
225
  // "Pins" a dynamic gallery: materializes the currently-resolved media as
216
226
  // real, editable image blocks and leaves dynamic mode.
217
227
  function convertToStatic() {
218
- // Build fresh blocks rather than reusing the preview's `dynamicImageBlocks`
219
- // so the materialized inner blocks get their own client IDs, distinct from
220
- // the (disabled) preview instances.
221
- replaceInnerBlocks(
222
- clientId,
223
- buildImageBlocks( dynamicMedia, imageAttributes )
224
- );
225
- setAttributes( { dynamicContent: undefined } );
228
+ // Batch the inner-block materialization and the attribute change into a
229
+ // single undo level so one undo reverts the whole conversion (see
230
+ // `enableDynamicMode`). Build fresh blocks rather than reusing the
231
+ // preview's `dynamicImageBlocks` so the materialized inner blocks get
232
+ // their own client IDs, distinct from the (disabled) preview instances.
233
+ registry.batch( () => {
234
+ replaceInnerBlocks(
235
+ clientId,
236
+ buildImageBlocks( dynamicMedia, imageAttributes )
237
+ );
238
+ __unstableMarkNextChangeAsNotPersistent();
239
+ setAttributes( { dynamicContent: undefined } );
240
+ } );
226
241
  }
227
242
 
228
243
  // Updates the source ordering within `dynamicContent.args`. Passing
@@ -94,7 +94,7 @@ function HeadingEdit( props ) {
94
94
  onChange={ onContentChange }
95
95
  onMerge={ mergeBlocks }
96
96
  onReplace={ onReplace }
97
- onRemove={ () => onReplace( [] ) }
97
+ onRemove={ onReplace ? () => onReplace( [] ) : undefined }
98
98
  placeholder={ placeholder || __( 'Heading' ) }
99
99
  { ...blockProps }
100
100
  />
package/src/html/modal.js CHANGED
@@ -13,7 +13,11 @@ import {
13
13
  __experimentalVStack as VStack,
14
14
  __experimentalHStack as HStack,
15
15
  } from '@wordpress/components';
16
- import { PlainText, store as blockEditorStore } from '@wordpress/block-editor';
16
+ import {
17
+ PlainText,
18
+ store as blockEditorStore,
19
+ privateApis as blockEditorPrivateApis,
20
+ } from '@wordpress/block-editor';
17
21
  import { fullscreen, square } from '@wordpress/icons';
18
22
  import { useViewportMatch } from '@wordpress/compose';
19
23
 
@@ -25,6 +29,7 @@ import Preview from './preview';
25
29
  import { parseContent, serializeContent } from './utils';
26
30
 
27
31
  const { Tabs } = unlock( componentsPrivateApis );
32
+ const { useNativeUndo } = unlock( blockEditorPrivateApis );
28
33
 
29
34
  export default function HTMLEditModal( { onRequestClose, content, onUpdate } ) {
30
35
  // Parse content into separate sections and use as initial state
@@ -33,6 +38,9 @@ export default function HTMLEditModal( { onRequestClose, content, onUpdate } ) {
33
38
  const [ editedCss, setEditedCss ] = useState( css );
34
39
  const [ editedJs, setEditedJs ] = useState( js );
35
40
  const [ isFullscreen, setIsFullscreen ] = useState( false );
41
+ // The fields hold local state that is only committed when the dialog is
42
+ // saved, so undo and redo within them must remain the browser's own.
43
+ const nativeUndoRef = useNativeUndo();
36
44
 
37
45
  const isMobileViewport = useViewportMatch( 'small', '<' );
38
46
 
@@ -132,7 +140,10 @@ export default function HTMLEditModal( { onRequestClose, content, onUpdate } ) {
132
140
  align="stretch"
133
141
  gap={ 8 }
134
142
  >
135
- <div className="block-library-html__modal-content">
143
+ <div
144
+ ref={ nativeUndoRef }
145
+ className="block-library-html__modal-content"
146
+ >
136
147
  <Tabs.TabPanel
137
148
  tabId="html"
138
149
  focusable={ false }
@@ -28,9 +28,7 @@ _Defined via the [`supports`](https://developer.wordpress.org/block-editor/refer
28
28
  - [`ariaLabel`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#arialabel): `true`
29
29
  - [`align`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#align): `"left"`, `"center"`, `"right"`
30
30
  - [`html`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#html): `false`
31
- - [`color`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#color):
32
- - [`background`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#color-background): `true`
33
- - [`text`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#color-text): `true`
31
+ - [`color`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#color): `true`
34
32
  - [`interactivity`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#interactivity):
35
33
  - `clientNavigation`: `true`
36
34
  - [`spacing`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#spacing):
@@ -33,9 +33,11 @@
33
33
  "align": [ "left", "center", "right" ],
34
34
  "html": false,
35
35
  "color": {
36
- "background": true,
37
- "text": true,
38
- "__experimentalSkipSerialization": true
36
+ "__experimentalSkipSerialization": true,
37
+ "__experimentalDefaultControls": {
38
+ "background": true,
39
+ "text": true
40
+ }
39
41
  },
40
42
  "interactivity": {
41
43
  "clientNavigation": true
@@ -66,10 +66,7 @@
66
66
  .wp-block-icon__inserter-grid-icons-list-item-title {
67
67
  font-size: $font-size-small;
68
68
  padding: $grid-unit-05 $grid-unit-05 $grid-unit-10;
69
- overflow: hidden;
70
- text-overflow: ellipsis;
71
- white-space: nowrap;
72
- width: 100%;
69
+ overflow-wrap: break-word;
73
70
  }
74
71
 
75
72
  .wp-block-icon__toolbar-content {
package/src/image/edit.js CHANGED
@@ -353,11 +353,7 @@ export function ImageEdit( {
353
353
 
354
354
  const isSideloading = useSelect(
355
355
  ( select ) => {
356
- if (
357
- ( ! window.__clientSideMediaProcessing &&
358
- ! window.__heicUploadSupport ) ||
359
- ! id
360
- ) {
356
+ if ( ! window.__clientSideMediaProcessing || ! id ) {
361
357
  return false;
362
358
  }
363
359
  return select( uploadStore ).isUploadingById( id );
@@ -22,12 +22,16 @@ figure.wp-block-image:not(.wp-block) {
22
22
  // width (e.g. a small animated GIF being transcoded to a video) otherwise
23
23
  // leaves the spinner off to one side. Wide and full alignments keep their
24
24
  // own width.
25
- &.is-transient:not(.alignwide):not(.alignfull) {
25
+ // The media editor swap state marks the img rather than the figure (its
26
+ // state lives alongside the img's load handlers), hence the :has() variant.
27
+ &.is-transient:not(.alignwide):not(.alignfull),
28
+ &:has(img.is-swapping-media):not(.alignwide):not(.alignfull) {
26
29
  width: fit-content;
27
30
  }
28
31
 
29
32
  .is-applying img,
30
- &.is-transient img {
33
+ &.is-transient img,
34
+ img.is-swapping-media {
31
35
  opacity: 0.3;
32
36
  }
33
37
 
@@ -123,49 +127,6 @@ figure.wp-block-image:not(.wp-block) {
123
127
  position: relative;
124
128
  }
125
129
 
126
- .wp-block-image__crop-area {
127
- position: relative;
128
- max-width: 100%;
129
- width: 100%;
130
- overflow: hidden;
131
-
132
- // stylelint-disable selector-class-pattern -- react-easy-crop library class names.
133
- .reactEasyCrop_Container {
134
- // The linked Image block has `pointer-events: none` applied. Override it
135
- // here to enable the crop action.
136
- pointer-events: auto;
137
-
138
- // This removes the border from the img within the image cropper so it
139
- // can be applied to the cropper itself. This then allows the image to be
140
- // cropped within the visual border providing more accurate editing and
141
- // smoother UX.
142
- .reactEasyCrop_Image {
143
- border: none;
144
- border-radius: 0; // Prevent's theme.json radius bleeding into cropper.
145
- }
146
- }
147
- // stylelint-enable selector-class-pattern
148
- }
149
-
150
- .wp-block-image__crop-icon {
151
- padding: 0 8px;
152
- min-width: 48px;
153
- display: flex;
154
- justify-content: center;
155
- align-items: center;
156
-
157
- svg {
158
- fill: currentColor;
159
- }
160
- }
161
-
162
- .wp-block-image__zoom {
163
- .components-popover__content {
164
- min-width: 260px;
165
- overflow: visible !important;
166
- }
167
- }
168
-
169
130
  .wp-block-image__toolbar_content_textarea__container {
170
131
  padding: $grid-unit;
171
132
  }
@@ -174,4 +135,3 @@ figure.wp-block-image:not(.wp-block) {
174
135
  // Corresponds to the size of the textarea in the block inspector.
175
136
  width: 250px;
176
137
  }
177
-
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
@@ -390,6 +395,12 @@ export default function Image( {
390
395
  );
391
396
  const { getBlock, getSettings } = useSelect( blockEditorStore );
392
397
  const cropButtonRef = useRef();
398
+ // URL of a freshly generated file (crop/rotate result) from the media
399
+ // editor that the browser may not have finished loading; cleared by the
400
+ // <img> load/error handlers, or by the settle effect below when the
401
+ // rendered image already shows it.
402
+ const [ pendingSwapUrl, setPendingSwapUrl ] = useState();
403
+ const isSwappingMedia = !! pendingSwapUrl;
393
404
  const handleMediaEditorModalClose = useCallback(
394
405
  () => cropButtonRef.current?.focus(),
395
406
  []
@@ -398,6 +409,7 @@ export default function Image( {
398
409
  attributes,
399
410
  setAttributes,
400
411
  onClose: handleMediaEditorModalClose,
412
+ onUrlChange: setPendingSwapUrl,
401
413
  } );
402
414
 
403
415
  const {
@@ -485,18 +497,34 @@ export default function Image( {
485
497
  };
486
498
  }, [ loadedNaturalWidth, loadedNaturalHeight, imageElement?.complete ] );
487
499
 
500
+ // A media editor update can be undone or superseded before its
501
+ // attributes land, leaving the rendered image untouched. No load event
502
+ // fires in that case, so clear the pending swap whenever the rendered
503
+ // image already shows the pending URL.
504
+ useEffect( () => {
505
+ if (
506
+ pendingSwapUrl &&
507
+ pendingSwapUrl === url &&
508
+ imageElement?.complete
509
+ ) {
510
+ setPendingSwapUrl( undefined );
511
+ }
512
+ }, [ pendingSwapUrl, url, imageElement ] );
513
+
488
514
  function onImageError() {
515
+ setPendingSwapUrl( undefined );
489
516
  setHasImageErrored( true );
490
517
 
491
518
  // Check if there's an embed block that handles this URL, e.g., instagram URL.
492
519
  // See: https://github.com/WordPress/gutenberg/pull/11472
493
520
  const embedBlock = createUpgradedEmbedBlock( { attributes: { url } } );
494
- if ( undefined !== embedBlock ) {
521
+ if ( undefined !== embedBlock && onReplace ) {
495
522
  onReplace( embedBlock );
496
523
  }
497
524
  }
498
525
 
499
526
  function onImageLoad( event ) {
527
+ setPendingSwapUrl( undefined );
500
528
  setHasImageErrored( false );
501
529
  setLoadedNaturalSize( {
502
530
  loadedNaturalWidth: event.target?.naturalWidth,
@@ -902,6 +930,10 @@ export default function Image( {
902
930
  aria-haspopup="dialog"
903
931
  icon={ crop }
904
932
  label={ __( 'Crop' ) }
933
+ // Disable rather than hide while the edited image
934
+ // loads, so the button keeps focus when the modal
935
+ // closes instead of dropping it to the canvas.
936
+ disabled={ isSwappingMedia }
905
937
  />
906
938
  ) }
907
939
  { showCoverControls && (
@@ -1173,7 +1205,9 @@ export default function Image( {
1173
1205
  onError={ onImageError }
1174
1206
  onLoad={ onImageLoad }
1175
1207
  ref={ setRefs }
1176
- className={ borderProps.className }
1208
+ className={ clsx( borderProps.className, {
1209
+ 'is-swapping-media': isSwappingMedia,
1210
+ } ) }
1177
1211
  width={ naturalWidth }
1178
1212
  height={ naturalHeight }
1179
1213
  style={ {
@@ -1222,7 +1256,7 @@ export default function Image( {
1222
1256
  ...shadowProps.style,
1223
1257
  } }
1224
1258
  />
1225
- { isUploading && <Spinner /> }
1259
+ { ( isUploading || isSwappingMedia ) && <Spinner /> }
1226
1260
  </>
1227
1261
  ) }
1228
1262
  </ImageWrapper>
@@ -80,6 +80,7 @@ async function runModalUpdate( {
80
80
  attributes,
81
81
  registryOptions = {},
82
82
  updatePayload = { id: attributes.id, url: 'updated.jpg' },
83
+ hookOptions = {},
83
84
  } ) {
84
85
  const registry = createRegistry( registryOptions );
85
86
  useRegistry.mockReturnValue( registry );
@@ -87,7 +88,11 @@ async function runModalUpdate( {
87
88
  const openMediaEditorModal = jest.fn();
88
89
  mockMediaEditorModalSetting( openMediaEditorModal );
89
90
  const { result } = renderHook( () =>
90
- useOpenImageMediaEditorModal( { attributes, setAttributes } )
91
+ useOpenImageMediaEditorModal( {
92
+ attributes,
93
+ setAttributes,
94
+ ...hookOptions,
95
+ } )
91
96
  );
92
97
  await act( async () => {
93
98
  await result.current();
@@ -105,6 +110,37 @@ describe( 'useOpenImageMediaEditorModal', () => {
105
110
  jest.clearAllMocks();
106
111
  } );
107
112
 
113
+ it( 'notifies onUrlChange when the update switches to a new attachment URL', async () => {
114
+ const onUrlChange = jest.fn();
115
+ await runModalUpdate( {
116
+ attributes: { id: 1, url: 'original.jpg', alt: '', caption: '' },
117
+ updatePayload: { id: 2, url: 'updated.jpg' },
118
+ hookOptions: { onUrlChange },
119
+ } );
120
+ expect( onUrlChange ).toHaveBeenCalledTimes( 1 );
121
+ expect( onUrlChange ).toHaveBeenCalledWith( 'updated.jpg' );
122
+ } );
123
+
124
+ it( 'does not notify onUrlChange for a same-attachment update', async () => {
125
+ const onUrlChange = jest.fn();
126
+ await runModalUpdate( {
127
+ attributes: { id: 1, url: 'original.jpg', alt: '', caption: '' },
128
+ updatePayload: { id: 1, url: 'original.jpg' },
129
+ hookOptions: { onUrlChange },
130
+ } );
131
+ expect( onUrlChange ).not.toHaveBeenCalled();
132
+ } );
133
+
134
+ it( 'does not notify onUrlChange when the update carries no URL', async () => {
135
+ const onUrlChange = jest.fn();
136
+ await runModalUpdate( {
137
+ attributes: { id: 1, url: 'original.jpg', alt: '', caption: '' },
138
+ updatePayload: { id: 2 },
139
+ hookOptions: { onUrlChange },
140
+ } );
141
+ expect( onUrlChange ).not.toHaveBeenCalled();
142
+ } );
143
+
108
144
  it( 'returns no opener when the media editor modal setting is unavailable', () => {
109
145
  useRegistry.mockReturnValue( createRegistry() );
110
146
  mockMediaEditorModalSetting( undefined );
@@ -137,6 +137,7 @@ export function useOpenImageMediaEditorModal( {
137
137
  attributes,
138
138
  setAttributes,
139
139
  onClose,
140
+ onUrlChange,
140
141
  } ) {
141
142
  // Keep this hook private to the Image block and pass the block attributes
142
143
  // object so the callsite stays compact. Destructure only the attributes
@@ -246,6 +247,12 @@ export function useOpenImageMediaEditorModal( {
246
247
  if ( newId !== currentBlockAttributes.id ) {
247
248
  nextAttributes.id = newId;
248
249
  nextAttributes.url = newUrl ?? currentBlockAttributes.url;
250
+ if ( nextAttributes.url !== currentBlockAttributes.url ) {
251
+ // The block is about to point at a freshly generated file
252
+ // the browser hasn't loaded yet; let the caller show a
253
+ // loading state until its <img> fires load/error.
254
+ onUrlChange?.( nextAttributes.url );
255
+ }
249
256
  blockAttributesRef.current = {
250
257
  ...blockAttributesRef.current,
251
258
  id: nextAttributes.id,
@@ -294,7 +301,7 @@ export function useOpenImageMediaEditorModal( {
294
301
  setAttributes( nextAttributes );
295
302
  }
296
303
  },
297
- [ resolveFreshAttachmentRecord, setAttributes ]
304
+ [ onUrlChange, resolveFreshAttachmentRecord, setAttributes ]
298
305
  );
299
306
 
300
307
  const openImageMediaEditorModal = useCallback( async () => {
@@ -58,8 +58,9 @@ function render_block_core_latest_posts( $attributes ) {
58
58
  $block_core_latest_posts_excerpt_length = $attributes['excerptLength'];
59
59
  add_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 );
60
60
 
61
- if ( ! empty( $attributes['categories'] ) ) {
62
- $args['category__in'] = array_column( $attributes['categories'], 'id' );
61
+ $categories = $attributes['categories'] ?? null;
62
+ if ( ! empty( $categories ) && is_array( $categories ) ) {
63
+ $args['category__in'] = array_column( $categories, 'id' );
63
64
  }
64
65
  if ( isset( $attributes['selectedAuthor'] ) ) {
65
66
  $args['author'] = $attributes['selectedAuthor'];
@@ -16,6 +16,8 @@ export default function useIndentListItem( clientId ) {
16
16
  getSelectionEnd,
17
17
  hasMultiSelection,
18
18
  getMultiSelectedBlockClientIds,
19
+ getBlockRootClientId,
20
+ getBlockAttributes,
19
21
  } = useSelect( blockEditorStore );
20
22
  return useCallback( () => {
21
23
  const _hasMultiSelection = hasMultiSelection();
@@ -27,9 +29,16 @@ export default function useIndentListItem( clientId ) {
27
29
  );
28
30
  const previousSiblingId = getPreviousBlockClientId( clientId );
29
31
  const newListItem = cloneBlock( getBlock( previousSiblingId ) );
32
+ // Get the parent list's attributes to inherit the ordered property
33
+ const parentListId = getBlockRootClientId( clientId );
34
+ const parentListAttributes = getBlockAttributes( parentListId );
30
35
  // If the sibling has no innerBlocks, create a new `list` block.
31
36
  if ( ! newListItem.innerBlocks?.length ) {
32
- newListItem.innerBlocks = [ createBlock( 'core/list' ) ];
37
+ newListItem.innerBlocks = [
38
+ createBlock( 'core/list', {
39
+ ordered: parentListAttributes.ordered,
40
+ } ),
41
+ ];
33
42
  }
34
43
  // A list item usually has one `list`, but it's possible to have
35
44
  // more. So we need to preserve the previous `list` blocks and
package/src/math/edit.js CHANGED
@@ -1,27 +1,24 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { __, sprintf } from '@wordpress/i18n';
4
+ import { __ } from '@wordpress/i18n';
5
5
  import {
6
6
  useBlockProps,
7
7
  store as blockEditorStore,
8
8
  } from '@wordpress/block-editor';
9
9
  import {
10
- TextareaControl,
11
10
  Popover,
12
- __experimentalVStack as VStack,
13
11
  privateApis as componentsPrivateApis,
14
12
  } from '@wordpress/components';
15
13
  import { useState, useEffect, useRef } from '@wordpress/element';
16
14
  import { useDispatch } from '@wordpress/data';
17
- import { speak } from '@wordpress/a11y';
18
15
 
19
16
  /**
20
17
  * Internal dependencies
21
18
  */
22
19
  import { unlock } from '../lock-unlock';
23
20
 
24
- const { Badge: WCBadge } = unlock( componentsPrivateApis );
21
+ const { ValidatedTextareaControl } = unlock( componentsPrivateApis );
25
22
 
26
23
  export default function MathEdit( { attributes, setAttributes, isSelected } ) {
27
24
  const { latex, mathML } = attributes;
@@ -29,6 +26,7 @@ export default function MathEdit( { attributes, setAttributes, isSelected } ) {
29
26
  const [ error, setError ] = useState( null );
30
27
  const [ latexToMathML, setLatexToMathML ] = useState();
31
28
  const initialLatex = useRef( latex );
29
+ const formRef = useRef();
32
30
  const { __unstableMarkNextChangeAsNotPersistent } =
33
31
  useDispatch( blockEditorStore );
34
32
 
@@ -74,63 +72,48 @@ export default function MathEdit( { attributes, setAttributes, isSelected } ) {
74
72
  offset={ 8 }
75
73
  anchor={ blockRef }
76
74
  focusOnMount={ false }
75
+ // Surface any parsing error before focus leaves the block.
76
+ // An invalid field is refocused, keeping the popover open.
77
+ onFocusOutside={ () => formRef.current?.reportValidity() }
77
78
  __unstableSlotName="__unstable-block-tools-after"
78
79
  >
79
- <div style={ { padding: '4px', minWidth: '300px' } }>
80
- <VStack spacing={ 1 }>
81
- <TextareaControl
82
- __next40pxDefaultSize
83
- label={ __( 'LaTeX math syntax' ) }
84
- hideLabelFromVision
85
- value={ latex }
86
- className="wp-block-math__textarea-control"
87
- onChange={ ( newLatex ) => {
88
- if ( ! latexToMathML ) {
89
- setAttributes( { latex: newLatex } );
90
- return;
91
- }
92
- let newMathML = '';
93
- try {
94
- newMathML = latexToMathML( newLatex, {
95
- displayMode: true,
96
- } );
97
- setError( null );
98
- } catch ( err ) {
99
- setError( err.message );
100
- speak(
101
- sprintf(
102
- /* translators: %s: error message returned when parsing LaTeX. */
103
- __(
104
- 'Error parsing mathematical expression: %s'
105
- ),
106
- err.message
107
- )
108
- );
109
- }
110
- setAttributes( {
111
- mathML: newMathML,
112
- latex: newLatex,
80
+ <form
81
+ ref={ formRef }
82
+ style={ { padding: '4px', minWidth: '300px' } }
83
+ onSubmit={ ( event ) => event.preventDefault() }
84
+ >
85
+ <ValidatedTextareaControl
86
+ label={ __( 'LaTeX math syntax' ) }
87
+ hideLabelFromVision
88
+ value={ latex }
89
+ className="wp-block-math__textarea-control"
90
+ customValidity={
91
+ error
92
+ ? { type: 'invalid', message: error }
93
+ : undefined
94
+ }
95
+ onChange={ ( newLatex ) => {
96
+ if ( ! latexToMathML ) {
97
+ setAttributes( { latex: newLatex } );
98
+ return;
99
+ }
100
+ let newMathML = '';
101
+ try {
102
+ newMathML = latexToMathML( newLatex, {
103
+ displayMode: true,
113
104
  } );
114
- } }
115
- placeholder={ __( 'e.g., x^2, \\frac{a}{b}' ) }
116
- />
117
- { error && (
118
- <>
119
- <WCBadge
120
- intent="error"
121
- className="wp-block-math__error"
122
- >
123
- { sprintf(
124
- /* translators: %s: error message returned when parsing LaTeX. */
125
- __( 'Error: %s' ),
126
- error
127
- ) }
128
- </WCBadge>
129
- <style children=".wp-block-math__error .components-badge__content{white-space:normal}" />
130
- </>
131
- ) }
132
- </VStack>
133
- </div>
105
+ setError( null );
106
+ } catch ( err ) {
107
+ setError( err.message );
108
+ }
109
+ setAttributes( {
110
+ mathML: newMathML,
111
+ latex: newLatex,
112
+ } );
113
+ } }
114
+ placeholder={ __( 'e.g., x^2, \\frac{a}{b}' ) }
115
+ />
116
+ </form>
134
117
  </Popover>
135
118
  ) }
136
119
  </div>
@@ -499,6 +499,7 @@ function MediaTextEdit( {
499
499
  <BlockVerticalAlignmentControl
500
500
  onChange={ onVerticalAlignmentChange }
501
501
  value={ verticalAlignment }
502
+ label={ __( 'Align media and text vertically' ) }
502
503
  />
503
504
  <ToolbarButton
504
505
  icon={ pullLeft }
@@ -31,8 +31,17 @@ function render_block_core_media_text( $attributes, $content ) {
31
31
 
32
32
  $has_media_on_right = 'right' === ( $attributes['mediaPosition'] ?? null );
33
33
  $image_fill = (bool) ( $attributes['imageFill'] ?? false );
34
- $focal_point = isset( $attributes['focalPoint'] ) ? round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%' : '50% 50%';
35
- $unique_id = 'wp-block-media-text__media-' . wp_unique_id();
34
+ $focal_point_attr = $attributes['focalPoint'] ?? null;
35
+ $focal_point_x = null;
36
+ $focal_point_y = null;
37
+ if ( is_array( $focal_point_attr ) ) {
38
+ $focal_point_x = isset( $focal_point_attr['x'] ) && is_numeric( $focal_point_attr['x'] ) ? $focal_point_attr['x'] : null;
39
+ $focal_point_y = isset( $focal_point_attr['y'] ) && is_numeric( $focal_point_attr['y'] ) ? $focal_point_attr['y'] : null;
40
+ }
41
+ $focal_point = null !== $focal_point_x && null !== $focal_point_y
42
+ ? round( $focal_point_x * 100 ) . '% ' . round( $focal_point_y * 100 ) . '%'
43
+ : '50% 50%';
44
+ $unique_id = 'wp-block-media-text__media-' . wp_unique_id();
36
45
 
37
46
  $block_tag_processor = new WP_HTML_Tag_Processor( $content );
38
47
  $block_query = array(
package/src/more/edit.js CHANGED
@@ -76,10 +76,13 @@ export default function MoreEdit( {
76
76
  setAttributes( { customText: value } )
77
77
  }
78
78
  disableLineBreaks
79
- __unstableOnSplitAtEnd={ () =>
80
- insertBlocksAfter(
81
- createBlock( getDefaultBlockName() )
82
- )
79
+ __unstableOnSplitAtEnd={
80
+ insertBlocksAfter
81
+ ? () =>
82
+ insertBlocksAfter(
83
+ createBlock( getDefaultBlockName() )
84
+ )
85
+ : undefined
83
86
  }
84
87
  />
85
88
  </div>