@wordpress/block-library 9.41.0 → 9.41.1-next.v.202603161435.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 (199) hide show
  1. package/build/cover/edit/cover-placeholder.cjs +7 -0
  2. package/build/cover/edit/cover-placeholder.cjs.map +2 -2
  3. package/build/html/block.json +2 -1
  4. package/build/html/modal.cjs +142 -147
  5. package/build/html/modal.cjs.map +3 -3
  6. package/build/icon/block.json +3 -12
  7. package/build/image/edit.cjs +7 -0
  8. package/build/image/edit.cjs.map +2 -2
  9. package/build/image/image.cjs +5 -9
  10. package/build/image/image.cjs.map +2 -2
  11. package/build/media-text/media-container.cjs +6 -0
  12. package/build/media-text/media-container.cjs.map +2 -2
  13. package/build/navigation/edit/index.cjs +21 -14
  14. package/build/navigation/edit/index.cjs.map +3 -3
  15. package/build/navigation/view.cjs +9 -2
  16. package/build/navigation/view.cjs.map +2 -2
  17. package/build/navigation-link/block.json +5 -0
  18. package/build/navigation-link/shared/use-link-preview.cjs +29 -0
  19. package/build/navigation-link/shared/use-link-preview.cjs.map +2 -2
  20. package/build/nextpage/block.json +0 -1
  21. package/build/paragraph/edit.cjs +1 -1
  22. package/build/paragraph/edit.cjs.map +1 -1
  23. package/build/playlist/edit.cjs +3 -23
  24. package/build/playlist/edit.cjs.map +3 -3
  25. package/build/playlist/utils.cjs +48 -0
  26. package/build/playlist/utils.cjs.map +7 -0
  27. package/build/playlist-track/block.json +0 -0
  28. package/build/post-excerpt/block.json +1 -3
  29. package/build/post-excerpt/deprecated.cjs +112 -0
  30. package/build/post-excerpt/deprecated.cjs.map +7 -0
  31. package/build/post-excerpt/edit.cjs +11 -30
  32. package/build/post-excerpt/edit.cjs.map +3 -3
  33. package/build/post-excerpt/index.cjs +3 -1
  34. package/build/post-excerpt/index.cjs.map +3 -3
  35. package/build/private-apis.cjs +3 -1
  36. package/build/private-apis.cjs.map +2 -2
  37. package/build/shortcode/block.json +2 -1
  38. package/build/site-logo/edit.cjs +1 -3
  39. package/build/site-logo/edit.cjs.map +2 -2
  40. package/build/tab/add-tab-toolbar-control.cjs +22 -5
  41. package/build/tab/add-tab-toolbar-control.cjs.map +2 -2
  42. package/build/tab/remove-tab-toolbar-control.cjs +19 -1
  43. package/build/tab/remove-tab-toolbar-control.cjs.map +2 -2
  44. package/build/tabs/edit.cjs +85 -7
  45. package/build/tabs/edit.cjs.map +2 -2
  46. package/build/tabs/index.cjs +12 -2
  47. package/build/tabs/index.cjs.map +2 -2
  48. package/build/tabs-menu/block.json +1 -6
  49. package/build/tabs-menu/edit.cjs +11 -151
  50. package/build/tabs-menu/edit.cjs.map +3 -3
  51. package/build/tabs-menu/save.cjs.map +2 -2
  52. package/build/tabs-menu-item/block.json +14 -11
  53. package/build/tabs-menu-item/controls.cjs +2 -133
  54. package/build/tabs-menu-item/controls.cjs.map +3 -3
  55. package/build/tabs-menu-item/edit.cjs +44 -56
  56. package/build/tabs-menu-item/edit.cjs.map +3 -3
  57. package/build/tabs-menu-item/save.cjs +0 -1
  58. package/build/tabs-menu-item/save.cjs.map +2 -2
  59. package/build/template-part/edit/index.cjs +6 -4
  60. package/build/template-part/edit/index.cjs.map +2 -2
  61. package/build/utils/media-control.cjs +72 -29
  62. package/build/utils/media-control.cjs.map +3 -3
  63. package/build-module/cover/edit/cover-placeholder.mjs +7 -0
  64. package/build-module/cover/edit/cover-placeholder.mjs.map +2 -2
  65. package/build-module/html/block.json +2 -1
  66. package/build-module/html/modal.mjs +144 -149
  67. package/build-module/html/modal.mjs.map +2 -2
  68. package/build-module/icon/block.json +3 -12
  69. package/build-module/image/edit.mjs +7 -0
  70. package/build-module/image/edit.mjs.map +2 -2
  71. package/build-module/image/image.mjs +5 -9
  72. package/build-module/image/image.mjs.map +2 -2
  73. package/build-module/media-text/media-container.mjs +7 -1
  74. package/build-module/media-text/media-container.mjs.map +2 -2
  75. package/build-module/navigation/edit/index.mjs +22 -14
  76. package/build-module/navigation/edit/index.mjs.map +2 -2
  77. package/build-module/navigation/view.mjs +9 -2
  78. package/build-module/navigation/view.mjs.map +2 -2
  79. package/build-module/navigation-link/block.json +5 -0
  80. package/build-module/navigation-link/shared/use-link-preview.mjs +28 -0
  81. package/build-module/navigation-link/shared/use-link-preview.mjs.map +2 -2
  82. package/build-module/nextpage/block.json +0 -1
  83. package/build-module/paragraph/edit.mjs +2 -2
  84. package/build-module/paragraph/edit.mjs.map +1 -1
  85. package/build-module/playlist/edit.mjs +2 -18
  86. package/build-module/playlist/edit.mjs.map +2 -2
  87. package/build-module/playlist/utils.mjs +23 -0
  88. package/build-module/playlist/utils.mjs.map +7 -0
  89. package/build-module/playlist-track/block.json +0 -0
  90. package/build-module/post-excerpt/block.json +1 -3
  91. package/build-module/post-excerpt/deprecated.mjs +81 -0
  92. package/build-module/post-excerpt/deprecated.mjs.map +7 -0
  93. package/build-module/post-excerpt/edit.mjs +12 -34
  94. package/build-module/post-excerpt/edit.mjs.map +2 -2
  95. package/build-module/post-excerpt/index.mjs +3 -1
  96. package/build-module/post-excerpt/index.mjs.map +2 -2
  97. package/build-module/private-apis.mjs +3 -1
  98. package/build-module/private-apis.mjs.map +2 -2
  99. package/build-module/shortcode/block.json +2 -1
  100. package/build-module/site-logo/edit.mjs +1 -3
  101. package/build-module/site-logo/edit.mjs.map +2 -2
  102. package/build-module/tab/add-tab-toolbar-control.mjs +22 -5
  103. package/build-module/tab/add-tab-toolbar-control.mjs.map +2 -2
  104. package/build-module/tab/remove-tab-toolbar-control.mjs +19 -1
  105. package/build-module/tab/remove-tab-toolbar-control.mjs.map +2 -2
  106. package/build-module/tabs/edit.mjs +87 -9
  107. package/build-module/tabs/edit.mjs.map +2 -2
  108. package/build-module/tabs/index.mjs +12 -2
  109. package/build-module/tabs/index.mjs.map +2 -2
  110. package/build-module/tabs-menu/block.json +1 -6
  111. package/build-module/tabs-menu/edit.mjs +13 -162
  112. package/build-module/tabs-menu/edit.mjs.map +2 -2
  113. package/build-module/tabs-menu/save.mjs.map +2 -2
  114. package/build-module/tabs-menu-item/block.json +14 -11
  115. package/build-module/tabs-menu-item/controls.mjs +4 -143
  116. package/build-module/tabs-menu-item/controls.mjs.map +2 -2
  117. package/build-module/tabs-menu-item/edit.mjs +45 -57
  118. package/build-module/tabs-menu-item/edit.mjs.map +3 -3
  119. package/build-module/tabs-menu-item/save.mjs +0 -1
  120. package/build-module/tabs-menu-item/save.mjs.map +2 -2
  121. package/build-module/template-part/edit/index.mjs +6 -4
  122. package/build-module/template-part/edit/index.mjs.map +2 -2
  123. package/build-module/utils/media-control.mjs +73 -30
  124. package/build-module/utils/media-control.mjs.map +2 -2
  125. package/build-style/common-rtl.css +1 -0
  126. package/build-style/common.css +1 -0
  127. package/build-style/editor-rtl.css +55 -17
  128. package/build-style/editor.css +55 -17
  129. package/build-style/html/editor-rtl.css +10 -6
  130. package/build-style/html/editor.css +10 -6
  131. package/build-style/navigation/style-rtl.css +15 -1
  132. package/build-style/navigation/style.css +15 -1
  133. package/build-style/navigation-overlay-close/style-rtl.css +3 -3
  134. package/build-style/navigation-overlay-close/style.css +3 -3
  135. package/build-style/playlist/style-rtl.css +4 -0
  136. package/build-style/playlist/style.css +4 -0
  137. package/build-style/style-rtl.css +23 -4
  138. package/build-style/style.css +23 -4
  139. package/build-style/tabs-menu/editor-rtl.css +5 -3
  140. package/build-style/tabs-menu/editor.css +5 -3
  141. package/package.json +38 -38
  142. package/src/accordion-item/index.php +17 -5
  143. package/src/common.scss +1 -0
  144. package/src/cover/edit/cover-placeholder.js +8 -0
  145. package/src/cover/index.php +8 -0
  146. package/src/details/index.php +47 -0
  147. package/src/html/block.json +2 -1
  148. package/src/html/editor.scss +15 -5
  149. package/src/html/modal.js +26 -22
  150. package/src/icon/block.json +3 -12
  151. package/src/image/edit.js +8 -0
  152. package/src/image/image.js +8 -13
  153. package/src/media-text/media-container.js +8 -1
  154. package/src/navigation/edit/index.js +26 -14
  155. package/src/navigation/index.php +27 -13
  156. package/src/navigation/style.scss +17 -1
  157. package/src/navigation/view.js +14 -2
  158. package/src/navigation-link/block.json +5 -0
  159. package/src/navigation-link/index.php +10 -10
  160. package/src/navigation-link/shared/test/use-link-preview.test.js +149 -0
  161. package/src/navigation-link/shared/use-link-preview.js +43 -1
  162. package/src/navigation-overlay-close/style.scss +3 -3
  163. package/src/navigation-submenu/index.php +17 -11
  164. package/src/nextpage/block.json +0 -1
  165. package/src/paragraph/edit.js +2 -2
  166. package/src/playlist/edit.js +1 -34
  167. package/src/playlist/style.scss +5 -0
  168. package/src/playlist/test/edit.js +1 -1
  169. package/src/playlist/utils.js +42 -0
  170. package/src/playlist-track/block.json +0 -0
  171. package/src/playlist-track/edit.js +0 -0
  172. package/src/playlist-track/index.js +0 -0
  173. package/src/playlist-track/index.php +0 -0
  174. package/src/playlist-track/init.js +0 -0
  175. package/src/playlist-track/style.scss +0 -0
  176. package/src/post-excerpt/block.json +1 -3
  177. package/src/post-excerpt/deprecated.js +84 -0
  178. package/src/post-excerpt/edit.js +14 -39
  179. package/src/post-excerpt/index.js +2 -0
  180. package/src/private-apis.js +2 -0
  181. package/src/shortcode/block.json +2 -1
  182. package/src/site-logo/edit.js +1 -3
  183. package/src/tab/add-tab-toolbar-control.js +48 -23
  184. package/src/tab/remove-tab-toolbar-control.js +30 -10
  185. package/src/tabs/edit.js +133 -10
  186. package/src/tabs/index.js +12 -2
  187. package/src/tabs-menu/block.json +1 -6
  188. package/src/tabs-menu/edit.js +13 -214
  189. package/src/tabs-menu/editor.scss +7 -3
  190. package/src/tabs-menu/index.php +42 -27
  191. package/src/tabs-menu/save.js +0 -4
  192. package/src/tabs-menu-item/block.json +14 -11
  193. package/src/tabs-menu-item/controls.js +4 -167
  194. package/src/tabs-menu-item/edit.js +60 -69
  195. package/src/tabs-menu-item/index.php +11 -23
  196. package/src/tabs-menu-item/save.js +0 -1
  197. package/src/template-part/edit/index.js +5 -1
  198. package/src/utils/media-control.js +61 -21
  199. package/src/utils/media-control.scss +54 -18
@@ -11,7 +11,8 @@ import { useDispatch, useSelect } from '@wordpress/data';
11
11
 
12
12
  /**
13
13
  * "Remove Tab" button in the block toolbar for the tab block.
14
- * Removes the currently active tab from the tab-panel block.
14
+ * Removes the currently active core/tab and its corresponding
15
+ * core/tabs-menu-item, keeping both in sync.
15
16
  *
16
17
  * @param {Object} props
17
18
  * @param {string} props.tabsClientId The client ID of the parent tabs block.
@@ -25,12 +26,17 @@ export default function RemoveTabToolbarControl( { tabsClientId } ) {
25
26
  __unstableMarkNextChangeAsNotPersistent,
26
27
  } = useDispatch( blockEditorStore );
27
28
 
28
- // Find the tab-panel block, active tab, and tab count within the tabs block
29
- const { activeTabClientId, tabCount, editorActiveTabIndex } = useSelect(
29
+ const {
30
+ activeTabClientId,
31
+ activeMenuItemClientId,
32
+ tabCount,
33
+ editorActiveTabIndex,
34
+ } = useSelect(
30
35
  ( select ) => {
31
36
  if ( ! tabsClientId ) {
32
37
  return {
33
38
  activeTabClientId: null,
39
+ activeMenuItemClientId: null,
34
40
  tabCount: 0,
35
41
  editorActiveTabIndex: 0,
36
42
  };
@@ -46,10 +52,24 @@ export default function RemoveTabToolbarControl( { tabsClientId } ) {
46
52
  const tabPanel = innerBlocks.find(
47
53
  ( block ) => block.name === 'core/tab-panel'
48
54
  );
55
+ const tabsMenu = innerBlocks.find(
56
+ ( block ) => block.name === 'core/tabs-menu'
57
+ );
49
58
  const tabs = tabPanel?.innerBlocks || [];
59
+ const menuItems = tabsMenu?.innerBlocks || [];
50
60
  const activeTab = tabs[ activeIndex ];
61
+ // Match menu item by anchor (e.g. "tab-1" → "tab-1-button").
62
+ const expectedMenuAnchor = activeTab?.attributes?.anchor
63
+ ? `${ activeTab.attributes.anchor }-button`
64
+ : null;
65
+ const activeMenuItem = expectedMenuAnchor
66
+ ? menuItems.find(
67
+ ( m ) => m.attributes?.anchor === expectedMenuAnchor
68
+ )
69
+ : menuItems[ activeIndex ];
51
70
  return {
52
71
  activeTabClientId: activeTab?.clientId || null,
72
+ activeMenuItemClientId: activeMenuItem?.clientId || null,
53
73
  tabCount: tabs.length,
54
74
  editorActiveTabIndex: activeIndex,
55
75
  };
@@ -62,28 +82,28 @@ export default function RemoveTabToolbarControl( { tabsClientId } ) {
62
82
  return;
63
83
  }
64
84
 
65
- // Calculate new active index after removal
85
+ // Calculate new active index after removal.
66
86
  const newActiveIndex =
67
87
  editorActiveTabIndex >= tabCount - 1
68
- ? tabCount - 2 // If removing last tab, select the previous one
69
- : editorActiveTabIndex; // Otherwise keep the same index (next tab shifts into position)
88
+ ? tabCount - 2
89
+ : editorActiveTabIndex;
70
90
 
71
- // Update the active tab index before removing
72
91
  __unstableMarkNextChangeAsNotPersistent();
73
92
  updateBlockAttributes( tabsClientId, {
74
93
  editorActiveTabIndex: newActiveIndex,
75
94
  } );
76
95
 
77
- // Remove the tab
96
+ // Remove the tab content block and the corresponding menu item.
78
97
  removeBlock( activeTabClientId, false );
98
+ if ( activeMenuItemClientId ) {
99
+ removeBlock( activeMenuItemClientId, false );
100
+ }
79
101
 
80
- // Select the tabs block after removal
81
102
  if ( tabsClientId ) {
82
103
  selectBlock( tabsClientId );
83
104
  }
84
105
  };
85
106
 
86
- // Don't show the button if there's only one tab or no active tab
87
107
  const isDisabled = tabCount <= 1 || ! activeTabClientId;
88
108
 
89
109
  return (
package/src/tabs/edit.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  BlockContextProvider,
8
8
  store as blockEditorStore,
9
9
  } from '@wordpress/block-editor';
10
- import { useSelect } from '@wordpress/data';
11
- import { useMemo, useEffect } from '@wordpress/element';
10
+ import { useSelect, useDispatch } from '@wordpress/data';
11
+ import { useMemo, useEffect, useRef } from '@wordpress/element';
12
12
 
13
13
  /**
14
14
  * Internal dependencies
@@ -23,6 +23,10 @@ const TABS_TEMPLATE = [
23
23
  remove: true,
24
24
  },
25
25
  },
26
+ [
27
+ [ 'core/tabs-menu-item', { anchor: 'tab-1-button' } ],
28
+ [ 'core/tabs-menu-item', { anchor: 'tab-2-button' } ],
29
+ ],
26
30
  ],
27
31
  [
28
32
  'core/tab-panel',
@@ -40,6 +44,14 @@ const TABS_TEMPLATE = [
40
44
  },
41
45
  [ [ 'core/paragraph' ] ],
42
46
  ],
47
+ [
48
+ 'core/tab',
49
+ {
50
+ anchor: 'tab-2',
51
+ label: 'Tab 2',
52
+ },
53
+ [ [ 'core/paragraph' ] ],
54
+ ],
43
55
  ],
44
56
  ],
45
57
  ];
@@ -62,30 +74,141 @@ function Edit( {
62
74
  }
63
75
  }, [] ); // eslint-disable-line react-hooks/exhaustive-deps
64
76
 
77
+ const { removeBlock } = useDispatch( blockEditorStore );
78
+
65
79
  /**
66
80
  * Construct a list of core/tab blocks, used to create tabs-list context.
81
+ * Also select menu items with their anchors for anchor-based deletion sync.
67
82
  */
68
- const tabs = useSelect(
83
+ const { tabs, menuItems } = useSelect(
69
84
  ( select ) => {
70
85
  const { getBlocks } = select( blockEditorStore );
71
86
  const innerBlocks = getBlocks( clientId );
72
87
 
73
- // Find tab-panel block and extract tab data
88
+ // Find tab-panel block and extract tab data.
74
89
  const tabPanel = innerBlocks.find(
75
90
  ( block ) => block.name === 'core/tab-panel'
76
91
  );
77
92
 
78
- if ( ! tabPanel ) {
79
- return [];
80
- }
81
-
82
- return tabPanel.innerBlocks.filter(
83
- ( block ) => block.name === 'core/tab'
93
+ // Find tabs-menu block and get its children with their anchors.
94
+ const tabsMenu = innerBlocks.find(
95
+ ( block ) => block.name === 'core/tabs-menu'
84
96
  );
97
+
98
+ return {
99
+ tabs: tabPanel
100
+ ? tabPanel.innerBlocks.filter(
101
+ ( block ) => block.name === 'core/tab'
102
+ )
103
+ : [],
104
+ menuItems: tabsMenu
105
+ ? getBlocks( tabsMenu.clientId )
106
+ .filter( ( b ) => b.name === 'core/tabs-menu-item' )
107
+ .map( ( b ) => ( {
108
+ clientId: b.clientId,
109
+ anchor: b.attributes.anchor ?? '',
110
+ } ) )
111
+ : [],
112
+ };
85
113
  },
86
114
  [ clientId ]
87
115
  );
88
116
 
117
+ /**
118
+ * Keep tabs and menu items in sync when either is deleted directly (e.g.
119
+ * via the Backspace key or List View).
120
+ *
121
+ * TODO: This effect only handles deletions. The two lists can get out of
122
+ * sync in other cases: if a user pastes a core/tab block into the tab-panel
123
+ * (or duplicates one), no corresponding tabs-menu-item is created; if a
124
+ * user drags and drops a tabs-menu-item, the tab panel is not copied with
125
+ * it. We should extend this effect to handle insertions, detecting when
126
+ * tabs.length > menuItems.length and inserting the missing menu
127
+ * item(s) at the correct index.
128
+ */
129
+ const prevSyncStateRef = useRef( null );
130
+ useEffect( () => {
131
+ const currentTabs = tabs.map( ( tab ) => ( {
132
+ clientId: tab.clientId,
133
+ anchor: tab.attributes.anchor ?? '',
134
+ } ) );
135
+
136
+ if ( prevSyncStateRef.current === null ) {
137
+ prevSyncStateRef.current = {
138
+ tabs: currentTabs,
139
+ menuItems: [ ...menuItems ],
140
+ };
141
+ return;
142
+ }
143
+
144
+ const { tabs: prevTabs, menuItems: prevMenuItems } =
145
+ prevSyncStateRef.current;
146
+
147
+ const tabsRemoved = currentTabs.length < prevTabs.length;
148
+ const menuItemsRemoved = menuItems.length < prevMenuItems.length;
149
+
150
+ // Update snapshot to the current state.
151
+ // Snapshot is updated eagerly; post-removal mutations keep it consistent
152
+ // so the next effect invocation sees a stable baseline.
153
+ prevSyncStateRef.current = {
154
+ tabs: currentTabs,
155
+ menuItems: [ ...menuItems ],
156
+ };
157
+
158
+ // Lists are in sync, nothing changed, or toolbar already removed both.
159
+ if (
160
+ ( ! tabsRemoved && ! menuItemsRemoved ) ||
161
+ ( tabsRemoved && menuItemsRemoved )
162
+ ) {
163
+ return;
164
+ }
165
+
166
+ const currentTabIds = new Set( currentTabs.map( ( t ) => t.clientId ) );
167
+ const currentMenuItemIds = new Set(
168
+ menuItems.map( ( m ) => m.clientId )
169
+ );
170
+
171
+ if ( tabsRemoved ) {
172
+ prevTabs.forEach( ( prevTab ) => {
173
+ if ( currentTabIds.has( prevTab.clientId ) ) {
174
+ return;
175
+ }
176
+ const expectedMenuAnchor = prevTab.anchor
177
+ ? `${ prevTab.anchor }-button`
178
+ : null;
179
+ const menuItemToRemove = expectedMenuAnchor
180
+ ? menuItems.find( ( m ) => m.anchor === expectedMenuAnchor )
181
+ : null;
182
+ if ( menuItemToRemove ) {
183
+ removeBlock( menuItemToRemove.clientId, false );
184
+ prevSyncStateRef.current.menuItems =
185
+ prevSyncStateRef.current.menuItems.filter(
186
+ ( m ) => m.clientId !== menuItemToRemove.clientId
187
+ );
188
+ }
189
+ } );
190
+ } else {
191
+ prevMenuItems.forEach( ( prevItem ) => {
192
+ if ( currentMenuItemIds.has( prevItem.clientId ) ) {
193
+ return;
194
+ }
195
+ const expectedTabAnchor =
196
+ prevItem.anchor?.replace( /-button$/, '' ) ?? '';
197
+ const tabToRemove = tabs.find(
198
+ ( tab ) =>
199
+ ( tab.attributes.anchor ?? '' ) === expectedTabAnchor
200
+ );
201
+ if ( tabToRemove ) {
202
+ removeBlock( tabToRemove.clientId, false );
203
+ prevSyncStateRef.current.tabs =
204
+ prevSyncStateRef.current.tabs.filter(
205
+ ( t ) => t.clientId !== tabToRemove.clientId
206
+ );
207
+ }
208
+ } );
209
+ }
210
+ }, [ tabs, menuItems, removeBlock ] );
211
+
89
212
  /**
90
213
  * Memoize context value to prevent unnecessary re-renders.
91
214
  */
package/src/tabs/index.js CHANGED
@@ -22,13 +22,23 @@ export const settings = {
22
22
  innerBlocks: [
23
23
  {
24
24
  name: 'core/tabs-menu',
25
- innerBlocks: [ { name: 'core/tabs-menu-item' } ],
25
+ innerBlocks: [
26
+ {
27
+ name: 'core/tabs-menu-item',
28
+ attributes: { anchor: 'tab-1-button' },
29
+ },
30
+ {
31
+ name: 'core/tabs-menu-item',
32
+ attributes: { anchor: 'tab-2-button' },
33
+ },
34
+ ],
26
35
  },
27
36
  {
28
37
  name: 'core/tab-panel',
29
- innerBlocks: [ 1, 2, 3 ].map( ( index ) => ( {
38
+ innerBlocks: [ 1, 2 ].map( ( index ) => ( {
30
39
  name: 'core/tab',
31
40
  attributes: {
41
+ anchor: `tab-${ index }`,
32
42
  label: sprintf(
33
43
  /** translators: %s: tab index number */
34
44
  __( 'Tab %s' ),
@@ -10,12 +10,7 @@
10
10
  "textdomain": "default",
11
11
  "parent": [ "core/tabs" ],
12
12
  "allowedBlocks": [ "core/tabs-menu-item" ],
13
- "usesContext": [
14
- "core/tabs-list",
15
- "core/tabs-id",
16
- "core/tabs-activeTabIndex",
17
- "core/tabs-editorActiveTabIndex"
18
- ],
13
+ "usesContext": [ "core/tabs-list" ],
19
14
  "attributes": {},
20
15
  "supports": {
21
16
  "html": false,
@@ -6,23 +6,12 @@ import clsx from 'clsx';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { __ } from '@wordpress/i18n';
10
9
  import {
11
10
  useBlockProps,
12
11
  useInnerBlocksProps,
13
- BlockContextProvider,
14
- __experimentalUseBlockPreview as useBlockPreview,
15
12
  store as blockEditorStore,
16
- useBlockEditContext,
17
13
  } from '@wordpress/block-editor';
18
- import { useSelect, useDispatch } from '@wordpress/data';
19
- import {
20
- memo,
21
- useMemo,
22
- useState,
23
- useEffect,
24
- useCallback,
25
- } from '@wordpress/element';
14
+ import { useSelect } from '@wordpress/data';
26
15
 
27
16
  /**
28
17
  * Internal dependencies
@@ -30,221 +19,31 @@ import {
30
19
  import AddTabToolbarControl from '../tab/add-tab-toolbar-control';
31
20
  import RemoveTabToolbarControl from '../tab/remove-tab-toolbar-control';
32
21
 
33
- const TABS_MENU_ITEM_TEMPLATE = [ [ 'core/tabs-menu-item', {} ] ];
34
- const EMPTY_ARRAY = [];
35
-
36
- /**
37
- * Preview component for non-active tab menu items.
38
- * Uses useBlockPreview to cache the rendering.
39
- *
40
- * @param {Object} props Component props.
41
- * @param {Array} props.blocks The blocks to preview.
42
- * @param {string} props.blockContextId The context ID for this block.
43
- * @param {boolean} props.isHidden Whether the preview is hidden.
44
- * @param {Function} props.setActiveBlockContextId Callback to set the active context ID.
45
- */
46
- function TabsMenuItemPreview( {
47
- blocks,
48
- blockContextId,
49
- isHidden,
50
- setActiveBlockContextId,
51
- } ) {
52
- const blockPreviewProps = useBlockPreview( { blocks } );
53
-
54
- const handleOnClick = () => {
55
- setActiveBlockContextId( blockContextId );
56
- };
57
-
58
- const style = {
59
- display: isHidden ? 'none' : 'flex',
60
- };
61
-
62
- return (
63
- <div
64
- { ...blockPreviewProps }
65
- tabIndex={ 0 }
66
- role="button"
67
- onClick={ handleOnClick }
68
- onKeyDown={ handleOnClick }
69
- style={ style }
70
- />
71
- );
72
- }
73
-
74
- const MemoizedTabsMenuItemPreview = memo( TabsMenuItemPreview );
75
-
76
- /**
77
- * The actual editable inner blocks for the active tab item.
78
- *
79
- * @param {Object} props Component props.
80
- * @param {Object} props.wrapperProps Props to pass to the wrapper element.
81
- * @param {Object} props.layout The layout object to pass to inner blocks.
82
- */
83
- function TabsMenuItemTemplateBlocks( { wrapperProps = {}, layout } ) {
84
- const innerBlocksProps = useInnerBlocksProps( wrapperProps, {
85
- template: TABS_MENU_ITEM_TEMPLATE,
86
- templateLock: 'all',
87
- renderAppender: false,
88
- layout,
89
- } );
90
- return innerBlocksProps.children;
91
- }
92
-
93
- function Edit( {
94
- context,
95
- clientId,
96
- __unstableLayoutClassNames: layoutClassNames,
97
- } ) {
98
- // Get the layout from block edit context to pass to inner blocks.
99
- // This ensures the correct orientation is used from the start.
100
- const { layout } = useBlockEditContext();
101
-
102
- const tabsId = context[ 'core/tabs-id' ] || null;
103
- const tabsList = context[ 'core/tabs-list' ] || EMPTY_ARRAY;
104
- const activeTabIndex = context[ 'core/tabs-activeTabIndex' ] ?? 0;
105
- const editorActiveTabIndex = context[ 'core/tabs-editorActiveTabIndex' ];
106
-
107
- // Memoize effectiveActiveIndex to ensure it updates when context changes
108
- const effectiveActiveIndex = useMemo( () => {
109
- return editorActiveTabIndex ?? activeTabIndex;
110
- }, [ editorActiveTabIndex, activeTabIndex ] );
111
-
112
- const { __unstableMarkNextChangeAsNotPersistent } =
113
- useDispatch( blockEditorStore );
114
- const { updateBlockAttributes } = useDispatch( blockEditorStore );
115
-
116
- // Track which tab context is "active" for editing (shows real inner blocks)
117
- const [ activeBlockContextId, setActiveBlockContextId ] = useState( null );
118
-
119
- // Get the inner blocks (the single tabs-menu-item template)
120
- const { blocks, tabsClientId } = useSelect(
121
- ( select ) => {
122
- const { getBlocks, getBlockRootClientId } =
123
- select( blockEditorStore );
124
- return {
125
- blocks: getBlocks( clientId ),
126
- tabsClientId: getBlockRootClientId( clientId ),
127
- };
128
- },
22
+ function Edit( { clientId, __unstableLayoutClassNames: layoutClassNames } ) {
23
+ const { tabsClientId } = useSelect(
24
+ ( select ) => ( {
25
+ tabsClientId:
26
+ select( blockEditorStore ).getBlockRootClientId( clientId ),
27
+ } ),
129
28
  [ clientId ]
130
29
  );
131
30
 
132
- // Build block contexts for each tab
133
- const blockContexts = useMemo( () => {
134
- return tabsList.map( ( tab, index ) => ( {
135
- 'core/tabs-menu-item-index': index,
136
- 'core/tabs-menu-item-id': tab.id || `tab-${ index }`,
137
- 'core/tabs-menu-item-label': tab.label || '',
138
- 'core/tabs-menu-item-clientId': tab.clientId,
139
- // Pass through parent context
140
- 'core/tabs-id': tabsId,
141
- 'core/tabs-list': tabsList,
142
- 'core/tabs-activeTabIndex': activeTabIndex,
143
- 'core/tabs-editorActiveTabIndex': editorActiveTabIndex,
144
- } ) );
145
- }, [ tabsList, tabsId, activeTabIndex, editorActiveTabIndex ] );
146
-
147
- // Generate a unique ID for each block context
148
- const getContextId = useCallback( ( blockContext ) => {
149
- return `tab-context-${ blockContext[ 'core/tabs-menu-item-index' ] }`;
150
- }, [] );
151
-
152
- // Set the first tab as active by default
153
- useEffect( () => {
154
- if ( blockContexts.length > 0 && activeBlockContextId === null ) {
155
- setActiveBlockContextId( getContextId( blockContexts[ 0 ] ) );
156
- }
157
- }, [ blockContexts, activeBlockContextId, getContextId ] );
158
-
159
- // Update active context when editorActiveTabIndex changes
160
- useEffect( () => {
161
- if (
162
- blockContexts.length > 0 &&
163
- effectiveActiveIndex < blockContexts.length
164
- ) {
165
- const newContextId = getContextId(
166
- blockContexts[ effectiveActiveIndex ]
167
- );
168
- setActiveBlockContextId( ( prevId ) =>
169
- prevId !== newContextId ? newContextId : prevId
170
- );
171
- }
172
- }, [ effectiveActiveIndex, blockContexts, getContextId ] );
173
-
174
- // Handle tab click to update parent tabs block's editorActiveTabIndex
175
- const handleTabContextClick = useCallback(
176
- ( index ) => {
177
- if ( tabsClientId && index !== effectiveActiveIndex ) {
178
- __unstableMarkNextChangeAsNotPersistent();
179
- updateBlockAttributes( tabsClientId, {
180
- editorActiveTabIndex: index,
181
- } );
182
- }
183
- },
184
- [
185
- tabsClientId,
186
- effectiveActiveIndex,
187
- updateBlockAttributes,
188
- __unstableMarkNextChangeAsNotPersistent,
189
- ]
190
- );
191
-
192
31
  const blockProps = useBlockProps( {
193
32
  className: clsx( layoutClassNames ),
194
33
  role: 'tablist',
195
34
  } );
196
35
 
197
- // If no tabs exist yet, show placeholder
198
- if ( tabsList.length === 0 ) {
199
- return (
200
- <>
201
- <AddTabToolbarControl tabsClientId={ tabsClientId } />
202
- <RemoveTabToolbarControl tabsClientId={ tabsClientId } />
203
- <div { ...blockProps }>
204
- <span className="tabs__tab-label tabs__tab-label--placeholder">
205
- { __( 'Add tabs to display menu' ) }
206
- </span>
207
- </div>
208
- </>
209
- );
210
- }
36
+ const innerBlocksProps = useInnerBlocksProps( blockProps, {
37
+ allowedBlocks: [ 'core/tabs-menu-item' ],
38
+ orientation: 'horizontal',
39
+ renderAppender: false,
40
+ } );
211
41
 
212
42
  return (
213
43
  <>
214
44
  <AddTabToolbarControl tabsClientId={ tabsClientId } />
215
45
  <RemoveTabToolbarControl tabsClientId={ tabsClientId } />
216
- <div { ...blockProps }>
217
- { blockContexts.map( ( blockContext, index ) => {
218
- const contextId = getContextId( blockContext );
219
- const isVisible = contextId === activeBlockContextId;
220
-
221
- return (
222
- <BlockContextProvider
223
- key={ contextId }
224
- value={ blockContext }
225
- >
226
- { isVisible ? (
227
- <TabsMenuItemTemplateBlocks
228
- wrapperProps={ {
229
- onClick: () =>
230
- handleTabContextClick( index ),
231
- } }
232
- layout={ layout }
233
- />
234
- ) : null }
235
- <MemoizedTabsMenuItemPreview
236
- blocks={ blocks }
237
- blockContextId={ contextId }
238
- setActiveBlockContextId={ ( id ) => {
239
- setActiveBlockContextId( id );
240
- handleTabContextClick( index );
241
- } }
242
- isHidden={ isVisible }
243
- />
244
- </BlockContextProvider>
245
- );
246
- } ) }
247
- </div>
46
+ <div { ...innerBlocksProps } />
248
47
  </>
249
48
  );
250
49
  }
@@ -1,6 +1,10 @@
1
1
  .wp-block-tabs-menu {
2
- .tabs__tab-label--placeholder {
3
- opacity: 0.5;
4
- font-style: italic;
2
+ // Allow the inner block list to be displayed in the flex layout, so the tab buttons appear as direct flex children in the editor.
3
+ > .block-editor-block-list__layout {
4
+ display: contents;
5
+ }
6
+
7
+ .block-editor-block-list__block:has(> .wp-block-tabs-menu-item) {
8
+ display: contents;
5
9
  }
6
10
  }
@@ -8,10 +8,14 @@
8
8
  /**
9
9
  * Render callback for core/tabs-menu.
10
10
  *
11
+ * Re-renders each tabs-menu-item inner block with per-item context (index, id,
12
+ * label) injected from the tabs-list, so the tabs-menu-item render callback
13
+ * can add the correct IAPI directives for each button.
14
+ *
11
15
  * @since 7.0.0
12
16
  *
13
17
  * @param array $attributes Block attributes.
14
- * @param string $content Block content (contains the tabs-menu-item template).
18
+ * @param string $content Block content (rendered inner blocks from save.js).
15
19
  * @param \WP_Block $block WP_Block instance.
16
20
  *
17
21
  * @return string Updated HTML.
@@ -20,44 +24,55 @@ function block_core_tabs_menu_render_callback( array $attributes, string $conten
20
24
  $tabs_list = $block->context['core/tabs-list'] ?? array();
21
25
 
22
26
  if ( empty( $tabs_list ) ) {
23
- return '';
27
+ return $content;
24
28
  }
25
29
 
26
- // Get the first inner block as template (tabs-menu-item)
27
- $inner_blocks = $block->parsed_block['innerBlocks'] ?? array();
28
- if ( empty( $inner_blocks ) ) {
29
- return '';
30
- }
31
- $template_block = $inner_blocks[0];
30
+ // Re-render each tabs-menu-item with per-item context (index, id, label).
31
+ // Match by anchor so the correct tab is found even when the two lists
32
+ // are in different orders.
33
+ $buttons_html = '';
34
+
35
+ foreach ( $block->parsed_block['innerBlocks'] ?? array() as $parsed_menu_item ) {
36
+ if ( 'core/tabs-menu-item' !== ( $parsed_menu_item['blockName'] ?? '' ) ) {
37
+ continue;
38
+ }
39
+
40
+ // Find the tab anchor from the menu item anchor (e.g. "tab-1-button" → "tab-1").
41
+ $menu_item_anchor = $parsed_menu_item['attrs']['anchor'] ?? '';
42
+ $tab_anchor = preg_replace( '/-button$/', '', $menu_item_anchor );
43
+
44
+ // Find the matching tab in $tabs_list by id.
45
+ $tab = null;
46
+ $tab_index = 0;
47
+ foreach ( $tabs_list as $index => $candidate ) {
48
+ if ( ( $candidate['id'] ?? '' ) === $tab_anchor ) {
49
+ $tab = $candidate;
50
+ $tab_index = $index;
51
+ break;
52
+ }
53
+ }
32
54
 
33
- // Build rendered tab items
34
- $tabs_markup = '';
35
- foreach ( $tabs_list as $index => $tab ) {
36
- // Create context for this specific tab
37
- $tab_context = array_merge(
55
+ // Skip menu items with no matching tab.
56
+ if ( null === $tab ) {
57
+ continue;
58
+ }
59
+
60
+ $item_context = array_merge(
38
61
  $block->context,
39
62
  array(
40
- 'core/tabs-menu-item-index' => $index,
63
+ 'core/tabs-menu-item-index' => $tab_index,
41
64
  'core/tabs-menu-item-id' => $tab['id'] ?? '',
42
65
  'core/tabs-menu-item-label' => $tab['label'] ?? '',
43
66
  )
44
67
  );
45
68
 
46
- // Create new WP_Block instance with template and context
47
- $tab_block = new WP_Block( $template_block, $tab_context );
48
-
49
- // Render the block
50
- $tabs_markup .= $tab_block->render();
69
+ $menu_item_block = new WP_Block( $parsed_menu_item, $item_context );
70
+ $buttons_html .= $menu_item_block->render();
51
71
  }
52
72
 
53
- // Find the template block and replace it in $content with $tabs_markup
54
- $content = preg_replace(
55
- '/<button\b[^>]*\bwp-block-tabs-menu-item__template\b[^>]*>.*?<\/button>/si',
56
- $tabs_markup,
57
- $content
58
- );
59
-
60
- return $content;
73
+ // Rebuild the wrapper using get_block_wrapper_attributes().
74
+ $wrapper_attributes = get_block_wrapper_attributes( array( 'role' => 'tablist' ) );
75
+ return sprintf( '<div %s>%s</div>', $wrapper_attributes, $buttons_html );
61
76
  }
62
77
 
63
78
  /**