@wordpress/block-library 9.41.1-next.v.202603102151.0 → 9.42.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 (140) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/cover/edit/cover-placeholder.cjs +7 -0
  3. package/build/cover/edit/cover-placeholder.cjs.map +2 -2
  4. package/build/html/modal.cjs +151 -229
  5. package/build/html/modal.cjs.map +2 -2
  6. package/build/image/edit.cjs +7 -0
  7. package/build/image/edit.cjs.map +2 -2
  8. package/build/media-text/media-container.cjs +6 -0
  9. package/build/media-text/media-container.cjs.map +2 -2
  10. package/build/navigation/edit/index.cjs +5 -4
  11. package/build/navigation/edit/index.cjs.map +2 -2
  12. package/build/navigation-link/shared/use-link-preview.cjs +29 -0
  13. package/build/navigation-link/shared/use-link-preview.cjs.map +2 -2
  14. package/build/nextpage/block.json +0 -1
  15. package/build/playlist-track/block.json +0 -0
  16. package/build/post-date/block.json +1 -3
  17. package/build/post-date/deprecated.cjs +82 -6
  18. package/build/post-date/deprecated.cjs.map +3 -3
  19. package/build/post-date/edit.cjs +49 -62
  20. package/build/post-date/edit.cjs.map +3 -3
  21. package/build/site-logo/edit.cjs +1 -3
  22. package/build/site-logo/edit.cjs.map +2 -2
  23. package/build/site-title/index.cjs +5 -1
  24. package/build/site-title/index.cjs.map +2 -2
  25. package/build/tab/add-tab-toolbar-control.cjs +22 -5
  26. package/build/tab/add-tab-toolbar-control.cjs.map +2 -2
  27. package/build/tab/remove-tab-toolbar-control.cjs +19 -1
  28. package/build/tab/remove-tab-toolbar-control.cjs.map +2 -2
  29. package/build/tabs/edit.cjs +85 -7
  30. package/build/tabs/edit.cjs.map +2 -2
  31. package/build/tabs/index.cjs +12 -2
  32. package/build/tabs/index.cjs.map +2 -2
  33. package/build/tabs-menu/block.json +1 -6
  34. package/build/tabs-menu/edit.cjs +11 -151
  35. package/build/tabs-menu/edit.cjs.map +3 -3
  36. package/build/tabs-menu/save.cjs.map +2 -2
  37. package/build/tabs-menu-item/block.json +14 -11
  38. package/build/tabs-menu-item/controls.cjs +2 -133
  39. package/build/tabs-menu-item/controls.cjs.map +3 -3
  40. package/build/tabs-menu-item/edit.cjs +44 -56
  41. package/build/tabs-menu-item/edit.cjs.map +3 -3
  42. package/build/tabs-menu-item/save.cjs +0 -1
  43. package/build/tabs-menu-item/save.cjs.map +2 -2
  44. package/build/utils/media-control.cjs +72 -29
  45. package/build/utils/media-control.cjs.map +3 -3
  46. package/build-module/cover/edit/cover-placeholder.mjs +7 -0
  47. package/build-module/cover/edit/cover-placeholder.mjs.map +2 -2
  48. package/build-module/html/modal.mjs +151 -229
  49. package/build-module/html/modal.mjs.map +2 -2
  50. package/build-module/image/edit.mjs +7 -0
  51. package/build-module/image/edit.mjs.map +2 -2
  52. package/build-module/media-text/media-container.mjs +7 -1
  53. package/build-module/media-text/media-container.mjs.map +2 -2
  54. package/build-module/navigation/edit/index.mjs +5 -4
  55. package/build-module/navigation/edit/index.mjs.map +2 -2
  56. package/build-module/navigation-link/shared/use-link-preview.mjs +28 -0
  57. package/build-module/navigation-link/shared/use-link-preview.mjs.map +2 -2
  58. package/build-module/nextpage/block.json +0 -1
  59. package/build-module/playlist-track/block.json +0 -0
  60. package/build-module/post-date/block.json +1 -3
  61. package/build-module/post-date/deprecated.mjs +82 -6
  62. package/build-module/post-date/deprecated.mjs.map +2 -2
  63. package/build-module/post-date/edit.mjs +49 -63
  64. package/build-module/post-date/edit.mjs.map +2 -2
  65. package/build-module/site-logo/edit.mjs +1 -3
  66. package/build-module/site-logo/edit.mjs.map +2 -2
  67. package/build-module/site-title/index.mjs +5 -1
  68. package/build-module/site-title/index.mjs.map +2 -2
  69. package/build-module/tab/add-tab-toolbar-control.mjs +22 -5
  70. package/build-module/tab/add-tab-toolbar-control.mjs.map +2 -2
  71. package/build-module/tab/remove-tab-toolbar-control.mjs +19 -1
  72. package/build-module/tab/remove-tab-toolbar-control.mjs.map +2 -2
  73. package/build-module/tabs/edit.mjs +87 -9
  74. package/build-module/tabs/edit.mjs.map +2 -2
  75. package/build-module/tabs/index.mjs +12 -2
  76. package/build-module/tabs/index.mjs.map +2 -2
  77. package/build-module/tabs-menu/block.json +1 -6
  78. package/build-module/tabs-menu/edit.mjs +13 -162
  79. package/build-module/tabs-menu/edit.mjs.map +2 -2
  80. package/build-module/tabs-menu/save.mjs.map +2 -2
  81. package/build-module/tabs-menu-item/block.json +14 -11
  82. package/build-module/tabs-menu-item/controls.mjs +4 -143
  83. package/build-module/tabs-menu-item/controls.mjs.map +2 -2
  84. package/build-module/tabs-menu-item/edit.mjs +45 -57
  85. package/build-module/tabs-menu-item/edit.mjs.map +3 -3
  86. package/build-module/tabs-menu-item/save.mjs +0 -1
  87. package/build-module/tabs-menu-item/save.mjs.map +2 -2
  88. package/build-module/utils/media-control.mjs +73 -30
  89. package/build-module/utils/media-control.mjs.map +2 -2
  90. package/build-style/editor-rtl.css +45 -11
  91. package/build-style/editor.css +45 -11
  92. package/build-style/navigation/style-rtl.css +4 -0
  93. package/build-style/navigation/style.css +4 -0
  94. package/build-style/navigation-overlay-close/style-rtl.css +3 -3
  95. package/build-style/navigation-overlay-close/style.css +3 -3
  96. package/build-style/style-rtl.css +7 -3
  97. package/build-style/style.css +7 -3
  98. package/build-style/tabs-menu/editor-rtl.css +5 -3
  99. package/build-style/tabs-menu/editor.css +5 -3
  100. package/package.json +38 -38
  101. package/src/cover/edit/cover-placeholder.js +8 -0
  102. package/src/html/modal.js +6 -77
  103. package/src/image/edit.js +8 -0
  104. package/src/media-text/media-container.js +8 -1
  105. package/src/navigation/edit/index.js +6 -4
  106. package/src/navigation/index.php +24 -17
  107. package/src/navigation/style.scss +10 -0
  108. package/src/navigation-link/index.php +9 -9
  109. package/src/navigation-link/shared/test/use-link-preview.test.js +149 -0
  110. package/src/navigation-link/shared/use-link-preview.js +43 -1
  111. package/src/navigation-overlay-close/style.scss +3 -3
  112. package/src/navigation-submenu/index.php +17 -11
  113. package/src/nextpage/block.json +0 -1
  114. package/src/playlist-track/block.json +0 -0
  115. package/src/playlist-track/edit.js +0 -0
  116. package/src/playlist-track/index.js +0 -0
  117. package/src/playlist-track/index.php +0 -0
  118. package/src/playlist-track/init.js +0 -0
  119. package/src/playlist-track/style.scss +0 -0
  120. package/src/post-date/block.json +1 -3
  121. package/src/post-date/deprecated.js +86 -6
  122. package/src/post-date/edit.js +65 -82
  123. package/src/site-logo/edit.js +1 -3
  124. package/src/site-title/index.js +5 -1
  125. package/src/tab/add-tab-toolbar-control.js +48 -23
  126. package/src/tab/remove-tab-toolbar-control.js +30 -10
  127. package/src/tabs/edit.js +133 -10
  128. package/src/tabs/index.js +12 -2
  129. package/src/tabs-menu/block.json +1 -6
  130. package/src/tabs-menu/edit.js +13 -214
  131. package/src/tabs-menu/editor.scss +7 -3
  132. package/src/tabs-menu/index.php +42 -27
  133. package/src/tabs-menu/save.js +0 -4
  134. package/src/tabs-menu-item/block.json +14 -11
  135. package/src/tabs-menu-item/controls.js +4 -167
  136. package/src/tabs-menu-item/edit.js +60 -69
  137. package/src/tabs-menu-item/index.php +11 -23
  138. package/src/tabs-menu-item/save.js +0 -1
  139. package/src/utils/media-control.js +61 -21
  140. package/src/utils/media-control.scss +54 -18
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
  /**
@@ -1,7 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
-
5
1
  /**
6
2
  * WordPress dependencies
7
3
  */
@@ -4,21 +4,25 @@
4
4
  "apiVersion": 3,
5
5
  "name": "core/tabs-menu-item",
6
6
  "title": "Tab Menu Item",
7
- "description": "A single tab button in the tabs menu. Used as a template for styling all tab buttons.",
7
+ "description": "A single tab button in the tabs menu.",
8
8
  "version": "1.0.0",
9
9
  "category": "design",
10
10
  "textdomain": "default",
11
11
  "parent": [ "core/tabs-menu" ],
12
12
  "usesContext": [
13
- "core/tabs-menu-item-index",
14
- "core/tabs-menu-item-id",
15
- "core/tabs-menu-item-label",
16
- "core/tabs-menu-item-clientId",
17
13
  "core/tabs-list",
18
14
  "core/tabs-activeTabIndex",
19
- "core/tabs-editorActiveTabIndex"
15
+ "core/tabs-editorActiveTabIndex",
16
+ "core/tabs-menu-item-index",
17
+ "core/tabs-menu-item-id",
18
+ "core/tabs-menu-item-label"
20
19
  ],
21
- "attributes": {},
20
+ "attributes": {
21
+ "anchor": {
22
+ "type": "string",
23
+ "default": ""
24
+ }
25
+ },
22
26
  "supports": {
23
27
  "html": false,
24
28
  "reusable": false,
@@ -31,7 +35,6 @@
31
35
  "text": true
32
36
  }
33
37
  },
34
- "shadow": true,
35
38
  "typography": {
36
39
  "fontSize": true,
37
40
  "__experimentalFontFamily": true,
@@ -40,9 +43,6 @@
40
43
  "fontSize": true
41
44
  }
42
45
  },
43
- "layout": {
44
- "allowEditing": false
45
- },
46
46
  "spacing": {
47
47
  "padding": true,
48
48
  "__experimentalDefaultControls": {
@@ -54,6 +54,9 @@
54
54
  "color": true,
55
55
  "width": true,
56
56
  "style": true
57
+ },
58
+ "layout": {
59
+ "allowEditing": false
57
60
  }
58
61
  },
59
62
  "editorScript": "file:./index.js",