@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
package/src/code/edit.js CHANGED
@@ -26,8 +26,13 @@ export default function CodeEdit( {
26
26
  aria-label={ __( 'Code' ) }
27
27
  preserveWhiteSpace
28
28
  __unstablePastePlainText
29
- __unstableOnSplitAtDoubleLineEnd={ () =>
30
- insertBlocksAfter( createBlock( getDefaultBlockName() ) )
29
+ __unstableOnSplitAtDoubleLineEnd={
30
+ insertBlocksAfter
31
+ ? () =>
32
+ insertBlocksAfter(
33
+ createBlock( getDefaultBlockName() )
34
+ )
35
+ : undefined
31
36
  }
32
37
  style={ { whiteSpace: 'break-spaces' } }
33
38
  />
@@ -100,6 +100,7 @@
100
100
  },
101
101
  "usesContext": [ "postId", "postType" ],
102
102
  "supports": {
103
+ "__experimentalOnEnter": true,
103
104
  "anchor": true,
104
105
  "align": true,
105
106
  "html": false,
@@ -35,6 +35,7 @@ export default function CoverBlockControls( {
35
35
  onEditMedia,
36
36
  editMediaButtonRef,
37
37
  showEditMediaButton,
38
+ isEditMediaDisabled,
38
39
  blockEditingMode,
39
40
  } ) {
40
41
  const {
@@ -123,6 +124,10 @@ export default function CoverBlockControls( {
123
124
  label={ __( 'Crop background image' ) }
124
125
  onClick={ onEditMedia }
125
126
  aria-haspopup="dialog"
127
+ // Disable rather than hide while the edited image
128
+ // loads, so the button keeps focus when the modal
129
+ // closes instead of dropping it to the canvas.
130
+ disabled={ isEditMediaDisabled }
126
131
  />
127
132
  ) }
128
133
  </BlockControls>
@@ -13,6 +13,7 @@ import {
13
13
  useLayoutEffect,
14
14
  useMemo,
15
15
  useRef,
16
+ useState,
16
17
  } from '@wordpress/element';
17
18
  import { Placeholder, SandBox, Spinner } from '@wordpress/components';
18
19
  import { compose, useResizeObserver } from '@wordpress/compose';
@@ -20,7 +21,6 @@ import {
20
21
  withColors,
21
22
  ColorPalette,
22
23
  useBlockProps,
23
- useSettings,
24
24
  useInnerBlocksProps,
25
25
  __experimentalUseGradient,
26
26
  store as blockEditorStore,
@@ -28,7 +28,8 @@ import {
28
28
  privateApis as blockEditorPrivateApis,
29
29
  } from '@wordpress/block-editor';
30
30
  import { __ } from '@wordpress/i18n';
31
- import { useSelect, useDispatch } from '@wordpress/data';
31
+ import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
32
+ import { createBlocksFromInnerBlocksTemplate } from '@wordpress/blocks';
32
33
  import { isBlobURL } from '@wordpress/blob';
33
34
  import { store as noticesStore } from '@wordpress/notices';
34
35
 
@@ -134,8 +135,9 @@ function CoverEdit( {
134
135
  []
135
136
  );
136
137
 
137
- const { __unstableMarkNextChangeAsNotPersistent } =
138
+ const { __unstableMarkNextChangeAsNotPersistent, replaceInnerBlocks } =
138
139
  useDispatch( blockEditorStore );
140
+ const registry = useRegistry();
139
141
 
140
142
  // Ref to access latest values after async operations (e.g. getMediaColor),
141
143
  // avoiding stale values that could overwrite concurrent remote changes.
@@ -225,6 +227,35 @@ function CoverEdit( {
225
227
  const { createErrorNotice } = useDispatch( noticesStore );
226
228
  const { gradientClass, gradientValue } = __experimentalUseGradient();
227
229
 
230
+ // Create the initial inner paragraph when the block gains a background
231
+ // and has no content yet.
232
+ const scaffoldInnerBlocks = () => {
233
+ const {
234
+ getBlocks,
235
+ isBlockSelected,
236
+ getSelectedBlocksInitialCaretPosition,
237
+ } = registry.select( blockEditorStore );
238
+ if ( getBlocks( clientId ).length > 0 ) {
239
+ return;
240
+ }
241
+ // Check for fontSize support before we pass a fontSize attribute to
242
+ // the innerBlocks.
243
+ const [ fontSizes ] = unlock(
244
+ registry.select( blockEditorStore )
245
+ ).getBlockSettings( clientId, 'typography.fontSizes' );
246
+ const hasFontSizes = fontSizes?.length > 0;
247
+ replaceInnerBlocks(
248
+ clientId,
249
+ createBlocksFromInnerBlocksTemplate(
250
+ getInnerBlocksTemplate( {
251
+ fontSize: hasFontSizes ? 'large' : undefined,
252
+ } )
253
+ ),
254
+ isBlockSelected( clientId ),
255
+ getSelectedBlocksInitialCaretPosition()
256
+ );
257
+ };
258
+
228
259
  const onSelectMedia = async ( newMedia ) => {
229
260
  const mediaAttributes = attributesFromMedia( newMedia );
230
261
  const isImage = [ newMedia?.type, newMedia?.media_type ].includes(
@@ -289,13 +320,17 @@ function CoverEdit( {
289
320
  }
290
321
  }
291
322
 
292
- setAttributes( {
293
- ...mediaAttributes,
294
- focalPoint: undefined,
295
- useFeaturedImage: undefined,
296
- dimRatio: newDimRatio,
297
- isDark: newIsDark,
298
- isUserOverlayColor: currentAttrs.isUserOverlayColor || false,
323
+ registry.batch( () => {
324
+ setAttributes( {
325
+ ...mediaAttributes,
326
+ focalPoint: undefined,
327
+ useFeaturedImage: undefined,
328
+ dimRatio: newDimRatio,
329
+ isDark: newIsDark,
330
+ isUserOverlayColor: currentAttrs.isUserOverlayColor || false,
331
+ } );
332
+
333
+ scaffoldInnerBlocks();
299
334
  } );
300
335
  };
301
336
 
@@ -345,9 +380,17 @@ function CoverEdit( {
345
380
  // Fold next attribute change into the same undo level as the setOverlayColor above.
346
381
  __unstableMarkNextChangeAsNotPersistent();
347
382
 
348
- setAttributes( {
349
- isUserOverlayColor: true,
350
- isDark: newIsDark,
383
+ registry.batch( () => {
384
+ setAttributes( {
385
+ isUserOverlayColor: true,
386
+ isDark: newIsDark,
387
+ } );
388
+
389
+ // Skip when the color is cleared: that returns the block to its
390
+ // placeholder, which only renders while there is no content.
391
+ if ( newOverlayColor ) {
392
+ scaffoldInnerBlocks();
393
+ }
351
394
  } );
352
395
  };
353
396
 
@@ -424,6 +467,12 @@ function CoverEdit( {
424
467
  return getBackgroundEmbedHtml( embedPreview.html );
425
468
  }, [ embedPreview, backgroundType ] );
426
469
 
470
+ // Set while the media editor has pointed the cover at a freshly
471
+ // generated file the browser hasn't finished loading; cleared by the
472
+ // background <img> load/error handlers (or immediately after
473
+ // setAttributes for CSS backgrounds, which never fire load events).
474
+ const [ isSwappingMedia, setIsSwappingMedia ] = useState( false );
475
+
427
476
  const isUploadingMedia = isTemporaryMedia( id, url );
428
477
 
429
478
  const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
@@ -477,22 +526,11 @@ function CoverEdit( {
477
526
  const ref = useRef();
478
527
  const blockProps = useBlockProps( { ref } );
479
528
 
480
- // Check for fontSize support before we pass a fontSize attribute to the innerBlocks.
481
- const [ fontSizes ] = useSettings( 'typography.fontSizes' );
482
- const hasFontSizes = fontSizes?.length > 0;
483
- const innerBlocksTemplate = getInnerBlocksTemplate( {
484
- fontSize: hasFontSizes ? 'large' : undefined,
485
- } );
486
-
487
529
  const innerBlocksProps = useInnerBlocksProps(
488
530
  {
489
531
  className: 'wp-block-cover__inner-container',
490
532
  },
491
533
  {
492
- // Avoid template sync when the `templateLock` value is `all` or `contentOnly`.
493
- // See: https://github.com/WordPress/gutenberg/pull/45632
494
- template: ! hasInnerBlocks ? innerBlocksTemplate : undefined,
495
- templateInsertUpdatesSelection: true,
496
534
  allowedBlocks,
497
535
  templateLock,
498
536
  dropZoneElement: ref.current,
@@ -526,6 +564,10 @@ function CoverEdit( {
526
564
  return;
527
565
  }
528
566
 
567
+ if ( newId !== id && newUrl ) {
568
+ setIsSwappingMedia( true );
569
+ }
570
+
529
571
  const nextAttributes = {
530
572
  id: newId,
531
573
  backgroundType: IMAGE_BACKGROUND_TYPE,
@@ -563,6 +605,17 @@ function CoverEdit( {
563
605
  }
564
606
 
565
607
  setAttributes( nextAttributes );
608
+
609
+ // A CSS background (parallax/repeated) renders as a div and
610
+ // never fires a load event; getMediaColor already fetched
611
+ // the file, so the swap is done once attributes are set.
612
+ const {
613
+ hasParallax: currentHasParallax,
614
+ isRepeated: currentIsRepeated,
615
+ } = propsRef.current.attributes;
616
+ if ( currentHasParallax || currentIsRepeated ) {
617
+ setIsSwappingMedia( false );
618
+ }
566
619
  },
567
620
  } );
568
621
  }, [
@@ -615,15 +668,24 @@ function CoverEdit( {
615
668
  averageBackgroundColor
616
669
  );
617
670
 
618
- setAttributes( {
619
- id: undefined,
620
- url: undefined,
621
- useFeaturedImage: newUseFeaturedImage,
622
- dimRatio: newDimRatio,
623
- backgroundType: useFeaturedImage
624
- ? IMAGE_BACKGROUND_TYPE
625
- : undefined,
626
- isDark: newIsDark,
671
+ registry.batch( () => {
672
+ setAttributes( {
673
+ id: undefined,
674
+ url: undefined,
675
+ useFeaturedImage: newUseFeaturedImage,
676
+ dimRatio: newDimRatio,
677
+ backgroundType: useFeaturedImage
678
+ ? IMAGE_BACKGROUND_TYPE
679
+ : undefined,
680
+ isDark: newIsDark,
681
+ } );
682
+
683
+ // Skip when the featured image is disabled: that can return the
684
+ // block to its placeholder, which only renders while there is no
685
+ // content.
686
+ if ( newUseFeaturedImage ) {
687
+ scaffoldInnerBlocks();
688
+ }
627
689
  } );
628
690
  };
629
691
 
@@ -640,6 +702,7 @@ function CoverEdit( {
640
702
  onEditMedia={ openCoverMediaEditorModal }
641
703
  editMediaButtonRef={ editMediaButtonRef }
642
704
  showEditMediaButton={ showEditMediaButton }
705
+ isEditMediaDisabled={ isSwappingMedia }
643
706
  />
644
707
  );
645
708
 
@@ -722,7 +785,7 @@ function CoverEdit( {
722
785
  {
723
786
  'is-dark-theme': isDark,
724
787
  'is-light': ! isDark,
725
- 'is-transient': isUploadingMedia,
788
+ 'is-transient': isUploadingMedia || isSwappingMedia,
726
789
  'has-parallax': hasParallax,
727
790
  'is-repeated': isRepeated,
728
791
  'has-custom-content-position':
@@ -762,6 +825,8 @@ function CoverEdit( {
762
825
  alt={ alt }
763
826
  src={ url }
764
827
  style={ mediaStyle }
828
+ onLoad={ () => setIsSwappingMedia( false ) }
829
+ onError={ () => setIsSwappingMedia( false ) }
765
830
  />
766
831
  ) : (
767
832
  <div
@@ -829,7 +894,7 @@ function CoverEdit( {
829
894
  />
830
895
  ) }
831
896
 
832
- { isUploadingMedia && <Spinner /> }
897
+ { ( isUploadingMedia || isSwappingMedia ) && <Spinner /> }
833
898
 
834
899
  <CoverPlaceholder
835
900
  disableMediaButtons
@@ -44,6 +44,12 @@
44
44
  .wp-block-cover__inner-container {
45
45
  z-index: 2;
46
46
  }
47
+
48
+ // Keep the loading spinner above the dim overlay (1) and the inner
49
+ // blocks (2) so it stays visible over child content while media loads.
50
+ .components-spinner {
51
+ z-index: 3;
52
+ }
47
53
  }
48
54
 
49
55
  // Shown while media is being uploaded
@@ -134,8 +134,15 @@ function render_block_core_cover( $attributes, $content ) {
134
134
  return $content;
135
135
  }
136
136
 
137
- $object_position = isset( $attributes['focalPoint'] )
138
- ? round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%'
137
+ $cover_focal_point = $attributes['focalPoint'] ?? null;
138
+ $focal_point_x = null;
139
+ $focal_point_y = null;
140
+ if ( is_array( $cover_focal_point ) ) {
141
+ $focal_point_x = isset( $cover_focal_point['x'] ) && is_numeric( $cover_focal_point['x'] ) ? $cover_focal_point['x'] : null;
142
+ $focal_point_y = isset( $cover_focal_point['y'] ) && is_numeric( $cover_focal_point['y'] ) ? $cover_focal_point['y'] : null;
143
+ }
144
+ $object_position = null !== $focal_point_x && null !== $focal_point_y
145
+ ? round( $focal_point_x * 100 ) . '% ' . round( $focal_point_y * 100 ) . '%'
139
146
  : null;
140
147
 
141
148
  if ( ! ( $attributes['hasParallax'] || $attributes['isRepeated'] ) ) {
package/src/editor.scss CHANGED
@@ -48,6 +48,7 @@
48
48
  @use "./social-link/editor.scss" as *;
49
49
  @use "./social-links/editor.scss" as *;
50
50
  @use "./spacer/editor.scss" as *;
51
+ @use "./tab-list/editor.scss" as *;
51
52
  @use "./table/editor.scss" as *;
52
53
  @use "./template-part/editor.scss" as *;
53
54
  @use "./term-template/editor.scss" as *;
package/src/embed/edit.js CHANGED
@@ -239,7 +239,9 @@ const EmbedEdit = ( props ) => {
239
239
  value={ url }
240
240
  cannotEmbed={ cannotEmbed }
241
241
  onChange={ ( value ) => setURL( value ) }
242
- fallback={ () => fallback( url, onReplace ) }
242
+ fallback={
243
+ onReplace ? () => fallback( url, onReplace ) : undefined
244
+ }
243
245
  tryAgain={ () => {
244
246
  invalidateResolution( 'getEmbedPreview', [ url ] );
245
247
  } }
@@ -70,13 +70,15 @@ export default function EmbedPlaceholder( {
70
70
  >
71
71
  { _x( 'Try again', 'button label' ) }
72
72
  </Button>
73
- <Button
74
- __next40pxDefaultSize
75
- variant="secondary"
76
- onClick={ fallback }
77
- >
78
- { _x( 'Convert to link', 'button label' ) }
79
- </Button>
73
+ { fallback && (
74
+ <Button
75
+ __next40pxDefaultSize
76
+ variant="secondary"
77
+ onClick={ fallback }
78
+ >
79
+ { _x( 'Convert to link', 'button label' ) }
80
+ </Button>
81
+ ) }
80
82
  </Stack>
81
83
  </Stack>
82
84
  ) }
@@ -20,12 +20,13 @@ function render_block_core_form( $attributes, $content ) {
20
20
  $processed_content->next_tag( 'form' );
21
21
 
22
22
  // Get the action for this form.
23
- $action = '';
24
- if ( isset( $attributes['action'] ) ) {
23
+ $action = '';
24
+ $form_action = $attributes['action'] ?? null;
25
+ if ( is_string( $form_action ) ) {
25
26
  $action = str_replace(
26
27
  array( '{SITE_URL}', '{ADMIN_URL}' ),
27
28
  array( site_url(), admin_url() ),
28
- $attributes['action']
29
+ $form_action
29
30
  );
30
31
  }
31
32
  $processed_content->set_attribute( 'action', esc_attr( $action ) );
@@ -55,7 +56,8 @@ function render_block_core_form( $attributes, $content ) {
55
56
  * @return string The extra fields.
56
57
  */
57
58
  function block_core_form_extra_fields_comment_form( $extra_fields, $attributes ) {
58
- if ( ! empty( $attributes['action'] ) && str_ends_with( $attributes['action'], '/wp-comments-post.php' ) ) {
59
+ $form_action = $attributes['action'] ?? null;
60
+ if ( ! empty( $form_action ) && is_string( $form_action ) && str_ends_with( $form_action, '/wp-comments-post.php' ) ) {
59
61
  $extra_fields .= '<input type="hidden" name="comment_post_ID" value="' . get_the_ID() . '" id="comment_post_ID">';
60
62
  }
61
63
  return $extra_fields;
@@ -74,14 +74,44 @@ function OrderControl( { orderby, order, onChange } ) {
74
74
  );
75
75
  }
76
76
 
77
+ /**
78
+ * Confirmation for leaving dynamic mode, shown from both the block toolbar and
79
+ * the Source panel so the two entry points explain the change identically.
80
+ *
81
+ * Detaching keeps the images the gallery currently shows but breaks the link to
82
+ * its source, so it's worth confirming — mirroring the dialog `GallerySourcePanel`
83
+ * shows for the opposite direction.
84
+ *
85
+ * @param {Object} props
86
+ * @param {Function} props.onConfirm Called when the user confirms detaching.
87
+ * @param {Function} props.onCancel Called when the user dismisses the dialog.
88
+ */
89
+ function DetachGalleryDialog( { onConfirm, onCancel } ) {
90
+ return (
91
+ <ConfirmDialog
92
+ isOpen
93
+ title={ __( 'Detach Gallery' ) }
94
+ __experimentalHideHeader={ false }
95
+ confirmButtonText={ __( 'Detach' ) }
96
+ onConfirm={ onConfirm }
97
+ onCancel={ onCancel }
98
+ size="medium"
99
+ >
100
+ { __(
101
+ 'The gallery displays the images attached to the post. Detaching will enable you to add, delete, or reorder images. However, new attachments will no longer be added automatically.'
102
+ ) }
103
+ </ConfirmDialog>
104
+ );
105
+ }
106
+
77
107
  /**
78
108
  * The Gallery block's "Source" inspector panel.
79
109
  *
80
- * In dynamic mode it shows the resolved source, a control to convert back to
81
- * individual images, and the source ordering. In static mode it offers the
82
- * entry point into dynamic mode and, since switching discards any hand-added
83
- * images, owns the confirmation dialog for that one-way change. Rendered inside
84
- * the block's `InspectorControls`, alongside the Settings panel.
110
+ * In dynamic mode it shows the resolved source, a control to detach the gallery
111
+ * from it, and the source ordering. In static mode it offers the entry point
112
+ * into dynamic mode. Either direction is a one-way change, so both are behind a
113
+ * confirmation dialog this panel owns. Rendered inside the block's
114
+ * `InspectorControls`, alongside the Settings panel.
85
115
  *
86
116
  * @param {Object} props
87
117
  * @param {Object} props.dynamic The `useDynamicGallery` result.
@@ -110,6 +140,7 @@ export function GallerySourcePanel( {
110
140
  const isDynamic = !! dynamicContent;
111
141
 
112
142
  const [ isConfirming, setIsConfirming ] = useState( false );
143
+ const [ isConfirmingDetach, setIsConfirmingDetach ] = useState( false );
113
144
 
114
145
  // Entering dynamic mode discards any hand-added images, so confirm first
115
146
  // when there are images to lose; otherwise switch straight away.
@@ -123,63 +154,76 @@ export function GallerySourcePanel( {
123
154
 
124
155
  if ( isDynamic ) {
125
156
  return (
126
- <ToolsPanel
127
- label={ __( 'Source' ) }
128
- resetAll={ resetSource }
129
- dropdownMenuProps={ dropdownMenuProps }
130
- >
131
- <div className="wp-block-gallery__source-settings">
132
- <p className="wp-block-gallery__source-description">
133
- { sourceDescriptor?.description ??
134
- __( 'Dynamic images.' ) }
135
- </p>
136
- <Button
137
- __next40pxDefaultSize
138
- variant="secondary"
139
- onClick={ convertToStatic }
140
- // Guard the race where the media is still resolving:
141
- // converting now would map over an incomplete (or empty)
142
- // list and produce a gallery missing images.
143
- disabled={ isResolvingDynamic }
144
- accessibleWhenDisabled
145
- >
146
- { __( 'Convert to individual images' ) }
147
- </Button>
148
- </div>
149
- { hasMoreImagesThanCap && (
150
- <Notice
151
- className="wp-block-gallery__source-notice"
152
- status="warning"
153
- isDismissible={ false }
154
- >
155
- { sprintf(
156
- /* translators: 1: number of images shown. 2: total number of matching images. */
157
- __(
158
- 'Only the first %1$d of %2$d images will be displayed.'
159
- ),
160
- MAX_IMAGES,
161
- dynamicMediaTotal
162
- ) }
163
- </Notice>
164
- ) }
165
- <ToolsPanelItem
166
- isShownByDefault
167
- label={ __( 'Order by' ) }
168
- hasValue={ () =>
169
- sourceOrderby !== DEFAULT_ORDERBY ||
170
- sourceOrder !== DEFAULT_ORDER
171
- }
172
- onDeselect={ () => setSourceOrder( undefined, undefined ) }
157
+ <>
158
+ <ToolsPanel
159
+ label={ __( 'Source' ) }
160
+ resetAll={ resetSource }
161
+ dropdownMenuProps={ dropdownMenuProps }
173
162
  >
174
- <OrderControl
175
- orderby={ sourceOrderby }
176
- order={ sourceOrder }
177
- onChange={ ( { orderby, order } ) =>
178
- setSourceOrder( orderby, order )
163
+ <div className="wp-block-gallery__source-settings">
164
+ <p className="wp-block-gallery__source-description">
165
+ { sourceDescriptor?.description ??
166
+ __( 'Dynamic images.' ) }
167
+ </p>
168
+ <Button
169
+ __next40pxDefaultSize
170
+ variant="secondary"
171
+ onClick={ () => setIsConfirmingDetach( true ) }
172
+ // Guard the race where the media is still resolving:
173
+ // detaching now would map over an incomplete (or
174
+ // empty) list and produce a gallery missing images.
175
+ disabled={ isResolvingDynamic }
176
+ accessibleWhenDisabled
177
+ >
178
+ { __( 'Detach Gallery' ) }
179
+ </Button>
180
+ </div>
181
+ { hasMoreImagesThanCap && (
182
+ <Notice
183
+ className="wp-block-gallery__source-notice"
184
+ status="warning"
185
+ isDismissible={ false }
186
+ >
187
+ { sprintf(
188
+ /* translators: 1: number of images shown. 2: total number of matching images. */
189
+ __(
190
+ 'Only the first %1$d of %2$d images will be displayed.'
191
+ ),
192
+ MAX_IMAGES,
193
+ dynamicMediaTotal
194
+ ) }
195
+ </Notice>
196
+ ) }
197
+ <ToolsPanelItem
198
+ isShownByDefault
199
+ label={ __( 'Order by' ) }
200
+ hasValue={ () =>
201
+ sourceOrderby !== DEFAULT_ORDERBY ||
202
+ sourceOrder !== DEFAULT_ORDER
203
+ }
204
+ onDeselect={ () =>
205
+ setSourceOrder( undefined, undefined )
179
206
  }
207
+ >
208
+ <OrderControl
209
+ orderby={ sourceOrderby }
210
+ order={ sourceOrder }
211
+ onChange={ ( { orderby, order } ) =>
212
+ setSourceOrder( orderby, order )
213
+ }
214
+ />
215
+ </ToolsPanelItem>
216
+ </ToolsPanel>
217
+ { isConfirmingDetach && (
218
+ <DetachGalleryDialog
219
+ onConfirm={ () => {
220
+ convertToStatic();
221
+ setIsConfirmingDetach( false );
222
+ } }
223
+ onCancel={ () => setIsConfirmingDetach( false ) }
180
224
  />
181
- </ToolsPanelItem>
182
- </ToolsPanel>
225
+ ) }
226
+ </>
183
227
  );
184
228
  }
185
229
 
@@ -266,7 +310,8 @@ function GalleryImagesPreview( { imageBlocks } ) {
266
310
  /**
267
311
  * Renders a dynamic-mode gallery on the canvas:
268
312
  *
269
- * - a block-toolbar control to convert back to individual images;
313
+ * - a block-toolbar control to detach the gallery from its source, confirmed in
314
+ * a dialog;
270
315
  * - the gallery `<figure>` wrapper holding a non-editable preview of the
271
316
  * resolved media (or a placeholder while resolving / when nothing is found),
272
317
  * with the gallery's provided context so the previewed images inherit
@@ -306,13 +351,15 @@ export function GalleryDynamicView( {
306
351
  convertToStatic,
307
352
  } = dynamic;
308
353
 
309
- // Converting to a static gallery materializes editable inner blocks, which
310
- // is a structural change. Only offer it when the block is fully editable:
354
+ // Detaching the gallery materializes editable inner blocks, which is a
355
+ // structural change. Only offer it when the block is fully editable:
311
356
  // under a content lock (e.g. inside a `contentOnly` group) the editing mode
312
357
  // is `'contentOnly'`/`'disabled'`, where structural toolbar controls are
313
358
  // hidden and the conversion shouldn't be possible.
314
359
  const blockEditingMode = useBlockEditingMode();
315
360
 
361
+ const [ isConfirmingDetach, setIsConfirmingDetach ] = useState( false );
362
+
316
363
  // Empty-state copy for the preview. Framed as forward-looking ("… will appear
317
364
  // here") rather than as an error, since the same empty result covers both a
318
365
  // post with no matching images and a template with no post in context yet —
@@ -326,18 +373,30 @@ export function GalleryDynamicView( {
326
373
  return (
327
374
  <>
328
375
  { blockEditingMode === 'default' && (
329
- <BlockControls group="other">
330
- <ToolbarButton
331
- onClick={ convertToStatic }
332
- // Same guard as the inspector's "Convert to individual
333
- // images": both call `convertToStatic`, which would map over
334
- // a still-resolving (or empty) media list. (`ToolbarButton`
335
- // stays focusable when disabled by default.)
336
- disabled={ isResolvingDynamic }
337
- >
338
- { __( 'Convert to images' ) }
339
- </ToolbarButton>
340
- </BlockControls>
376
+ <>
377
+ <BlockControls group="other">
378
+ <ToolbarButton
379
+ onClick={ () => setIsConfirmingDetach( true ) }
380
+ // Same guard as the inspector's "Detach Gallery": both end in
381
+ // `convertToStatic`, which would map over a
382
+ // still-resolving (or empty) media list.
383
+ // (`ToolbarButton` stays focusable when disabled by
384
+ // default.)
385
+ disabled={ isResolvingDynamic }
386
+ >
387
+ { __( 'Detach' ) }
388
+ </ToolbarButton>
389
+ </BlockControls>
390
+ { isConfirmingDetach && (
391
+ <DetachGalleryDialog
392
+ onConfirm={ () => {
393
+ convertToStatic();
394
+ setIsConfirmingDetach( false );
395
+ } }
396
+ onCancel={ () => setIsConfirmingDetach( false ) }
397
+ />
398
+ ) }
399
+ </>
341
400
  ) }
342
401
  <figure { ...blockProps }>
343
402
  { dynamicImageBlocks.length ? (