@wordpress/block-library 9.34.1-next.2f1c7c01b.0 → 9.35.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 (149) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/block/edit.js +2 -2
  3. package/build/block/edit.js.map +2 -2
  4. package/build/block-keyboard-shortcuts/index.js +17 -7
  5. package/build/block-keyboard-shortcuts/index.js.map +2 -2
  6. package/build/cover/deprecated.js +15 -3
  7. package/build/cover/deprecated.js.map +2 -2
  8. package/build/cover/edit/inspector-controls.js +1 -1
  9. package/build/cover/edit/inspector-controls.js.map +2 -2
  10. package/build/cover/transforms.js +10 -2
  11. package/build/cover/transforms.js.map +2 -2
  12. package/build/embed/icons.js +2 -2
  13. package/build/embed/icons.js.map +2 -2
  14. package/build/embed/variations.js +3 -3
  15. package/build/embed/variations.js.map +2 -2
  16. package/build/heading/index.js +3 -1
  17. package/build/heading/index.js.map +3 -3
  18. package/build/heading/transforms.js +10 -3
  19. package/build/heading/transforms.js.map +2 -2
  20. package/build/heading/variations.js +55 -0
  21. package/build/heading/variations.js.map +7 -0
  22. package/build/html/edit.js +54 -44
  23. package/build/html/edit.js.map +3 -3
  24. package/build/html/modal.js +328 -0
  25. package/build/html/modal.js.map +7 -0
  26. package/build/html/utils.js +72 -0
  27. package/build/html/utils.js.map +7 -0
  28. package/build/navigation-link/edit.js +25 -10
  29. package/build/navigation-link/edit.js.map +2 -2
  30. package/build/navigation-link/link-ui/index.js +8 -3
  31. package/build/navigation-link/link-ui/index.js.map +2 -2
  32. package/build/navigation-link/shared/controls.js +42 -7
  33. package/build/navigation-link/shared/controls.js.map +2 -2
  34. package/build/navigation-link/shared/use-entity-binding.js +31 -2
  35. package/build/navigation-link/shared/use-entity-binding.js.map +3 -3
  36. package/build/paragraph/block.json +1 -3
  37. package/build/paragraph/deprecated.js +65 -12
  38. package/build/paragraph/deprecated.js.map +2 -2
  39. package/build/paragraph/edit.js +14 -25
  40. package/build/paragraph/edit.js.map +2 -2
  41. package/build/paragraph/index.js +3 -1
  42. package/build/paragraph/index.js.map +3 -3
  43. package/build/paragraph/save.js +3 -3
  44. package/build/paragraph/save.js.map +2 -2
  45. package/build/paragraph/transforms.js +7 -1
  46. package/build/paragraph/transforms.js.map +2 -2
  47. package/build/paragraph/variations.js +57 -0
  48. package/build/paragraph/variations.js.map +7 -0
  49. package/build-module/block/edit.js +2 -2
  50. package/build-module/block/edit.js.map +2 -2
  51. package/build-module/block-keyboard-shortcuts/index.js +17 -7
  52. package/build-module/block-keyboard-shortcuts/index.js.map +2 -2
  53. package/build-module/cover/deprecated.js +15 -3
  54. package/build-module/cover/deprecated.js.map +2 -2
  55. package/build-module/cover/edit/inspector-controls.js +1 -1
  56. package/build-module/cover/edit/inspector-controls.js.map +2 -2
  57. package/build-module/cover/transforms.js +10 -2
  58. package/build-module/cover/transforms.js.map +2 -2
  59. package/build-module/embed/icons.js +2 -2
  60. package/build-module/embed/icons.js.map +2 -2
  61. package/build-module/embed/variations.js +3 -3
  62. package/build-module/embed/variations.js.map +2 -2
  63. package/build-module/heading/index.js +3 -1
  64. package/build-module/heading/index.js.map +2 -2
  65. package/build-module/heading/transforms.js +10 -3
  66. package/build-module/heading/transforms.js.map +2 -2
  67. package/build-module/heading/variations.js +34 -0
  68. package/build-module/heading/variations.js.map +7 -0
  69. package/build-module/html/edit.js +62 -51
  70. package/build-module/html/edit.js.map +2 -2
  71. package/build-module/html/modal.js +304 -0
  72. package/build-module/html/modal.js.map +7 -0
  73. package/build-module/html/utils.js +46 -0
  74. package/build-module/html/utils.js.map +7 -0
  75. package/build-module/navigation-link/edit.js +25 -10
  76. package/build-module/navigation-link/edit.js.map +2 -2
  77. package/build-module/navigation-link/link-ui/index.js +8 -3
  78. package/build-module/navigation-link/link-ui/index.js.map +2 -2
  79. package/build-module/navigation-link/shared/controls.js +42 -7
  80. package/build-module/navigation-link/shared/controls.js.map +2 -2
  81. package/build-module/navigation-link/shared/use-entity-binding.js +35 -3
  82. package/build-module/navigation-link/shared/use-entity-binding.js.map +2 -2
  83. package/build-module/paragraph/block.json +1 -3
  84. package/build-module/paragraph/deprecated.js +65 -12
  85. package/build-module/paragraph/deprecated.js.map +2 -2
  86. package/build-module/paragraph/edit.js +14 -26
  87. package/build-module/paragraph/edit.js.map +2 -2
  88. package/build-module/paragraph/index.js +3 -1
  89. package/build-module/paragraph/index.js.map +2 -2
  90. package/build-module/paragraph/save.js +3 -3
  91. package/build-module/paragraph/save.js.map +2 -2
  92. package/build-module/paragraph/transforms.js +7 -1
  93. package/build-module/paragraph/transforms.js.map +2 -2
  94. package/build-module/paragraph/variations.js +36 -0
  95. package/build-module/paragraph/variations.js.map +7 -0
  96. package/build-style/accordion-heading/style-rtl.css +19 -3
  97. package/build-style/accordion-heading/style.css +19 -3
  98. package/build-style/accordion-panel/style-rtl.css +4 -1
  99. package/build-style/accordion-panel/style.css +4 -1
  100. package/build-style/common-rtl.css +3 -3
  101. package/build-style/common.css +3 -3
  102. package/build-style/editor-rtl.css +62 -21
  103. package/build-style/editor.css +62 -21
  104. package/build-style/embed/style-rtl.css +5 -0
  105. package/build-style/embed/style.css +5 -0
  106. package/build-style/html/editor-rtl.css +55 -21
  107. package/build-style/html/editor.css +55 -21
  108. package/build-style/navigation-link/editor-rtl.css +7 -0
  109. package/build-style/navigation-link/editor.css +7 -0
  110. package/build-style/style-rtl.css +31 -7
  111. package/build-style/style.css +31 -7
  112. package/package.json +37 -37
  113. package/src/accordion-heading/style.scss +40 -7
  114. package/src/accordion-panel/style.scss +6 -1
  115. package/src/block/edit.js +2 -2
  116. package/src/block-keyboard-shortcuts/index.js +23 -9
  117. package/src/common.scss +6 -5
  118. package/src/cover/deprecated.js +15 -3
  119. package/src/cover/edit/inspector-controls.js +1 -1
  120. package/src/cover/transforms.js +10 -2
  121. package/src/embed/icons.js +2 -4
  122. package/src/embed/style.scss +6 -0
  123. package/src/embed/variations.js +3 -3
  124. package/src/heading/index.js +2 -0
  125. package/src/heading/transforms.js +10 -3
  126. package/src/heading/variations.js +37 -0
  127. package/src/html/edit.js +62 -56
  128. package/src/html/editor.scss +69 -10
  129. package/src/html/modal.js +290 -0
  130. package/src/html/test/utils.js +234 -0
  131. package/src/html/utils.js +75 -0
  132. package/src/navigation-link/edit.js +44 -13
  133. package/src/navigation-link/editor.scss +7 -0
  134. package/src/navigation-link/index.php +65 -2
  135. package/src/navigation-link/link-ui/index.js +9 -8
  136. package/src/navigation-link/shared/controls.js +70 -12
  137. package/src/navigation-link/shared/test/controls.js +5 -0
  138. package/src/navigation-link/shared/test/use-entity-binding.js +14 -1
  139. package/src/navigation-link/shared/use-entity-binding.js +57 -9
  140. package/src/paragraph/block.json +1 -3
  141. package/src/paragraph/deprecated.js +87 -20
  142. package/src/paragraph/edit.js +7 -18
  143. package/src/paragraph/edit.native.js +18 -6
  144. package/src/paragraph/index.js +2 -0
  145. package/src/paragraph/save.js +4 -3
  146. package/src/paragraph/test/edit.native.js +5 -5
  147. package/src/paragraph/transforms.js +7 -1
  148. package/src/paragraph/variations.js +39 -0
  149. package/tsconfig.tsbuildinfo +1 -1
@@ -97,21 +97,35 @@ const useIsInvalidLink = ( kind, type, id, enabled ) => {
97
97
  const hasId = Number.isInteger( id );
98
98
  const blockEditingMode = useBlockEditingMode();
99
99
 
100
- const postStatus = useSelect(
100
+ const { postStatus, isDeleted } = useSelect(
101
101
  ( select ) => {
102
102
  if ( ! isPostType ) {
103
- return null;
103
+ return { postStatus: null, isDeleted: false };
104
104
  }
105
105
 
106
106
  // Fetching the posts status is an "expensive" operation. Especially for sites with large navigations.
107
107
  // When the block is rendered in a template or other disabled contexts we can skip this check in order
108
108
  // to avoid all these additional requests that don't really add any value in that mode.
109
109
  if ( blockEditingMode === 'disabled' || ! enabled ) {
110
- return null;
110
+ return { postStatus: null, isDeleted: false };
111
111
  }
112
112
 
113
- const { getEntityRecord } = select( coreStore );
114
- return getEntityRecord( 'postType', type, id )?.status;
113
+ const { getEntityRecord, hasFinishedResolution } =
114
+ select( coreStore );
115
+ const entityRecord = getEntityRecord( 'postType', type, id );
116
+ const hasResolved = hasFinishedResolution( 'getEntityRecord', [
117
+ 'postType',
118
+ type,
119
+ id,
120
+ ] );
121
+
122
+ // If resolution has finished and entityRecord is undefined, the entity was deleted.
123
+ const deleted = hasResolved && entityRecord === undefined;
124
+
125
+ return {
126
+ postStatus: entityRecord?.status,
127
+ isDeleted: deleted,
128
+ };
115
129
  },
116
130
  [ isPostType, blockEditingMode, enabled, type, id ]
117
131
  );
@@ -121,11 +135,13 @@ const useIsInvalidLink = ( kind, type, id, enabled ) => {
121
135
  // 2. It has an id.
122
136
  // 3. It's neither null, nor undefined, as valid items might be either of those while loading.
123
137
  // If those conditions are met, check if
124
- // 1. The post status is published.
125
- // 2. The Navigation Link item has no label.
138
+ // 1. The post status is trash (trashed).
139
+ // 2. The entity doesn't exist (deleted).
126
140
  // If either of those is true, invalidate.
127
141
  const isInvalid =
128
- isPostType && hasId && postStatus && 'trash' === postStatus;
142
+ isPostType &&
143
+ hasId &&
144
+ ( isDeleted || ( postStatus && 'trash' === postStatus ) );
129
145
  const isDraft = 'draft' === postStatus;
130
146
 
131
147
  return [ isInvalid, isDraft ];
@@ -248,7 +264,12 @@ export default function NavigationLinkEdit( {
248
264
  const { getBlocks } = useSelect( blockEditorStore );
249
265
 
250
266
  // URL binding logic
251
- const { clearBinding, createBinding } = useEntityBinding( {
267
+ const {
268
+ clearBinding,
269
+ createBinding,
270
+ hasUrlBinding,
271
+ isBoundEntityAvailable,
272
+ } = useEntityBinding( {
252
273
  clientId,
253
274
  attributes,
254
275
  } );
@@ -405,14 +426,23 @@ export default function NavigationLinkEdit( {
405
426
  }
406
427
  );
407
428
 
408
- if ( ! url || isInvalid || isDraft ) {
429
+ if (
430
+ ! url ||
431
+ isInvalid ||
432
+ isDraft ||
433
+ ( hasUrlBinding && ! isBoundEntityAvailable )
434
+ ) {
409
435
  blockProps.onClick = () => {
410
436
  setIsLinkOpen( true );
411
437
  };
412
438
  }
413
439
 
414
440
  const classes = clsx( 'wp-block-navigation-item__content', {
415
- 'wp-block-navigation-link__placeholder': ! url || isInvalid || isDraft,
441
+ 'wp-block-navigation-link__placeholder':
442
+ ! url ||
443
+ isInvalid ||
444
+ isDraft ||
445
+ ( hasUrlBinding && ! isBoundEntityAvailable ),
416
446
  } );
417
447
 
418
448
  const missingText = getMissingText( type );
@@ -531,9 +561,10 @@ export default function NavigationLinkEdit( {
531
561
  link={ attributes }
532
562
  onClose={ () => {
533
563
  setIsLinkOpen( false );
534
- // If there is no link then remove the auto-inserted block.
564
+ // If there is no link and no binding, remove the auto-inserted block.
535
565
  // This avoids empty blocks which can provided a poor UX.
536
- if ( ! url ) {
566
+ // Don't remove if binding exists (even if entity is unavailable) so user can fix it.
567
+ if ( ! url && ! hasUrlBinding ) {
537
568
  onReplace( [] );
538
569
  } else if ( isNewLink.current ) {
539
570
  // If we just created a new link, select it
@@ -143,3 +143,10 @@
143
143
  text-transform: uppercase;
144
144
  }
145
145
  }
146
+
147
+ /**
148
+ * Error text styling for missing entity help text.
149
+ */
150
+ .navigation-link-control__error-text {
151
+ color: $alert-red;
152
+ }
@@ -312,11 +312,50 @@ function build_variation_for_navigation_link( $entity, $kind ) {
312
312
  $title = '';
313
313
  $description = '';
314
314
 
315
+ // Get default labels based on entity type
316
+ $default_labels = null;
317
+ if ( $entity instanceof WP_Post_Type ) {
318
+ $default_labels = WP_Post_Type::get_default_labels();
319
+ } elseif ( $entity instanceof WP_Taxonomy ) {
320
+ $default_labels = WP_Taxonomy::get_default_labels();
321
+ }
322
+
323
+ // Get title and check if it's default
324
+ $is_default_title = false;
315
325
  if ( property_exists( $entity->labels, 'item_link' ) ) {
316
326
  $title = $entity->labels->item_link;
327
+ if ( isset( $default_labels['item_link'] ) ) {
328
+ $is_default_title = in_array( $title, $default_labels['item_link'], true );
329
+ }
317
330
  }
331
+
332
+ // Get description and check if it's default
333
+ $is_default_description = false;
318
334
  if ( property_exists( $entity->labels, 'item_link_description' ) ) {
319
335
  $description = $entity->labels->item_link_description;
336
+ if ( isset( $default_labels['item_link_description'] ) ) {
337
+ $is_default_description = in_array( $description, $default_labels['item_link_description'], true );
338
+ }
339
+ }
340
+
341
+ // Calculate singular name once (used for both title and description)
342
+ $singular = isset( $entity->labels->singular_name ) ? $entity->labels->singular_name : ucfirst( $entity->name );
343
+
344
+ // Set default title if needed
345
+ if ( $is_default_title || '' === $title ) {
346
+ /* translators: %s: Singular label of the entity. */
347
+ $title = sprintf( __( '%s link' ), $singular );
348
+ }
349
+
350
+ // Default description if needed.
351
+ // Use a single space character instead of an empty string to prevent fallback to the
352
+ // block.json default description ("Add a page, link, or another item to your navigation.").
353
+ // An empty string would be treated as missing and trigger the fallback, while a single
354
+ // space appears blank in the UI but prevents the fallback behavior.
355
+ // We avoid generating descriptions like "A link to a %s" to prevent grammatical errors
356
+ // (e.g., "A link to a event" should be "A link to an event").
357
+ if ( $is_default_description || '' === $description ) {
358
+ $description = ' ';
320
359
  }
321
360
 
322
361
  $variation = array(
@@ -368,6 +407,7 @@ function build_variation_for_navigation_link( $entity, $kind ) {
368
407
  *
369
408
  * @param array $variations Array of registered variations for a block type.
370
409
  * @param WP_Block_Type $block_type The full block type object.
410
+ * @return array Numerically indexed array of block variations.
371
411
  */
372
412
  function block_core_navigation_link_filter_variations( $variations, $block_type ) {
373
413
  if ( 'core/navigation-link' !== $block_type->name ) {
@@ -375,7 +415,28 @@ function block_core_navigation_link_filter_variations( $variations, $block_type
375
415
  }
376
416
 
377
417
  $generated_variations = block_core_navigation_link_build_variations();
378
- return array_merge( $variations, $generated_variations );
418
+
419
+ /*
420
+ * IMPORTANT: Order matters for deduplication.
421
+ *
422
+ * The variations returned from this filter are bootstrapped to JavaScript and
423
+ * processed by the block variations reducer. The reducer uses `getUniqueItemsByName()`
424
+ * (packages/blocks/src/store/reducer.js:51-57) which keeps the FIRST variation with
425
+ * a given 'name' and discards later duplicates when processing the array in order.
426
+ *
427
+ * By placing generated variations first in `array_merge()`, the improved
428
+ * labels (e.g., "Product link" instead of generic "Post Link") are processed first
429
+ * and preserved. The generic incoming variations are then discarded as duplicates.
430
+ *
431
+ * Why `array_merge()` instead of manual deduplication?
432
+ * - Both arrays use numeric indices (0, 1, 2...), so `array_merge()` concatenates
433
+ * and re-indexes them sequentially, preserving order
434
+ * - The reducer handles deduplication, so it is not needed here
435
+ * - This keeps the PHP code simple and relies on the established JavaScript behavior
436
+ *
437
+ * See: https://github.com/WordPress/gutenberg/pull/72517
438
+ */
439
+ return array_merge( $generated_variations, $variations );
379
440
  }
380
441
 
381
442
  /**
@@ -419,7 +480,9 @@ function block_core_navigation_link_build_variations() {
419
480
  }
420
481
  }
421
482
 
422
- return array_merge( $built_ins, $variations );
483
+ $all_variations = array_merge( $built_ins, $variations );
484
+
485
+ return $all_variations;
423
486
  }
424
487
 
425
488
  /**
@@ -26,6 +26,7 @@ import { useInstanceId } from '@wordpress/compose';
26
26
  */
27
27
  import { LinkUIPageCreator } from './page-creator';
28
28
  import LinkUIBlockInserter from './block-inserter';
29
+ import { useEntityBinding } from '../shared/use-entity-binding';
29
30
 
30
31
  /**
31
32
  * Given the Link block's type attribute, return the query params to give to
@@ -66,7 +67,8 @@ export function getSuggestionsQuery( type, kind ) {
66
67
  }
67
68
 
68
69
  function UnforwardedLinkUI( props, ref ) {
69
- const { label, url, opensInNewTab, type, kind, id, metadata } = props.link;
70
+ const { label, url, opensInNewTab, type, kind, id } = props.link;
71
+ const { clientId } = props;
70
72
  const postType = type || 'page';
71
73
 
72
74
  const [ addingBlock, setAddingBlock ] = useState( false );
@@ -78,12 +80,11 @@ function UnforwardedLinkUI( props, ref ) {
78
80
  name: postType,
79
81
  } );
80
82
 
81
- // Check if there's a URL binding with the new binding sources
82
- // Only enable handleEntities when there's actually a binding present
83
- const hasUrlBinding =
84
- ( metadata?.bindings?.url?.source === 'core/post-data' ||
85
- metadata?.bindings?.url?.source === 'core/term-data' ) &&
86
- !! id;
83
+ // Use the entity binding hook to get binding status
84
+ const { isBoundEntityAvailable } = useEntityBinding( {
85
+ clientId,
86
+ attributes: props.link,
87
+ } );
87
88
 
88
89
  // Memoize link value to avoid overriding the LinkControl's internal state.
89
90
  // This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407.
@@ -152,7 +153,7 @@ function UnforwardedLinkUI( props, ref ) {
152
153
  onChange={ props.onChange }
153
154
  onRemove={ props.onRemove }
154
155
  onCancel={ props.onCancel }
155
- handleEntities={ hasUrlBinding }
156
+ handleEntities={ isBoundEntityAvailable }
156
157
  renderControlBottom={ () => {
157
158
  // Don't show the tools when there is submitted link (preview state).
158
159
  if ( link?.url?.length ) {
@@ -87,10 +87,11 @@ export function Controls( { attributes, setAttributes, clientId } ) {
87
87
  }, [ url ] );
88
88
 
89
89
  // Use the entity binding hook internally
90
- const { hasUrlBinding, clearBinding } = useEntityBinding( {
91
- clientId,
92
- attributes,
93
- } );
90
+ const { hasUrlBinding, isBoundEntityAvailable, clearBinding } =
91
+ useEntityBinding( {
92
+ clientId,
93
+ attributes,
94
+ } );
94
95
 
95
96
  // Get direct store dispatch to bypass setBoundAttributes wrapper
96
97
  const { updateBlockAttributes } = useDispatch( blockEditorStore );
@@ -111,8 +112,7 @@ export function Controls( { attributes, setAttributes, clientId } ) {
111
112
  };
112
113
 
113
114
  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.
115
+ // Only want to focus the input if the url is not bound to an entity.
116
116
  if ( ! hasUrlBinding && shouldFocusURLInputRef.current ) {
117
117
  // focuses and highlights the url input value, giving the user
118
118
  // the ability to delete the value quickly or edit it.
@@ -165,12 +165,28 @@ export function Controls( { attributes, setAttributes, clientId } ) {
165
165
  __next40pxDefaultSize
166
166
  id={ inputId }
167
167
  label={ __( 'Link' ) }
168
- value={ inputValue ? safeDecodeURI( inputValue ) : '' }
168
+ value={ ( () => {
169
+ if ( hasUrlBinding && ! isBoundEntityAvailable ) {
170
+ return '';
171
+ }
172
+ return inputValue ? safeDecodeURI( inputValue ) : '';
173
+ } )() }
169
174
  autoComplete="off"
170
175
  type="url"
171
176
  disabled={ hasUrlBinding }
177
+ aria-invalid={
178
+ hasUrlBinding && ! isBoundEntityAvailable
179
+ ? 'true'
180
+ : undefined
181
+ }
182
+ aria-describedby={ helpTextId }
183
+ className={
184
+ hasUrlBinding && ! isBoundEntityAvailable
185
+ ? 'navigation-link-control__input-with-error-suffix'
186
+ : undefined
187
+ }
172
188
  onChange={ ( newValue ) => {
173
- if ( hasUrlBinding ) {
189
+ if ( isBoundEntityAvailable ) {
174
190
  return;
175
191
  }
176
192
 
@@ -180,13 +196,13 @@ export function Controls( { attributes, setAttributes, clientId } ) {
180
196
  setInputValue( newValue );
181
197
  } }
182
198
  onFocus={ () => {
183
- if ( hasUrlBinding ) {
199
+ if ( isBoundEntityAvailable ) {
184
200
  return;
185
201
  }
186
202
  lastURLRef.current = url;
187
203
  } }
188
204
  onBlur={ () => {
189
- if ( hasUrlBinding ) {
205
+ if ( isBoundEntityAvailable ) {
190
206
  return;
191
207
  }
192
208
 
@@ -204,11 +220,19 @@ export function Controls( { attributes, setAttributes, clientId } ) {
204
220
  } );
205
221
  } }
206
222
  help={
207
- hasUrlBinding && (
208
- <BindingHelpText
223
+ hasUrlBinding && ! isBoundEntityAvailable ? (
224
+ <MissingEntityHelpText
225
+ id={ helpTextId }
209
226
  type={ attributes.type }
210
227
  kind={ attributes.kind }
211
228
  />
229
+ ) : (
230
+ isBoundEntityAvailable && (
231
+ <BindingHelpText
232
+ type={ attributes.type }
233
+ kind={ attributes.kind }
234
+ />
235
+ )
212
236
  )
213
237
  }
214
238
  suffix={
@@ -225,6 +249,11 @@ export function Controls( { attributes, setAttributes, clientId } ) {
225
249
  showTooltip
226
250
  label={ __( 'Unsync and edit' ) }
227
251
  __next40pxDefaultSize
252
+ className={
253
+ hasUrlBinding && ! isBoundEntityAvailable
254
+ ? 'navigation-link-control__error-suffix-button'
255
+ : undefined
256
+ }
228
257
  />
229
258
  )
230
259
  }
@@ -306,3 +335,32 @@ function BindingHelpText( { type, kind } ) {
306
335
  entityType
307
336
  );
308
337
  }
338
+
339
+ /**
340
+ * Component to display error help text for missing entity bindings.
341
+ *
342
+ * @param {Object} props - Component props
343
+ * @param {string} props.id - ID for the help text element (for aria-describedby)
344
+ * @param {string} props.type - The entity type
345
+ * @param {string} props.kind - The entity kind
346
+ * @return {JSX.Element} Error help text component
347
+ */
348
+ function MissingEntityHelpText( { id, type, kind } ) {
349
+ const entityType = getEntityTypeName( type, kind );
350
+ return (
351
+ <span
352
+ id={ id }
353
+ className="navigation-link-control__error-text"
354
+ role="alert"
355
+ aria-live="polite"
356
+ >
357
+ { sprintf(
358
+ /* translators: %s is the entity type (e.g., "page", "post", "category") */
359
+ __(
360
+ 'Synced %s is missing. Please update or remove this link.'
361
+ ),
362
+ entityType
363
+ ) }
364
+ </span>
365
+ );
366
+ }
@@ -28,6 +28,7 @@ jest.mock( '../../../utils/hooks', () => ( {
28
28
  jest.mock( '../use-entity-binding', () => ( {
29
29
  useEntityBinding: jest.fn( () => ( {
30
30
  hasUrlBinding: false,
31
+ isBoundEntityAvailable: false,
31
32
  clearBinding: jest.fn(),
32
33
  } ) ),
33
34
  } ) );
@@ -202,6 +203,7 @@ describe( 'Controls', () => {
202
203
  const { useEntityBinding } = require( '../use-entity-binding' );
203
204
  useEntityBinding.mockReturnValue( {
204
205
  hasUrlBinding: true,
206
+ isBoundEntityAvailable: true,
205
207
  clearBinding: jest.fn(),
206
208
  } );
207
209
 
@@ -225,6 +227,7 @@ describe( 'Controls', () => {
225
227
  const { useEntityBinding } = require( '../use-entity-binding' );
226
228
  useEntityBinding.mockReturnValue( {
227
229
  hasUrlBinding: true,
230
+ isBoundEntityAvailable: true,
228
231
  clearBinding: jest.fn(),
229
232
  } );
230
233
 
@@ -262,6 +265,7 @@ describe( 'Controls', () => {
262
265
  const { useEntityBinding } = require( '../use-entity-binding' );
263
266
  useEntityBinding.mockReturnValue( {
264
267
  hasUrlBinding: true,
268
+ isBoundEntityAvailable: true,
265
269
  clearBinding: jest.fn(),
266
270
  } );
267
271
 
@@ -285,6 +289,7 @@ describe( 'Controls', () => {
285
289
  const { useEntityBinding } = require( '../use-entity-binding' );
286
290
  useEntityBinding.mockReturnValue( {
287
291
  hasUrlBinding: true,
292
+ isBoundEntityAvailable: true,
288
293
  clearBinding: jest.fn(),
289
294
  } );
290
295
 
@@ -18,12 +18,23 @@ import {
18
18
  // Mock the entire @wordpress/block-editor module
19
19
  jest.mock( '@wordpress/block-editor', () => ( {
20
20
  useBlockBindingsUtils: jest.fn(),
21
+ useBlockEditingMode: jest.fn(),
21
22
  } ) );
22
23
 
24
+ // Mock useSelect specifically to avoid needing to set up full data store
25
+ jest.mock( '@wordpress/data/src/components/use-select', () => {
26
+ const mock = jest.fn();
27
+ return mock;
28
+ } );
29
+
23
30
  /**
24
31
  * WordPress dependencies
25
32
  */
26
- import { useBlockBindingsUtils } from '@wordpress/block-editor';
33
+ import {
34
+ useBlockBindingsUtils,
35
+ useBlockEditingMode,
36
+ } from '@wordpress/block-editor';
37
+ import { useSelect } from '@wordpress/data';
27
38
 
28
39
  describe( 'useEntityBinding', () => {
29
40
  const mockUpdateBlockBindings = jest.fn();
@@ -33,6 +44,8 @@ describe( 'useEntityBinding', () => {
33
44
  useBlockBindingsUtils.mockReturnValue( {
34
45
  updateBlockBindings: mockUpdateBlockBindings,
35
46
  } );
47
+ useBlockEditingMode.mockReturnValue( 'default' );
48
+ useSelect.mockReturnValue( true );
36
49
  } );
37
50
 
38
51
  describe( 'hasUrlBinding', () => {
@@ -2,7 +2,12 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { useCallback } from '@wordpress/element';
5
- import { useBlockBindingsUtils } from '@wordpress/block-editor';
5
+ import {
6
+ useBlockBindingsUtils,
7
+ useBlockEditingMode,
8
+ } from '@wordpress/block-editor';
9
+ import { useSelect } from '@wordpress/data';
10
+ import { store as coreStore } from '@wordpress/core-data';
6
11
 
7
12
  /**
8
13
  * Builds entity binding configuration for navigation link URLs.
@@ -16,7 +21,7 @@ import { useBlockBindingsUtils } from '@wordpress/block-editor';
16
21
  * @throws {Error} If kind is not 'post-type' or 'taxonomy'
17
22
  */
18
23
  export function buildNavigationLinkEntityBinding( kind ) {
19
- // Validate kind parameter exists
24
+ // Validate kind parameter exists.
20
25
  if ( kind === undefined ) {
21
26
  throw new Error(
22
27
  'buildNavigationLinkEntityBinding requires a kind parameter. ' +
@@ -24,7 +29,7 @@ export function buildNavigationLinkEntityBinding( kind ) {
24
29
  );
25
30
  }
26
31
 
27
- // Validate kind parameter value
32
+ // Validate kind parameter value.
28
33
  if ( kind !== 'post-type' && kind !== 'taxonomy' ) {
29
34
  throw new Error(
30
35
  `Invalid kind "${ kind }" provided to buildNavigationLinkEntityBinding. ` +
@@ -57,7 +62,8 @@ export function buildNavigationLinkEntityBinding( kind ) {
57
62
  */
58
63
  export function useEntityBinding( { clientId, attributes } ) {
59
64
  const { updateBlockBindings } = useBlockBindingsUtils( clientId );
60
- const { metadata, id, kind } = attributes;
65
+ const { metadata, id, kind, type } = attributes;
66
+ const blockEditingMode = useBlockEditingMode();
61
67
 
62
68
  const hasUrlBinding = !! metadata?.bindings?.url && !! id;
63
69
  const expectedSource =
@@ -65,6 +71,47 @@ export function useEntityBinding( { clientId, attributes } ) {
65
71
  const hasCorrectBinding =
66
72
  hasUrlBinding && metadata?.bindings?.url?.source === expectedSource;
67
73
 
74
+ // Check if the bound entity is available (not deleted).
75
+ const isBoundEntityAvailable = useSelect(
76
+ ( select ) => {
77
+ // First check: metadata/binding must exist
78
+ if ( ! hasCorrectBinding || ! id ) {
79
+ return false;
80
+ }
81
+
82
+ const isPostType = kind === 'post-type';
83
+ const isTaxonomy = kind === 'taxonomy';
84
+
85
+ // Only check entity availability for post types and taxonomies.
86
+ if ( ! isPostType && ! isTaxonomy ) {
87
+ return false;
88
+ }
89
+
90
+ // Skip check in disabled contexts to avoid unnecessary requests.
91
+ if ( blockEditingMode === 'disabled' ) {
92
+ return true; // Assume available in disabled contexts.
93
+ }
94
+
95
+ // Second check: entity must exist
96
+ const { getEntityRecord, hasFinishedResolution } =
97
+ select( coreStore );
98
+
99
+ // Use the correct entity type based on kind.
100
+ const entityType = isTaxonomy ? 'taxonomy' : 'postType';
101
+ const entityRecord = getEntityRecord( entityType, type, id );
102
+ const hasResolved = hasFinishedResolution( 'getEntityRecord', [
103
+ entityType,
104
+ type,
105
+ id,
106
+ ] );
107
+
108
+ // If resolution has finished and entityRecord is undefined, the entity was deleted.
109
+ // Return true if entity exists, false if deleted.
110
+ return hasResolved ? entityRecord !== undefined : true;
111
+ },
112
+ [ kind, type, id, hasCorrectBinding, blockEditingMode ]
113
+ );
114
+
68
115
  const clearBinding = useCallback( () => {
69
116
  if ( hasUrlBinding ) {
70
117
  updateBlockBindings( { url: undefined } );
@@ -73,11 +120,11 @@ export function useEntityBinding( { clientId, attributes } ) {
73
120
 
74
121
  const createBinding = useCallback(
75
122
  ( updatedAttributes ) => {
76
- // Use updated attributes if provided, otherwise fall back to closure attributes
77
- // updatedAttributes needed to access the most up-to-date data when called synchronously
123
+ // Use updated attributes if provided, otherwise fall back to closure attributes.
124
+ // updatedAttributes needed to access the most up-to-date data when called synchronously.
78
125
  const kindToUse = updatedAttributes?.kind ?? kind;
79
126
 
80
- // Avoid creating binding if no kind is provided
127
+ // Avoid creating binding if no kind is provided.
81
128
  if ( ! kindToUse ) {
82
129
  return;
83
130
  }
@@ -91,14 +138,15 @@ export function useEntityBinding( { clientId, attributes } ) {
91
138
  'Failed to create entity binding:',
92
139
  error.message
93
140
  );
94
- // Don't create binding if validation fails
141
+ // Don't create binding if validation fails.
95
142
  }
96
143
  },
97
- [ updateBlockBindings, kind, id ]
144
+ [ updateBlockBindings, kind ]
98
145
  );
99
146
 
100
147
  return {
101
148
  hasUrlBinding: hasCorrectBinding,
149
+ isBoundEntityAvailable,
102
150
  clearBinding,
103
151
  createBinding,
104
152
  };
@@ -8,9 +8,6 @@
8
8
  "keywords": [ "text" ],
9
9
  "textdomain": "default",
10
10
  "attributes": {
11
- "align": {
12
- "type": "string"
13
- },
14
11
  "content": {
15
12
  "type": "rich-text",
16
13
  "source": "rich-text",
@@ -58,6 +55,7 @@
58
55
  "typography": {
59
56
  "fontSize": true,
60
57
  "lineHeight": true,
58
+ "textAlign": true,
61
59
  "__experimentalFontFamily": true,
62
60
  "__experimentalTextDecoration": true,
63
61
  "__experimentalFontStyle": true,