@wordpress/block-library 9.36.1-next.8b30e05b0.0 → 9.37.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 (302) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/audio/index.js +1 -1
  3. package/build/audio/index.js.map +2 -2
  4. package/build/avatar/edit.js +13 -38
  5. package/build/avatar/edit.js.map +2 -2
  6. package/build/breadcrumbs/block.json +3 -2
  7. package/build/breadcrumbs/edit.js +18 -18
  8. package/build/breadcrumbs/edit.js.map +2 -2
  9. package/build/button/block.json +2 -3
  10. package/build/button/deprecated.js +203 -0
  11. package/build/button/deprecated.js.map +3 -3
  12. package/build/button/edit.js +13 -24
  13. package/build/button/edit.js.map +3 -3
  14. package/build/button/index.js +1 -1
  15. package/build/button/index.js.map +2 -2
  16. package/build/button/save.js +0 -2
  17. package/build/button/save.js.map +2 -2
  18. package/build/code/index.js +1 -1
  19. package/build/code/index.js.map +2 -2
  20. package/build/comments-pagination-numbers/block.json +7 -0
  21. package/build/cover/index.js +2 -2
  22. package/build/cover/index.js.map +2 -2
  23. package/build/details/index.js +1 -1
  24. package/build/details/index.js.map +2 -2
  25. package/build/file/index.js +1 -1
  26. package/build/file/index.js.map +2 -2
  27. package/build/freeform/block.json +0 -1
  28. package/build/gallery/edit.js +1 -1
  29. package/build/gallery/edit.js.map +2 -2
  30. package/build/gallery/transforms.js +7 -3
  31. package/build/gallery/transforms.js.map +2 -2
  32. package/build/heading/index.js +1 -1
  33. package/build/heading/index.js.map +2 -2
  34. package/build/html/modal.js +18 -7
  35. package/build/html/modal.js.map +2 -2
  36. package/build/image/index.js +1 -1
  37. package/build/image/index.js.map +2 -2
  38. package/build/index.js +9 -3
  39. package/build/index.js.map +2 -2
  40. package/build/list-item/index.js +1 -1
  41. package/build/list-item/index.js.map +2 -2
  42. package/build/math/edit.js +14 -2
  43. package/build/math/edit.js.map +2 -2
  44. package/build/media-text/index.js +1 -1
  45. package/build/media-text/index.js.map +2 -2
  46. package/build/more/index.js +1 -1
  47. package/build/more/index.js.map +2 -2
  48. package/build/navigation/block.json +3 -0
  49. package/build/navigation/edit/index.js +57 -83
  50. package/build/navigation/edit/index.js.map +3 -3
  51. package/build/navigation/edit/overlay-menu-preview-button.js +96 -0
  52. package/build/navigation/edit/overlay-menu-preview-button.js.map +7 -0
  53. package/build/navigation/edit/overlay-menu-preview-controls.js +90 -0
  54. package/build/navigation/edit/overlay-menu-preview-controls.js.map +7 -0
  55. package/build/navigation/edit/overlay-panel.js +86 -0
  56. package/build/navigation/edit/overlay-panel.js.map +7 -0
  57. package/build/navigation/edit/overlay-template-part-selector.js +163 -0
  58. package/build/navigation/edit/overlay-template-part-selector.js.map +7 -0
  59. package/build/navigation/edit/overlay-visibility-control.js +54 -0
  60. package/build/navigation/edit/overlay-visibility-control.js.map +7 -0
  61. package/build/navigation/edit/responsive-wrapper.js +14 -2
  62. package/build/navigation/edit/responsive-wrapper.js.map +2 -2
  63. package/build/navigation-link/edit.js +7 -21
  64. package/build/navigation-link/edit.js.map +2 -2
  65. package/build/navigation-link/index.js +1 -1
  66. package/build/navigation-link/index.js.map +2 -2
  67. package/build/navigation-link/shared/index.js +5 -2
  68. package/build/navigation-link/shared/index.js.map +2 -2
  69. package/build/navigation-link/shared/use-handle-link-change.js +84 -0
  70. package/build/navigation-link/shared/use-handle-link-change.js.map +7 -0
  71. package/build/navigation-submenu/index.js +1 -1
  72. package/build/navigation-submenu/index.js.map +2 -2
  73. package/build/page-list-item/edit.js +4 -3
  74. package/build/page-list-item/edit.js.map +2 -2
  75. package/build/paragraph/index.js +1 -1
  76. package/build/paragraph/index.js.map +2 -2
  77. package/build/post-title/edit.js +6 -4
  78. package/build/post-title/edit.js.map +2 -2
  79. package/build/preformatted/index.js +1 -1
  80. package/build/preformatted/index.js.map +2 -2
  81. package/build/pullquote/index.js +1 -1
  82. package/build/pullquote/index.js.map +2 -2
  83. package/build/query/edit/inspector-controls/author-control.js +0 -1
  84. package/build/query/edit/inspector-controls/author-control.js.map +2 -2
  85. package/build/query/edit/inspector-controls/format-controls.js +0 -1
  86. package/build/query/edit/inspector-controls/format-controls.js.map +2 -2
  87. package/build/query/edit/inspector-controls/parent-control.js +1 -2
  88. package/build/query/edit/inspector-controls/parent-control.js.map +2 -2
  89. package/build/query/edit/inspector-controls/taxonomy-controls.js +0 -1
  90. package/build/query/edit/inspector-controls/taxonomy-controls.js.map +2 -2
  91. package/build/search/index.js +1 -1
  92. package/build/search/index.js.map +2 -2
  93. package/build/social-link/index.js +1 -1
  94. package/build/social-link/index.js.map +2 -2
  95. package/build/template-part/edit/index.js +3 -31
  96. package/build/template-part/edit/index.js.map +2 -2
  97. package/build/template-part/edit/utils/get-template-part-icon.js +9 -4
  98. package/build/template-part/edit/utils/get-template-part-icon.js.map +3 -3
  99. package/build/template-part/edit/utils/hooks.js +3 -2
  100. package/build/template-part/edit/utils/hooks.js.map +2 -2
  101. package/build/terms-query/edit/inspector-controls/include-control.js +0 -1
  102. package/build/terms-query/edit/inspector-controls/include-control.js.map +2 -2
  103. package/build/utils/deprecated-text-align-attributes.js +70 -0
  104. package/build/utils/deprecated-text-align-attributes.js.map +7 -0
  105. package/build/utils/migrate-text-align.js +42 -0
  106. package/build/utils/migrate-text-align.js.map +7 -0
  107. package/build/verse/index.js +1 -1
  108. package/build/verse/index.js.map +2 -2
  109. package/build/video/index.js +1 -1
  110. package/build/video/index.js.map +2 -2
  111. package/build-module/audio/index.js +1 -1
  112. package/build-module/audio/index.js.map +2 -2
  113. package/build-module/avatar/edit.js +13 -38
  114. package/build-module/avatar/edit.js.map +2 -2
  115. package/build-module/breadcrumbs/block.json +3 -2
  116. package/build-module/breadcrumbs/edit.js +18 -18
  117. package/build-module/breadcrumbs/edit.js.map +2 -2
  118. package/build-module/button/block.json +2 -3
  119. package/build-module/button/deprecated.js +205 -1
  120. package/build-module/button/deprecated.js.map +2 -2
  121. package/build-module/button/edit.js +13 -25
  122. package/build-module/button/edit.js.map +2 -2
  123. package/build-module/button/index.js +1 -1
  124. package/build-module/button/index.js.map +2 -2
  125. package/build-module/button/save.js +0 -2
  126. package/build-module/button/save.js.map +2 -2
  127. package/build-module/code/index.js +1 -1
  128. package/build-module/code/index.js.map +2 -2
  129. package/build-module/comments-pagination-numbers/block.json +7 -0
  130. package/build-module/cover/index.js +2 -2
  131. package/build-module/cover/index.js.map +2 -2
  132. package/build-module/details/index.js +1 -1
  133. package/build-module/details/index.js.map +2 -2
  134. package/build-module/file/index.js +1 -1
  135. package/build-module/file/index.js.map +2 -2
  136. package/build-module/freeform/block.json +0 -1
  137. package/build-module/gallery/edit.js +1 -1
  138. package/build-module/gallery/edit.js.map +2 -2
  139. package/build-module/gallery/transforms.js +7 -3
  140. package/build-module/gallery/transforms.js.map +2 -2
  141. package/build-module/heading/index.js +1 -1
  142. package/build-module/heading/index.js.map +2 -2
  143. package/build-module/html/modal.js +19 -7
  144. package/build-module/html/modal.js.map +2 -2
  145. package/build-module/image/index.js +1 -1
  146. package/build-module/image/index.js.map +2 -2
  147. package/build-module/index.js +9 -3
  148. package/build-module/index.js.map +2 -2
  149. package/build-module/list-item/index.js +1 -1
  150. package/build-module/list-item/index.js.map +2 -2
  151. package/build-module/math/edit.js +15 -3
  152. package/build-module/math/edit.js.map +2 -2
  153. package/build-module/media-text/index.js +1 -1
  154. package/build-module/media-text/index.js.map +2 -2
  155. package/build-module/more/index.js +1 -1
  156. package/build-module/more/index.js.map +2 -2
  157. package/build-module/navigation/block.json +3 -0
  158. package/build-module/navigation/edit/index.js +58 -88
  159. package/build-module/navigation/edit/index.js.map +2 -2
  160. package/build-module/navigation/edit/overlay-menu-preview-button.js +65 -0
  161. package/build-module/navigation/edit/overlay-menu-preview-button.js.map +7 -0
  162. package/build-module/navigation/edit/overlay-menu-preview-controls.js +64 -0
  163. package/build-module/navigation/edit/overlay-menu-preview-controls.js.map +7 -0
  164. package/build-module/navigation/edit/overlay-panel.js +58 -0
  165. package/build-module/navigation/edit/overlay-panel.js.map +7 -0
  166. package/build-module/navigation/edit/overlay-template-part-selector.js +142 -0
  167. package/build-module/navigation/edit/overlay-template-part-selector.js.map +7 -0
  168. package/build-module/navigation/edit/overlay-visibility-control.js +36 -0
  169. package/build-module/navigation/edit/overlay-visibility-control.js.map +7 -0
  170. package/build-module/navigation/edit/responsive-wrapper.js +14 -2
  171. package/build-module/navigation/edit/responsive-wrapper.js.map +2 -2
  172. package/build-module/navigation-link/edit.js +9 -23
  173. package/build-module/navigation-link/edit.js.map +2 -2
  174. package/build-module/navigation-link/index.js +1 -1
  175. package/build-module/navigation-link/index.js.map +2 -2
  176. package/build-module/navigation-link/shared/index.js +3 -1
  177. package/build-module/navigation-link/shared/index.js.map +2 -2
  178. package/build-module/navigation-link/shared/use-handle-link-change.js +59 -0
  179. package/build-module/navigation-link/shared/use-handle-link-change.js.map +7 -0
  180. package/build-module/navigation-submenu/index.js +1 -1
  181. package/build-module/navigation-submenu/index.js.map +2 -2
  182. package/build-module/page-list-item/edit.js +4 -3
  183. package/build-module/page-list-item/edit.js.map +2 -2
  184. package/build-module/paragraph/index.js +1 -1
  185. package/build-module/paragraph/index.js.map +2 -2
  186. package/build-module/post-title/edit.js +6 -4
  187. package/build-module/post-title/edit.js.map +2 -2
  188. package/build-module/preformatted/index.js +1 -1
  189. package/build-module/preformatted/index.js.map +2 -2
  190. package/build-module/pullquote/index.js +1 -1
  191. package/build-module/pullquote/index.js.map +2 -2
  192. package/build-module/query/edit/inspector-controls/author-control.js +0 -1
  193. package/build-module/query/edit/inspector-controls/author-control.js.map +2 -2
  194. package/build-module/query/edit/inspector-controls/format-controls.js +0 -1
  195. package/build-module/query/edit/inspector-controls/format-controls.js.map +2 -2
  196. package/build-module/query/edit/inspector-controls/parent-control.js +1 -2
  197. package/build-module/query/edit/inspector-controls/parent-control.js.map +2 -2
  198. package/build-module/query/edit/inspector-controls/taxonomy-controls.js +0 -1
  199. package/build-module/query/edit/inspector-controls/taxonomy-controls.js.map +2 -2
  200. package/build-module/search/index.js +1 -1
  201. package/build-module/search/index.js.map +2 -2
  202. package/build-module/social-link/index.js +1 -1
  203. package/build-module/social-link/index.js.map +2 -2
  204. package/build-module/template-part/edit/index.js +3 -31
  205. package/build-module/template-part/edit/index.js.map +2 -2
  206. package/build-module/template-part/edit/utils/get-template-part-icon.js +10 -4
  207. package/build-module/template-part/edit/utils/get-template-part-icon.js.map +2 -2
  208. package/build-module/template-part/edit/utils/hooks.js +3 -2
  209. package/build-module/template-part/edit/utils/hooks.js.map +2 -2
  210. package/build-module/terms-query/edit/inspector-controls/include-control.js +0 -1
  211. package/build-module/terms-query/edit/inspector-controls/include-control.js.map +2 -2
  212. package/build-module/utils/deprecated-text-align-attributes.js +39 -0
  213. package/build-module/utils/deprecated-text-align-attributes.js.map +7 -0
  214. package/build-module/utils/migrate-text-align.js +21 -0
  215. package/build-module/utils/migrate-text-align.js.map +7 -0
  216. package/build-module/verse/index.js +1 -1
  217. package/build-module/verse/index.js.map +2 -2
  218. package/build-module/video/index.js +1 -1
  219. package/build-module/video/index.js.map +2 -2
  220. package/build-style/accordion-heading/style-rtl.css +2 -18
  221. package/build-style/accordion-heading/style.css +2 -18
  222. package/build-style/classic-rtl.css +24 -0
  223. package/build-style/classic.css +24 -0
  224. package/build-style/common-rtl.css +4 -4
  225. package/build-style/common.css +4 -0
  226. package/build-style/editor-rtl.css +5 -0
  227. package/build-style/editor.css +5 -0
  228. package/build-style/navigation/editor-rtl.css +5 -0
  229. package/build-style/navigation/editor.css +5 -0
  230. package/build-style/style-rtl.css +8 -22
  231. package/build-style/style.css +8 -18
  232. package/build-style/verse/style-rtl.css +2 -0
  233. package/build-style/verse/style.css +2 -0
  234. package/package.json +40 -38
  235. package/src/accordion-heading/style.scss +2 -30
  236. package/src/audio/index.js +1 -1
  237. package/src/avatar/edit.js +68 -83
  238. package/src/breadcrumbs/block.json +3 -2
  239. package/src/breadcrumbs/edit.js +18 -18
  240. package/src/breadcrumbs/index.php +38 -17
  241. package/src/button/block.json +2 -3
  242. package/src/button/deprecated.js +214 -0
  243. package/src/button/edit.js +2 -11
  244. package/src/button/index.js +1 -1
  245. package/src/button/save.js +0 -2
  246. package/src/classic.scss +38 -0
  247. package/src/code/index.js +1 -1
  248. package/src/comments-pagination-numbers/block.json +7 -0
  249. package/src/common.scss +4 -0
  250. package/src/cover/index.js +2 -2
  251. package/src/details/index.js +1 -1
  252. package/src/file/index.js +1 -1
  253. package/src/freeform/block.json +0 -1
  254. package/src/gallery/edit.js +4 -1
  255. package/src/gallery/transforms.js +6 -2
  256. package/src/heading/index.js +1 -1
  257. package/src/html/modal.js +39 -20
  258. package/src/image/index.js +1 -1
  259. package/src/index.js +9 -3
  260. package/src/list-item/index.js +1 -1
  261. package/src/math/edit.js +15 -3
  262. package/src/media-text/index.js +1 -1
  263. package/src/more/index.js +1 -1
  264. package/src/navigation/block.json +3 -0
  265. package/src/navigation/edit/index.js +77 -82
  266. package/src/navigation/edit/overlay-menu-preview-button.js +82 -0
  267. package/src/navigation/edit/overlay-menu-preview-controls.js +65 -0
  268. package/src/navigation/edit/overlay-panel.js +78 -0
  269. package/src/navigation/edit/overlay-template-part-selector.js +198 -0
  270. package/src/navigation/edit/overlay-visibility-control.js +40 -0
  271. package/src/navigation/edit/responsive-wrapper.js +16 -1
  272. package/src/navigation/edit/test/overlay-template-part-selector.js +461 -0
  273. package/src/navigation/editor.scss +5 -0
  274. package/src/navigation-link/edit.js +9 -26
  275. package/src/navigation-link/index.js +1 -1
  276. package/src/navigation-link/shared/index.js +1 -0
  277. package/src/navigation-link/shared/test/use-handle-link-change.test.js +804 -0
  278. package/src/navigation-link/shared/use-handle-link-change.js +91 -0
  279. package/src/navigation-submenu/index.js +1 -1
  280. package/src/page-list/index.php +3 -4
  281. package/src/page-list-item/edit.js +4 -3
  282. package/src/paragraph/index.js +1 -1
  283. package/src/post-title/edit.js +8 -4
  284. package/src/post-title/index.php +1 -1
  285. package/src/preformatted/index.js +1 -1
  286. package/src/pullquote/index.js +1 -1
  287. package/src/query/edit/inspector-controls/author-control.js +0 -1
  288. package/src/query/edit/inspector-controls/format-controls.js +0 -1
  289. package/src/query/edit/inspector-controls/parent-control.js +0 -1
  290. package/src/query/edit/inspector-controls/taxonomy-controls.js +0 -1
  291. package/src/search/index.js +1 -1
  292. package/src/social-link/index.js +1 -1
  293. package/src/template-part/edit/index.js +5 -41
  294. package/src/template-part/edit/utils/get-template-part-icon.js +23 -4
  295. package/src/template-part/edit/utils/hooks.js +10 -2
  296. package/src/terms-query/edit/inspector-controls/include-control.js +0 -1
  297. package/src/utils/deprecated-text-align-attributes.js +45 -0
  298. package/src/utils/migrate-text-align.js +22 -0
  299. package/src/verse/index.js +1 -1
  300. package/src/verse/style.scss +4 -0
  301. package/src/video/index.js +1 -1
  302. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,804 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import { renderHook } from '@testing-library/react';
9
+
10
+ // Mock the entire @wordpress/block-editor module
11
+ jest.mock( '@wordpress/block-editor', () => ( {
12
+ store: {},
13
+ } ) );
14
+
15
+ // Mock the entire @wordpress/core-data module
16
+ jest.mock( '@wordpress/core-data', () => ( {
17
+ store: {},
18
+ } ) );
19
+
20
+ // Mock useDispatch specifically to avoid needing to set up full data store
21
+ jest.mock( '@wordpress/data', () => ( {
22
+ useDispatch: jest.fn(),
23
+ createSelector: jest.fn( ( fn ) => fn ),
24
+ createRegistrySelector: jest.fn( ( fn ) => fn ),
25
+ createReduxStore: jest.fn( () => ( {} ) ),
26
+ combineReducers: jest.fn( ( reducers ) => ( state = {}, action ) => {
27
+ const newState = {};
28
+ Object.keys( reducers ).forEach( ( key ) => {
29
+ newState[ key ] = reducers[ key ]( state[ key ], action );
30
+ } );
31
+ return newState;
32
+ } ),
33
+ register: jest.fn(),
34
+ } ) );
35
+
36
+ /**
37
+ * WordPress dependencies
38
+ */
39
+ import { useDispatch } from '@wordpress/data';
40
+
41
+ /**
42
+ * Internal dependencies
43
+ */
44
+ import { useHandleLinkChange } from '../use-handle-link-change';
45
+ import { updateAttributes } from '../update-attributes';
46
+ import { useEntityBinding } from '../use-entity-binding';
47
+
48
+ // Mock internal dependencies
49
+ jest.mock( '../update-attributes' );
50
+ jest.mock( '../use-entity-binding' );
51
+
52
+ describe( 'useHandleLinkChange', () => {
53
+ let mockSetAttributes;
54
+ let mockUpdateBlockAttributes;
55
+ let mockCreateBinding;
56
+ let mockClearBinding;
57
+
58
+ const clientId = 'test-client-id';
59
+
60
+ beforeEach( () => {
61
+ // Reset mocks
62
+ mockSetAttributes = jest.fn();
63
+ mockUpdateBlockAttributes = jest.fn();
64
+ mockCreateBinding = jest.fn();
65
+ mockClearBinding = jest.fn();
66
+
67
+ // Mock useDispatch
68
+ useDispatch.mockReturnValue( {
69
+ updateBlockAttributes: mockUpdateBlockAttributes,
70
+ } );
71
+
72
+ // Mock useEntityBinding
73
+ useEntityBinding.mockReturnValue( {
74
+ hasUrlBinding: false,
75
+ createBinding: mockCreateBinding,
76
+ clearBinding: mockClearBinding,
77
+ } );
78
+
79
+ // Mock updateAttributes to return default values
80
+ updateAttributes.mockImplementation( ( attrs ) => ( {
81
+ isEntityLink: !! ( attrs.id && attrs.kind !== 'custom' ),
82
+ attributes: attrs,
83
+ } ) );
84
+ } );
85
+
86
+ afterEach( () => {
87
+ jest.clearAllMocks();
88
+ } );
89
+
90
+ describe( 'creating new entity links', () => {
91
+ it( 'should create binding for new page link', () => {
92
+ const attributes = {};
93
+
94
+ const { result } = renderHook( () =>
95
+ useHandleLinkChange( {
96
+ clientId,
97
+ attributes,
98
+ setAttributes: mockSetAttributes,
99
+ } )
100
+ );
101
+
102
+ const updatedLink = {
103
+ id: 123,
104
+ url: 'https://example.com/page',
105
+ title: 'Test Page',
106
+ kind: 'post-type',
107
+ type: 'page',
108
+ };
109
+
110
+ result.current( updatedLink );
111
+
112
+ expect( updateAttributes ).toHaveBeenCalledWith(
113
+ expect.objectContaining( {
114
+ url: 'https://example.com/page',
115
+ kind: 'post-type',
116
+ type: 'page',
117
+ id: 123,
118
+ title: 'Test Page',
119
+ } ),
120
+ mockSetAttributes,
121
+ attributes
122
+ );
123
+ expect( mockCreateBinding ).toHaveBeenCalledWith(
124
+ expect.objectContaining( {
125
+ id: 123,
126
+ kind: 'post-type',
127
+ type: 'page',
128
+ } )
129
+ );
130
+ } );
131
+
132
+ it( 'should create binding for new post link', () => {
133
+ const attributes = {};
134
+
135
+ const { result } = renderHook( () =>
136
+ useHandleLinkChange( {
137
+ clientId,
138
+ attributes,
139
+ setAttributes: mockSetAttributes,
140
+ } )
141
+ );
142
+
143
+ const updatedLink = {
144
+ id: 456,
145
+ url: 'https://example.com/post',
146
+ title: 'Test Post',
147
+ kind: 'post-type',
148
+ type: 'post',
149
+ };
150
+
151
+ result.current( updatedLink );
152
+
153
+ expect( updateAttributes ).toHaveBeenCalledWith(
154
+ expect.objectContaining( {
155
+ url: 'https://example.com/post',
156
+ kind: 'post-type',
157
+ type: 'post',
158
+ id: 456,
159
+ title: 'Test Post',
160
+ } ),
161
+ mockSetAttributes,
162
+ attributes
163
+ );
164
+ expect( mockCreateBinding ).toHaveBeenCalledWith(
165
+ expect.objectContaining( {
166
+ id: 456,
167
+ kind: 'post-type',
168
+ type: 'post',
169
+ } )
170
+ );
171
+ } );
172
+
173
+ it( 'should create binding for new taxonomy link', () => {
174
+ const attributes = {};
175
+
176
+ const { result } = renderHook( () =>
177
+ useHandleLinkChange( {
178
+ clientId,
179
+ attributes,
180
+ setAttributes: mockSetAttributes,
181
+ } )
182
+ );
183
+
184
+ const updatedLink = {
185
+ id: 789,
186
+ url: 'https://example.com/category/news',
187
+ title: 'News',
188
+ kind: 'taxonomy',
189
+ type: 'category',
190
+ };
191
+
192
+ result.current( updatedLink );
193
+
194
+ expect( updateAttributes ).toHaveBeenCalledWith(
195
+ expect.objectContaining( {
196
+ url: 'https://example.com/category/news',
197
+ kind: 'taxonomy',
198
+ type: 'category',
199
+ id: 789,
200
+ title: 'News',
201
+ } ),
202
+ mockSetAttributes,
203
+ attributes
204
+ );
205
+ expect( mockCreateBinding ).toHaveBeenCalledWith(
206
+ expect.objectContaining( {
207
+ id: 789,
208
+ kind: 'taxonomy',
209
+ type: 'category',
210
+ } )
211
+ );
212
+ } );
213
+ } );
214
+
215
+ describe( 'creating new custom links', () => {
216
+ it( 'should create new custom URL link with correct attributes', () => {
217
+ updateAttributes.mockImplementation( ( attrs ) => ( {
218
+ isEntityLink: false,
219
+ attributes: attrs,
220
+ } ) );
221
+
222
+ const attributes = {};
223
+
224
+ const { result } = renderHook( () =>
225
+ useHandleLinkChange( {
226
+ clientId,
227
+ attributes,
228
+ setAttributes: mockSetAttributes,
229
+ } )
230
+ );
231
+
232
+ const updatedLink = {
233
+ url: 'https://custom-url.com',
234
+ title: 'Custom Link',
235
+ };
236
+
237
+ result.current( updatedLink );
238
+
239
+ expect( updateAttributes ).toHaveBeenCalledWith(
240
+ expect.objectContaining( {
241
+ url: 'https://custom-url.com',
242
+ title: 'Custom Link',
243
+ kind: undefined,
244
+ type: undefined,
245
+ id: undefined,
246
+ } ),
247
+ mockSetAttributes,
248
+ attributes
249
+ );
250
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
251
+ } );
252
+
253
+ it( 'should create mailto link with correct attributes', () => {
254
+ updateAttributes.mockImplementation( ( attrs ) => ( {
255
+ isEntityLink: false,
256
+ attributes: { ...attrs, kind: 'custom' },
257
+ } ) );
258
+
259
+ const attributes = {};
260
+
261
+ const { result } = renderHook( () =>
262
+ useHandleLinkChange( {
263
+ clientId,
264
+ attributes,
265
+ setAttributes: mockSetAttributes,
266
+ } )
267
+ );
268
+
269
+ const updatedLink = {
270
+ id: 'mailto:test@example.com',
271
+ url: 'mailto:test@example.com',
272
+ title: 'mailto:test@example.com',
273
+ type: 'mailto',
274
+ };
275
+
276
+ result.current( updatedLink );
277
+
278
+ expect( updateAttributes ).toHaveBeenCalledWith(
279
+ expect.objectContaining( {
280
+ id: 'mailto:test@example.com',
281
+ url: 'mailto:test@example.com',
282
+ title: 'mailto:test@example.com',
283
+ type: 'mailto',
284
+ kind: undefined,
285
+ } ),
286
+ mockSetAttributes,
287
+ attributes
288
+ );
289
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
290
+ } );
291
+
292
+ it( 'should create tel link with correct attributes', () => {
293
+ updateAttributes.mockImplementation( ( attrs ) => ( {
294
+ isEntityLink: false,
295
+ attributes: { ...attrs, kind: 'custom' },
296
+ } ) );
297
+
298
+ const attributes = {};
299
+
300
+ const { result } = renderHook( () =>
301
+ useHandleLinkChange( {
302
+ clientId,
303
+ attributes,
304
+ setAttributes: mockSetAttributes,
305
+ } )
306
+ );
307
+
308
+ const updatedLink = {
309
+ id: 'tel:5555555',
310
+ url: 'tel:5555555',
311
+ title: 'tel:5555555',
312
+ type: 'tel',
313
+ };
314
+
315
+ result.current( updatedLink );
316
+
317
+ expect( updateAttributes ).toHaveBeenCalledWith(
318
+ expect.objectContaining( {
319
+ id: 'tel:5555555',
320
+ url: 'tel:5555555',
321
+ title: 'tel:5555555',
322
+ type: 'tel',
323
+ kind: undefined,
324
+ } ),
325
+ mockSetAttributes,
326
+ attributes
327
+ );
328
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
329
+ } );
330
+ } );
331
+
332
+ describe( 'transitioning from entity to custom link', () => {
333
+ it( 'should use direct store dispatch when converting bound entity to custom link', () => {
334
+ // Mock that we have a URL binding
335
+ useEntityBinding.mockReturnValue( {
336
+ hasUrlBinding: true,
337
+ createBinding: mockCreateBinding,
338
+ clearBinding: mockClearBinding,
339
+ } );
340
+
341
+ const attributes = {
342
+ id: 123,
343
+ url: 'https://example.com/page',
344
+ kind: 'post-type',
345
+ type: 'page',
346
+ };
347
+
348
+ const { result } = renderHook( () =>
349
+ useHandleLinkChange( {
350
+ clientId,
351
+ attributes,
352
+ setAttributes: mockSetAttributes,
353
+ } )
354
+ );
355
+
356
+ // Convert to custom link (no id)
357
+ const updatedLink = {
358
+ url: 'https://custom-url.com',
359
+ title: 'Custom URL',
360
+ };
361
+
362
+ result.current( updatedLink );
363
+
364
+ // Should clear binding first
365
+ expect( mockClearBinding ).toHaveBeenCalled();
366
+
367
+ // Should use direct store dispatch instead of setAttributes
368
+ expect( mockUpdateBlockAttributes ).toHaveBeenCalledWith(
369
+ clientId,
370
+ {
371
+ url: 'https://custom-url.com',
372
+ kind: 'custom',
373
+ type: 'custom',
374
+ id: undefined,
375
+ }
376
+ );
377
+
378
+ // Should NOT call updateAttributes in this path
379
+ expect( updateAttributes ).not.toHaveBeenCalled();
380
+ } );
381
+
382
+ it( 'should handle transition from bound page link to custom URL', () => {
383
+ useEntityBinding.mockReturnValue( {
384
+ hasUrlBinding: true,
385
+ createBinding: mockCreateBinding,
386
+ clearBinding: mockClearBinding,
387
+ } );
388
+
389
+ const attributes = {
390
+ id: 456,
391
+ url: 'https://example.com/my-page',
392
+ label: 'My Page',
393
+ kind: 'post-type',
394
+ type: 'page',
395
+ };
396
+
397
+ const { result } = renderHook( () =>
398
+ useHandleLinkChange( {
399
+ clientId,
400
+ attributes,
401
+ setAttributes: mockSetAttributes,
402
+ } )
403
+ );
404
+
405
+ const updatedLink = {
406
+ url: 'https://external-site.com',
407
+ };
408
+
409
+ result.current( updatedLink );
410
+
411
+ expect( mockClearBinding ).toHaveBeenCalled();
412
+ expect( mockUpdateBlockAttributes ).toHaveBeenCalledWith(
413
+ clientId,
414
+ expect.objectContaining( {
415
+ url: 'https://external-site.com',
416
+ kind: 'custom',
417
+ type: 'custom',
418
+ id: undefined,
419
+ } )
420
+ );
421
+ } );
422
+ } );
423
+
424
+ describe( 'updating existing links', () => {
425
+ it( 'should update entity link to another entity link', () => {
426
+ const attributes = {
427
+ id: 123,
428
+ url: 'https://example.com/page-1',
429
+ label: 'Page 1',
430
+ kind: 'post-type',
431
+ type: 'page',
432
+ };
433
+
434
+ const { result } = renderHook( () =>
435
+ useHandleLinkChange( {
436
+ clientId,
437
+ attributes,
438
+ setAttributes: mockSetAttributes,
439
+ } )
440
+ );
441
+
442
+ const updatedLink = {
443
+ id: 456,
444
+ url: 'https://example.com/page-2',
445
+ title: 'Page 2',
446
+ kind: 'post-type',
447
+ type: 'page',
448
+ };
449
+
450
+ result.current( updatedLink );
451
+
452
+ expect( updateAttributes ).toHaveBeenCalledWith(
453
+ expect.objectContaining( {
454
+ id: 456,
455
+ url: 'https://example.com/page-2',
456
+ kind: 'post-type',
457
+ type: 'page',
458
+ } ),
459
+ mockSetAttributes,
460
+ attributes
461
+ );
462
+ expect( mockCreateBinding ).toHaveBeenCalled();
463
+ } );
464
+
465
+ it( 'should preserve label when updating existing link with label', () => {
466
+ const attributes = {
467
+ id: 123,
468
+ url: 'https://example.com/page',
469
+ label: 'Custom Label',
470
+ kind: 'post-type',
471
+ type: 'page',
472
+ };
473
+
474
+ const { result } = renderHook( () =>
475
+ useHandleLinkChange( {
476
+ clientId,
477
+ attributes,
478
+ setAttributes: mockSetAttributes,
479
+ } )
480
+ );
481
+
482
+ const updatedLink = {
483
+ id: 456,
484
+ url: 'https://example.com/new-page',
485
+ title: 'New Page Title',
486
+ kind: 'post-type',
487
+ type: 'page',
488
+ };
489
+
490
+ result.current( updatedLink );
491
+
492
+ // Should not include title in attrs when label exists
493
+ expect( updateAttributes ).toHaveBeenCalledWith(
494
+ expect.not.objectContaining( {
495
+ title: 'New Page Title',
496
+ } ),
497
+ mockSetAttributes,
498
+ attributes
499
+ );
500
+ } );
501
+
502
+ it( 'should include title when creating link without existing label', () => {
503
+ const attributes = {};
504
+
505
+ const { result } = renderHook( () =>
506
+ useHandleLinkChange( {
507
+ clientId,
508
+ attributes,
509
+ setAttributes: mockSetAttributes,
510
+ } )
511
+ );
512
+
513
+ const updatedLink = {
514
+ id: 123,
515
+ url: 'https://example.com/page',
516
+ title: 'Page Title',
517
+ kind: 'post-type',
518
+ type: 'page',
519
+ };
520
+
521
+ result.current( updatedLink );
522
+
523
+ expect( updateAttributes ).toHaveBeenCalledWith(
524
+ expect.objectContaining( {
525
+ title: 'Page Title',
526
+ } ),
527
+ mockSetAttributes,
528
+ attributes
529
+ );
530
+ } );
531
+
532
+ it( 'should include title when link has no URL (new link)', () => {
533
+ const attributes = {
534
+ label: '',
535
+ };
536
+
537
+ const { result } = renderHook( () =>
538
+ useHandleLinkChange( {
539
+ clientId,
540
+ attributes,
541
+ setAttributes: mockSetAttributes,
542
+ } )
543
+ );
544
+
545
+ const updatedLink = {
546
+ id: 123,
547
+ url: 'https://example.com/page',
548
+ title: 'Page Title',
549
+ kind: 'post-type',
550
+ type: 'page',
551
+ };
552
+
553
+ result.current( updatedLink );
554
+
555
+ expect( updateAttributes ).toHaveBeenCalledWith(
556
+ expect.objectContaining( {
557
+ title: 'Page Title',
558
+ } ),
559
+ mockSetAttributes,
560
+ attributes
561
+ );
562
+ } );
563
+
564
+ it( 'should update custom link to another custom link', () => {
565
+ updateAttributes.mockImplementation( ( attrs ) => ( {
566
+ isEntityLink: false,
567
+ attributes: { ...attrs, kind: 'custom' },
568
+ } ) );
569
+
570
+ const attributes = {
571
+ url: 'https://old-custom-url.com',
572
+ label: 'Old Custom Link',
573
+ kind: 'custom',
574
+ };
575
+
576
+ const { result } = renderHook( () =>
577
+ useHandleLinkChange( {
578
+ clientId,
579
+ attributes,
580
+ setAttributes: mockSetAttributes,
581
+ } )
582
+ );
583
+
584
+ const updatedLink = {
585
+ url: 'https://new-custom-url.com',
586
+ title: 'New Custom Link',
587
+ };
588
+
589
+ result.current( updatedLink );
590
+
591
+ expect( updateAttributes ).toHaveBeenCalledWith(
592
+ expect.objectContaining( {
593
+ url: 'https://new-custom-url.com',
594
+ kind: undefined,
595
+ type: undefined,
596
+ id: undefined,
597
+ } ),
598
+ mockSetAttributes,
599
+ attributes
600
+ );
601
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
602
+ } );
603
+
604
+ it( 'should update custom link to entity link', () => {
605
+ updateAttributes.mockImplementation( ( attrs ) => ( {
606
+ isEntityLink: true,
607
+ attributes: attrs,
608
+ } ) );
609
+
610
+ const attributes = {
611
+ url: 'https://custom-url.com',
612
+ label: 'Custom Link',
613
+ kind: 'custom',
614
+ };
615
+
616
+ const { result } = renderHook( () =>
617
+ useHandleLinkChange( {
618
+ clientId,
619
+ attributes,
620
+ setAttributes: mockSetAttributes,
621
+ } )
622
+ );
623
+
624
+ const updatedLink = {
625
+ id: 123,
626
+ url: 'https://example.com/page',
627
+ title: 'Page Title',
628
+ kind: 'post-type',
629
+ type: 'page',
630
+ };
631
+
632
+ result.current( updatedLink );
633
+
634
+ expect( updateAttributes ).toHaveBeenCalledWith(
635
+ expect.objectContaining( {
636
+ id: 123,
637
+ url: 'https://example.com/page',
638
+ kind: 'post-type',
639
+ type: 'page',
640
+ } ),
641
+ mockSetAttributes,
642
+ attributes
643
+ );
644
+ expect( mockCreateBinding ).toHaveBeenCalled();
645
+ } );
646
+ } );
647
+
648
+ describe( 'edge cases', () => {
649
+ it( 'should return early if updatedLink is null', () => {
650
+ const attributes = {};
651
+
652
+ const { result } = renderHook( () =>
653
+ useHandleLinkChange( {
654
+ clientId,
655
+ attributes,
656
+ setAttributes: mockSetAttributes,
657
+ } )
658
+ );
659
+
660
+ result.current( null );
661
+
662
+ expect( updateAttributes ).not.toHaveBeenCalled();
663
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
664
+ expect( mockClearBinding ).not.toHaveBeenCalled();
665
+ } );
666
+
667
+ it( 'should return early if updatedLink is undefined', () => {
668
+ const attributes = {};
669
+
670
+ const { result } = renderHook( () =>
671
+ useHandleLinkChange( {
672
+ clientId,
673
+ attributes,
674
+ setAttributes: mockSetAttributes,
675
+ } )
676
+ );
677
+
678
+ result.current( undefined );
679
+
680
+ expect( updateAttributes ).not.toHaveBeenCalled();
681
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
682
+ expect( mockClearBinding ).not.toHaveBeenCalled();
683
+ } );
684
+
685
+ it( 'should handle link with only URL (no other properties)', () => {
686
+ updateAttributes.mockImplementation( ( attrs ) => ( {
687
+ isEntityLink: false,
688
+ attributes: { ...attrs, kind: 'custom' },
689
+ } ) );
690
+
691
+ const attributes = {};
692
+
693
+ const { result } = renderHook( () =>
694
+ useHandleLinkChange( {
695
+ clientId,
696
+ attributes,
697
+ setAttributes: mockSetAttributes,
698
+ } )
699
+ );
700
+
701
+ const updatedLink = {
702
+ url: 'https://example.com',
703
+ };
704
+
705
+ result.current( updatedLink );
706
+
707
+ expect( updateAttributes ).toHaveBeenCalledWith(
708
+ expect.objectContaining( {
709
+ url: 'https://example.com',
710
+ kind: undefined,
711
+ type: undefined,
712
+ id: undefined,
713
+ } ),
714
+ mockSetAttributes,
715
+ attributes
716
+ );
717
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
718
+ } );
719
+
720
+ it( 'should not create binding for custom link even with id', () => {
721
+ updateAttributes.mockImplementation( ( attrs ) => ( {
722
+ isEntityLink: false,
723
+ attributes: attrs,
724
+ } ) );
725
+
726
+ const attributes = {};
727
+
728
+ const { result } = renderHook( () =>
729
+ useHandleLinkChange( {
730
+ clientId,
731
+ attributes,
732
+ setAttributes: mockSetAttributes,
733
+ } )
734
+ );
735
+
736
+ const updatedLink = {
737
+ id: 123,
738
+ url: 'https://example.com/page',
739
+ title: 'Page',
740
+ kind: 'custom',
741
+ type: 'custom',
742
+ };
743
+
744
+ result.current( updatedLink );
745
+
746
+ expect( updateAttributes ).toHaveBeenCalledWith(
747
+ expect.objectContaining( {
748
+ id: 123,
749
+ url: 'https://example.com/page',
750
+ kind: 'custom',
751
+ type: 'custom',
752
+ } ),
753
+ mockSetAttributes,
754
+ attributes
755
+ );
756
+ expect( mockCreateBinding ).not.toHaveBeenCalled();
757
+ } );
758
+ } );
759
+
760
+ describe( 'callback memoization', () => {
761
+ it( 'should return same callback reference when dependencies do not change', () => {
762
+ const attributes = { url: 'https://example.com' };
763
+
764
+ const { result, rerender } = renderHook( () =>
765
+ useHandleLinkChange( {
766
+ clientId,
767
+ attributes,
768
+ setAttributes: mockSetAttributes,
769
+ } )
770
+ );
771
+
772
+ const firstCallback = result.current;
773
+
774
+ rerender();
775
+
776
+ const secondCallback = result.current;
777
+
778
+ expect( firstCallback ).toBe( secondCallback );
779
+ } );
780
+
781
+ it( 'should return new callback reference when attributes change', () => {
782
+ let attributes = { url: 'https://example.com' };
783
+
784
+ const { result, rerender } = renderHook(
785
+ ( { attrs } ) =>
786
+ useHandleLinkChange( {
787
+ clientId,
788
+ attributes: attrs,
789
+ setAttributes: mockSetAttributes,
790
+ } ),
791
+ { initialProps: { attrs: attributes } }
792
+ );
793
+
794
+ const firstCallback = result.current;
795
+
796
+ attributes = { url: 'https://different.com' };
797
+ rerender( { attrs: attributes } );
798
+
799
+ const secondCallback = result.current;
800
+
801
+ expect( firstCallback ).not.toBe( secondCallback );
802
+ } );
803
+ } );
804
+ } );