@wordpress/block-library 10.4.0 → 10.5.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 (239) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/build/accordion/view.cjs +2 -5
  3. package/build/accordion/view.cjs.map +2 -2
  4. package/build/accordion-heading/block.json +1 -0
  5. package/build/audio/edit.cjs.map +2 -2
  6. package/build/avatar/edit.cjs.map +2 -2
  7. package/build/block/block.json +2 -1
  8. package/build/column/edit.cjs +1 -1
  9. package/build/column/edit.cjs.map +2 -2
  10. package/build/columns/edit.cjs.map +2 -2
  11. package/build/columns/transforms.cjs +161 -2
  12. package/build/columns/transforms.cjs.map +2 -2
  13. package/build/cover/edit/block-controls.cjs +1 -1
  14. package/build/cover/edit/block-controls.cjs.map +2 -2
  15. package/build/cover/edit/index.cjs +1 -1
  16. package/build/cover/edit/index.cjs.map +2 -2
  17. package/build/cover/edit/inspector-controls.cjs.map +3 -3
  18. package/build/embed/embed-placeholder.cjs.map +2 -2
  19. package/build/gallery/transforms.cjs +8 -0
  20. package/build/gallery/transforms.cjs.map +2 -2
  21. package/build/group/block.json +1 -1
  22. package/build/heading/transforms.cjs +36 -0
  23. package/build/heading/transforms.cjs.map +2 -2
  24. package/build/heading/variations.cjs +13 -1
  25. package/build/heading/variations.cjs.map +2 -2
  26. package/build/home-link/edit.cjs.map +3 -3
  27. package/build/icon/edit.cjs +7 -1
  28. package/build/icon/edit.cjs.map +2 -2
  29. package/build/image/image.cjs +1 -1
  30. package/build/image/image.cjs.map +3 -3
  31. package/build/list/block.json +1 -0
  32. package/build/math/edit.cjs +4 -5
  33. package/build/math/edit.cjs.map +3 -3
  34. package/build/media-text/edit.cjs.map +3 -3
  35. package/build/more/block.json +1 -1
  36. package/build/navigation-link/shared/controls.cjs.map +3 -3
  37. package/build/playlist/block.json +0 -1
  38. package/build/playlist/edit.cjs +46 -22
  39. package/build/playlist/edit.cjs.map +2 -2
  40. package/build/playlist-track/block.json +16 -8
  41. package/build/playlist-track/edit.cjs +74 -67
  42. package/build/playlist-track/edit.cjs.map +3 -3
  43. package/build/post-template/edit.cjs +5 -1
  44. package/build/post-template/edit.cjs.map +2 -2
  45. package/build/post-title/edit.cjs +3 -8
  46. package/build/post-title/edit.cjs.map +2 -2
  47. package/build/private-apis.cjs +0 -12
  48. package/build/private-apis.cjs.map +3 -3
  49. package/build/query/block.json +3 -0
  50. package/build/query-no-results/block.json +16 -1
  51. package/build/rss/edit.cjs.map +2 -2
  52. package/build/site-logo/edit.cjs +1 -1
  53. package/build/site-logo/edit.cjs.map +2 -2
  54. package/build/site-title/edit.cjs +2 -6
  55. package/build/site-title/edit.cjs.map +2 -2
  56. package/build/table-of-contents/block.json +2 -1
  57. package/build/table-of-contents/deprecated.cjs +165 -0
  58. package/build/table-of-contents/deprecated.cjs.map +7 -0
  59. package/build/table-of-contents/index.cjs +2 -2
  60. package/build/table-of-contents/index.cjs.map +3 -3
  61. package/build/tabs/view.cjs +32 -2
  62. package/build/tabs/view.cjs.map +2 -2
  63. package/build/term-template/edit.cjs +5 -1
  64. package/build/term-template/edit.cjs.map +2 -2
  65. package/build-module/accordion/view.mjs +2 -5
  66. package/build-module/accordion/view.mjs.map +2 -2
  67. package/build-module/accordion-heading/block.json +1 -0
  68. package/build-module/audio/edit.mjs.map +2 -2
  69. package/build-module/avatar/edit.mjs.map +2 -2
  70. package/build-module/block/block.json +2 -1
  71. package/build-module/column/edit.mjs +1 -1
  72. package/build-module/column/edit.mjs.map +2 -2
  73. package/build-module/columns/edit.mjs.map +2 -2
  74. package/build-module/columns/transforms.mjs +164 -3
  75. package/build-module/columns/transforms.mjs.map +2 -2
  76. package/build-module/cover/edit/block-controls.mjs +1 -1
  77. package/build-module/cover/edit/block-controls.mjs.map +2 -2
  78. package/build-module/cover/edit/index.mjs +1 -1
  79. package/build-module/cover/edit/index.mjs.map +2 -2
  80. package/build-module/cover/edit/inspector-controls.mjs +2 -2
  81. package/build-module/cover/edit/inspector-controls.mjs.map +2 -2
  82. package/build-module/embed/embed-placeholder.mjs +2 -2
  83. package/build-module/embed/embed-placeholder.mjs.map +1 -1
  84. package/build-module/gallery/transforms.mjs +8 -0
  85. package/build-module/gallery/transforms.mjs.map +2 -2
  86. package/build-module/group/block.json +1 -1
  87. package/build-module/heading/transforms.mjs +36 -0
  88. package/build-module/heading/transforms.mjs.map +2 -2
  89. package/build-module/heading/variations.mjs +13 -1
  90. package/build-module/heading/variations.mjs.map +2 -2
  91. package/build-module/home-link/edit.mjs +2 -2
  92. package/build-module/home-link/edit.mjs.map +2 -2
  93. package/build-module/icon/edit.mjs +7 -1
  94. package/build-module/icon/edit.mjs.map +2 -2
  95. package/build-module/image/image.mjs +4 -4
  96. package/build-module/image/image.mjs.map +2 -2
  97. package/build-module/list/block.json +1 -0
  98. package/build-module/math/edit.mjs +4 -8
  99. package/build-module/math/edit.mjs.map +2 -2
  100. package/build-module/media-text/edit.mjs +2 -2
  101. package/build-module/media-text/edit.mjs.map +2 -2
  102. package/build-module/more/block.json +1 -1
  103. package/build-module/navigation-link/shared/controls.mjs +2 -2
  104. package/build-module/navigation-link/shared/controls.mjs.map +2 -2
  105. package/build-module/playlist/block.json +0 -1
  106. package/build-module/playlist/edit.mjs +46 -22
  107. package/build-module/playlist/edit.mjs.map +2 -2
  108. package/build-module/playlist-track/block.json +16 -8
  109. package/build-module/playlist-track/edit.mjs +76 -69
  110. package/build-module/playlist-track/edit.mjs.map +2 -2
  111. package/build-module/post-template/edit.mjs +5 -1
  112. package/build-module/post-template/edit.mjs.map +2 -2
  113. package/build-module/post-title/edit.mjs +3 -8
  114. package/build-module/post-title/edit.mjs.map +2 -2
  115. package/build-module/private-apis.mjs +0 -2
  116. package/build-module/private-apis.mjs.map +2 -2
  117. package/build-module/query/block.json +3 -0
  118. package/build-module/query-no-results/block.json +16 -1
  119. package/build-module/rss/edit.mjs +2 -2
  120. package/build-module/rss/edit.mjs.map +1 -1
  121. package/build-module/site-logo/edit.mjs +1 -1
  122. package/build-module/site-logo/edit.mjs.map +2 -2
  123. package/build-module/site-title/edit.mjs +2 -6
  124. package/build-module/site-title/edit.mjs.map +2 -2
  125. package/build-module/table-of-contents/block.json +2 -1
  126. package/build-module/table-of-contents/deprecated.mjs +144 -0
  127. package/build-module/table-of-contents/deprecated.mjs.map +7 -0
  128. package/build-module/table-of-contents/index.mjs +2 -2
  129. package/build-module/table-of-contents/index.mjs.map +2 -2
  130. package/build-module/tabs/view.mjs +32 -2
  131. package/build-module/tabs/view.mjs.map +2 -2
  132. package/build-module/term-template/edit.mjs +5 -1
  133. package/build-module/term-template/edit.mjs.map +2 -2
  134. package/build-style/categories/style-rtl.css +2 -1
  135. package/build-style/categories/style.css +2 -1
  136. package/build-style/comments/editor-rtl.css +8 -10
  137. package/build-style/comments/editor.css +8 -10
  138. package/build-style/comments/style-rtl.css +8 -7
  139. package/build-style/comments/style.css +8 -7
  140. package/build-style/editor-rtl.css +8 -10
  141. package/build-style/editor.css +8 -10
  142. package/build-style/form-input/style-rtl.css +7 -7
  143. package/build-style/form-input/style.css +7 -7
  144. package/build-style/post-comments-form/style-rtl.css +7 -6
  145. package/build-style/post-comments-form/style.css +7 -6
  146. package/build-style/query-no-results/style-rtl.css +3 -0
  147. package/build-style/query-no-results/style.css +3 -0
  148. package/build-style/search/style-rtl.css +1 -1
  149. package/build-style/search/style.css +1 -1
  150. package/build-style/search/theme-rtl.css +1 -1
  151. package/build-style/search/theme.css +1 -1
  152. package/build-style/style-rtl.css +34 -23
  153. package/build-style/style.css +34 -23
  154. package/build-style/tab-panel/style-rtl.css +5 -1
  155. package/build-style/tab-panel/style.css +5 -1
  156. package/build-style/theme-rtl.css +1 -1
  157. package/build-style/theme.css +1 -1
  158. package/package.json +43 -43
  159. package/src/accordion/view.js +2 -5
  160. package/src/accordion-heading/README.md +1 -0
  161. package/src/accordion-heading/block.json +1 -0
  162. package/src/audio/edit.js +13 -0
  163. package/src/avatar/edit.js +78 -0
  164. package/src/block/README.md +1 -0
  165. package/src/block/block.json +2 -1
  166. package/src/categories/style.scss +4 -4
  167. package/src/column/edit.js +15 -1
  168. package/src/columns/edit.js +18 -0
  169. package/src/columns/test/transforms.js +517 -1
  170. package/src/columns/transforms.js +207 -2
  171. package/src/comments/style.scss +7 -10
  172. package/src/cover/edit/block-controls.js +1 -1
  173. package/src/cover/edit/index.js +1 -1
  174. package/src/cover/edit/inspector-controls.js +2 -2
  175. package/src/embed/embed-placeholder.js +2 -2
  176. package/src/form-input/style.scss +2 -2
  177. package/src/gallery/test/edit.js +57 -0
  178. package/src/gallery/test/transforms.js +57 -0
  179. package/src/gallery/transforms.js +8 -0
  180. package/src/group/README.md +1 -1
  181. package/src/group/block.json +1 -1
  182. package/src/heading/transforms.js +36 -0
  183. package/src/heading/variations.js +12 -0
  184. package/src/home-link/edit.js +2 -2
  185. package/src/icon/edit.js +9 -1
  186. package/src/image/image.js +4 -4
  187. package/src/list/README.md +1 -0
  188. package/src/list/block.json +1 -0
  189. package/src/math/edit.js +4 -9
  190. package/src/media-text/edit.js +2 -2
  191. package/src/more/README.md +1 -1
  192. package/src/more/block.json +1 -1
  193. package/src/navigation/index.php +8 -2
  194. package/src/navigation-link/shared/controls.js +2 -2
  195. package/src/playlist/block.json +0 -1
  196. package/src/playlist/edit.js +54 -21
  197. package/src/playlist/test/edit-component.js +78 -27
  198. package/src/playlist-track/README.md +8 -8
  199. package/src/playlist-track/block.json +16 -8
  200. package/src/playlist-track/edit.js +56 -27
  201. package/src/playlist-track/test/edit.js +145 -41
  202. package/src/post-comments-form/style.scss +7 -9
  203. package/src/post-template/edit.js +10 -1
  204. package/src/post-title/edit.js +2 -9
  205. package/src/private-apis.js +0 -2
  206. package/src/query/README.md +2 -0
  207. package/src/query/block.json +3 -0
  208. package/src/query-no-results/README.md +3 -0
  209. package/src/query-no-results/block.json +16 -1
  210. package/src/query-no-results/style.scss +5 -0
  211. package/src/rss/edit.js +2 -2
  212. package/src/search/style.scss +1 -1
  213. package/src/search/theme.scss +2 -4
  214. package/src/site-logo/edit.js +1 -1
  215. package/src/site-title/edit.js +1 -10
  216. package/src/social-link/index.php +6 -13
  217. package/src/style.scss +1 -0
  218. package/src/tab-panel/index.php +14 -5
  219. package/src/tab-panel/style.scss +7 -1
  220. package/src/table-of-contents/README.md +6 -6
  221. package/src/table-of-contents/block.json +2 -1
  222. package/src/table-of-contents/deprecated.js +173 -0
  223. package/src/table-of-contents/index.js +2 -2
  224. package/src/table-of-contents/index.php +16 -3
  225. package/src/tabs/index.php +5 -5
  226. package/src/tabs/view.js +32 -2
  227. package/src/term-description/index.php +1 -1
  228. package/src/term-template/edit.js +10 -1
  229. package/src/utils/test/waveform-player-dependency.js +0 -74
  230. package/build/block-keyboard-shortcuts/index.cjs +0 -131
  231. package/build/block-keyboard-shortcuts/index.cjs.map +0 -7
  232. package/build/table-of-contents/save.cjs +0 -55
  233. package/build/table-of-contents/save.cjs.map +0 -7
  234. package/build-module/block-keyboard-shortcuts/index.mjs +0 -113
  235. package/build-module/block-keyboard-shortcuts/index.mjs.map +0 -7
  236. package/build-module/table-of-contents/save.mjs +0 -24
  237. package/build-module/table-of-contents/save.mjs.map +0 -7
  238. package/src/block-keyboard-shortcuts/index.js +0 -135
  239. package/src/table-of-contents/save.js +0 -22
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "supports": {
39
39
  "anchor": true,
40
+ "align": [ "wide", "full" ],
40
41
  "html": false,
41
42
  "__experimentalBorder": {
42
43
  "color": true,
package/src/math/edit.js CHANGED
@@ -3,15 +3,10 @@ import {
3
3
  useBlockProps,
4
4
  store as blockEditorStore,
5
5
  } from '@wordpress/block-editor';
6
- import {
7
- Popover,
8
- privateApis as componentsPrivateApis,
9
- } from '@wordpress/components';
6
+ import { Popover } from '@wordpress/components';
7
+ import { ValidatedTextareaControl } from '@wordpress/ui';
10
8
  import { useState, useEffect, useRef } from '@wordpress/element';
11
9
  import { useDispatch } from '@wordpress/data';
12
- import { unlock } from '../lock-unlock';
13
-
14
- const { ValidatedTextareaControl } = unlock( componentsPrivateApis );
15
10
 
16
11
  export default function MathEdit( { attributes, setAttributes, isSelected } ) {
17
12
  const { latex, mathML } = attributes;
@@ -78,14 +73,14 @@ export default function MathEdit( { attributes, setAttributes, isSelected } ) {
78
73
  <ValidatedTextareaControl
79
74
  label={ __( 'LaTeX math syntax' ) }
80
75
  hideLabelFromVision
81
- value={ latex }
76
+ value={ latex ?? '' }
82
77
  className="wp-block-math__textarea-control"
83
78
  customValidity={
84
79
  error
85
80
  ? { type: 'invalid', message: error }
86
81
  : undefined
87
82
  }
88
- onChange={ ( newLatex ) => {
83
+ onValueChange={ ( newLatex ) => {
89
84
  if ( ! latexToMathML ) {
90
85
  setAttributes( { latex: newLatex } );
91
86
  return;
@@ -15,7 +15,7 @@ import {
15
15
  } from '@wordpress/block-editor';
16
16
  import {
17
17
  RangeControl,
18
- TextareaControl,
18
+ TextareaControl as WCTextareaControl,
19
19
  ToggleControl,
20
20
  ToolbarButton,
21
21
  ExternalLink,
@@ -431,7 +431,7 @@ function MediaTextEdit( {
431
431
  hasValue={ () => !! mediaAlt }
432
432
  onDeselect={ () => setAttributes( { mediaAlt: '' } ) }
433
433
  >
434
- <TextareaControl
434
+ <WCTextareaControl
435
435
  label={ __( 'Alternative text' ) }
436
436
  value={ mediaAlt }
437
437
  onChange={ onMediaAltChange }
@@ -1,7 +1,7 @@
1
1
  # More
2
2
 
3
3
  <!-- START TOKEN(Autogenerated block API docs) -->
4
- Content before this block will be shown in the excerpt on your archives page.
4
+ Content before this block is shown in the excerpt on blog and archive pages. Where a theme shows the full content instead, visitors get a “Read more” link to the rest of the post.
5
5
 
6
6
  - **Name:** `core/more`
7
7
  - **Category:** [design](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/core-blocks-design/)
@@ -4,7 +4,7 @@
4
4
  "name": "core/more",
5
5
  "title": "More",
6
6
  "category": "design",
7
- "description": "Content before this block will be shown in the excerpt on your archives page.",
7
+ "description": "Content before this block is shown in the excerpt on blog and archive pages. Where a theme shows the full content instead, visitors get a “Read more” link to the rest of the post.",
8
8
  "keywords": [ "read more" ],
9
9
  "textdomain": "default",
10
10
  "attributes": {
@@ -279,8 +279,14 @@ class WP_Navigation_Block_Renderer {
279
279
 
280
280
  foreach ( $inner_blocks as $inner_block ) {
281
281
  $inner_block_markup = static::get_markup_for_inner_block( $inner_block );
282
- $p = new WP_HTML_Tag_Processor( $inner_block_markup );
283
- $is_list_item = $p->next_tag( 'LI' );
282
+ // Skip hidden blocks (e.g. hidden via block visibility) that render
283
+ // as an empty string. Without this check, empty markup is mistaken
284
+ // for a non-list-item and incorrectly closes the open <ul>.
285
+ if ( '' === $inner_block_markup ) {
286
+ continue;
287
+ }
288
+ $p = new WP_HTML_Tag_Processor( $inner_block_markup );
289
+ $is_list_item = $p->next_tag( 'LI' );
284
290
 
285
291
  if ( $is_list_item && ! $is_list_open ) {
286
292
  $is_list_open = true;
@@ -4,7 +4,7 @@ import {
4
4
  __experimentalToolsPanelItem as ToolsPanelItem,
5
5
  CheckboxControl,
6
6
  TextControl,
7
- TextareaControl,
7
+ TextareaControl as WCTextareaControl,
8
8
  } from '@wordpress/components';
9
9
  import { __, sprintf } from '@wordpress/i18n';
10
10
  import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
@@ -265,7 +265,7 @@ export function Controls( {
265
265
  onDeselect={ () => setAttributes( { description: '' } ) }
266
266
  isShownByDefault={ ! isContentOnly }
267
267
  >
268
- <TextareaControl
268
+ <WCTextareaControl
269
269
  label={ __( 'Description' ) }
270
270
  value={ description || '' }
271
271
  onChange={ ( descriptionValue ) => {
@@ -69,7 +69,6 @@
69
69
  "supports": {
70
70
  "anchor": true,
71
71
  "align": true,
72
- "__experimentalExposeControlsToChildren": true,
73
72
  "color": {
74
73
  "gradients": true,
75
74
  "link": true,
@@ -86,7 +86,13 @@ const PlaylistEdit = ( {
86
86
 
87
87
  const blockProps = useBlockProps();
88
88
  const waveformPanelId = `${ clientId }-waveform`;
89
- const { replaceInnerBlocks, selectBlock } = useDispatch( blockEditorStore );
89
+ const {
90
+ replaceInnerBlocks,
91
+ replaceBlocks,
92
+ insertBlocks,
93
+ selectBlock,
94
+ __unstableMarkNextChangeAsNotPersistent,
95
+ } = useDispatch( blockEditorStore );
90
96
  const { createErrorNotice } = useDispatch( noticesStore );
91
97
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
92
98
  const colorGradientSettings = useMultipleOriginColorsAndGradients();
@@ -164,6 +170,23 @@ const PlaylistEdit = ( {
164
170
  }
165
171
  }, [ currentTrackClientId, setCurrentTrackClientId, validTracks ] );
166
172
 
173
+ // Clean up placeholder tracks when reverting back to playlist placeholder state
174
+ useEffect( () => {
175
+ if ( validTracks.length > 0 || innerBlockTracks.length === 0 ) {
176
+ return;
177
+ }
178
+
179
+ // Remove any lingering placeholder track blocks
180
+ __unstableMarkNextChangeAsNotPersistent();
181
+ replaceInnerBlocks( clientId, [] );
182
+ }, [
183
+ clientId,
184
+ innerBlockTracks,
185
+ replaceInnerBlocks,
186
+ validTracks,
187
+ __unstableMarkNextChangeAsNotPersistent,
188
+ ] );
189
+
167
190
  const createTrackBlocks = useCallback(
168
191
  ( media ) => {
169
192
  if ( ! media ) {
@@ -248,27 +271,36 @@ const PlaylistEdit = ( {
248
271
  return;
249
272
  }
250
273
 
251
- const nextBlocks = [ ...validTracks, ...newBlocks ];
252
274
  setCurrentTrackClientId( newBlocks[ 0 ].clientId );
253
- replaceInnerBlocks( clientId, nextBlocks );
275
+ insertBlocks( newBlocks, undefined, clientId, false );
254
276
  selectBlock( newBlocks[ 0 ].clientId );
255
277
  },
256
278
  [
257
279
  clientId,
258
280
  createTrackBlocks,
259
- replaceInnerBlocks,
281
+ insertBlocks,
260
282
  selectBlock,
261
283
  setCurrentTrackClientId,
262
284
  validTracks,
263
285
  ]
264
286
  );
265
287
 
288
+ const removeTrack = useCallback(
289
+ ( trackClientId ) => {
290
+ // Not `removeBlock`: it removes the container along with its
291
+ // last inner block, so this would take the playlist with it.
292
+ replaceBlocks( trackClientId, [] );
293
+ },
294
+ [ replaceBlocks ]
295
+ );
296
+
266
297
  const playlistContext = useMemo(
267
298
  () => ( {
268
299
  currentTrackClientId,
269
300
  setCurrentTrackClientId,
301
+ removeTrack,
270
302
  } ),
271
- [ currentTrackClientId, setCurrentTrackClientId ]
303
+ [ currentTrackClientId, setCurrentTrackClientId, removeTrack ]
272
304
  );
273
305
 
274
306
  // Get current track data by finding the track with matching client ID.
@@ -439,10 +471,20 @@ const PlaylistEdit = ( {
439
471
  );
440
472
  }
441
473
 
442
- const innerBlocksProps = useInnerBlocksProps( blockProps, {
443
- __experimentalAppenderTagName: 'li',
444
- renderAppender: false,
445
- } );
474
+ const { children: innerBlocks, ...trackListProps } = useInnerBlocksProps(
475
+ {
476
+ className: clsx( 'wp-block-playlist__tracklist', {
477
+ 'wp-block-playlist__tracklist-is-hidden': ! showTracklist,
478
+ 'wp-block-playlist__tracklist-show-numbers': showNumbers,
479
+ 'wp-block-playlist__tracklist-length-is-hidden':
480
+ ! showTrackLength,
481
+ } ),
482
+ },
483
+ {
484
+ __experimentalAppenderTagName: 'li',
485
+ renderAppender: false,
486
+ }
487
+ );
446
488
 
447
489
  if ( tracks.length === 0 ) {
448
490
  return (
@@ -471,7 +513,7 @@ const PlaylistEdit = ( {
471
513
 
472
514
  return (
473
515
  <>
474
- <BlockControls group="other" __experimentalShareWithChildBlocks>
516
+ <BlockControls group="other">
475
517
  <MediaReplaceFlow
476
518
  name={ __( 'Add track' ) }
477
519
  onSelect={ onAddTracks }
@@ -693,18 +735,9 @@ const PlaylistEdit = ( {
693
735
  showPlayButtonArtwork={ showPlayButtonArtwork === true }
694
736
  />
695
737
  </Disabled>
696
- <ol
697
- className={ clsx( 'wp-block-playlist__tracklist', {
698
- 'wp-block-playlist__tracklist-is-hidden':
699
- ! showTracklist,
700
- 'wp-block-playlist__tracklist-show-numbers':
701
- showNumbers,
702
- 'wp-block-playlist__tracklist-length-is-hidden':
703
- ! showTrackLength,
704
- } ) }
705
- >
738
+ <ol { ...trackListProps }>
706
739
  <PlaylistContext.Provider value={ playlistContext }>
707
- { innerBlocksProps.children }
740
+ { innerBlocks }
708
741
  </PlaylistContext.Provider>
709
742
  </ol>
710
743
  <Caption
@@ -4,6 +4,9 @@ import PlaylistEdit from '../edit';
4
4
 
5
5
  let mediaPlaceholderProps;
6
6
  let mediaReplaceFlowProps;
7
+ // The tracks the playlist holds. The inner blocks mock renders these, so a
8
+ // test can look at what ended up in the tracklist.
9
+ let mockTracks = [];
7
10
 
8
11
  jest.mock( '@wordpress/block-editor', () => ( {
9
12
  store: {},
@@ -33,7 +36,11 @@ jest.mock( '@wordpress/block-editor', () => ( {
33
36
  useBlockProps: () => ( { className: 'wp-block-playlist' } ),
34
37
  useInnerBlocksProps: ( blockProps ) => ( {
35
38
  ...blockProps,
36
- children: <li data-testid="playlist-track" />,
39
+ children: mockTracks.map( ( track ) => (
40
+ <li key={ track.clientId } data-testid="playlist-track">
41
+ { track.attributes.title }
42
+ </li>
43
+ ) ),
37
44
  } ),
38
45
  __experimentalColorGradientSettingsDropdown: () => <div />,
39
46
  __experimentalUseMultipleOriginColorsAndGradients: () => ( {
@@ -106,36 +113,58 @@ const defaultAttributes = {
106
113
 
107
114
  describe( 'PlaylistEdit', () => {
108
115
  let replaceInnerBlocks;
116
+ let insertBlocks;
109
117
  let selectBlock;
110
118
 
119
+ // The playlist reads its tracks from the store and changes them through
120
+ // dispatched actions. Keeping a list here lets a test assert which tracks
121
+ // the playlist ends up with, rather than which action it reached for.
122
+ const trackStore = ( initial ) => {
123
+ mockTracks = initial;
124
+ useSelect.mockImplementation( () => ( {
125
+ innerBlockTracks: mockTracks,
126
+ } ) );
127
+ insertBlocks.mockImplementation( ( blocks, index ) => {
128
+ const at = index ?? mockTracks.length;
129
+ mockTracks = [
130
+ ...mockTracks.slice( 0, at ),
131
+ ...blocks,
132
+ ...mockTracks.slice( at ),
133
+ ];
134
+ } );
135
+ replaceInnerBlocks.mockImplementation( ( _clientId, next ) => {
136
+ mockTracks = next;
137
+ } );
138
+ };
139
+ const trackClientIds = () => mockTracks.map( ( track ) => track.clientId );
140
+
111
141
  beforeEach( () => {
112
142
  mediaPlaceholderProps = undefined;
113
143
  mediaReplaceFlowProps = undefined;
114
144
  replaceInnerBlocks = jest.fn();
145
+ insertBlocks = jest.fn();
115
146
  selectBlock = jest.fn();
116
147
  useDispatch.mockReturnValue( {
117
148
  createErrorNotice: jest.fn(),
118
149
  replaceInnerBlocks,
150
+ insertBlocks,
119
151
  selectBlock,
152
+ __unstableMarkNextChangeAsNotPersistent: jest.fn(),
120
153
  } );
121
- useSelect.mockReturnValue( {
122
- innerBlockTracks: [
123
- {
124
- clientId: 'track-1',
125
- attributes: {
126
- id: 1,
127
- src: 'https://example.com/audio.mp3',
128
- title: 'Sample track',
129
- },
154
+ trackStore( [
155
+ {
156
+ clientId: 'track-1',
157
+ attributes: {
158
+ id: 1,
159
+ src: 'https://example.com/audio.mp3',
160
+ title: 'Sample track',
130
161
  },
131
- ],
132
- } );
162
+ },
163
+ ] );
133
164
  } );
134
165
 
135
166
  it( 'lets users select audio tracks individually from the Media Library', () => {
136
- useSelect.mockReturnValue( {
137
- innerBlockTracks: [],
138
- } );
167
+ trackStore( [] );
139
168
 
140
169
  render(
141
170
  <PlaylistEdit
@@ -186,6 +215,39 @@ describe( 'PlaylistEdit', () => {
186
215
  expect( screen.getByTestId( 'playlist-track' ) ).toBeInTheDocument();
187
216
  } );
188
217
 
218
+ it( 'preserves placeholder tracks when adding tracks', () => {
219
+ trackStore( [
220
+ {
221
+ clientId: 'track-1',
222
+ attributes: {
223
+ id: 1,
224
+ src: 'https://example.com/audio.mp3',
225
+ title: 'Sample track',
226
+ },
227
+ },
228
+ // A track the user added but has not filled in yet.
229
+ { clientId: 'placeholder-track', attributes: {} },
230
+ ] );
231
+
232
+ render(
233
+ <PlaylistEdit
234
+ attributes={ defaultAttributes }
235
+ clientId="playlist-1"
236
+ insertBlocksAfter={ jest.fn() }
237
+ isSelected={ false }
238
+ setAttributes={ jest.fn() }
239
+ />
240
+ );
241
+
242
+ fireEvent.click( screen.getByRole( 'button', { name: 'Add track' } ) );
243
+
244
+ expect( trackClientIds() ).toEqual( [
245
+ 'track-1',
246
+ 'placeholder-track',
247
+ 'new-track',
248
+ ] );
249
+ } );
250
+
189
251
  it( 'adds tracks from the add track control', () => {
190
252
  render(
191
253
  <PlaylistEdit
@@ -203,18 +265,7 @@ describe( 'PlaylistEdit', () => {
203
265
  } )
204
266
  );
205
267
 
206
- expect( replaceInnerBlocks ).toHaveBeenCalledWith( 'playlist-1', [
207
- expect.objectContaining( { clientId: 'track-1' } ),
208
- expect.objectContaining( {
209
- clientId: 'new-track',
210
- name: 'core/playlist-track',
211
- attributes: expect.objectContaining( {
212
- id: 2,
213
- src: 'https://example.com/second-track.mp3',
214
- title: 'Second track',
215
- } ),
216
- } ),
217
- ] );
268
+ expect( trackClientIds() ).toEqual( [ 'track-1', 'new-track' ] );
218
269
  expect( selectBlock ).toHaveBeenCalledWith( 'new-track' );
219
270
  } );
220
271
  } );
@@ -21,15 +21,15 @@ _Defined via the [`attributes`](https://developer.wordpress.org/block-editor/ref
21
21
  | Attribute | [Type](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#type-validation) | [Default](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#default-value) | Description |
22
22
  |-----------|------|---------|-------------|
23
23
  | `blob` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `local` |
24
- | `id` | `number` | — | |
25
- | `src` | `string` | — | |
24
+ | `id` | `number` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
25
+ | `src` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
26
26
  | `type` | `string` | `"audio"` | — |
27
- | `album` | `string` | — | |
28
- | `artist` | `string` | — | |
29
- | `image` | `string` | — | |
30
- | `imageAlt` | `string` | — | |
31
- | `length` | `string` | — | |
32
- | `title` | `string` | — | |
27
+ | `album` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
28
+ | `artist` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
29
+ | `image` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
30
+ | `imageAlt` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
31
+ | `length` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
32
+ | `title` | `string` | — | [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
33
33
 
34
34
  ## Supports
35
35
 
@@ -15,32 +15,40 @@
15
15
  "role": "local"
16
16
  },
17
17
  "id": {
18
- "type": "number"
18
+ "type": "number",
19
+ "role": "content"
19
20
  },
20
21
  "src": {
21
- "type": "string"
22
+ "type": "string",
23
+ "role": "content"
22
24
  },
23
25
  "type": {
24
26
  "type": "string",
25
27
  "default": "audio"
26
28
  },
27
29
  "album": {
28
- "type": "string"
30
+ "type": "string",
31
+ "role": "content"
29
32
  },
30
33
  "artist": {
31
- "type": "string"
34
+ "type": "string",
35
+ "role": "content"
32
36
  },
33
37
  "image": {
34
- "type": "string"
38
+ "type": "string",
39
+ "role": "content"
35
40
  },
36
41
  "imageAlt": {
37
- "type": "string"
42
+ "type": "string",
43
+ "role": "content"
38
44
  },
39
45
  "length": {
40
- "type": "string"
46
+ "type": "string",
47
+ "role": "content"
41
48
  },
42
49
  "title": {
43
- "type": "string"
50
+ "type": "string",
51
+ "role": "content"
44
52
  }
45
53
  },
46
54
  "supports": {
@@ -15,7 +15,7 @@ import {
15
15
  Button,
16
16
  PanelBody,
17
17
  TextControl,
18
- TextareaControl,
18
+ TextareaControl as WCTextareaControl,
19
19
  BaseControl,
20
20
  Spinner,
21
21
  } from '@wordpress/components';
@@ -46,11 +46,27 @@ const PlaylistTrackEdit = ( {
46
46
  const showImages = context?.showImages ?? true;
47
47
  const imageButton = useRef();
48
48
  const blockProps = useBlockProps();
49
- const { currentTrackClientId, setCurrentTrackClientId } =
49
+ const { currentTrackClientId, setCurrentTrackClientId, removeTrack } =
50
50
  useContext( PlaylistContext );
51
51
  const { createErrorNotice } = useDispatch( noticesStore );
52
- function onUploadError( message ) {
52
+
53
+ /**
54
+ * Handle audio upload errors.
55
+ *
56
+ * @param {string} message Error message to show.
57
+ * @param {Object} [options]
58
+ * @param {boolean} [options.removeTrackOnError] Remove the track as well. Useful for drag/drop where a track is optimistically created before upload finishes.
59
+ */
60
+ function onUploadError( message, { removeTrackOnError = false } = {} ) {
53
61
  createErrorNotice( message, { type: 'snackbar' } );
62
+
63
+ if ( removeTrackOnError ) {
64
+ removeTrack( clientId );
65
+ return;
66
+ }
67
+
68
+ // Set temporaryURL back to default state
69
+ setTemporaryURL();
54
70
  }
55
71
  const hasTrackSource = !! src || !! temporaryURL;
56
72
 
@@ -70,11 +86,14 @@ const PlaylistTrackEdit = ( {
70
86
  setCurrentTrackClientId,
71
87
  ] );
72
88
 
89
+ // Handles drag/drop uploads
73
90
  useUploadMediaFromBlobURL( {
74
91
  url: temporaryURL,
75
92
  allowedTypes: ALLOWED_MEDIA_TYPES,
76
93
  onChange: onSelectTrack,
77
- onError: onUploadError,
94
+ onError: ( message ) => {
95
+ onUploadError( message, { removeTrackOnError: true } );
96
+ },
78
97
  } );
79
98
 
80
99
  function onSelectTrack( media ) {
@@ -144,20 +163,33 @@ const PlaylistTrackEdit = ( {
144
163
 
145
164
  return (
146
165
  <>
147
- <BlockControls group="other">
148
- <MediaReplaceFlow
149
- name={ __( 'Replace' ) }
150
- onSelect={ onSelectTrack }
151
- accept="audio/*"
152
- mediaId={ id }
153
- mediaURL={ src }
154
- allowedTypes={ ALLOWED_MEDIA_TYPES }
155
- onError={ onUploadError }
156
- variant="toolbar"
157
- />
158
- </BlockControls>
166
+ { /* Only show if this is a single selection (not multiselect) */ }
167
+ { isSelected && (
168
+ <BlockControls group="other">
169
+ <MediaReplaceFlow
170
+ name={ __( 'Replace' ) }
171
+ onSelect={ onSelectTrack }
172
+ accept="audio/*"
173
+ mediaId={ id }
174
+ mediaURL={ src }
175
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
176
+ onError={ onUploadError }
177
+ variant="toolbar"
178
+ />
179
+ </BlockControls>
180
+ ) }
159
181
  <InspectorControls>
160
182
  <PanelBody title={ __( 'Settings' ) }>
183
+ { /* Only show if this is a single selection (not multiselect) */ }
184
+ { isSelected && (
185
+ <TextControl
186
+ label={ __( 'Title' ) }
187
+ value={ title ? stripHTML( title ) : '' }
188
+ onChange={ ( titleValue ) => {
189
+ setAttributes( { title: titleValue } );
190
+ } }
191
+ />
192
+ ) }
161
193
  <TextControl
162
194
  label={ __( 'Artist' ) }
163
195
  value={ artist ? stripHTML( artist ) : '' }
@@ -172,13 +204,6 @@ const PlaylistTrackEdit = ( {
172
204
  setAttributes( { album: albumValue } );
173
205
  } }
174
206
  />
175
- <TextControl
176
- label={ __( 'Title' ) }
177
- value={ title ? stripHTML( title ) : '' }
178
- onChange={ ( titleValue ) => {
179
- setAttributes( { title: titleValue } );
180
- } }
181
- />
182
207
  <MediaUploadCheck>
183
208
  <BaseControl>
184
209
  <BaseControl.VisualLabel>
@@ -225,7 +250,7 @@ const PlaylistTrackEdit = ( {
225
250
  </BaseControl>
226
251
  </MediaUploadCheck>
227
252
  { !! image && (
228
- <TextareaControl
253
+ <WCTextareaControl
229
254
  label={ __( 'Alternative text' ) }
230
255
  value={ imageAlt || '' }
231
256
  onChange={ ( value ) =>
@@ -251,7 +276,6 @@ const PlaylistTrackEdit = ( {
251
276
  </PanelBody>
252
277
  </InspectorControls>
253
278
  <li { ...blockProps }>
254
- { !! temporaryURL && <Spinner /> }
255
279
  <button
256
280
  className="wp-block-playlist-track__button"
257
281
  onClick={ () => setCurrentTrackClientId( clientId ) }
@@ -271,7 +295,8 @@ const PlaylistTrackEdit = ( {
271
295
  tagName="span"
272
296
  className="wp-block-playlist-track__title"
273
297
  value={ title }
274
- placeholder={ __( 'Add title' ) }
298
+ aria-label={ __( 'Track title' ) }
299
+ placeholder={ __( 'Track title' ) }
275
300
  onChange={ ( value ) => {
276
301
  setAttributes( { title: value } );
277
302
  } }
@@ -282,7 +307,8 @@ const PlaylistTrackEdit = ( {
282
307
  tagName="span"
283
308
  className="wp-block-playlist-track__artist"
284
309
  value={ artist }
285
- placeholder={ __( 'Add artist' ) }
310
+ aria-label={ __( 'Track artist' ) }
311
+ placeholder={ __( 'Track artist' ) }
286
312
  onChange={ ( value ) =>
287
313
  setAttributes( { artist: value } )
288
314
  }
@@ -300,6 +326,9 @@ const PlaylistTrackEdit = ( {
300
326
  </span>
301
327
  ) }
302
328
  { length }
329
+ { !! temporaryURL && (
330
+ <Spinner className="wp-block-playlist-track__spinner" />
331
+ ) }
303
332
  </span>
304
333
  <span className="screen-reader-text">{ __( 'Play' ) }</span>
305
334
  </button>