@wordpress/block-library 9.33.1-next.ff1cebbba.0 → 9.33.2-next.36001005c.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 (173) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +1 -1
  3. package/build/breadcrumbs/block.json +8 -2
  4. package/build/breadcrumbs/edit.js +113 -12
  5. package/build/breadcrumbs/edit.js.map +2 -2
  6. package/build/buttons/transforms.js +7 -7
  7. package/build/buttons/transforms.js.map +2 -2
  8. package/build/code/transforms.js +19 -15
  9. package/build/code/transforms.js.map +2 -2
  10. package/build/heading/index.js +1 -3
  11. package/build/heading/index.js.map +3 -3
  12. package/build/heading/transforms.js +22 -20
  13. package/build/heading/transforms.js.map +2 -2
  14. package/build/index.js +5 -3
  15. package/build/index.js.map +2 -2
  16. package/build/math/block.json +21 -0
  17. package/build/math/edit.js +132 -0
  18. package/build/math/edit.js.map +7 -0
  19. package/build/math/index.js +63 -0
  20. package/build/math/index.js.map +7 -0
  21. package/build/math/init.js +35 -0
  22. package/build/math/init.js.map +7 -0
  23. package/build/math/save.js +40 -0
  24. package/build/math/save.js.map +7 -0
  25. package/build/navigation/edit/menu-inspector-controls.js +2 -2
  26. package/build/navigation/edit/menu-inspector-controls.js.map +2 -2
  27. package/build/navigation-link/edit.js +5 -2
  28. package/build/navigation-link/edit.js.map +2 -2
  29. package/build/navigation-link/link-ui/index.js +1 -1
  30. package/build/navigation-link/link-ui/index.js.map +2 -2
  31. package/build/navigation-link/shared/controls.js +39 -16
  32. package/build/navigation-link/shared/controls.js.map +3 -3
  33. package/build/navigation-link/shared/index.js +2 -0
  34. package/build/navigation-link/shared/index.js.map +2 -2
  35. package/build/navigation-link/shared/update-attributes.js +3 -1
  36. package/build/navigation-link/shared/update-attributes.js.map +2 -2
  37. package/build/navigation-link/shared/use-entity-binding.js +46 -13
  38. package/build/navigation-link/shared/use-entity-binding.js.map +2 -2
  39. package/build/navigation-submenu/edit.js +5 -2
  40. package/build/navigation-submenu/edit.js.map +2 -2
  41. package/build/page-list/use-convert-to-navigation-links.js +6 -1
  42. package/build/page-list/use-convert-to-navigation-links.js.map +2 -2
  43. package/build/paragraph/index.js +1 -3
  44. package/build/paragraph/index.js.map +3 -3
  45. package/build/post-date/deprecated.js +98 -2
  46. package/build/post-date/deprecated.js.map +2 -2
  47. package/build/post-date/edit.js +1 -1
  48. package/build/post-date/edit.js.map +1 -1
  49. package/build/post-date/variations.js +4 -4
  50. package/build/post-date/variations.js.map +2 -2
  51. package/build/term-template/block.json +0 -1
  52. package/build/term-template/edit.js +4 -1
  53. package/build/term-template/edit.js.map +2 -2
  54. package/build/terms-query/block.json +0 -1
  55. package/build/utils/get-transformed-attributes.js +82 -0
  56. package/build/utils/get-transformed-attributes.js.map +7 -0
  57. package/build-module/breadcrumbs/block.json +8 -2
  58. package/build-module/breadcrumbs/edit.js +117 -14
  59. package/build-module/breadcrumbs/edit.js.map +2 -2
  60. package/build-module/buttons/transforms.js +7 -7
  61. package/build-module/buttons/transforms.js.map +2 -2
  62. package/build-module/code/transforms.js +19 -15
  63. package/build-module/code/transforms.js.map +2 -2
  64. package/build-module/heading/index.js +1 -3
  65. package/build-module/heading/index.js.map +2 -2
  66. package/build-module/heading/transforms.js +22 -20
  67. package/build-module/heading/transforms.js.map +2 -2
  68. package/build-module/index.js +5 -3
  69. package/build-module/index.js.map +2 -2
  70. package/build-module/math/block.json +21 -0
  71. package/build-module/math/edit.js +110 -0
  72. package/build-module/math/edit.js.map +7 -0
  73. package/build-module/math/index.js +26 -0
  74. package/build-module/math/index.js.map +7 -0
  75. package/build-module/math/init.js +8 -0
  76. package/build-module/math/init.js.map +7 -0
  77. package/build-module/math/save.js +20 -0
  78. package/build-module/math/save.js.map +7 -0
  79. package/build-module/navigation/edit/menu-inspector-controls.js +2 -2
  80. package/build-module/navigation/edit/menu-inspector-controls.js.map +2 -2
  81. package/build-module/navigation-link/edit.js +5 -2
  82. package/build-module/navigation-link/edit.js.map +2 -2
  83. package/build-module/navigation-link/link-ui/index.js +1 -1
  84. package/build-module/navigation-link/link-ui/index.js.map +2 -2
  85. package/build-module/navigation-link/shared/controls.js +40 -17
  86. package/build-module/navigation-link/shared/controls.js.map +2 -2
  87. package/build-module/navigation-link/shared/index.js +5 -1
  88. package/build-module/navigation-link/shared/index.js.map +2 -2
  89. package/build-module/navigation-link/shared/update-attributes.js +3 -1
  90. package/build-module/navigation-link/shared/update-attributes.js.map +2 -2
  91. package/build-module/navigation-link/shared/use-entity-binding.js +45 -13
  92. package/build-module/navigation-link/shared/use-entity-binding.js.map +2 -2
  93. package/build-module/navigation-submenu/edit.js +5 -2
  94. package/build-module/navigation-submenu/edit.js.map +2 -2
  95. package/build-module/page-list/use-convert-to-navigation-links.js +6 -1
  96. package/build-module/page-list/use-convert-to-navigation-links.js.map +2 -2
  97. package/build-module/paragraph/index.js +1 -3
  98. package/build-module/paragraph/index.js.map +2 -2
  99. package/build-module/post-date/deprecated.js +98 -2
  100. package/build-module/post-date/deprecated.js.map +2 -2
  101. package/build-module/post-date/edit.js +1 -1
  102. package/build-module/post-date/edit.js.map +1 -1
  103. package/build-module/post-date/variations.js +4 -4
  104. package/build-module/post-date/variations.js.map +2 -2
  105. package/build-module/term-template/block.json +0 -1
  106. package/build-module/term-template/edit.js +4 -1
  107. package/build-module/term-template/edit.js.map +2 -2
  108. package/build-module/terms-query/block.json +0 -1
  109. package/build-module/utils/get-transformed-attributes.js +58 -0
  110. package/build-module/utils/get-transformed-attributes.js.map +7 -0
  111. package/build-style/editor-rtl.css +2 -2
  112. package/build-style/editor.css +2 -2
  113. package/build-style/navigation-link/editor-rtl.css +1 -1
  114. package/build-style/navigation-link/editor.css +1 -1
  115. package/build-style/video/editor-rtl.css +1 -1
  116. package/build-style/video/editor.css +1 -1
  117. package/package.json +61 -36
  118. package/src/breadcrumbs/block.json +8 -2
  119. package/src/breadcrumbs/edit.js +163 -18
  120. package/src/breadcrumbs/index.php +118 -16
  121. package/src/buttons/transforms.js +6 -6
  122. package/src/code/transforms.js +16 -14
  123. package/src/heading/index.js +0 -2
  124. package/src/heading/transforms.js +25 -24
  125. package/src/index.js +5 -3
  126. package/src/math/block.json +21 -0
  127. package/src/math/edit.js +123 -0
  128. package/src/math/index.js +31 -0
  129. package/src/math/init.js +4 -0
  130. package/src/math/save.js +20 -0
  131. package/src/navigation/edit/menu-inspector-controls.js +7 -6
  132. package/src/navigation-link/edit.js +5 -2
  133. package/src/navigation-link/editor.scss +1 -1
  134. package/src/navigation-link/index.php +4 -18
  135. package/src/navigation-link/link-ui/index.js +4 -2
  136. package/src/navigation-link/shared/controls.js +69 -20
  137. package/src/navigation-link/shared/index.js +4 -1
  138. package/src/navigation-link/shared/test/controls.js +14 -9
  139. package/src/navigation-link/shared/test/update-attributes.test.js +8 -0
  140. package/src/navigation-link/shared/test/use-entity-binding.js +132 -17
  141. package/src/navigation-link/shared/update-attributes.js +1 -0
  142. package/src/navigation-link/shared/use-entity-binding.js +74 -19
  143. package/src/navigation-submenu/edit.js +5 -2
  144. package/src/navigation-submenu/index.php +3 -17
  145. package/src/page-list/test/{convert-to-links-modal.js → convert-to-navigation-links.js} +67 -0
  146. package/src/page-list/use-convert-to-navigation-links.js +11 -1
  147. package/src/paragraph/index.js +0 -2
  148. package/src/post-date/deprecated.js +104 -2
  149. package/src/post-date/edit.js +1 -1
  150. package/src/post-date/index.php +3 -3
  151. package/src/post-date/variations.js +5 -4
  152. package/src/term-template/block.json +0 -1
  153. package/src/term-template/edit.js +4 -1
  154. package/src/term-template/index.php +4 -6
  155. package/src/terms-query/block.json +0 -1
  156. package/src/utils/get-transformed-attributes.js +98 -0
  157. package/src/video/editor.scss +1 -1
  158. package/tsconfig.json +1 -0
  159. package/build/heading/variations.js +0 -48
  160. package/build/heading/variations.js.map +0 -7
  161. package/build/paragraph/variations.js +0 -48
  162. package/build/paragraph/variations.js.map +0 -7
  163. package/build/utils/get-transformed-metadata.js +0 -65
  164. package/build/utils/get-transformed-metadata.js.map +0 -7
  165. package/build-module/heading/variations.js +0 -28
  166. package/build-module/heading/variations.js.map +0 -7
  167. package/build-module/paragraph/variations.js +0 -28
  168. package/build-module/paragraph/variations.js.map +0 -7
  169. package/build-module/utils/get-transformed-metadata.js +0 -41
  170. package/build-module/utils/get-transformed-metadata.js.map +0 -7
  171. package/src/heading/variations.js +0 -29
  172. package/src/paragraph/variations.js +0 -29
  173. package/src/utils/get-transformed-metadata.js +0 -69
@@ -202,20 +202,6 @@ function render_block_core_navigation_link( $attributes, $content, $block ) {
202
202
  return '';
203
203
  }
204
204
 
205
- // Resolve URL binding if present
206
- $url = $attributes['url'] ?? '';
207
- if ( isset( $attributes['metadata']['bindings']['url']['source'] ) ) {
208
- $binding = $attributes['metadata']['bindings']['url'];
209
- $source = get_block_bindings_source( $binding['source'] );
210
- if ( $source ) {
211
- $source_args = $binding['args'] ?? array();
212
- $resolved_url = $source->get_value( $source_args, $block, 'url' );
213
- if ( $resolved_url ) {
214
- $url = $resolved_url;
215
- }
216
- }
217
- }
218
-
219
205
  $font_sizes = block_core_navigation_link_build_css_font_sizes( $block->context );
220
206
  $classes = array_merge(
221
207
  $font_sizes['css_classes']
@@ -227,9 +213,9 @@ function render_block_core_navigation_link( $attributes, $content, $block ) {
227
213
  $kind = empty( $attributes['kind'] ) ? 'post_type' : str_replace( '-', '_', $attributes['kind'] );
228
214
  $is_active = ! empty( $attributes['id'] ) && get_queried_object_id() === (int) $attributes['id'] && ! empty( get_queried_object()->$kind );
229
215
 
230
- if ( is_post_type_archive() && ! empty( $url ) ) {
216
+ if ( is_post_type_archive() && ! empty( $attributes['url'] ) ) {
231
217
  $queried_archive_link = get_post_type_archive_link( get_queried_object()->name );
232
- if ( $url === $queried_archive_link ) {
218
+ if ( $attributes['url'] === $queried_archive_link ) {
233
219
  $is_active = true;
234
220
  }
235
221
  }
@@ -245,8 +231,8 @@ function render_block_core_navigation_link( $attributes, $content, $block ) {
245
231
  '<a class="wp-block-navigation-item__content" ';
246
232
 
247
233
  // Start appending HTML attributes to anchor tag.
248
- if ( ! empty( $url ) ) {
249
- $html .= ' href="' . esc_url( block_core_navigation_link_maybe_urldecode( $url ) ) . '"';
234
+ if ( isset( $attributes['url'] ) ) {
235
+ $html .= ' href="' . esc_url( block_core_navigation_link_maybe_urldecode( $attributes['url'] ) ) . '"';
250
236
  }
251
237
 
252
238
  if ( $is_active ) {
@@ -78,10 +78,12 @@ function UnforwardedLinkUI( props, ref ) {
78
78
  name: postType,
79
79
  } );
80
80
 
81
- // Check if there's a URL binding with the core/entity source
81
+ // Check if there's a URL binding with the new binding sources
82
82
  // Only enable handleEntities when there's actually a binding present
83
83
  const hasUrlBinding =
84
- metadata?.bindings?.url?.source === 'core/entity' && !! id;
84
+ ( metadata?.bindings?.url?.source === 'core/post-data' ||
85
+ metadata?.bindings?.url?.source === 'core/term-data' ) &&
86
+ !! id;
85
87
 
86
88
  // Memoize link value to avoid overriding the LinkControl's internal state.
87
89
  // This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407.
@@ -11,11 +11,13 @@ import {
11
11
  TextareaControl,
12
12
  } from '@wordpress/components';
13
13
  import { __, sprintf } from '@wordpress/i18n';
14
- import { useRef } from '@wordpress/element';
14
+ import { useRef, useEffect, useState } from '@wordpress/element';
15
15
  import { useInstanceId } from '@wordpress/compose';
16
16
  import { safeDecodeURI } from '@wordpress/url';
17
17
  import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
18
18
  import { linkOff as unlinkIcon } from '@wordpress/icons';
19
+ import { useDispatch } from '@wordpress/data';
20
+ import { store as blockEditorStore } from '@wordpress/block-editor';
19
21
 
20
22
  /**
21
23
  * Internal dependencies
@@ -70,23 +72,55 @@ export function Controls( { attributes, setAttributes, clientId } ) {
70
72
  const { label, url, description, rel, opensInNewTab } = attributes;
71
73
  const lastURLRef = useRef( url );
72
74
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
75
+ const urlInputRef = useRef();
76
+ const shouldFocusURLInputRef = useRef( false );
73
77
  const inputId = useInstanceId( Controls, 'link-input' );
74
78
  const helpTextId = `${ inputId }__help`;
75
79
 
80
+ // Local state to control the input value
81
+ const [ inputValue, setInputValue ] = useState( url );
82
+
83
+ // Sync local state when url prop changes (e.g., from undo/redo or external updates)
84
+ useEffect( () => {
85
+ setInputValue( url );
86
+ lastURLRef.current = url;
87
+ }, [ url ] );
88
+
76
89
  // Use the entity binding hook internally
77
90
  const { hasUrlBinding, clearBinding } = useEntityBinding( {
78
91
  clientId,
79
92
  attributes,
80
93
  } );
81
94
 
82
- const editBoundLink = () => {
83
- // Remove the binding
95
+ // Get direct store dispatch to bypass setBoundAttributes wrapper
96
+ const { updateBlockAttributes } = useDispatch( blockEditorStore );
97
+
98
+ const unsyncBoundLink = () => {
99
+ // Clear the binding first
84
100
  clearBinding();
85
101
 
86
- // Clear url and id to allow picking a new entity (keep type and kind)
87
- setAttributes( { url: undefined, id: undefined } );
102
+ // Use direct store dispatch to bypass block bindings safeguards
103
+ // which prevent updates to bound attributes when calling setAttributes.
104
+ // setAttributes is actually setBoundAttributes, a wrapper function that
105
+ // processes attributes through the binding system.
106
+ // See: packages/block-editor/src/components/block-edit/edit.js
107
+ updateBlockAttributes( clientId, {
108
+ url: lastURLRef.current, // set the lastURLRef as the new editable value so we avoid bugs from empty link states
109
+ id: undefined,
110
+ } );
88
111
  };
89
112
 
113
+ useEffect( () => {
114
+ // Checking for ! hasUrlBinding is a defensive check, as we would
115
+ // only want to focus the input if the url is not bound to an entity.
116
+ if ( ! hasUrlBinding && shouldFocusURLInputRef.current ) {
117
+ // focuses and highlights the url input value, giving the user
118
+ // the ability to delete the value quickly or edit it.
119
+ urlInputRef.current?.select();
120
+ }
121
+ shouldFocusURLInputRef.current = false;
122
+ }, [ hasUrlBinding ] );
123
+
90
124
  return (
91
125
  <ToolsPanel
92
126
  label={ __( 'Settings' ) }
@@ -126,22 +160,25 @@ export function Controls( { attributes, setAttributes, clientId } ) {
126
160
  isShownByDefault
127
161
  >
128
162
  <InputControl
163
+ ref={ urlInputRef }
129
164
  __nextHasNoMarginBottom
130
165
  __next40pxDefaultSize
131
166
  id={ inputId }
132
167
  label={ __( 'Link' ) }
133
- value={ url ? safeDecodeURI( url ) : '' }
134
- onChange={ ( urlValue ) => {
135
- if ( hasUrlBinding ) {
136
- return; // Prevent editing when URL is bound
137
- }
138
- setAttributes( {
139
- url: encodeURI( safeDecodeURI( urlValue ) ),
140
- } );
141
- } }
168
+ value={ inputValue ? safeDecodeURI( inputValue ) : '' }
142
169
  autoComplete="off"
143
170
  type="url"
144
171
  disabled={ hasUrlBinding }
172
+ onChange={ ( newValue ) => {
173
+ if ( hasUrlBinding ) {
174
+ return;
175
+ }
176
+
177
+ // Defer updating the url attribute until onBlur to prevent the canvas from
178
+ // treating a temporary empty value as a committed value, which replaces the
179
+ // label with placeholder text.
180
+ setInputValue( newValue );
181
+ } }
145
182
  onFocus={ () => {
146
183
  if ( hasUrlBinding ) {
147
184
  return;
@@ -152,12 +189,19 @@ export function Controls( { attributes, setAttributes, clientId } ) {
152
189
  if ( hasUrlBinding ) {
153
190
  return;
154
191
  }
192
+
193
+ const finalValue = ! inputValue
194
+ ? lastURLRef.current
195
+ : inputValue;
196
+
197
+ // Update local state immediately so input reflects the reverted value if the value was cleared
198
+ setInputValue( finalValue );
199
+
155
200
  // Defer the updateAttributes call to ensure entity connection isn't severed by accident.
156
- updateAttributes(
157
- { url: ! url ? lastURLRef.current : url },
158
- setAttributes,
159
- { ...attributes, url: lastURLRef.current }
160
- );
201
+ updateAttributes( { url: finalValue }, setAttributes, {
202
+ ...attributes,
203
+ url: lastURLRef.current,
204
+ } );
161
205
  } }
162
206
  help={
163
207
  hasUrlBinding && (
@@ -171,7 +215,12 @@ export function Controls( { attributes, setAttributes, clientId } ) {
171
215
  hasUrlBinding && (
172
216
  <Button
173
217
  icon={ unlinkIcon }
174
- onClick={ editBoundLink }
218
+ onClick={ () => {
219
+ unsyncBoundLink();
220
+ // Focus management to send focus to the URL input
221
+ // on next render after disabled state is removed.
222
+ shouldFocusURLInputRef.current = true;
223
+ } }
175
224
  aria-describedby={ helpTextId }
176
225
  showTooltip
177
226
  label={ __( 'Unsync and edit' ) }
@@ -7,5 +7,8 @@
7
7
 
8
8
  export { Controls } from './controls';
9
9
  export { updateAttributes } from './update-attributes';
10
- export { useEntityBinding } from './use-entity-binding';
10
+ export {
11
+ useEntityBinding,
12
+ buildNavigationLinkEntityBinding,
13
+ } from './use-entity-binding';
11
14
  export { LinkUI } from '../link-ui';
@@ -6,6 +6,7 @@
6
6
  * External dependencies
7
7
  */
8
8
  import { render, screen, fireEvent } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
9
10
 
10
11
  /**
11
12
  * Internal dependencies
@@ -95,18 +96,22 @@ describe( 'Controls', () => {
95
96
  expect( urlInput.value ).toBe( 'https://example.com/test page' );
96
97
  } );
97
98
 
98
- it( 'encodes URL values when changed', () => {
99
+ it( 'calls updateAttributes with new URL on blur', async () => {
100
+ const user = userEvent.setup();
99
101
  render( <Controls { ...defaultProps } /> );
100
102
 
101
103
  const urlInput = screen.getByLabelText( 'Link' );
102
104
 
103
- fireEvent.change( urlInput, {
104
- target: { value: 'https://example.com/test page' },
105
- } );
105
+ await user.click( urlInput );
106
+ await user.clear( urlInput );
107
+ await user.type( urlInput, 'https://example.com/test page' );
108
+ await user.tab();
106
109
 
107
- expect( defaultProps.setAttributes ).toHaveBeenCalledWith( {
108
- url: 'https://example.com/test%20page',
109
- } );
110
+ expect( mockUpdateAttributes ).toHaveBeenCalledWith(
111
+ { url: 'https://example.com/test page' },
112
+ defaultProps.setAttributes,
113
+ { ...defaultProps.attributes, url: 'https://example.com' }
114
+ );
110
115
  } );
111
116
 
112
117
  it( 'calls updateAttributes on URL blur', () => {
@@ -143,11 +148,11 @@ describe( 'Controls', () => {
143
148
  target: { value: 'https://new.com' },
144
149
  } );
145
150
 
146
- // Blur should call updateAttributes with the current URL (since url exists)
151
+ // Blur should call updateAttributes with the new URL value from the input
147
152
  fireEvent.blur( urlInput );
148
153
 
149
154
  expect( mockUpdateAttributes ).toHaveBeenCalledWith(
150
- { url: 'https://different.com' }, // Current URL from attributes (not input value)
155
+ { url: 'https://new.com' }, // New URL from input value
151
156
  defaultProps.setAttributes,
152
157
  {
153
158
  ...propsWithDifferentUrl.attributes,
@@ -1153,6 +1153,7 @@ describe( 'updateAttributes', () => {
1153
1153
 
1154
1154
  expect( result ).toEqual( {
1155
1155
  isEntityLink: true,
1156
+ attributes: expect.any( Object ),
1156
1157
  } );
1157
1158
  } );
1158
1159
 
@@ -1173,6 +1174,7 @@ describe( 'updateAttributes', () => {
1173
1174
 
1174
1175
  expect( result ).toEqual( {
1175
1176
  isEntityLink: false,
1177
+ attributes: expect.any( Object ),
1176
1178
  } );
1177
1179
  } );
1178
1180
 
@@ -1190,6 +1192,7 @@ describe( 'updateAttributes', () => {
1190
1192
 
1191
1193
  expect( result ).toEqual( {
1192
1194
  isEntityLink: false,
1195
+ attributes: expect.any( Object ),
1193
1196
  } );
1194
1197
  } );
1195
1198
 
@@ -1215,6 +1218,7 @@ describe( 'updateAttributes', () => {
1215
1218
  // Should return false because the link was severed and converted to custom
1216
1219
  expect( result ).toEqual( {
1217
1220
  isEntityLink: false,
1221
+ attributes: expect.any( Object ),
1218
1222
  } );
1219
1223
  } );
1220
1224
 
@@ -1240,6 +1244,7 @@ describe( 'updateAttributes', () => {
1240
1244
  // Should return true because entity link is preserved
1241
1245
  expect( result ).toEqual( {
1242
1246
  isEntityLink: true,
1247
+ attributes: expect.any( Object ),
1243
1248
  } );
1244
1249
  } );
1245
1250
 
@@ -1260,6 +1265,7 @@ describe( 'updateAttributes', () => {
1260
1265
  // mailto links have kind: 'custom', so isEntityLink should be false
1261
1266
  expect( result ).toEqual( {
1262
1267
  isEntityLink: false,
1268
+ attributes: expect.any( Object ),
1263
1269
  } );
1264
1270
  } );
1265
1271
 
@@ -1280,6 +1286,7 @@ describe( 'updateAttributes', () => {
1280
1286
  // tel links have kind: 'custom', so isEntityLink should be false
1281
1287
  expect( result ).toEqual( {
1282
1288
  isEntityLink: false,
1289
+ attributes: expect.any( Object ),
1283
1290
  } );
1284
1291
  } );
1285
1292
 
@@ -1300,6 +1307,7 @@ describe( 'updateAttributes', () => {
1300
1307
 
1301
1308
  expect( result ).toEqual( {
1302
1309
  isEntityLink: true,
1310
+ attributes: expect.any( Object ),
1303
1311
  } );
1304
1312
  } );
1305
1313
  } );
@@ -10,7 +10,10 @@ import { renderHook, act } from '@testing-library/react';
10
10
  /**
11
11
  * Internal dependencies
12
12
  */
13
- import { useEntityBinding } from '../use-entity-binding';
13
+ import {
14
+ useEntityBinding,
15
+ buildNavigationLinkEntityBinding,
16
+ } from '../use-entity-binding';
14
17
 
15
18
  // Mock the entire @wordpress/block-editor module
16
19
  jest.mock( '@wordpress/block-editor', () => ( {
@@ -49,17 +52,18 @@ describe( 'useEntityBinding', () => {
49
52
  expect( result.current.hasUrlBinding ).toBe( false );
50
53
  } );
51
54
 
52
- it( 'should return true when core/entity binding exists with id', () => {
55
+ it( 'should return true when core/post-data binding exists with id for post-type', () => {
53
56
  const attributes = {
54
57
  metadata: {
55
58
  bindings: {
56
59
  url: {
57
- source: 'core/entity',
58
- args: { key: 'url' },
60
+ source: 'core/post-data',
61
+ args: { field: 'link' },
59
62
  },
60
63
  },
61
64
  },
62
65
  id: 123,
66
+ kind: 'post-type',
63
67
  };
64
68
 
65
69
  const { result } = renderHook( () =>
@@ -72,17 +76,42 @@ describe( 'useEntityBinding', () => {
72
76
  expect( result.current.hasUrlBinding ).toBe( true );
73
77
  } );
74
78
 
75
- it( 'should return false when source is not core/entity', () => {
79
+ it( 'should return true when core/term-data binding exists with id for taxonomy', () => {
80
+ const attributes = {
81
+ metadata: {
82
+ bindings: {
83
+ url: {
84
+ source: 'core/term-data',
85
+ args: { field: 'link' },
86
+ },
87
+ },
88
+ },
89
+ id: 123,
90
+ kind: 'taxonomy',
91
+ };
92
+
93
+ const { result } = renderHook( () =>
94
+ useEntityBinding( {
95
+ clientId: 'test-client-id',
96
+ attributes,
97
+ } )
98
+ );
99
+
100
+ expect( result.current.hasUrlBinding ).toBe( true );
101
+ } );
102
+
103
+ it( 'should return false when source is not core/post-data or core/term-data', () => {
76
104
  const attributes = {
77
105
  metadata: {
78
106
  bindings: {
79
107
  url: {
80
108
  source: 'some-other-source',
81
- args: { key: 'url' },
109
+ args: { field: 'url' },
82
110
  },
83
111
  },
84
112
  },
85
113
  id: 123,
114
+ kind: 'post-type',
86
115
  };
87
116
 
88
117
  const { result } = renderHook( () =>
@@ -95,17 +124,18 @@ describe( 'useEntityBinding', () => {
95
124
  expect( result.current.hasUrlBinding ).toBe( false );
96
125
  } );
97
126
 
98
- it( 'should return false when core/entity binding exists but no id', () => {
127
+ it( 'should return false when core/post-data binding exists but no id', () => {
99
128
  const attributes = {
100
129
  metadata: {
101
130
  bindings: {
102
131
  url: {
103
- source: 'core/entity',
104
- args: { key: 'url' },
132
+ source: 'core/post-data',
133
+ args: { field: 'link' },
105
134
  },
106
135
  },
107
136
  },
108
137
  id: null,
138
+ kind: 'post-type',
109
139
  };
110
140
 
111
141
  const { result } = renderHook( () =>
@@ -147,12 +177,13 @@ describe( 'useEntityBinding', () => {
147
177
  metadata: {
148
178
  bindings: {
149
179
  url: {
150
- source: 'core/entity',
151
- args: { key: 'url' },
180
+ source: 'core/post-data',
181
+ args: { field: 'link' },
152
182
  },
153
183
  },
154
184
  },
155
185
  id: 123,
186
+ kind: 'post-type',
156
187
  };
157
188
 
158
189
  const { result } = renderHook( () =>
@@ -171,7 +202,7 @@ describe( 'useEntityBinding', () => {
171
202
  } );
172
203
  } );
173
204
 
174
- it( 'should NOT clear binding when clearBinding is called and no binding exists', () => {
205
+ it( 'should NOT call updateBlockBindings when clearBinding is called and no binding exists', () => {
175
206
  const attributes = {
176
207
  metadata: {},
177
208
  id: null,
@@ -191,7 +222,7 @@ describe( 'useEntityBinding', () => {
191
222
  expect( mockUpdateBlockBindings ).not.toHaveBeenCalled();
192
223
  } );
193
224
 
194
- it( 'should NOT clear binding when binding metadata exists but source is null', () => {
225
+ it( 'should call updateBlockBindings when clearBinding is called and binding exists even with null source', () => {
195
226
  const attributes = {
196
227
  metadata: {
197
228
  bindings: {
@@ -215,13 +246,16 @@ describe( 'useEntityBinding', () => {
215
246
  result.current.clearBinding();
216
247
  } );
217
248
 
218
- expect( mockUpdateBlockBindings ).not.toHaveBeenCalled();
249
+ expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( {
250
+ url: undefined,
251
+ } );
219
252
  } );
220
253
 
221
- it( 'should create binding when createBinding is called', () => {
254
+ it( 'should create core/post-data binding when createBinding is called for post-type', () => {
222
255
  const attributes = {
223
256
  metadata: {},
224
257
  id: null,
258
+ kind: 'post-type',
225
259
  };
226
260
 
227
261
  const { result } = renderHook( () =>
@@ -237,11 +271,92 @@ describe( 'useEntityBinding', () => {
237
271
 
238
272
  expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( {
239
273
  url: {
240
- source: 'core/entity',
274
+ source: 'core/post-data',
241
275
  args: {
242
- key: 'url',
276
+ field: 'link',
243
277
  },
244
278
  },
245
279
  } );
246
280
  } );
281
+
282
+ describe( 'buildNavigationLinkEntityBinding', () => {
283
+ it( 'returns correct binding for post-type', () => {
284
+ const binding = buildNavigationLinkEntityBinding( 'post-type' );
285
+ expect( binding ).toEqual( {
286
+ url: {
287
+ source: 'core/post-data',
288
+ args: { field: 'link' },
289
+ },
290
+ } );
291
+ } );
292
+
293
+ it( 'returns correct binding for taxonomy', () => {
294
+ const binding = buildNavigationLinkEntityBinding( 'taxonomy' );
295
+ expect( binding ).toEqual( {
296
+ url: {
297
+ source: 'core/term-data',
298
+ args: { field: 'link' },
299
+ },
300
+ } );
301
+ } );
302
+
303
+ it( 'throws error when called without parameter', () => {
304
+ expect( () => {
305
+ buildNavigationLinkEntityBinding();
306
+ } ).toThrow(
307
+ 'buildNavigationLinkEntityBinding requires a kind parameter'
308
+ );
309
+ } );
310
+
311
+ it( 'throws error for invalid kind', () => {
312
+ expect( () => {
313
+ buildNavigationLinkEntityBinding( 'invalid-kind' );
314
+ } ).toThrow( 'Invalid kind "invalid-kind"' );
315
+ } );
316
+
317
+ it( 'throws error for null kind', () => {
318
+ expect( () => {
319
+ buildNavigationLinkEntityBinding( null );
320
+ } ).toThrow( 'Invalid kind "null"' );
321
+ } );
322
+
323
+ it( 'throws error for empty string', () => {
324
+ expect( () => {
325
+ buildNavigationLinkEntityBinding( '' );
326
+ } ).toThrow( 'Invalid kind ""' );
327
+ } );
328
+
329
+ it( 'handles invalid kind gracefully in createBinding', () => {
330
+ const consoleSpy = jest
331
+ .spyOn( console, 'warn' )
332
+ .mockImplementation();
333
+
334
+ const attributes = {
335
+ metadata: {},
336
+ id: null,
337
+ kind: 'invalid-kind',
338
+ };
339
+
340
+ const { result } = renderHook( () =>
341
+ useEntityBinding( {
342
+ clientId: 'test-client-id',
343
+ attributes,
344
+ } )
345
+ );
346
+
347
+ act( () => {
348
+ result.current.createBinding();
349
+ } );
350
+
351
+ expect( consoleSpy ).toHaveBeenCalledWith(
352
+ 'Failed to create entity binding:',
353
+ expect.stringContaining( 'Invalid kind "invalid-kind"' )
354
+ );
355
+
356
+ // Should not call updateBlockBindings when validation fails
357
+ expect( mockUpdateBlockBindings ).not.toHaveBeenCalled();
358
+
359
+ consoleSpy.mockRestore();
360
+ } );
361
+ } );
247
362
  } );
@@ -222,5 +222,6 @@ export const updateAttributes = (
222
222
 
223
223
  return {
224
224
  isEntityLink: !! finalId && finalKind !== 'custom',
225
+ attributes, // Return the computed attributes object
225
226
  };
226
227
  };