@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
@@ -75,6 +75,7 @@ import AccessibleMenuDescription from './accessible-menu-description';
75
75
  import { unlock } from '../../lock-unlock';
76
76
  import { useToolsPanelDropdownMenuProps } from '../../utils/hooks';
77
77
  import { isWithinNavigationOverlay } from '../../utils/is-within-overlay';
78
+ import useLayoutCustomProperties from './use-layout-custom-properties';
78
79
  import {
79
80
  DEFAULT_BLOCK,
80
81
  NAVIGATION_OVERLAY_TEMPLATE_PART_AREA,
@@ -280,6 +281,11 @@ function Navigation( {
280
281
  } = attributes;
281
282
 
282
283
  const ref = attributes.ref;
284
+ useLayoutCustomProperties( {
285
+ clientId,
286
+ layout: attributes.layout,
287
+ style: attributes.style,
288
+ } );
283
289
 
284
290
  const setRef = useCallback(
285
291
  ( postId ) => {
@@ -297,22 +303,30 @@ function Navigation( {
297
303
  onNavigateToEntityRecord,
298
304
  currentTheme,
299
305
  editorDisabledResponsive,
300
- } = useSelect( ( select ) => {
301
- const { getSettings } = select( blockEditorStore );
302
- const settings = getSettings();
303
-
304
- return {
305
- isPreviewMode: settings.isPreviewMode,
306
- onNavigateToEntityRecord: settings?.onNavigateToEntityRecord,
307
- // Needed to construct the template part ID for the overlay preview.
308
- currentTheme: select( coreStore ).getCurrentTheme()?.stylesheet,
309
- // When editing a navigation post directly in an isolated editor,
310
- // always show navigation expanded (no hamburger) so users can see
311
- // and interact with all menu items.
312
- editorDisabledResponsive:
313
- !! settings?.[ isNavigationPostEditorKey ],
314
- };
315
- }, [] );
306
+ hasSelectedStyleState,
307
+ } = useSelect(
308
+ ( select ) => {
309
+ const {
310
+ getSettings,
311
+ hasSelectedStyleState: hasSelectedBlockStyleState,
312
+ } = unlock( select( blockEditorStore ) );
313
+ const settings = getSettings();
314
+
315
+ return {
316
+ isPreviewMode: settings.isPreviewMode,
317
+ onNavigateToEntityRecord: settings?.onNavigateToEntityRecord,
318
+ // Needed to construct the template part ID for the overlay preview.
319
+ currentTheme: select( coreStore ).getCurrentTheme()?.stylesheet,
320
+ // When editing a navigation post directly in an isolated editor,
321
+ // always show navigation expanded (no hamburger) so users can see
322
+ // and interact with all menu items.
323
+ editorDisabledResponsive:
324
+ !! settings?.[ isNavigationPostEditorKey ],
325
+ hasSelectedStyleState: hasSelectedBlockStyleState( clientId ),
326
+ };
327
+ },
328
+ [ clientId ]
329
+ );
316
330
  const hasAlreadyRendered = isPreviewMode ? false : recursionDetected;
317
331
 
318
332
  const blockEditingMode = useBlockEditingMode();
@@ -924,26 +938,28 @@ function Navigation( {
924
938
  />
925
939
  </InspectorControls>
926
940
  ) }
927
- <InspectorControls group="color">
928
- { /*
929
- * Avoid useMultipleOriginColorsAndGradients and detectColors
930
- * on block mount. InspectorControls only mounts this component
931
- * when the block is selected.
932
- * */ }
933
- <ColorTools
934
- textColor={ textColor }
935
- setTextColor={ setTextColor }
936
- backgroundColor={ backgroundColor }
937
- setBackgroundColor={ setBackgroundColor }
938
- overlayTextColor={ overlayTextColor }
939
- setOverlayTextColor={ setOverlayTextColor }
940
- overlayBackgroundColor={ overlayBackgroundColor }
941
- setOverlayBackgroundColor={ setOverlayBackgroundColor }
942
- clientId={ clientId }
943
- navRef={ navRef }
944
- hasCustomOverlay={ !! overlay }
945
- />
946
- </InspectorControls>
941
+ { ! hasSelectedStyleState && (
942
+ <InspectorControls group="color">
943
+ { /*
944
+ * Avoid useMultipleOriginColorsAndGradients and detectColors
945
+ * on block mount. InspectorControls only mounts this component
946
+ * when the block is selected.
947
+ * */ }
948
+ <ColorTools
949
+ textColor={ textColor }
950
+ setTextColor={ setTextColor }
951
+ backgroundColor={ backgroundColor }
952
+ setBackgroundColor={ setBackgroundColor }
953
+ overlayTextColor={ overlayTextColor }
954
+ setOverlayTextColor={ setOverlayTextColor }
955
+ overlayBackgroundColor={ overlayBackgroundColor }
956
+ setOverlayBackgroundColor={ setOverlayBackgroundColor }
957
+ clientId={ clientId }
958
+ navRef={ navRef }
959
+ hasCustomOverlay={ !! overlay }
960
+ />
961
+ </InspectorControls>
962
+ ) }
947
963
  </>
948
964
  );
949
965
 
@@ -0,0 +1,138 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { renderHook } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useSettings, useStyleOverride } from '@wordpress/block-editor';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import useLayoutCustomProperties from '../use-layout-custom-properties';
15
+
16
+ const mockGetResponsiveMediaQueries = jest.fn();
17
+
18
+ jest.mock( '@wordpress/block-editor', () => ( {
19
+ useSettings: jest.fn(),
20
+ useStyleOverride: jest.fn(),
21
+ } ) );
22
+
23
+ jest.mock( '@wordpress/global-styles-engine', () => ( {
24
+ privateApis: {},
25
+ } ) );
26
+
27
+ jest.mock( '../../../lock-unlock', () => ( {
28
+ unlock: () => ( {
29
+ getResponsiveMediaQueries: ( ...args ) =>
30
+ mockGetResponsiveMediaQueries( ...args ),
31
+ } ),
32
+ } ) );
33
+
34
+ describe( 'Navigation layout custom properties', () => {
35
+ beforeEach( () => {
36
+ jest.clearAllMocks();
37
+ useSettings.mockReturnValue( [ { mobile: 480, tablet: 782 } ] );
38
+ mockGetResponsiveMediaQueries.mockReturnValue( {
39
+ '@tablet': '@media (480px < width <= 782px)',
40
+ '@mobile': '@media (width <= 480px)',
41
+ } );
42
+ } );
43
+
44
+ describe( 'vertical layouts', () => {
45
+ it.each( [
46
+ [ 'left', 'initial', 'flex-start' ],
47
+ [ 'center', 'center', 'center' ],
48
+ [ 'right', 'flex-end', 'flex-end' ],
49
+ [ 'space-between', 'space-between', 'flex-start' ],
50
+ ] )(
51
+ 'reproduces the vertical %s class cascade',
52
+ ( justifyContent, justify, align ) => {
53
+ renderHook( () =>
54
+ useLayoutCustomProperties( {
55
+ clientId: 'test',
56
+ style: {
57
+ '@mobile': {
58
+ layout: {
59
+ orientation: 'vertical',
60
+ justifyContent,
61
+ flexWrap: 'nowrap',
62
+ },
63
+ },
64
+ },
65
+ } )
66
+ );
67
+
68
+ const { css } = useStyleOverride.mock.calls.at( -1 )[ 0 ];
69
+ expect( css ).toContain(
70
+ '--navigation-layout-direction: column;'
71
+ );
72
+ expect( css ).toContain( '--navigation-layout-wrap: nowrap;' );
73
+ expect( css ).toContain(
74
+ `--navigation-layout-justify: ${ justify };`
75
+ );
76
+ expect( css ).toContain(
77
+ `--navigation-layout-align: ${ align };`
78
+ );
79
+ }
80
+ );
81
+ } );
82
+
83
+ describe( 'responsive styles', () => {
84
+ it( 'merges viewport overrides with the base layout', () => {
85
+ renderHook( () =>
86
+ useLayoutCustomProperties( {
87
+ clientId: 'test',
88
+ layout: {
89
+ justifyContent: 'right',
90
+ flexWrap: 'nowrap',
91
+ },
92
+ style: {
93
+ '@mobile': {
94
+ layout: { orientation: 'vertical' },
95
+ },
96
+ },
97
+ } )
98
+ );
99
+
100
+ expect( useStyleOverride ).toHaveBeenCalledWith( {
101
+ css: '@media (width <= 480px){#block-test {--navigation-layout-justification-setting: flex-end;--navigation-layout-direction: column;--navigation-layout-wrap: nowrap;--navigation-layout-justify: flex-end;--navigation-layout-align: flex-end;}}',
102
+ } );
103
+ } );
104
+
105
+ it( 'uses default values for explicit viewport resets', () => {
106
+ renderHook( () =>
107
+ useLayoutCustomProperties( {
108
+ clientId: 'test',
109
+ layout: { justifyContent: 'right' },
110
+ style: {
111
+ '@tablet': {
112
+ layout: { justifyContent: null },
113
+ },
114
+ },
115
+ } )
116
+ );
117
+
118
+ expect( useStyleOverride ).toHaveBeenCalledWith( {
119
+ css: expect.stringContaining(
120
+ '--navigation-layout-justify: flex-start;'
121
+ ),
122
+ } );
123
+ } );
124
+
125
+ it( 'does not emit styles without a viewport layout override', () => {
126
+ renderHook( () =>
127
+ useLayoutCustomProperties( {
128
+ clientId: 'test',
129
+ style: {
130
+ '@mobile': { spacing: { blockGap: '10px' } },
131
+ },
132
+ } )
133
+ );
134
+
135
+ expect( useStyleOverride ).toHaveBeenCalledWith( { css: '' } );
136
+ } );
137
+ } );
138
+ } );
@@ -0,0 +1,84 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useSettings, useStyleOverride } from '@wordpress/block-editor';
5
+ import { privateApis as globalStylesEnginePrivateApis } from '@wordpress/global-styles-engine';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { unlock } from '../../lock-unlock';
11
+
12
+ const { getResponsiveMediaQueries } = unlock( globalStylesEnginePrivateApis );
13
+
14
+ const JUSTIFICATION_VALUES = {
15
+ left: 'flex-start',
16
+ center: 'center',
17
+ right: 'flex-end',
18
+ 'space-between': 'space-between',
19
+ };
20
+
21
+ /**
22
+ * Adds responsive Navigation layout custom properties to the editor canvas.
23
+ *
24
+ * @param {Object} options Options.
25
+ * @param {string} options.clientId Block client ID.
26
+ * @param {Object} options.layout Base layout configuration.
27
+ * @param {Object} options.style Block style attribute.
28
+ */
29
+ export default function useLayoutCustomProperties( {
30
+ clientId,
31
+ layout,
32
+ style,
33
+ } ) {
34
+ const [ viewportSettings ] = useSettings( 'viewport' );
35
+ const selector = `#block-${ clientId }`;
36
+ const css = Object.entries( getResponsiveMediaQueries( viewportSettings ) )
37
+ .map( ( [ viewport, mediaQuery ] ) => {
38
+ const viewportLayout = style?.[ viewport ]?.layout;
39
+ if (
40
+ ! viewportLayout ||
41
+ typeof viewportLayout !== 'object' ||
42
+ Array.isArray( viewportLayout ) ||
43
+ ! Object.keys( viewportLayout ).length
44
+ ) {
45
+ return '';
46
+ }
47
+
48
+ const effectiveLayout = { ...layout, ...viewportLayout };
49
+ const justifyContent = Object.hasOwn(
50
+ JUSTIFICATION_VALUES,
51
+ effectiveLayout.justifyContent
52
+ )
53
+ ? effectiveLayout.justifyContent
54
+ : 'left';
55
+ const justification = JUSTIFICATION_VALUES[ justifyContent ];
56
+ const isVertical = effectiveLayout.orientation === 'vertical';
57
+ let align = 'center';
58
+ if ( isVertical ) {
59
+ align = [ 'center', 'right' ].includes( justifyContent )
60
+ ? justification
61
+ : 'flex-start';
62
+ }
63
+ const justify =
64
+ isVertical && justifyContent === 'left'
65
+ ? 'initial'
66
+ : justification;
67
+ const declarations = {
68
+ '--navigation-layout-justification-setting': justification,
69
+ '--navigation-layout-direction': isVertical ? 'column' : 'row',
70
+ '--navigation-layout-wrap':
71
+ effectiveLayout.flexWrap === 'nowrap' ? 'nowrap' : 'wrap',
72
+ '--navigation-layout-justify': justify,
73
+ '--navigation-layout-align': align,
74
+ };
75
+ const cssDeclarations = Object.entries( declarations )
76
+ .map( ( [ property, value ] ) => `${ property }: ${ value };` )
77
+ .join( '' );
78
+
79
+ return `${ mediaQuery }{${ selector } {${ cssDeclarations }}}`;
80
+ } )
81
+ .join( '' );
82
+
83
+ useStyleOverride( { css } );
84
+ }
@@ -16,7 +16,7 @@
16
16
  * will always have a value even for legacy blocks. We check the legacy openSubmenusOnClick
17
17
  * attribute first to preserve original behavior for blocks saved before the migration.
18
18
  *
19
- * @since 6.9.0
19
+ * @since 7.0.0
20
20
  *
21
21
  * @param array $attributes Block attributes containing submenuVisibility and/or openSubmenusOnClick.
22
22
  * @return string The visibility mode: 'hover', 'click', or 'always'.
@@ -40,6 +40,45 @@ function block_core_navigation_get_submenu_visibility( $attributes ) {
40
40
  return $submenu_visibility ?? 'hover';
41
41
  }
42
42
 
43
+ /**
44
+ * Returns the custom properties used by the Navigation block for a layout.
45
+ *
46
+ * @since 7.1.0
47
+ *
48
+ * @param array $layout Layout configuration.
49
+ * @return array Navigation layout custom property declarations.
50
+ */
51
+ function block_core_navigation_get_layout_custom_property_declarations( $layout ) {
52
+ $justification_values = array(
53
+ 'left' => 'flex-start',
54
+ 'center' => 'center',
55
+ 'right' => 'flex-end',
56
+ 'space-between' => 'space-between',
57
+ );
58
+ $justify_content = is_array( $layout ) ? ( $layout['justifyContent'] ?? 'left' ) : 'left';
59
+ if ( ! is_string( $justify_content ) || ! isset( $justification_values[ $justify_content ] ) ) {
60
+ $justify_content = 'left';
61
+ }
62
+
63
+ $justification = $justification_values[ $justify_content ];
64
+ $is_vertical = is_array( $layout ) && 'vertical' === ( $layout['orientation'] ?? null );
65
+ $align = 'center';
66
+ $justify = $justification;
67
+
68
+ if ( $is_vertical ) {
69
+ $align = in_array( $justify_content, array( 'center', 'right' ), true ) ? $justification : 'flex-start';
70
+ $justify = 'left' === $justify_content ? 'initial' : $justification;
71
+ }
72
+
73
+ return array(
74
+ '--navigation-layout-justification-setting' => $justification,
75
+ '--navigation-layout-direction' => $is_vertical ? 'column' : 'row',
76
+ '--navigation-layout-wrap' => is_array( $layout ) && 'nowrap' === ( $layout['flexWrap'] ?? null ) ? 'nowrap' : 'wrap',
77
+ '--navigation-layout-justify' => $justify,
78
+ '--navigation-layout-align' => $align,
79
+ );
80
+ }
81
+
43
82
  /**
44
83
  * Helper functions used to render the navigation block.
45
84
  *
@@ -576,12 +615,13 @@ class WP_Navigation_Block_Renderer {
576
615
  'space-between' => 'items-justified-space-between',
577
616
  );
578
617
 
579
- $layout_class = '';
618
+ $layout_class = '';
619
+ $nav_justify_content = $attributes['layout']['justifyContent'] ?? null;
580
620
  if (
581
- isset( $attributes['layout']['justifyContent'] ) &&
582
- isset( $layout_justification[ $attributes['layout']['justifyContent'] ] )
621
+ is_string( $nav_justify_content ) &&
622
+ isset( $layout_justification[ $nav_justify_content ] )
583
623
  ) {
584
- $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ];
624
+ $layout_class .= $layout_justification[ $nav_justify_content ];
585
625
  }
586
626
  if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) {
587
627
  $layout_class .= ' is-vertical';
@@ -1211,7 +1251,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut
1211
1251
  )
1212
1252
  ) ) {
1213
1253
  $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' );
1214
- $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isMenuOpen' );
1254
+ $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isSubmenuOpen' );
1215
1255
  // The `aria-expanded` attribute for SSR is already added in the submenu block.
1216
1256
  }
1217
1257
  // Add directives to the submenu.
@@ -1558,13 +1598,18 @@ function register_block_core_navigation() {
1558
1598
  add_action( 'init', 'register_block_core_navigation' );
1559
1599
 
1560
1600
  /**
1561
- * Adds the Navigation block state class to inner list containers.
1601
+ * Adds Navigation block support classes to inner list containers.
1562
1602
  *
1563
1603
  * State block support adds the generated `wp-states-*` class to the outer
1564
1604
  * block wrapper. The Navigation block renders its menu items inside an inner
1565
1605
  * `wp-block-navigation__container` list, so the same state class is also needed
1566
1606
  * there for state styles to apply directly to the menu list.
1567
1607
  *
1608
+ * Navigation also uses layout classes on its outer wrapper to define custom
1609
+ * properties consumed by its inner containers. Viewport layout styles cannot
1610
+ * change those classes, so equivalent custom properties and a scoping class
1611
+ * are generated for each configured viewport layout.
1612
+ *
1568
1613
  * Currently this is required as a workaround because of how difficult it is for nav
1569
1614
  * child blocks to inherit styles through the complex responsive nav block html. The
1570
1615
  * bug in https://github.com/WordPress/gutenberg/issues/62690 also prevents inheritance.
@@ -1575,25 +1620,83 @@ add_action( 'init', 'register_block_core_navigation' );
1575
1620
  * @param array $block The full block, including name and attributes.
1576
1621
  * @return string The updated block content.
1577
1622
  */
1578
- function block_core_navigation_add_state_class_to_container( $block_content, $block ) {
1623
+ function block_core_navigation_add_support_classes_to_container( $block_content, $block ) {
1579
1624
  if ( 'core/navigation' !== ( $block['blockName'] ?? null ) || empty( $block_content ) ) {
1580
1625
  return $block_content;
1581
1626
  }
1582
1627
 
1628
+ $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array();
1629
+ $style = is_array( $attributes['style'] ?? null ) ? $attributes['style'] : array();
1630
+ if (
1631
+ defined( 'IS_GUTENBERG_PLUGIN' ) &&
1632
+ IS_GUTENBERG_PLUGIN &&
1633
+ function_exists( 'gutenberg_resolve_style_state_aliases' )
1634
+ ) {
1635
+ $style = gutenberg_resolve_style_state_aliases( $style, 'core/navigation' );
1636
+ }
1637
+
1638
+ $global_settings = wp_get_global_settings();
1639
+ $viewport_settings = $global_settings['viewport'] ?? null;
1640
+ $responsive_media_queries = array();
1641
+ if ( method_exists( 'WP_Theme_JSON_Gutenberg', 'get_viewport_media_queries' ) ) {
1642
+ $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings );
1643
+ } elseif ( method_exists( 'WP_Theme_JSON', 'get_viewport_media_queries' ) ) {
1644
+ $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
1645
+ }
1646
+
1647
+ $styles = array();
1648
+ $base_layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array();
1649
+ foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1650
+ $viewport_style = is_array( $style[ $breakpoint ] ?? null ) ? $style[ $breakpoint ] : array();
1651
+ $viewport_layout = is_array( $viewport_style['layout'] ?? null ) ? $viewport_style['layout'] : array();
1652
+ if ( empty( $viewport_layout ) ) {
1653
+ continue;
1654
+ }
1655
+
1656
+ $styles[] = array(
1657
+ 'declarations' => block_core_navigation_get_layout_custom_property_declarations(
1658
+ array_replace( $base_layout, $viewport_layout )
1659
+ ),
1660
+ 'rules_group' => $media_query,
1661
+ );
1662
+ }
1663
+
1583
1664
  $processor = new WP_HTML_Tag_Processor( $block_content );
1584
1665
  if ( ! $processor->next_tag() ) {
1585
1666
  return $block_content;
1586
1667
  }
1587
1668
 
1588
1669
  $class_attribute = $processor->get_attribute( 'class' );
1589
- if ( ! is_string( $class_attribute ) || ! preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute, $matches ) ) {
1670
+ $state_class = null;
1671
+ if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute, $matches ) ) {
1672
+ $state_class = $matches[0];
1673
+ }
1674
+
1675
+ $layout_class = null;
1676
+ if ( ! empty( $styles ) ) {
1677
+ $layout_class = wp_unique_id( 'wp-block-navigation-' );
1678
+ // The inner selector includes both Navigation classes so it overrides the
1679
+ // default layout custom properties set by `.wp-block-navigation.items-*`.
1680
+ $selector = ".wp-block-navigation.{$layout_class},.wp-block-navigation.wp-block-navigation__container.{$layout_class}";
1681
+ foreach ( $styles as &$style_rule ) {
1682
+ $style_rule['selector'] = $selector;
1683
+ }
1684
+ unset( $style_rule );
1685
+
1686
+ $processor->add_class( $layout_class );
1687
+ wp_style_engine_get_stylesheet_from_css_rules(
1688
+ $styles,
1689
+ array( 'context' => 'block-supports' )
1690
+ );
1691
+ }
1692
+
1693
+ if ( null === $state_class && null === $layout_class ) {
1590
1694
  return $block_content;
1591
1695
  }
1592
1696
 
1593
- $state_class = $matches[0];
1594
1697
  while ( $processor->next_tag() ) {
1595
1698
  // Custom overlay content can include nested Navigation blocks.
1596
- // Avoid applying the outer Navigation state class to an inner nav block.
1699
+ // Avoid applying the outer Navigation classes to an inner nav block.
1597
1700
  if ( $processor->has_class( 'wp-block-navigation' ) && ! $processor->has_class( 'wp-block-navigation__container' ) ) {
1598
1701
  break;
1599
1702
  }
@@ -1602,6 +1705,14 @@ function block_core_navigation_add_state_class_to_container( $block_content, $bl
1602
1705
  continue;
1603
1706
  }
1604
1707
 
1708
+ if ( null !== $layout_class ) {
1709
+ $processor->add_class( $layout_class );
1710
+ }
1711
+
1712
+ if ( null === $state_class ) {
1713
+ continue;
1714
+ }
1715
+
1605
1716
  $class_attribute = $processor->get_attribute( 'class' );
1606
1717
  if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute ) ) {
1607
1718
  continue;
@@ -1613,7 +1724,7 @@ function block_core_navigation_add_state_class_to_container( $block_content, $bl
1613
1724
  return $processor->get_updated_html();
1614
1725
  }
1615
1726
 
1616
- add_filter( 'render_block', 'block_core_navigation_add_state_class_to_container', 11, 2 );
1727
+ add_filter( 'render_block', 'block_core_navigation_add_support_classes_to_container', 11, 2 );
1617
1728
 
1618
1729
  /**
1619
1730
  * Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly.
@@ -0,0 +1,51 @@
1
+ let mockRegisteredStore;
2
+ let mockCurrentContext;
3
+
4
+ jest.mock( '@wordpress/interactivity', () => ( {
5
+ store: ( name, config ) => {
6
+ mockRegisteredStore = config;
7
+ return config;
8
+ },
9
+ getContext: () => mockCurrentContext,
10
+ getElement: () => ( { ref: {} } ),
11
+ withSyncEvent: ( fn ) => fn,
12
+ } ) );
13
+
14
+ describe( 'Navigation view script', () => {
15
+ beforeEach( async () => {
16
+ jest.resetModules();
17
+ mockRegisteredStore = null;
18
+ mockCurrentContext = null;
19
+ // Import the view.js script to register the store
20
+ await import( '../view.js' );
21
+ } );
22
+
23
+ it( 'updates submenuOpenedBy.hover when hovering a submenu inside an open overlay', () => {
24
+ // Mock the context for a submenu inside an open overlay
25
+ mockCurrentContext = {
26
+ type: 'submenu',
27
+ submenuOpenedBy: { click: false, hover: false, focus: false },
28
+ overlayOpenedBy: { click: true, hover: false, focus: false },
29
+ };
30
+
31
+ const { state, actions } = mockRegisteredStore;
32
+
33
+ // Verify initial state
34
+ expect( state.isSubmenuOpen ).toBe( true ); // because overlay is open
35
+ expect( mockCurrentContext.submenuOpenedBy.hover ).toBe( false );
36
+
37
+ // Simulate hover enter
38
+ actions.openMenuOnHover( { pointerType: 'mouse' } );
39
+
40
+ // Verify that hover state is tracked even though submenu is already "open" visually
41
+ expect( mockCurrentContext.submenuOpenedBy.hover ).toBe( true );
42
+ expect( state.isSubmenuOpen ).toBe( true ); // remains true
43
+
44
+ // Simulate hover leave
45
+ actions.closeMenuOnHover( { pointerType: 'mouse' } );
46
+
47
+ // Verify hover state is cleared
48
+ expect( mockCurrentContext.submenuOpenedBy.hover ).toBe( false );
49
+ expect( state.isSubmenuOpen ).toBe( true ); // remains true because overlay is still open
50
+ } );
51
+ } );
@@ -80,6 +80,17 @@ const { state, actions } = store(
80
80
  ? ctx.overlayOpenedBy
81
81
  : ctx.submenuOpenedBy;
82
82
  },
83
+ get isSubmenuOpen() {
84
+ const ctx = getContext();
85
+ // Once the overlay itself is open, its styles always expand
86
+ // every submenu regardless of hover/click/focus state, so the
87
+ // toggle's `aria-expanded` should reflect that immediately
88
+ // instead of waiting for a hover/click/focus interaction.
89
+ const isOverlayOpen =
90
+ Object.values( ctx.overlayOpenedBy || {} ).filter( Boolean )
91
+ .length > 0;
92
+ return isOverlayOpen || state.isMenuOpen;
93
+ },
83
94
  },
84
95
  actions: {
85
96
  openMenuOnHover( event ) {
@@ -88,13 +99,8 @@ const { state, actions } = store(
88
99
  if ( event?.pointerType === 'touch' ) {
89
100
  return;
90
101
  }
91
- const { type, overlayOpenedBy } = getContext();
92
- if (
93
- type === 'submenu' &&
94
- // Only open on hover if the overlay is closed.
95
- Object.values( overlayOpenedBy || {} ).filter( Boolean )
96
- .length === 0
97
- ) {
102
+ const { type } = getContext();
103
+ if ( type === 'submenu' ) {
98
104
  actions.openMenu( 'hover' );
99
105
  }
100
106
  },
@@ -102,13 +108,8 @@ const { state, actions } = store(
102
108
  if ( event?.pointerType === 'touch' ) {
103
109
  return;
104
110
  }
105
- const { type, overlayOpenedBy } = getContext();
106
- if (
107
- type === 'submenu' &&
108
- // Only close on hover if the overlay is closed.
109
- Object.values( overlayOpenedBy || {} ).filter( Boolean )
110
- .length === 0
111
- ) {
111
+ const { type } = getContext();
112
+ if ( type === 'submenu' ) {
112
113
  actions.closeMenu( 'hover' );
113
114
  }
114
115
  },
@@ -425,12 +425,15 @@ export default function NavigationLinkEdit( {
425
425
  }
426
426
  onMerge={ mergeBlocks }
427
427
  onReplace={ onReplace }
428
- __unstableOnSplitAtEnd={ () =>
429
- insertBlocksAfter(
430
- createBlock(
431
- 'core/navigation-link'
432
- )
433
- )
428
+ __unstableOnSplitAtEnd={
429
+ insertBlocksAfter
430
+ ? () =>
431
+ insertBlocksAfter(
432
+ createBlock(
433
+ 'core/navigation-link'
434
+ )
435
+ )
436
+ : undefined
434
437
  }
435
438
  aria-label={ __(
436
439
  'Navigation link text'
@@ -470,7 +473,8 @@ export default function NavigationLinkEdit( {
470
473
  // If there is no link and no binding, remove the auto-inserted block.
471
474
  // This avoids empty blocks which can provided a poor UX.
472
475
  // Don't remove if binding exists (even if entity is unavailable) so user can fix it.
473
- if ( ! url && ! hasUrlBinding ) {
476
+ // `onReplace` is undefined when the block can't be removed.
477
+ if ( ! url && ! hasUrlBinding && onReplace ) {
474
478
  onReplace( [] );
475
479
  return;
476
480
  }