@wordpress/block-library 9.40.2-next.v.202602241322.0 → 9.41.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 (153) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/accordion/view.cjs +0 -34
  3. package/build/accordion/view.cjs.map +2 -2
  4. package/build/button/block.json +11 -3
  5. package/build/button/deprecated.cjs +246 -13
  6. package/build/button/deprecated.cjs.map +2 -2
  7. package/build/button/edit.cjs +45 -58
  8. package/build/button/edit.cjs.map +3 -3
  9. package/build/button/save.cjs +3 -7
  10. package/build/button/save.cjs.map +2 -2
  11. package/build/button/utils.cjs +59 -0
  12. package/build/button/utils.cjs.map +7 -0
  13. package/build/image/image.cjs +1 -1
  14. package/build/image/image.cjs.map +2 -2
  15. package/build/navigation/edit/index.cjs +4 -2
  16. package/build/navigation/edit/index.cjs.map +3 -3
  17. package/build/navigation/edit/leaf-more-menu.cjs +68 -6
  18. package/build/navigation/edit/leaf-more-menu.cjs.map +3 -3
  19. package/build/navigation/edit/menu-inspector-controls.cjs +20 -91
  20. package/build/navigation/edit/menu-inspector-controls.cjs.map +3 -3
  21. package/build/navigation/edit/navigation-link-ui.cjs +97 -0
  22. package/build/navigation/edit/navigation-link-ui.cjs.map +7 -0
  23. package/build/navigation/edit/navigation-list-view-header.cjs +86 -0
  24. package/build/navigation/edit/navigation-list-view-header.cjs.map +7 -0
  25. package/build/navigation/edit/navigation-menu-selector.cjs +4 -2
  26. package/build/navigation/edit/navigation-menu-selector.cjs.map +3 -3
  27. package/build/navigation/edit/placeholder/index.cjs +2 -2
  28. package/build/navigation/edit/placeholder/index.cjs.map +3 -3
  29. package/build/navigation-link/shared/controls.cjs +29 -52
  30. package/build/navigation-link/shared/controls.cjs.map +3 -3
  31. package/build/navigation-link/shared/use-link-preview.cjs +8 -9
  32. package/build/navigation-link/shared/use-link-preview.cjs.map +2 -2
  33. package/build/page-list-item/edit.cjs +6 -3
  34. package/build/page-list-item/edit.cjs.map +2 -2
  35. package/build/playlist/edit.cjs +43 -136
  36. package/build/playlist/edit.cjs.map +3 -3
  37. package/build/playlist/view.cjs +56 -38
  38. package/build/playlist/view.cjs.map +2 -2
  39. package/build/playlist-track/edit.cjs +0 -1
  40. package/build/playlist-track/edit.cjs.map +2 -2
  41. package/build/post-title/block.json +3 -0
  42. package/build/post-title/edit.cjs +2 -2
  43. package/build/post-title/edit.cjs.map +2 -2
  44. package/build/utils/waveform-player.cjs +68 -0
  45. package/build/utils/waveform-player.cjs.map +7 -0
  46. package/build/utils/waveform-utils.cjs +171 -0
  47. package/build/utils/waveform-utils.cjs.map +7 -0
  48. package/build-module/accordion/view.mjs +1 -35
  49. package/build-module/accordion/view.mjs.map +2 -2
  50. package/build-module/button/block.json +11 -3
  51. package/build-module/button/deprecated.mjs +246 -13
  52. package/build-module/button/deprecated.mjs.map +2 -2
  53. package/build-module/button/edit.mjs +47 -63
  54. package/build-module/button/edit.mjs.map +2 -2
  55. package/build-module/button/save.mjs +3 -7
  56. package/build-module/button/save.mjs.map +2 -2
  57. package/build-module/button/utils.mjs +33 -0
  58. package/build-module/button/utils.mjs.map +7 -0
  59. package/build-module/image/image.mjs +1 -1
  60. package/build-module/image/image.mjs.map +2 -2
  61. package/build-module/navigation/edit/index.mjs +4 -2
  62. package/build-module/navigation/edit/index.mjs.map +2 -2
  63. package/build-module/navigation/edit/leaf-more-menu.mjs +73 -7
  64. package/build-module/navigation/edit/leaf-more-menu.mjs.map +2 -2
  65. package/build-module/navigation/edit/menu-inspector-controls.mjs +21 -101
  66. package/build-module/navigation/edit/menu-inspector-controls.mjs.map +2 -2
  67. package/build-module/navigation/edit/navigation-link-ui.mjs +76 -0
  68. package/build-module/navigation/edit/navigation-link-ui.mjs.map +7 -0
  69. package/build-module/navigation/edit/navigation-list-view-header.mjs +58 -0
  70. package/build-module/navigation/edit/navigation-list-view-header.mjs.map +7 -0
  71. package/build-module/navigation/edit/navigation-menu-selector.mjs +5 -3
  72. package/build-module/navigation/edit/navigation-menu-selector.mjs.map +2 -2
  73. package/build-module/navigation/edit/placeholder/index.mjs +2 -2
  74. package/build-module/navigation/edit/placeholder/index.mjs.map +2 -2
  75. package/build-module/navigation-link/shared/controls.mjs +29 -53
  76. package/build-module/navigation-link/shared/controls.mjs.map +2 -2
  77. package/build-module/navigation-link/shared/use-link-preview.mjs +8 -9
  78. package/build-module/navigation-link/shared/use-link-preview.mjs.map +2 -2
  79. package/build-module/page-list-item/edit.mjs +6 -3
  80. package/build-module/page-list-item/edit.mjs.map +2 -2
  81. package/build-module/playlist/edit.mjs +41 -139
  82. package/build-module/playlist/edit.mjs.map +2 -2
  83. package/build-module/playlist/view.mjs +56 -38
  84. package/build-module/playlist/view.mjs.map +2 -2
  85. package/build-module/playlist-track/edit.mjs +0 -1
  86. package/build-module/playlist-track/edit.mjs.map +2 -2
  87. package/build-module/post-title/block.json +3 -0
  88. package/build-module/post-title/edit.mjs +2 -2
  89. package/build-module/post-title/edit.mjs.map +2 -2
  90. package/build-module/utils/waveform-player.mjs +43 -0
  91. package/build-module/utils/waveform-player.mjs.map +7 -0
  92. package/build-module/utils/waveform-utils.mjs +131 -0
  93. package/build-module/utils/waveform-utils.mjs.map +7 -0
  94. package/build-style/button/style-rtl.css +6 -0
  95. package/build-style/button/style.css +6 -0
  96. package/build-style/editor-rtl.css +13 -3
  97. package/build-style/editor.css +13 -3
  98. package/build-style/navigation-link/editor-rtl.css +10 -0
  99. package/build-style/navigation-link/editor.css +10 -0
  100. package/build-style/playlist/editor-rtl.css +3 -3
  101. package/build-style/playlist/editor.css +3 -3
  102. package/build-style/playlist/style-rtl.css +351 -17
  103. package/build-style/playlist/style.css +351 -17
  104. package/build-style/style-rtl.css +357 -17
  105. package/build-style/style.css +357 -17
  106. package/package.json +39 -38
  107. package/src/accordion/view.js +1 -44
  108. package/src/accordion-item/index.php +0 -1
  109. package/src/button/block.json +11 -3
  110. package/src/button/deprecated.js +254 -16
  111. package/src/button/edit.js +50 -61
  112. package/src/button/index.php +68 -0
  113. package/src/button/save.js +2 -8
  114. package/src/button/style.scss +49 -7
  115. package/src/button/test/utils.js +84 -0
  116. package/src/button/utils.js +42 -0
  117. package/src/cover/index.php +4 -4
  118. package/src/image/image.js +14 -15
  119. package/src/image/index.php +3 -1
  120. package/src/navigation/edit/index.js +4 -2
  121. package/src/navigation/edit/leaf-more-menu.js +86 -11
  122. package/src/navigation/edit/menu-inspector-controls.js +23 -142
  123. package/src/navigation/edit/navigation-link-ui.js +115 -0
  124. package/src/navigation/edit/navigation-list-view-header.js +62 -0
  125. package/src/navigation/edit/navigation-menu-selector.js +5 -3
  126. package/src/navigation/edit/placeholder/index.js +3 -2
  127. package/src/navigation/edit/test/navigation-menu-selector.js +23 -20
  128. package/src/navigation-link/editor.scss +18 -0
  129. package/src/navigation-link/shared/controls.js +35 -62
  130. package/src/navigation-link/shared/test/controls.js +5 -5
  131. package/src/navigation-link/shared/test/use-link-preview.test.js +19 -1
  132. package/src/navigation-link/shared/use-link-preview.js +14 -15
  133. package/src/page-list/index.php +1 -1
  134. package/src/page-list-item/edit.js +8 -7
  135. package/src/playlist/edit.js +60 -154
  136. package/src/playlist/editor.scss +3 -3
  137. package/src/playlist/index.php +15 -40
  138. package/src/playlist/style.scss +34 -27
  139. package/src/playlist/test/edit.js +137 -0
  140. package/src/playlist/view.js +97 -40
  141. package/src/playlist-track/edit.js +0 -1
  142. package/src/post-title/block.json +3 -0
  143. package/src/post-title/edit.js +4 -2
  144. package/src/query-title/index.php +1 -1
  145. package/src/search/index.php +1 -1
  146. package/src/utils/test/waveform-utils.js +328 -0
  147. package/src/utils/waveform-player.js +77 -0
  148. package/src/utils/waveform-utils.js +232 -0
  149. package/build/navigation/use-navigation-entities.cjs +0 -67
  150. package/build/navigation/use-navigation-entities.cjs.map +0 -7
  151. package/build-module/navigation/use-navigation-entities.mjs +0 -46
  152. package/build-module/navigation/use-navigation-entities.mjs.map +0 -7
  153. package/src/navigation/use-navigation-entities.js +0 -72
@@ -55,16 +55,41 @@ $blocks-block__margin: 0.5em;
55
55
  }
56
56
  }
57
57
 
58
+ &[class*="wp-block-button__width"] {
59
+ width:
60
+ calc(var(--wp--block-button--width) * 1% -
61
+ (
62
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
63
+ ( 1 - var(--wp--block-button--width) / 100 )
64
+ ));
65
+ }
66
+
67
+ // Legacy width classes for backwards compatibility.
58
68
  &.wp-block-button__width-25 {
59
- width: calc(25% - (var(--wp--style--block-gap, #{$blocks-block__margin}) * 0.75));
69
+ width:
70
+ calc(25% -
71
+ (
72
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
73
+ 0.75
74
+ ));
60
75
  }
61
76
 
62
77
  &.wp-block-button__width-50 {
63
- width: calc(50% - (var(--wp--style--block-gap, #{$blocks-block__margin}) * 0.5));
78
+ width:
79
+ calc(50% -
80
+ (
81
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
82
+ 0.5
83
+ ));
64
84
  }
65
85
 
66
86
  &.wp-block-button__width-75 {
67
- width: calc(75% - (var(--wp--style--block-gap, #{$blocks-block__margin}) * 0.25));
87
+ width:
88
+ calc(75% -
89
+ (
90
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
91
+ 0.25
92
+ ));
68
93
  }
69
94
 
70
95
  &.wp-block-button__width-100 {
@@ -75,6 +100,11 @@ $blocks-block__margin: 0.5em;
75
100
 
76
101
  // For vertical buttons, gap is not factored into width calculations.
77
102
  .wp-block-buttons.is-vertical > .wp-block-button {
103
+ &[class*="wp-block-button__width"] {
104
+ width: calc(var(--wp--block-button--width) * 1%);
105
+ }
106
+
107
+ // Legacy width classes for backwards compatibility.
78
108
  &.wp-block-button__width-25 {
79
109
  width: 25%;
80
110
  }
@@ -110,13 +140,25 @@ $blocks-block__margin: 0.5em;
110
140
  padding: 0.667em 1.333em;
111
141
  }
112
142
 
113
- :where(.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-text-color)),
114
- :where(.wp-block-button .wp-block-button__link.is-style-outline:not(.has-text-color)) {
143
+ :where(
144
+ .wp-block-button.is-style-outline
145
+ > .wp-block-button__link:not(.has-text-color)
146
+ ),
147
+ :where(
148
+ .wp-block-button
149
+ .wp-block-button__link.is-style-outline:not(.has-text-color)
150
+ ) {
115
151
  color: currentColor;
116
152
  }
117
153
 
118
- :where(.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-background)),
119
- :where(.wp-block-button .wp-block-button__link.is-style-outline:not(.has-background)) {
154
+ :where(
155
+ .wp-block-button.is-style-outline
156
+ > .wp-block-button__link:not(.has-background)
157
+ ),
158
+ :where(
159
+ .wp-block-button
160
+ .wp-block-button__link.is-style-outline:not(.has-background)
161
+ ) {
120
162
  background-color: transparent;
121
163
  // background-image is required to overwrite a gradient background
122
164
  background-image: none;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { getWidthClasses, isPercentageWidth } from '../utils';
5
+
6
+ describe( 'isPercentageWidth', () => {
7
+ it( 'should return true for percentage values', () => {
8
+ expect( isPercentageWidth( '50%' ) ).toBe( true );
9
+ expect( isPercentageWidth( '100%' ) ).toBe( true );
10
+ expect( isPercentageWidth( '33.5%' ) ).toBe( true );
11
+ } );
12
+
13
+ it( 'should return false for non-percentage values', () => {
14
+ expect( isPercentageWidth( '200px' ) ).toBe( false );
15
+ expect( isPercentageWidth( '10em' ) ).toBe( false );
16
+ expect( isPercentageWidth( undefined ) ).toBe( false );
17
+ expect( isPercentageWidth( null ) ).toBe( false );
18
+ } );
19
+
20
+ it( 'should return false for preset strings', () => {
21
+ expect( isPercentageWidth( 'var:preset|dimension|custom-width' ) ).toBe(
22
+ false
23
+ );
24
+ } );
25
+ } );
26
+
27
+ describe( 'getWidthClasses', () => {
28
+ it( 'should return empty object when no width is provided', () => {
29
+ expect( getWidthClasses( undefined ) ).toEqual( {} );
30
+ expect( getWidthClasses( '' ) ).toEqual( {} );
31
+ expect( getWidthClasses( null ) ).toEqual( {} );
32
+ } );
33
+
34
+ it( 'should return percentage classes for standard percentage widths', () => {
35
+ expect( getWidthClasses( '25%' ) ).toEqual( {
36
+ 'has-custom-width': true,
37
+ 'wp-block-button__width': true,
38
+ 'wp-block-button__width-25': true,
39
+ } );
40
+
41
+ expect( getWidthClasses( '50%' ) ).toEqual( {
42
+ 'has-custom-width': true,
43
+ 'wp-block-button__width': true,
44
+ 'wp-block-button__width-50': true,
45
+ } );
46
+
47
+ expect( getWidthClasses( '75%' ) ).toEqual( {
48
+ 'has-custom-width': true,
49
+ 'wp-block-button__width': true,
50
+ 'wp-block-button__width-75': true,
51
+ } );
52
+
53
+ expect( getWidthClasses( '100%' ) ).toEqual( {
54
+ 'has-custom-width': true,
55
+ 'wp-block-button__width': true,
56
+ 'wp-block-button__width-100': true,
57
+ } );
58
+ } );
59
+
60
+ it( 'should return generic percentage classes for non-standard percentage widths', () => {
61
+ expect( getWidthClasses( '33%' ) ).toEqual( {
62
+ 'has-custom-width': true,
63
+ 'wp-block-button__width': true,
64
+ } );
65
+ } );
66
+
67
+ it( 'should return only has-custom-width for non-percentage values', () => {
68
+ expect( getWidthClasses( '200px' ) ).toEqual( {
69
+ 'has-custom-width': true,
70
+ } );
71
+
72
+ expect( getWidthClasses( '10em' ) ).toEqual( {
73
+ 'has-custom-width': true,
74
+ } );
75
+ } );
76
+
77
+ it( 'should return only has-custom-width for resolved non-percentage preset values', () => {
78
+ // When a preset resolves to a non-percentage value (e.g., 200px),
79
+ // the resolved value is passed to getWidthClasses, not the preset string.
80
+ expect( getWidthClasses( '300px' ) ).toEqual( {
81
+ 'has-custom-width': true,
82
+ } );
83
+ } );
84
+ } );
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Returns whether the given width value is a percentage.
3
+ *
4
+ * @param {string} width - The width value.
5
+ * @return {boolean} True if the width is a percentage value.
6
+ */
7
+ export function isPercentageWidth( width ) {
8
+ return typeof width === 'string' && width.endsWith( '%' );
9
+ }
10
+
11
+ /**
12
+ * Returns the width classes for the button based on the width attribute.
13
+ *
14
+ * @param {string} width - The width value (e.g., '25%', '50%', '75%', '100%', or custom value).
15
+ * @return {Object} Object with width-related class names as keys and true as values.
16
+ */
17
+ export function getWidthClasses( width ) {
18
+ if ( ! width ) {
19
+ return {};
20
+ }
21
+
22
+ if ( isPercentageWidth( width ) ) {
23
+ const legacyWidthClasses = {
24
+ '25%': 'wp-block-button__width-25',
25
+ '50%': 'wp-block-button__width-50',
26
+ '75%': 'wp-block-button__width-75',
27
+ '100%': 'wp-block-button__width-100',
28
+ };
29
+ return {
30
+ 'has-custom-width': true,
31
+ 'wp-block-button__width': true,
32
+ // Maintain legacy class for backwards compatibility.
33
+ ...( legacyWidthClasses[ width ] && {
34
+ [ legacyWidthClasses[ width ] ]: true,
35
+ } ),
36
+ };
37
+ }
38
+
39
+ return {
40
+ 'has-custom-width': true,
41
+ };
42
+ }
@@ -39,13 +39,13 @@ function render_block_core_cover( $attributes, $content ) {
39
39
  $lower_src = strtolower( $iframe_src );
40
40
  $provider = null;
41
41
 
42
- if ( strpos( $lower_src, 'youtube.com' ) !== false || strpos( $lower_src, 'youtu.be' ) !== false ) {
42
+ if ( str_contains( $lower_src, 'youtube.com' ) || str_contains( $lower_src, 'youtu.be' ) ) {
43
43
  $provider = 'youtube';
44
- } elseif ( strpos( $lower_src, 'vimeo.com' ) !== false ) {
44
+ } elseif ( str_contains( $lower_src, 'vimeo.com' ) ) {
45
45
  $provider = 'vimeo';
46
- } elseif ( strpos( $lower_src, 'videopress.com' ) !== false ) {
46
+ } elseif ( str_contains( $lower_src, 'videopress.com' ) ) {
47
47
  $provider = 'videopress';
48
- } elseif ( strpos( $lower_src, 'wordpress.tv' ) !== false ) {
48
+ } elseif ( str_contains( $lower_src, 'wordpress.tv' ) ) {
49
49
  $provider = 'wordpress-tv';
50
50
  }
51
51
 
@@ -1241,21 +1241,20 @@ export default function Image( {
1241
1241
  } );
1242
1242
  };
1243
1243
 
1244
- const featuredImageControl = (
1245
- <BlockSettingsMenuControls>
1246
- { ( { selectedClientIds } ) =>
1247
- selectedClientIds.length === 1 &&
1248
- ! isDescendentOfQueryLoop &&
1249
- postId &&
1250
- id &&
1251
- clientId === selectedClientIds[ 0 ] && (
1252
- <MenuItem onClick={ setPostFeatureImage }>
1253
- { __( 'Set as featured image' ) }
1254
- </MenuItem>
1255
- )
1256
- }
1257
- </BlockSettingsMenuControls>
1258
- );
1244
+ const featuredImageControl =
1245
+ ! isDescendentOfQueryLoop && postId && id ? (
1246
+ <BlockSettingsMenuControls>
1247
+ { ( { canEdit, selectedClientIds } ) =>
1248
+ canEdit &&
1249
+ selectedClientIds.length === 1 &&
1250
+ clientId === selectedClientIds[ 0 ] && (
1251
+ <MenuItem onClick={ setPostFeatureImage }>
1252
+ { __( 'Set as featured image' ) }
1253
+ </MenuItem>
1254
+ )
1255
+ }
1256
+ </BlockSettingsMenuControls>
1257
+ ) : null;
1259
1258
 
1260
1259
  return (
1261
1260
  <>
@@ -218,7 +218,9 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan
218
218
  if ( isset( $block['attrs']['id'] ) ) {
219
219
  $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
220
220
  $img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
221
- $img_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'] );
221
+ $has_dimensions = ( $img_metadata['width'] ?? '' ) && ( $img_metadata['height'] ?? '' );
222
+ $srcset_size = $has_dimensions ? array( $img_metadata['width'], $img_metadata['height'] ) : 'large';
223
+ $img_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'], $srcset_size );
222
224
  $img_width = $img_metadata['width'] ?? 'none';
223
225
  $img_height = $img_metadata['height'] ?? 'none';
224
226
  }
@@ -55,7 +55,6 @@ import { useInstanceId } from '@wordpress/compose';
55
55
  * Internal dependencies
56
56
  */
57
57
  import useNavigationMenu from '../use-navigation-menu';
58
- import useNavigationEntities from '../use-navigation-entities';
59
58
  import Placeholder from './placeholder';
60
59
  import ResponsiveWrapper from './responsive-wrapper';
61
60
  import NavigationInnerBlocks from './inner-blocks';
@@ -339,7 +338,10 @@ function Navigation( {
339
338
 
340
339
  // Preload classic menus, so that they don't suddenly pop-in when viewing
341
340
  // the Select Menu dropdown.
342
- const { menus: classicMenus } = useNavigationEntities();
341
+ const { records: classicMenus } = useEntityRecords( 'root', 'menu', {
342
+ per_page: -1,
343
+ context: 'view',
344
+ } );
343
345
 
344
346
  const [ showNavigationMenuStatusNotice, hideNavigationMenuStatusNotice ] =
345
347
  useNavigationNotice( {
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { createBlock } from '@wordpress/blocks';
4
+ import {
5
+ createBlock,
6
+ hasBlockSupport,
7
+ store as blocksStore,
8
+ } from '@wordpress/blocks';
5
9
  import {
6
10
  addSubmenu,
7
11
  chevronUp,
@@ -103,8 +107,14 @@ export default function LeafMoreMenu( props ) {
103
107
  const { block } = props;
104
108
  const { clientId } = block;
105
109
 
106
- const { moveBlocksDown, moveBlocksUp, removeBlocks } =
107
- useDispatch( blockEditorStore );
110
+ const {
111
+ moveBlocksDown,
112
+ moveBlocksUp,
113
+ removeBlocks,
114
+ duplicateBlocks,
115
+ insertBeforeBlock,
116
+ insertAfterBlock,
117
+ } = useDispatch( blockEditorStore );
108
118
 
109
119
  const removeLabel = sprintf(
110
120
  /* translators: %s: block name */
@@ -112,14 +122,45 @@ export default function LeafMoreMenu( props ) {
112
122
  BlockTitle( { clientId, maximumLength: 25 } )
113
123
  );
114
124
 
115
- const rootClientId = useSelect(
116
- ( select ) => {
117
- const { getBlockRootClientId } = select( blockEditorStore );
118
-
119
- return getBlockRootClientId( clientId );
120
- },
121
- [ clientId ]
122
- );
125
+ const { rootClientId, canDuplicate, canInsertBlock, isFirst, isLast } =
126
+ useSelect(
127
+ ( select ) => {
128
+ const {
129
+ getBlockRootClientId,
130
+ canInsertBlockType,
131
+ getDirectInsertBlock,
132
+ getBlockIndex,
133
+ getBlockCount,
134
+ } = select( blockEditorStore );
135
+ const { getDefaultBlockName } = select( blocksStore );
136
+
137
+ const _rootClientId = getBlockRootClientId( clientId );
138
+ const canInsertDefaultBlock = canInsertBlockType(
139
+ getDefaultBlockName(),
140
+ _rootClientId
141
+ );
142
+ const directInsertBlock = _rootClientId
143
+ ? getDirectInsertBlock( _rootClientId )
144
+ : null;
145
+
146
+ return {
147
+ rootClientId: _rootClientId,
148
+ canDuplicate:
149
+ !! block &&
150
+ hasBlockSupport( block.name, 'multiple', true ) &&
151
+ canInsertBlockType( block.name, _rootClientId ),
152
+ canInsertBlock:
153
+ ( canInsertDefaultBlock || !! directInsertBlock ) &&
154
+ !! block &&
155
+ canInsertBlockType( block.name, _rootClientId ),
156
+ isFirst: getBlockIndex( clientId ) === 0,
157
+ isLast:
158
+ getBlockIndex( clientId ) ===
159
+ getBlockCount( _rootClientId ) - 1,
160
+ };
161
+ },
162
+ [ clientId, block ]
163
+ );
123
164
 
124
165
  return (
125
166
  <DropdownMenu
@@ -135,6 +176,8 @@ export default function LeafMoreMenu( props ) {
135
176
  <MenuGroup>
136
177
  <MenuItem
137
178
  icon={ chevronUp }
179
+ disabled={ isFirst }
180
+ accessibleWhenDisabled
138
181
  onClick={ () => {
139
182
  moveBlocksUp( [ clientId ], rootClientId );
140
183
  onClose();
@@ -144,6 +187,8 @@ export default function LeafMoreMenu( props ) {
144
187
  </MenuItem>
145
188
  <MenuItem
146
189
  icon={ chevronDown }
190
+ disabled={ isLast }
191
+ accessibleWhenDisabled
147
192
  onClick={ () => {
148
193
  moveBlocksDown( [ clientId ], rootClientId );
149
194
  onClose();
@@ -158,6 +203,36 @@ export default function LeafMoreMenu( props ) {
158
203
  expand={ props.expand }
159
204
  setInsertedBlock={ props.setInsertedBlock }
160
205
  />
206
+ { canDuplicate && (
207
+ <MenuItem
208
+ onClick={ () => {
209
+ duplicateBlocks( [ clientId ] );
210
+ onClose();
211
+ } }
212
+ >
213
+ { __( 'Duplicate' ) }
214
+ </MenuItem>
215
+ ) }
216
+ { canInsertBlock && (
217
+ <>
218
+ <MenuItem
219
+ onClick={ () => {
220
+ insertBeforeBlock( clientId );
221
+ onClose();
222
+ } }
223
+ >
224
+ { __( 'Add before' ) }
225
+ </MenuItem>
226
+ <MenuItem
227
+ onClick={ () => {
228
+ insertAfterBlock( clientId );
229
+ onClose();
230
+ } }
231
+ >
232
+ { __( 'Add after' ) }
233
+ </MenuItem>
234
+ </>
235
+ ) }
161
236
  </MenuGroup>
162
237
  <MenuGroup>
163
238
  <MenuItem
@@ -6,12 +6,7 @@ import {
6
6
  InspectorControls,
7
7
  store as blockEditorStore,
8
8
  } from '@wordpress/block-editor';
9
- import {
10
- PanelBody,
11
- Spinner,
12
- __experimentalHStack as HStack,
13
- __experimentalHeading as Heading,
14
- } from '@wordpress/components';
9
+ import { PanelBody, Spinner } from '@wordpress/components';
15
10
  import { useSelect, useDispatch } from '@wordpress/data';
16
11
  import { __, sprintf } from '@wordpress/i18n';
17
12
  import { useContext } from '@wordpress/element';
@@ -24,121 +19,18 @@ import { unlock } from '../../lock-unlock';
24
19
  import DeletedNavigationWarning from './deleted-navigation-warning';
25
20
  import useNavigationMenu from '../use-navigation-menu';
26
21
  import LeafMoreMenu from './leaf-more-menu';
27
- import {
28
- LinkUI,
29
- updateAttributes,
30
- useEntityBinding,
31
- } from '../../navigation-link/shared';
22
+ import { NavigationLinkUI } from './navigation-link-ui';
23
+ import NavigationListViewHeader from './navigation-list-view-header';
32
24
 
33
25
  const actionLabel =
34
26
  /* translators: %s: The name of a menu. */ __( "Switch to '%s'" );
35
- const BLOCKS_WITH_LINK_UI_SUPPORT = [
36
- 'core/navigation-link',
37
- 'core/navigation-submenu',
38
- ];
39
27
  const {
40
28
  PrivateListView,
41
- useBlockDisplayTitle,
42
29
  PrivateBlockContext,
43
30
  useListViewPanelState,
31
+ useBlockDisplayTitle,
44
32
  } = unlock( blockEditorPrivateApis );
45
33
 
46
- function AdditionalBlockContent( { block, insertedBlock, setInsertedBlock } ) {
47
- const { updateBlockAttributes, removeBlock } =
48
- useDispatch( blockEditorStore );
49
-
50
- const supportsLinkControls = BLOCKS_WITH_LINK_UI_SUPPORT?.includes(
51
- insertedBlock?.name
52
- );
53
- const blockWasJustInserted = insertedBlock?.clientId === block.clientId;
54
- const showLinkControls = supportsLinkControls && blockWasJustInserted;
55
-
56
- // Get binding utilities for the inserted block
57
- const { createBinding, clearBinding } = useEntityBinding( {
58
- clientId: insertedBlock?.clientId,
59
- attributes: insertedBlock?.attributes || {},
60
- } );
61
-
62
- if ( ! showLinkControls ) {
63
- return null;
64
- }
65
-
66
- /**
67
- * Cleanup function for auto-inserted Navigation Link blocks.
68
- *
69
- * Removes the block if it has no URL and clears the inserted block state.
70
- * This ensures consistent cleanup behavior across different contexts.
71
- */
72
- const cleanupInsertedBlock = () => {
73
- // Prevent automatic block selection when removing blocks in list view context
74
- // This avoids focus stealing that would close the list view and switch to canvas
75
- const shouldAutoSelectBlock = false;
76
-
77
- // Follows the exact same pattern as Navigation Link block's onClose handler
78
- // If there is no URL then remove the auto-inserted block to avoid empty blocks
79
- if ( ! insertedBlock?.attributes?.url && insertedBlock?.clientId ) {
80
- // Remove the block entirely to avoid poor UX
81
- // This matches the Navigation Link block's behavior
82
- removeBlock( insertedBlock.clientId, shouldAutoSelectBlock );
83
- }
84
- setInsertedBlock( null );
85
- };
86
-
87
- const setInsertedBlockAttributes =
88
- ( _insertedBlockClientId ) => ( _updatedAttributes ) => {
89
- if ( ! _insertedBlockClientId ) {
90
- return;
91
- }
92
- updateBlockAttributes( _insertedBlockClientId, _updatedAttributes );
93
- };
94
-
95
- // Wrapper function to clean up original block when a new block is selected
96
- const handleSetInsertedBlock = ( newBlock ) => {
97
- // Prevent automatic block selection when removing blocks in list view context
98
- // This avoids focus stealing that would close the list view and switch to canvas
99
- const shouldAutoSelectBlock = false;
100
-
101
- // If we have an existing inserted block and a new block is being set,
102
- // remove the original block to avoid duplicates
103
- if ( insertedBlock?.clientId && newBlock ) {
104
- removeBlock( insertedBlock.clientId, shouldAutoSelectBlock );
105
- }
106
- setInsertedBlock( newBlock );
107
- };
108
-
109
- return (
110
- <LinkUI
111
- clientId={ insertedBlock?.clientId }
112
- link={ insertedBlock?.attributes }
113
- onBlockInsert={ handleSetInsertedBlock }
114
- onClose={ () => {
115
- // Use cleanup function
116
- cleanupInsertedBlock();
117
- } }
118
- onChange={ ( updatedValue ) => {
119
- // updateAttributes determines the final state and returns metadata
120
- const { isEntityLink, attributes: updatedAttributes } =
121
- updateAttributes(
122
- updatedValue,
123
- setInsertedBlockAttributes( insertedBlock?.clientId ),
124
- insertedBlock?.attributes
125
- );
126
-
127
- // Handle URL binding based on the final computed state
128
- // Only create bindings for entity links (posts, pages, taxonomies)
129
- // Never create bindings for custom links (manual URLs)
130
- if ( isEntityLink ) {
131
- createBinding( updatedAttributes );
132
- } else {
133
- clearBinding();
134
- }
135
-
136
- setInsertedBlock( null );
137
- } }
138
- />
139
- );
140
- }
141
-
142
34
  const MainContent = ( {
143
35
  clientId,
144
36
  currentMenuId,
@@ -194,7 +86,7 @@ const MainContent = ( {
194
86
  description={ description }
195
87
  showAppender
196
88
  blockSettingsMenu={ LeafMoreMenu }
197
- additionalBlockContent={ AdditionalBlockContent }
89
+ additionalBlockContent={ NavigationLinkUI }
198
90
  onSelect={ openListViewContentPanel }
199
91
  />
200
92
  </div>
@@ -232,34 +124,23 @@ const MenuInspectorControls = ( props ) => {
232
124
  return (
233
125
  <InspectorControls group="list">
234
126
  <PanelBody title={ null }>
235
- <HStack className="wp-block-navigation-off-canvas-editor__header">
236
- <Heading
237
- className="wp-block-navigation-off-canvas-editor__title"
238
- level={ 2 }
239
- >
240
- { blockTitle }
241
- </Heading>
242
- { blockEditingMode === 'default' && (
243
- <NavigationMenuSelector
244
- currentMenuId={ currentMenuId }
245
- onSelectClassicMenu={ onSelectClassicMenu }
246
- onSelectNavigationMenu={
247
- onSelectNavigationMenu
248
- }
249
- onCreateNew={ onCreateNew }
250
- createNavigationMenuIsSuccess={
251
- createNavigationMenuIsSuccess
252
- }
253
- createNavigationMenuIsError={
254
- createNavigationMenuIsError
255
- }
256
- actionLabel={ actionLabel }
257
- isManageMenusButtonDisabled={
258
- isManageMenusButtonDisabled
259
- }
260
- />
261
- ) }
262
- </HStack>
127
+ <NavigationListViewHeader
128
+ clientId={ clientId }
129
+ blockEditingMode={ blockEditingMode }
130
+ currentMenuId={ currentMenuId }
131
+ onSelectClassicMenu={ onSelectClassicMenu }
132
+ onSelectNavigationMenu={ onSelectNavigationMenu }
133
+ onCreateNew={ onCreateNew }
134
+ createNavigationMenuIsSuccess={
135
+ createNavigationMenuIsSuccess
136
+ }
137
+ createNavigationMenuIsError={
138
+ createNavigationMenuIsError
139
+ }
140
+ isManageMenusButtonDisabled={
141
+ isManageMenusButtonDisabled
142
+ }
143
+ />
263
144
  <MainContent
264
145
  { ...props }
265
146
  expandRevision={ expandRevision }
@@ -273,7 +154,7 @@ const MenuInspectorControls = ( props ) => {
273
154
  return (
274
155
  <InspectorControls group="list">
275
156
  <PanelBody
276
- title={ __( 'Navigation' ) }
157
+ title={ blockTitle }
277
158
  opened={ isOpened }
278
159
  onToggle={ handleToggle }
279
160
  >