@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
@@ -0,0 +1,139 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useDispatch, useSelect } from '@wordpress/data';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import PlaylistEdit from '../edit';
15
+
16
+ jest.mock( '@wordpress/block-editor', () => ( {
17
+ store: {},
18
+ BlockControls: ( { children } ) => <div>{ children }</div>,
19
+ BlockIcon: () => <span />,
20
+ InspectorControls: ( { children } ) => <div>{ children }</div>,
21
+ MediaPlaceholder: () => <div />,
22
+ MediaReplaceFlow: () => <div />,
23
+ useBlockProps: () => ( { className: 'wp-block-playlist' } ),
24
+ useInnerBlocksProps: ( blockProps ) => ( {
25
+ ...blockProps,
26
+ children: <li data-testid="playlist-track" />,
27
+ } ),
28
+ __experimentalColorGradientSettingsDropdown: () => <div />,
29
+ __experimentalUseMultipleOriginColorsAndGradients: () => ( {
30
+ colors: [],
31
+ gradients: [],
32
+ disableCustomColors: true,
33
+ disableCustomGradients: true,
34
+ } ),
35
+ } ) );
36
+
37
+ jest.mock( '@wordpress/blocks', () => ( {
38
+ createBlock: jest.fn( ( name, attributes ) => ( {
39
+ name,
40
+ attributes,
41
+ clientId: 'new-track',
42
+ } ) ),
43
+ } ) );
44
+
45
+ jest.mock( '@wordpress/blob', () => ( {
46
+ createBlobURL: jest.fn( () => 'blob:track' ),
47
+ } ) );
48
+
49
+ jest.mock( '@wordpress/components', () => ( {
50
+ Disabled: ( { children } ) => <div>{ children }</div>,
51
+ SelectControl: () => <div />,
52
+ ToggleControl: () => <div />,
53
+ __experimentalToolsPanel: ( { children } ) => <div>{ children }</div>,
54
+ __experimentalToolsPanelItem: ( { children } ) => <div>{ children }</div>,
55
+ } ) );
56
+
57
+ jest.mock( '@wordpress/data', () => ( {
58
+ useDispatch: jest.fn(),
59
+ useSelect: jest.fn(),
60
+ } ) );
61
+
62
+ jest.mock( '@wordpress/i18n', () => ( {
63
+ __: ( text ) => text,
64
+ _x: ( text ) => text,
65
+ } ) );
66
+
67
+ jest.mock( '@wordpress/icons', () => ( {
68
+ playlist: 'playlist',
69
+ } ) );
70
+
71
+ jest.mock( '@wordpress/notices', () => ( {
72
+ store: {},
73
+ } ) );
74
+
75
+ jest.mock( '../../utils/caption', () => ( {
76
+ Caption: () => <figcaption />,
77
+ } ) );
78
+
79
+ jest.mock( '../../utils/hooks', () => ( {
80
+ useToolsPanelDropdownMenuProps: () => ( {} ),
81
+ } ) );
82
+
83
+ jest.mock( '../../utils/waveform-player', () => ( {
84
+ WaveformPlayer: () => <div />,
85
+ } ) );
86
+
87
+ const defaultAttributes = {
88
+ order: 'asc',
89
+ showTracklist: true,
90
+ showNumbers: true,
91
+ showImages: true,
92
+ showPlayButtonArtwork: false,
93
+ showArtists: true,
94
+ showTrackLength: true,
95
+ };
96
+
97
+ describe( 'PlaylistEdit', () => {
98
+ beforeEach( () => {
99
+ useDispatch.mockReturnValue( {
100
+ createErrorNotice: jest.fn(),
101
+ replaceInnerBlocks: jest.fn(),
102
+ selectBlock: jest.fn(),
103
+ } );
104
+ useSelect.mockReturnValue( {
105
+ innerBlockTracks: [
106
+ {
107
+ clientId: 'track-1',
108
+ attributes: {
109
+ id: 1,
110
+ src: 'https://example.com/audio.mp3',
111
+ title: 'Sample track',
112
+ },
113
+ },
114
+ ],
115
+ } );
116
+ } );
117
+
118
+ it( 'keeps track blocks mounted when the tracklist is hidden', () => {
119
+ render(
120
+ <PlaylistEdit
121
+ attributes={ {
122
+ ...defaultAttributes,
123
+ showTracklist: false,
124
+ } }
125
+ clientId="playlist-1"
126
+ insertBlocksAfter={ jest.fn() }
127
+ isSelected={ false }
128
+ setAttributes={ jest.fn() }
129
+ />
130
+ );
131
+
132
+ const tracklist = screen.getByRole( 'list' );
133
+
134
+ expect( tracklist ).toHaveClass(
135
+ 'wp-block-playlist__tracklist-is-hidden'
136
+ );
137
+ expect( screen.getByTestId( 'playlist-track' ) ).toBeInTheDocument();
138
+ } );
139
+ } );
@@ -46,6 +46,36 @@ describe( 'Playlist block edit utilities', () => {
46
46
  expect( result.imageAlt ).toBeUndefined();
47
47
  } );
48
48
 
49
+ it( 'should transform raw uploaded attachment data to track attributes', () => {
50
+ const media = {
51
+ id: 123,
52
+ source_url: 'https://example.com/song.mp3',
53
+ title: { raw: 'My &amp; Song' },
54
+ media_details: {
55
+ artist: 'Media Details Artist',
56
+ album: 'Media Details Album',
57
+ length_formatted: '4:30',
58
+ },
59
+ image: {
60
+ source_url: 'https://example.com/cover.jpg',
61
+ alt_text: 'A black and white portrait',
62
+ },
63
+ };
64
+
65
+ const result = getTrackAttributes( media );
66
+
67
+ expect( result ).toEqual( {
68
+ id: 123,
69
+ src: 'https://example.com/song.mp3',
70
+ title: 'My & Song',
71
+ artist: 'Media Details Artist',
72
+ album: 'Media Details Album',
73
+ length: '4:30',
74
+ image: 'https://example.com/cover.jpg',
75
+ imageAlt: 'A black and white portrait',
76
+ } );
77
+ } );
78
+
49
79
  it( 'should fall back to meta.artist when artist is not available', () => {
50
80
  const media = {
51
81
  url: 'https://example.com/song.mp3',
@@ -2,6 +2,25 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
+ import { decodeEntities } from '@wordpress/html-entities';
6
+
7
+ function getMediaUrl( media ) {
8
+ return media?.url ?? media?.source_url;
9
+ }
10
+
11
+ function getMediaTitle( media ) {
12
+ const title = media?.title;
13
+
14
+ if ( title === undefined ) {
15
+ return undefined;
16
+ }
17
+
18
+ if ( typeof title === 'string' ) {
19
+ return title;
20
+ }
21
+
22
+ return decodeEntities( title?.raw || title?.rendered || '' );
23
+ }
5
24
 
6
25
  /**
7
26
  * Transform media library image data into track image attributes.
@@ -10,7 +29,7 @@ import { __ } from '@wordpress/i18n';
10
29
  * @return {Object} Track image attributes for the playlist-track block.
11
30
  */
12
31
  export function getTrackImageAttributes( image ) {
13
- const imageSrc = image?.src ?? image?.url;
32
+ const imageSrc = image?.src ?? getMediaUrl( image );
14
33
 
15
34
  // Prevent using the default media attachment icon as the track image.
16
35
  if ( imageSrc?.endsWith( '/images/media/audio.svg' ) ) {
@@ -34,10 +53,12 @@ export function getTrackImageAttributes( image ) {
34
53
  * @return {Object} Track attributes for the playlist-track block.
35
54
  */
36
55
  export function getTrackAttributes( media ) {
56
+ const mediaUrl = getMediaUrl( media );
57
+
37
58
  return {
38
- id: media.id || media.url, // Attachment ID or URL.
39
- src: media.url,
40
- title: media.title,
59
+ id: media.id || mediaUrl, // Attachment ID or URL.
60
+ src: mediaUrl,
61
+ title: getMediaTitle( media ),
41
62
  artist:
42
63
  media.artist ||
43
64
  media?.meta?.artist ||
@@ -53,7 +53,7 @@ const PlaylistTrackEdit = ( {
53
53
  const showImages = context?.showImages ?? true;
54
54
  const imageButton = useRef();
55
55
  const blockProps = useBlockProps();
56
- const { currentTrackClientId, setCurrentTrackClientId } =
56
+ const { currentTrackClientId, setCurrentTrackClientId, addTracks } =
57
57
  useContext( PlaylistContext );
58
58
  const { createErrorNotice } = useDispatch( noticesStore );
59
59
  function onUploadError( message ) {
@@ -78,14 +78,16 @@ const PlaylistTrackEdit = ( {
78
78
  ] );
79
79
 
80
80
  useUploadMediaFromBlobURL( {
81
- src: temporaryURL,
81
+ url: temporaryURL,
82
82
  allowedTypes: ALLOWED_MEDIA_TYPES,
83
83
  onChange: onSelectTrack,
84
84
  onError: onUploadError,
85
85
  } );
86
86
 
87
87
  function onSelectTrack( media ) {
88
- if ( ! media || ! media.url ) {
88
+ const mediaUrl = media?.url ?? media?.source_url;
89
+
90
+ if ( ! media || ! mediaUrl ) {
89
91
  // In this case there was an error and we should continue in the editing state
90
92
  // previous attributes should be removed because they may be temporary blob urls.
91
93
  setAttributes( {
@@ -103,8 +105,8 @@ const PlaylistTrackEdit = ( {
103
105
  return;
104
106
  }
105
107
 
106
- if ( isBlobURL( media.url ) ) {
107
- setTemporaryURL( media.url );
108
+ if ( isBlobURL( mediaUrl ) ) {
109
+ setTemporaryURL( mediaUrl );
108
110
  return;
109
111
  }
110
112
 
@@ -158,8 +160,23 @@ const PlaylistTrackEdit = ( {
158
160
  mediaURL={ src }
159
161
  allowedTypes={ ALLOWED_MEDIA_TYPES }
160
162
  onError={ onUploadError }
163
+ variant="toolbar"
161
164
  />
162
165
  </BlockControls>
166
+ { !! addTracks && (
167
+ <BlockControls group="block">
168
+ <MediaReplaceFlow
169
+ name={ __( 'Add' ) }
170
+ onSelect={ addTracks }
171
+ accept="audio/*"
172
+ multiple
173
+ handleUpload={ false }
174
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
175
+ onError={ onUploadError }
176
+ variant="toolbar"
177
+ />
178
+ </BlockControls>
179
+ ) }
163
180
  <InspectorControls>
164
181
  <PanelBody title={ __( 'Settings' ) }>
165
182
  <TextControl
@@ -8,7 +8,7 @@
8
8
  /**
9
9
  * Renders the `core/playlist-track` block on server.
10
10
  *
11
- * @since 6.9.0
11
+ * @since 7.1.0
12
12
  *
13
13
  * @param array $attributes The block attributes.
14
14
  * @param string $content The block content.
@@ -27,11 +27,12 @@ function render_block_core_playlist_track( $attributes, $content = '', $block =
27
27
  $show_images = $block->context['showImages'];
28
28
  }
29
29
 
30
- $artist = $attributes['artist'] ?? '';
31
- $image = $attributes['image'] ?? '';
32
- $alt = $attributes['imageAlt'] ?? '';
33
- $length = $attributes['length'] ?? '';
34
- $title = isset( $attributes['title'] ) && ! empty( $attributes['title'] ) ? $attributes['title'] : __( 'Unknown title' );
30
+ $track_image = $attributes['image'] ?? null;
31
+ $image = is_string( $track_image ) ? $track_image : '';
32
+ $artist = $attributes['artist'] ?? '';
33
+ $alt = $attributes['imageAlt'] ?? '';
34
+ $length = $attributes['length'] ?? '';
35
+ $title = isset( $attributes['title'] ) && ! empty( $attributes['title'] ) ? $attributes['title'] : __( 'Unknown title' );
35
36
 
36
37
  $html = '<li ' . $wrapper_attributes . '>';
37
38
  $html .= '<button data-wp-on--click="actions.changeTrack" data-wp-bind--aria-current="state.isCurrentTrack" class="wp-block-playlist-track__button">';
@@ -68,7 +69,7 @@ function render_block_core_playlist_track( $attributes, $content = '', $block =
68
69
  /**
69
70
  * Registers the `core/playlist-track` block on server.
70
71
  *
71
- * @since 6.9.0
72
+ * @since 7.1.0
72
73
  */
73
74
  function register_block_core_playlist_track() {
74
75
  register_block_type_from_metadata(
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { fireEvent, render, screen } from '@testing-library/react';
4
+ import { fireEvent, render, screen, within } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -13,13 +13,22 @@ import { useDispatch } from '@wordpress/data';
13
13
  */
14
14
  import PlaylistTrackEdit from '../edit';
15
15
  import { PlaylistContext } from '../../playlist/context';
16
+ import { useUploadMediaFromBlobURL } from '../../utils/hooks';
17
+
18
+ let mockMediaReplaceFlowProps;
16
19
 
17
20
  jest.mock( '@wordpress/block-editor', () => ( {
18
- BlockControls: ( { children } ) => <div>{ children }</div>,
21
+ BlockControls: ( { children, group = 'default' } ) => (
22
+ <div data-testid={ `block-controls-${ group }` }>{ children }</div>
23
+ ),
19
24
  BlockIcon: () => <span />,
20
25
  InspectorControls: ( { children } ) => <div>{ children }</div>,
21
26
  MediaPlaceholder: () => <div />,
22
- MediaReplaceFlow: () => <div />,
27
+ MediaReplaceFlow: ( props ) => {
28
+ mockMediaReplaceFlowProps = props;
29
+ const { name, onSelect } = props;
30
+ return <button onClick={ () => onSelect( {} ) }>{ name }</button>;
31
+ },
23
32
  MediaUpload: ( { render: renderMediaUpload } ) =>
24
33
  renderMediaUpload( { open: jest.fn() } ),
25
34
  MediaUploadCheck: ( { children } ) => <div>{ children }</div>,
@@ -71,12 +80,14 @@ const defaultAttributes = {
71
80
  function renderEdit( props = {} ) {
72
81
  const setAttributes = jest.fn();
73
82
  const setCurrentTrackClientId = props.setCurrentTrackClientId || jest.fn();
83
+ const addTracks = props.addTracks;
74
84
 
75
85
  render(
76
86
  <PlaylistContext.Provider
77
87
  value={ {
78
88
  currentTrackClientId: props.currentTrackClientId ?? null,
79
89
  setCurrentTrackClientId,
90
+ addTracks,
80
91
  } }
81
92
  >
82
93
  <PlaylistTrackEdit
@@ -101,9 +112,11 @@ function renderEdit( props = {} ) {
101
112
 
102
113
  describe( 'PlaylistTrackEdit', () => {
103
114
  beforeEach( () => {
115
+ mockMediaReplaceFlowProps = undefined;
104
116
  useDispatch.mockReturnValue( {
105
117
  createErrorNotice: jest.fn(),
106
118
  } );
119
+ useUploadMediaFromBlobURL.mockClear();
107
120
  } );
108
121
 
109
122
  it( 'allows the track image alternative text to be edited', () => {
@@ -166,4 +179,75 @@ describe( 'PlaylistTrackEdit', () => {
166
179
 
167
180
  expect( setCurrentTrackClientId ).not.toHaveBeenCalled();
168
181
  } );
182
+
183
+ it( 'uploads temporary blob tracks', () => {
184
+ renderEdit( {
185
+ attributes: {
186
+ blob: 'blob:https://example.com/temporary-track',
187
+ src: undefined,
188
+ },
189
+ } );
190
+
191
+ expect( useUploadMediaFromBlobURL ).toHaveBeenCalledWith(
192
+ expect.objectContaining( {
193
+ url: 'blob:https://example.com/temporary-track',
194
+ } )
195
+ );
196
+ } );
197
+
198
+ it( 'allows tracks to be added from the track toolbar', () => {
199
+ const addTracks = jest.fn();
200
+ renderEdit( { addTracks } );
201
+
202
+ fireEvent.click( screen.getByRole( 'button', { name: 'Add' } ) );
203
+
204
+ expect( addTracks ).toHaveBeenCalledWith( {} );
205
+ } );
206
+
207
+ it( 'renders the add track control in a different toolbar group from replace', () => {
208
+ renderEdit( { addTracks: jest.fn() } );
209
+
210
+ expect(
211
+ within( screen.getByTestId( 'block-controls-other' ) ).getByRole(
212
+ 'button',
213
+ { name: 'Replace' }
214
+ )
215
+ ).toBeInTheDocument();
216
+ expect(
217
+ within( screen.getByTestId( 'block-controls-block' ) ).getByRole(
218
+ 'button',
219
+ { name: 'Add' }
220
+ )
221
+ ).toBeInTheDocument();
222
+ } );
223
+
224
+ it( 'preserves the current track source when a replacement upload fails', () => {
225
+ const { setAttributes } = renderEdit();
226
+
227
+ mockMediaReplaceFlowProps.onSelect();
228
+
229
+ expect( setAttributes ).toHaveBeenCalledTimes( 1 );
230
+ expect( setAttributes.mock.calls[ 0 ][ 0 ] ).not.toHaveProperty(
231
+ 'src'
232
+ );
233
+ } );
234
+
235
+ it( 'accepts raw uploaded attachment data when replacing a track', () => {
236
+ const { setAttributes } = renderEdit();
237
+
238
+ mockMediaReplaceFlowProps.onSelect( {
239
+ id: 2,
240
+ source_url: 'https://example.com/replacement.mp3',
241
+ title: { raw: 'Replacement &amp; Track' },
242
+ } );
243
+
244
+ expect( setAttributes ).toHaveBeenCalledWith(
245
+ expect.objectContaining( {
246
+ blob: undefined,
247
+ id: 2,
248
+ src: 'https://example.com/replacement.mp3',
249
+ title: 'Replacement & Track',
250
+ } )
251
+ );
252
+ } );
169
253
  } );
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
5
  import {
6
+ InnerBlocks,
6
7
  InspectorControls,
7
8
  useBlockProps,
8
9
  useInnerBlocksProps,
@@ -77,6 +78,26 @@ function ReadOnlyContent( {
77
78
  );
78
79
  }
79
80
 
81
+ function EditableContentLoaded( {
82
+ blockProps,
83
+ blocks,
84
+ onInput,
85
+ onChange,
86
+ tagName: TagName = 'div',
87
+ } ) {
88
+ const props = useInnerBlocksProps( blockProps, {
89
+ value: blocks,
90
+ onInput,
91
+ onChange,
92
+ // Show a writing prompt for empty content, even when the block
93
+ // is not selected, so the content area remains discoverable.
94
+ renderAppender: blocks?.length
95
+ ? undefined
96
+ : InnerBlocks.DefaultBlockAppender,
97
+ } );
98
+ return <TagName { ...props } />;
99
+ }
100
+
80
101
  function EditableContent( { context = {}, tagName: TagName = 'div' } ) {
81
102
  const { postType, postId } = context;
82
103
 
@@ -86,31 +107,39 @@ function EditableContent( { context = {}, tagName: TagName = 'div' } ) {
86
107
  { id: postId }
87
108
  );
88
109
 
89
- const entityRecord = useSelect(
90
- ( select ) => {
91
- return select( coreStore ).getEntityRecord(
110
+ // Wait for the entity record before mounting the inner blocks area, so
111
+ // that an empty `blocks` value means an empty post rather than one that
112
+ // has not loaded yet.
113
+ const hasLoadedRecord = useSelect(
114
+ ( select ) =>
115
+ select( coreStore ).getEntityRecord(
92
116
  'postType',
93
117
  postType,
94
118
  postId
95
- );
96
- },
119
+ ) !== undefined ||
120
+ select( coreStore ).hasFinishedResolution( 'getEntityRecord', [
121
+ 'postType',
122
+ postType,
123
+ postId,
124
+ ] ),
97
125
  [ postType, postId ]
98
126
  );
99
127
 
100
- const hasInnerBlocks = !! entityRecord?.content?.raw || blocks?.length;
128
+ const blockProps = useBlockProps( { className: 'entry-content' } );
101
129
 
102
- const initialInnerBlocks = [ [ 'core/paragraph' ] ];
130
+ if ( ! hasLoadedRecord ) {
131
+ return <TagName { ...blockProps } />;
132
+ }
103
133
 
104
- const props = useInnerBlocksProps(
105
- useBlockProps( { className: 'entry-content' } ),
106
- {
107
- value: blocks,
108
- onInput,
109
- onChange,
110
- template: ! hasInnerBlocks ? initialInnerBlocks : undefined,
111
- }
134
+ return (
135
+ <EditableContentLoaded
136
+ blockProps={ blockProps }
137
+ blocks={ blocks }
138
+ onInput={ onInput }
139
+ onChange={ onChange }
140
+ tagName={ TagName }
141
+ />
112
142
  );
113
- return <TagName { ...props } />;
114
143
  }
115
144
 
116
145
  function Content( props ) {
@@ -73,8 +73,11 @@ function render_block_core_post_excerpt( $attributes, $content, $block ) {
73
73
  $show_more_on_new_line = ! isset( $attributes['showMoreOnNewLine'] ) || $attributes['showMoreOnNewLine'];
74
74
  if ( $show_more_on_new_line && ! empty( $more_text ) ) {
75
75
  $content .= '</p><p class="wp-block-post-excerpt__more-text">' . $more_text . '</p>';
76
+ } elseif ( empty( $more_text ) ) {
77
+ $content .= '</p>';
76
78
  } else {
77
- $content .= " $more_text</p>";
79
+ $separator = '' === $excerpt ? '' : ' ';
80
+ $content .= $separator . $more_text . '</p>';
78
81
  }
79
82
  remove_filter( 'excerpt_more', $filter_excerpt_more );
80
83
  return sprintf( '<div %1$s>%2$s</div>', $wrapper_attributes, $content );
@@ -182,6 +182,7 @@ export default function PostFeaturedImageEdit( {
182
182
  const borderProps = useBorderProps( attributes );
183
183
  const shadowProps = getShadowClassesAndStyles( attributes );
184
184
  const blockEditingMode = useBlockEditingMode();
185
+ const aspectRatioStyle = aspectRatio === 'auto' ? undefined : aspectRatio;
185
186
 
186
187
  const placeholder = ( content ) => {
187
188
  return (
@@ -192,7 +193,7 @@ export default function PostFeaturedImageEdit( {
192
193
  ) }
193
194
  withIllustration
194
195
  style={ {
195
- aspectRatio,
196
+ aspectRatio: aspectRatioStyle,
196
197
  height: hasDimensionValue( height )
197
198
  ? height
198
199
  : hasDimensionValue( width ) && 'auto',
@@ -426,7 +427,7 @@ export default function PostFeaturedImageEdit( {
426
427
  const imageStyles = {
427
428
  ...borderProps.style,
428
429
  ...shadowProps.style,
429
- aspectRatio,
430
+ aspectRatio: aspectRatioStyle,
430
431
  height: hasDimensionValue( height )
431
432
  ? height
432
433
  : hasDimensionValue( width ) && 'auto',
@@ -44,7 +44,10 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
44
44
  $has_height = array_key_exists( 'height', $attributes ) && null !== $attributes['height'] && '' !== $attributes['height'];
45
45
 
46
46
  if ( ! empty( $attributes['aspectRatio'] ) ) {
47
- $extra_styles .= esc_attr( safecss_filter_attr( 'aspect-ratio:' . $attributes['aspectRatio'] ) ) . ';';
47
+ // If there's no non-default value set, let the image's width and height attributes determine its intrinsic aspect ratio.
48
+ if ( 'auto' !== $attributes['aspectRatio'] ) {
49
+ $extra_styles .= esc_attr( safecss_filter_attr( 'aspect-ratio:' . $attributes['aspectRatio'] ) ) . ';';
50
+ }
48
51
  $extra_styles .= 'width:100%;';
49
52
  }
50
53
 
@@ -133,10 +133,13 @@ export default function PostTermsEdit( {
133
133
  setAttributes( { suffix: value } )
134
134
  }
135
135
  tagName="span"
136
- __unstableOnSplitAtEnd={ () =>
137
- insertBlocksAfter(
138
- createBlock( getDefaultBlockName() )
139
- )
136
+ __unstableOnSplitAtEnd={
137
+ insertBlocksAfter
138
+ ? () =>
139
+ insertBlocksAfter(
140
+ createBlock( getDefaultBlockName() )
141
+ )
142
+ : undefined
140
143
  }
141
144
  />
142
145
  ) }
@@ -63,9 +63,11 @@ export default function PostTitleEdit( {
63
63
  postId
64
64
  );
65
65
  const [ link ] = useEntityProp( 'postType', postType, 'link', postId );
66
- const onSplitAtEnd = () => {
67
- insertBlocksAfter( createBlock( getDefaultBlockName() ) );
68
- };
66
+ const onSplitAtEnd = insertBlocksAfter
67
+ ? () => {
68
+ insertBlocksAfter( createBlock( getDefaultBlockName() ) );
69
+ }
70
+ : undefined;
69
71
  const blockProps = useBlockProps();
70
72
  const blockEditingMode = useBlockEditingMode();
71
73
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
@@ -33,8 +33,13 @@ export default function PreformattedEdit( {
33
33
  onMerge={ mergeBlocks }
34
34
  { ...blockProps }
35
35
  __unstablePastePlainText
36
- __unstableOnSplitAtDoubleLineEnd={ () =>
37
- insertBlocksAfter( createBlock( getDefaultBlockName() ) )
36
+ __unstableOnSplitAtDoubleLineEnd={
37
+ insertBlocksAfter
38
+ ? () =>
39
+ insertBlocksAfter(
40
+ createBlock( getDefaultBlockName() )
41
+ )
42
+ : undefined
38
43
  }
39
44
  />
40
45
  );
@@ -55,10 +55,15 @@ function PullQuoteEdit( props ) {
55
55
  } )
56
56
  }
57
57
  className="wp-block-pullquote__citation"
58
- __unstableOnSplitAtEnd={ () =>
59
- insertBlocksAfter(
60
- createBlock( getDefaultBlockName() )
61
- )
58
+ __unstableOnSplitAtEnd={
59
+ insertBlocksAfter
60
+ ? () =>
61
+ insertBlocksAfter(
62
+ createBlock(
63
+ getDefaultBlockName()
64
+ )
65
+ )
66
+ : undefined
62
67
  }
63
68
  />
64
69
  ) }