@wordpress/block-editor 15.9.1-next.8b30e05b0.0 → 15.10.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 (196) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +8 -0
  3. package/build/components/block-alignment-matrix-control/index.js +1 -8
  4. package/build/components/block-alignment-matrix-control/index.js.map +2 -2
  5. package/build/components/block-bindings/attribute-control.js +172 -0
  6. package/build/components/block-bindings/attribute-control.js.map +7 -0
  7. package/build/components/block-bindings/index.js +47 -0
  8. package/build/components/block-bindings/index.js.map +7 -0
  9. package/build/components/block-bindings/source-fields-list.js +135 -0
  10. package/build/components/block-bindings/source-fields-list.js.map +7 -0
  11. package/build/components/block-bindings/use-block-bindings-utils.js +66 -0
  12. package/build/components/block-bindings/use-block-bindings-utils.js.map +7 -0
  13. package/build/components/block-edit/edit.js +1 -3
  14. package/build/components/block-edit/edit.js.map +2 -2
  15. package/build/components/block-inspector/edit-contents.js +93 -14
  16. package/build/components/block-inspector/edit-contents.js.map +3 -3
  17. package/build/components/block-inspector/index.js +44 -28
  18. package/build/components/block-inspector/index.js.map +2 -2
  19. package/build/components/block-settings-menu-controls/edit-section-menu-item.js +39 -9
  20. package/build/components/block-settings-menu-controls/edit-section-menu-item.js.map +3 -3
  21. package/build/components/block-styles/preview-panel.js +3 -5
  22. package/build/components/block-styles/preview-panel.js.map +2 -2
  23. package/build/components/block-styles/use-styles-for-block.js +2 -2
  24. package/build/components/block-styles/use-styles-for-block.js.map +2 -2
  25. package/build/components/block-toolbar/index.js +1 -8
  26. package/build/components/block-toolbar/index.js.map +3 -3
  27. package/build/components/content-only-controls/index.js +2 -25
  28. package/build/components/content-only-controls/index.js.map +2 -2
  29. package/build/components/content-only-controls/link/index.js +3 -3
  30. package/build/components/content-only-controls/link/index.js.map +2 -2
  31. package/build/components/content-only-controls/media/index.js +3 -3
  32. package/build/components/content-only-controls/media/index.js.map +2 -2
  33. package/build/components/content-only-controls/rich-text/index.js +3 -2
  34. package/build/components/content-only-controls/rich-text/index.js.map +2 -2
  35. package/build/components/dimensions-tool/width-height-tool.js +4 -16
  36. package/build/components/dimensions-tool/width-height-tool.js.map +3 -3
  37. package/build/components/grid/grid-item-resizer.js +9 -5
  38. package/build/components/grid/grid-item-resizer.js.map +2 -2
  39. package/build/components/image-editor/cropper.js +3 -34
  40. package/build/components/image-editor/cropper.js.map +3 -3
  41. package/build/components/image-editor/index.js +9 -3
  42. package/build/components/image-editor/index.js.map +2 -2
  43. package/build/components/image-editor/use-transform-image.js +62 -32
  44. package/build/components/image-editor/use-transform-image.js.map +2 -2
  45. package/build/components/image-editor/zoom-dropdown.js +2 -2
  46. package/build/components/image-editor/zoom-dropdown.js.map +2 -2
  47. package/build/components/index.js +7 -3
  48. package/build/components/index.js.map +2 -2
  49. package/build/components/inserter/hooks/use-insertion-point.js +5 -2
  50. package/build/components/inserter/hooks/use-insertion-point.js.map +2 -2
  51. package/build/components/inserter-draggable-blocks/index.js +8 -4
  52. package/build/components/inserter-draggable-blocks/index.js.map +2 -2
  53. package/build/components/inspector-controls-tabs/content-tab.js +3 -2
  54. package/build/components/inspector-controls-tabs/content-tab.js.map +2 -2
  55. package/build/components/link-control/index.js +1 -1
  56. package/build/components/link-control/index.js.map +2 -2
  57. package/build/components/link-control/search-input.js +2 -2
  58. package/build/components/link-control/search-input.js.map +2 -2
  59. package/build/hooks/block-bindings.js +22 -260
  60. package/build/hooks/block-bindings.js.map +3 -3
  61. package/build/layouts/grid.js +23 -28
  62. package/build/layouts/grid.js.map +2 -2
  63. package/build/private-apis.js +1 -0
  64. package/build/private-apis.js.map +2 -2
  65. package/build/store/private-keys.js +3 -0
  66. package/build/store/private-keys.js.map +2 -2
  67. package/build/store/private-selectors.js +2 -1
  68. package/build/store/private-selectors.js.map +2 -2
  69. package/build/store/reducer.js +3 -2
  70. package/build/store/reducer.js.map +2 -2
  71. package/build/utils/block-bindings.js +2 -44
  72. package/build/utils/block-bindings.js.map +3 -3
  73. package/build/utils/index.js +2 -5
  74. package/build/utils/index.js.map +2 -2
  75. package/build-module/components/block-alignment-matrix-control/index.js +1 -8
  76. package/build-module/components/block-alignment-matrix-control/index.js.map +2 -2
  77. package/build-module/components/block-bindings/attribute-control.js +150 -0
  78. package/build-module/components/block-bindings/attribute-control.js.map +7 -0
  79. package/build-module/components/block-bindings/index.js +10 -0
  80. package/build-module/components/block-bindings/index.js.map +7 -0
  81. package/build-module/components/block-bindings/source-fields-list.js +104 -0
  82. package/build-module/components/block-bindings/source-fields-list.js.map +7 -0
  83. package/build-module/components/block-bindings/use-block-bindings-utils.js +45 -0
  84. package/build-module/components/block-bindings/use-block-bindings-utils.js.map +7 -0
  85. package/build-module/components/block-edit/edit.js +1 -3
  86. package/build-module/components/block-edit/edit.js.map +2 -2
  87. package/build-module/components/block-inspector/edit-contents.js +93 -14
  88. package/build-module/components/block-inspector/edit-contents.js.map +2 -2
  89. package/build-module/components/block-inspector/index.js +44 -28
  90. package/build-module/components/block-inspector/index.js.map +2 -2
  91. package/build-module/components/block-settings-menu-controls/edit-section-menu-item.js +39 -9
  92. package/build-module/components/block-settings-menu-controls/edit-section-menu-item.js.map +2 -2
  93. package/build-module/components/block-styles/preview-panel.js +3 -5
  94. package/build-module/components/block-styles/preview-panel.js.map +2 -2
  95. package/build-module/components/block-styles/use-styles-for-block.js +2 -2
  96. package/build-module/components/block-styles/use-styles-for-block.js.map +2 -2
  97. package/build-module/components/block-toolbar/index.js +1 -8
  98. package/build-module/components/block-toolbar/index.js.map +2 -2
  99. package/build-module/components/content-only-controls/index.js +2 -25
  100. package/build-module/components/content-only-controls/index.js.map +2 -2
  101. package/build-module/components/content-only-controls/link/index.js +3 -3
  102. package/build-module/components/content-only-controls/link/index.js.map +2 -2
  103. package/build-module/components/content-only-controls/media/index.js +3 -3
  104. package/build-module/components/content-only-controls/media/index.js.map +2 -2
  105. package/build-module/components/content-only-controls/rich-text/index.js +3 -2
  106. package/build-module/components/content-only-controls/rich-text/index.js.map +2 -2
  107. package/build-module/components/dimensions-tool/width-height-tool.js +4 -6
  108. package/build-module/components/dimensions-tool/width-height-tool.js.map +2 -2
  109. package/build-module/components/grid/grid-item-resizer.js +9 -5
  110. package/build-module/components/grid/grid-item-resizer.js.map +2 -2
  111. package/build-module/components/image-editor/cropper.js +3 -34
  112. package/build-module/components/image-editor/cropper.js.map +2 -2
  113. package/build-module/components/image-editor/index.js +9 -3
  114. package/build-module/components/image-editor/index.js.map +2 -2
  115. package/build-module/components/image-editor/use-transform-image.js +63 -33
  116. package/build-module/components/image-editor/use-transform-image.js.map +2 -2
  117. package/build-module/components/image-editor/zoom-dropdown.js +2 -2
  118. package/build-module/components/image-editor/zoom-dropdown.js.map +2 -2
  119. package/build-module/components/index.js +74 -68
  120. package/build-module/components/index.js.map +2 -2
  121. package/build-module/components/inserter/hooks/use-insertion-point.js +5 -2
  122. package/build-module/components/inserter/hooks/use-insertion-point.js.map +2 -2
  123. package/build-module/components/inserter-draggable-blocks/index.js +8 -4
  124. package/build-module/components/inserter-draggable-blocks/index.js.map +2 -2
  125. package/build-module/components/inspector-controls-tabs/content-tab.js +3 -2
  126. package/build-module/components/inspector-controls-tabs/content-tab.js.map +2 -2
  127. package/build-module/components/link-control/index.js +1 -1
  128. package/build-module/components/link-control/index.js.map +2 -2
  129. package/build-module/components/link-control/search-input.js +2 -2
  130. package/build-module/components/link-control/search-input.js.map +2 -2
  131. package/build-module/hooks/block-bindings.js +27 -270
  132. package/build-module/hooks/block-bindings.js.map +2 -2
  133. package/build-module/layouts/grid.js +23 -28
  134. package/build-module/layouts/grid.js.map +2 -2
  135. package/build-module/private-apis.js +3 -1
  136. package/build-module/private-apis.js.map +2 -2
  137. package/build-module/store/private-keys.js +2 -0
  138. package/build-module/store/private-keys.js.map +2 -2
  139. package/build-module/store/private-selectors.js +4 -2
  140. package/build-module/store/private-selectors.js.map +2 -2
  141. package/build-module/store/reducer.js +4 -3
  142. package/build-module/store/reducer.js.map +2 -2
  143. package/build-module/utils/block-bindings.js +1 -42
  144. package/build-module/utils/block-bindings.js.map +2 -2
  145. package/build-module/utils/index.js +1 -3
  146. package/build-module/utils/index.js.map +2 -2
  147. package/build-style/style-rtl.css +6 -6
  148. package/build-style/style.css +6 -6
  149. package/package.json +39 -40
  150. package/src/components/block-alignment-matrix-control/index.js +1 -5
  151. package/src/components/block-bindings/attribute-control.js +174 -0
  152. package/src/components/block-bindings/index.js +6 -0
  153. package/src/components/block-bindings/source-fields-list.js +130 -0
  154. package/src/components/block-bindings/use-block-bindings-utils.js +156 -0
  155. package/src/components/block-edit/edit.js +1 -3
  156. package/src/components/block-inspector/edit-contents.js +108 -18
  157. package/src/components/block-inspector/index.js +53 -30
  158. package/src/components/block-settings-menu-controls/edit-section-menu-item.js +50 -6
  159. package/src/components/block-styles/preview-panel.js +3 -5
  160. package/src/components/block-styles/use-styles-for-block.js +2 -2
  161. package/src/components/block-toolbar/index.js +1 -6
  162. package/src/components/block-toolbar/style.scss +6 -6
  163. package/src/components/content-only-controls/index.js +2 -27
  164. package/src/components/content-only-controls/link/index.js +3 -3
  165. package/src/components/content-only-controls/media/index.js +3 -3
  166. package/src/components/content-only-controls/rich-text/index.js +3 -2
  167. package/src/components/dimensions-tool/width-height-tool.js +6 -13
  168. package/src/components/grid/grid-item-resizer.js +18 -5
  169. package/src/components/image-editor/cropper.js +3 -32
  170. package/src/components/image-editor/index.js +34 -29
  171. package/src/components/image-editor/use-transform-image.js +80 -34
  172. package/src/components/image-editor/zoom-dropdown.js +2 -2
  173. package/src/components/index.js +5 -1
  174. package/src/components/inserter/hooks/use-insertion-point.js +3 -0
  175. package/src/components/inserter/style.scss +1 -1
  176. package/src/components/inserter-draggable-blocks/index.js +19 -8
  177. package/src/components/inspector-controls-tabs/content-tab.js +6 -2
  178. package/src/components/link-control/index.js +1 -1
  179. package/src/components/link-control/search-input.js +8 -2
  180. package/src/components/link-control/test/index.js +146 -7
  181. package/src/hooks/block-bindings.js +27 -347
  182. package/src/layouts/grid.js +40 -72
  183. package/src/layouts/test/grid.js +14 -0
  184. package/src/private-apis.js +2 -0
  185. package/src/store/private-keys.js +1 -0
  186. package/src/store/private-selectors.js +8 -1
  187. package/src/store/reducer.js +10 -3
  188. package/src/utils/block-bindings.js +0 -157
  189. package/src/utils/index.js +0 -1
  190. package/tsconfig.json +1 -0
  191. package/build/components/block-toolbar/block-name-context.js +0 -30
  192. package/build/components/block-toolbar/block-name-context.js.map +0 -7
  193. package/build-module/components/block-toolbar/block-name-context.js +0 -9
  194. package/build-module/components/block-toolbar/block-name-context.js.map +0 -7
  195. package/src/components/block-toolbar/block-name-context.js +0 -9
  196. /package/src/{utils → components/block-bindings}/test/use-block-bindings-utils.js +0 -0
@@ -15,14 +15,18 @@ const ContentTab = ( { rootClientId, contentClientIds } ) => {
15
15
  return null;
16
16
  }
17
17
 
18
+ const shouldShowContentOnlyControls =
19
+ window?.__experimentalContentOnlyPatternInsertion &&
20
+ window?.__experimentalContentOnlyInspectorFields;
21
+
18
22
  return (
19
23
  <>
20
- { ! window?.__experimentalContentOnlyPatternInsertion && (
24
+ { ! shouldShowContentOnlyControls && (
21
25
  <PanelBody title={ __( 'Content' ) }>
22
26
  <BlockQuickNavigation clientIds={ contentClientIds } />
23
27
  </PanelBody>
24
28
  ) }
25
- { window?.__experimentalContentOnlyPatternInsertion && (
29
+ { shouldShowContentOnlyControls && (
26
30
  <ContentOnlyControls rootClientId={ rootClientId } />
27
31
  ) }
28
32
  </>
@@ -23,6 +23,7 @@ import { isShallowEqualObjects } from '@wordpress/is-shallow-equal';
23
23
  import { useSelect, useDispatch } from '@wordpress/data';
24
24
  import { store as preferencesStore } from '@wordpress/preferences';
25
25
  import { keyboardReturn, linkOff } from '@wordpress/icons';
26
+ import deprecated from '@wordpress/deprecated';
26
27
 
27
28
  /**
28
29
  * Internal dependencies
@@ -35,7 +36,6 @@ import useCreatePage from './use-create-page';
35
36
  import useInternalValue from './use-internal-value';
36
37
  import { ViewerFill } from './viewer-slot';
37
38
  import { DEFAULT_LINK_SETTINGS } from './constants';
38
- import deprecated from '@wordpress/deprecated';
39
39
 
40
40
  /**
41
41
  * Default properties associated with a link control value.
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { forwardRef, useState } from '@wordpress/element';
5
5
  import { __ } from '@wordpress/i18n';
6
+ import deprecated from '@wordpress/deprecated';
6
7
 
7
8
  /**
8
9
  * Internal dependencies
@@ -11,7 +12,6 @@ import { URLInput } from '../';
11
12
  import LinkControlSearchResults from './search-results';
12
13
  import { CREATE_TYPE } from './constants';
13
14
  import useSearchHandler from './use-search-handler';
14
- import deprecated from '@wordpress/deprecated';
15
15
 
16
16
  // Must be a function as otherwise URLInput will default
17
17
  // to the fetchLinkSuggestions passed in block editor settings
@@ -106,7 +106,13 @@ const LinkControlSearchInput = forwardRef(
106
106
  allowDirectEntry ||
107
107
  ( suggestion && Object.keys( suggestion ).length >= 1 )
108
108
  ) {
109
- const { id, url, ...restLinkProps } = currentLink ?? {};
109
+ // Strip out id, url, kind, and type from the current link to prevent
110
+ // entity metadata from persisting when switching to a different link type.
111
+ // For example, when changing from an entity link (kind: 'post-type', type: 'page')
112
+ // to a custom URL (type: 'link', no kind), we need to ensure the old 'kind'
113
+ // doesn't carry over. We do want to preserve other properites like title, though.
114
+ const { id, url, kind, type, ...restLinkProps } =
115
+ currentLink ?? {};
110
116
  onSelect(
111
117
  // Some direct entries don't have types or IDs, and we still need to clear the previous ones.
112
118
  { ...restLinkProps, ...suggestion },
@@ -1621,7 +1621,7 @@ describe( 'Selecting links', () => {
1621
1621
  id: '1',
1622
1622
  title: 'https://www.wordpress.org',
1623
1623
  url: 'https://www.wordpress.org',
1624
- type: 'URL',
1624
+ type: 'link',
1625
1625
  },
1626
1626
  ], // Url.
1627
1627
  ] )(
@@ -1683,7 +1683,7 @@ describe( 'Selecting links', () => {
1683
1683
  id: '1',
1684
1684
  title: 'https://www.wordpress.org',
1685
1685
  url: 'https://www.wordpress.org',
1686
- type: 'URL',
1686
+ type: 'link',
1687
1687
  },
1688
1688
  ], // Url.
1689
1689
  ] )(
@@ -2135,7 +2135,7 @@ describe( 'Rich link previews', () => {
2135
2135
  id: '1',
2136
2136
  title: 'WordPress.org', // Customize this for differentiation in assertions.
2137
2137
  url: 'https://www.wordpress.org',
2138
- type: 'URL',
2138
+ type: 'link',
2139
2139
  };
2140
2140
 
2141
2141
  beforeAll( () => {
@@ -2800,14 +2800,153 @@ describe( 'Entity handling', () => {
2800
2800
  } );
2801
2801
  await user.click( applyButton );
2802
2802
 
2803
- // Verify that onChange was called with the entity link severed
2804
- // id, kind, and type should be undefined to indicate it's no longer an entity
2803
+ // Verify that onChange was called with entity metadata cleared.
2804
+ // Kind should be undefined (no longer an entity).
2805
+ // Note: Currently when clicking Apply (vs selecting a suggestion),
2806
+ // type and id are also undefined - that's a separate issue with the
2807
+ // TODO: Apply button handler not processing URLs through handleDirectEntry,
2808
+ // so the shape of the data for a custom link can be different depending on
2809
+ // how it was submitted.
2805
2810
  expect( onChange ).toHaveBeenCalledWith(
2806
2811
  expect.objectContaining( {
2807
- url: customUrl,
2808
- id: undefined,
2812
+ url: 'www.wordpress.org',
2809
2813
  kind: undefined,
2814
+ } )
2815
+ );
2816
+ } );
2817
+
2818
+ it( 'should clear entity metadata (type/kind) when changing from page link to custom link via suggestion', async () => {
2819
+ const user = userEvent.setup();
2820
+
2821
+ // Start with an entity link that has type and kind
2822
+ const pageLink = {
2823
+ id: 123,
2824
+ url: 'https://example.com/page',
2825
+ title: 'Test Page',
2826
+ type: 'page',
2827
+ kind: 'post-type',
2828
+ };
2829
+
2830
+ const onChange = jest.fn();
2831
+
2832
+ // Mock search suggestions to return a custom URL
2833
+ // URL suggestions have an id and type but no 'kind' (which indicates entity metadata)
2834
+ mockFetchSearchSuggestions.mockImplementation( ( searchTerm ) => {
2835
+ const suggestions = [
2836
+ {
2837
+ id: uniqueId(),
2838
+ title: searchTerm,
2839
+ url: searchTerm,
2840
+ type: 'link', // URL suggestions have type 'link'
2841
+ // Importantly: no 'kind' property (entities have kind)
2842
+ },
2843
+ ];
2844
+ return Promise.resolve( suggestions );
2845
+ } );
2846
+
2847
+ render(
2848
+ <LinkControl
2849
+ value={ pageLink }
2850
+ handleEntities
2851
+ forceIsEditingLink
2852
+ onChange={ onChange }
2853
+ />
2854
+ );
2855
+
2856
+ const searchInput = screen.getByRole( 'combobox', {
2857
+ name: 'Search or type URL',
2858
+ } );
2859
+
2860
+ // Initially should be disabled because it's an entity
2861
+ expect( searchInput ).toBeDisabled();
2862
+
2863
+ // Click the unsync button to enable editing
2864
+ const unlinkButton = screen.getByRole( 'button', {
2865
+ name: 'Unsync and edit',
2866
+ } );
2867
+ await user.click( unlinkButton );
2868
+
2869
+ // Input should now be enabled and value should be cleared
2870
+ expect( searchInput ).toBeEnabled();
2871
+ expect( searchInput ).toHaveValue( '' );
2872
+
2873
+ // Type a custom URL
2874
+ await user.type( searchInput, 'https://custom-url.com' );
2875
+
2876
+ // Wait for suggestions to appear
2877
+ const suggestionsList = await screen.findByRole( 'listbox' );
2878
+ expect( suggestionsList ).toBeVisible();
2879
+
2880
+ // Select the custom URL suggestion (not clicking Apply button)
2881
+ const urlSuggestion = screen.getByRole( 'option', {
2882
+ name: /https:\/\/custom-url\.com/,
2883
+ } );
2884
+ await user.click( urlSuggestion );
2885
+
2886
+ // Verify that onChange was called with id, type and kind explicitly set to undefined
2887
+ // This is the critical fix - when selecting a custom URL suggestion after unlinking,
2888
+ // entity metadata (type/kind) should be cleared (not just when using the Apply button)
2889
+ expect( onChange ).toHaveBeenCalledWith(
2890
+ expect.objectContaining( {
2891
+ url: 'https://custom-url.com',
2892
+ type: 'link',
2893
+ kind: undefined,
2894
+ } )
2895
+ );
2896
+ } );
2897
+
2898
+ it( 'should clear entity metadata when pressing Enter for direct entry (without clicking suggestion)', async () => {
2899
+ const user = userEvent.setup();
2900
+ const onChange = jest.fn();
2901
+
2902
+ const pageLink = {
2903
+ id: 123,
2904
+ url: 'https://example.com/page',
2905
+ title: 'Test Page',
2906
+ type: 'page',
2907
+ kind: 'post-type',
2908
+ };
2909
+
2910
+ render(
2911
+ <LinkControl
2912
+ value={ pageLink }
2913
+ handleEntities
2914
+ forceIsEditingLink
2915
+ onChange={ onChange }
2916
+ />
2917
+ );
2918
+
2919
+ const searchInput = screen.getByRole( 'combobox', {
2920
+ name: 'Search or type URL',
2921
+ } );
2922
+
2923
+ // Initially should be disabled because it's an entity
2924
+ expect( searchInput ).toBeDisabled();
2925
+
2926
+ // Click the unsync button to enable editing
2927
+ const unlinkButton = screen.getByRole( 'button', {
2928
+ name: 'Unsync and edit',
2929
+ } );
2930
+ await user.click( unlinkButton );
2931
+
2932
+ // Input should now be enabled and value should be cleared
2933
+ expect( searchInput ).toBeEnabled();
2934
+ expect( searchInput ).toHaveValue( '' );
2935
+
2936
+ // Type a custom URL
2937
+ await user.type( searchInput, 'https://direct-entry.com' );
2938
+
2939
+ // Press Enter WITHOUT clicking the suggestion (direct entry path)
2940
+ triggerEnter( searchInput );
2941
+
2942
+ // Verify that onChange was called with type and kind explicitly set to undefined
2943
+ // This tests the direct entry path in onSubmit (lines 157-165 in search-input.js)
2944
+ // where the user types a URL and presses Enter without selecting from suggestions
2945
+ expect( onChange ).toHaveBeenCalledWith(
2946
+ expect.objectContaining( {
2947
+ url: 'https://direct-entry.com',
2810
2948
  type: undefined,
2949
+ kind: undefined,
2811
2950
  } )
2812
2951
  );
2813
2952
  } );
@@ -1,25 +1,12 @@
1
- /**
2
- * External dependencies
3
- */
4
- import fastDeepEqual from 'fast-deep-equal/es6';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
9
4
  import { __ } from '@wordpress/i18n';
10
- import {
11
- getBlockBindingsSource,
12
- getBlockType,
13
- store as blockStore,
14
- } from '@wordpress/blocks';
5
+ import { store as blocksStore } from '@wordpress/blocks';
15
6
  import {
16
7
  __experimentalItemGroup as ItemGroup,
17
- __experimentalItem as Item,
18
8
  __experimentalText as Text,
19
9
  __experimentalToolsPanel as ToolsPanel,
20
- __experimentalToolsPanelItem as ToolsPanelItem,
21
- __experimentalVStack as VStack,
22
- privateApis as componentsPrivateApis,
23
10
  } from '@wordpress/components';
24
11
  import { useSelect } from '@wordpress/data';
25
12
  import { useContext } from '@wordpress/element';
@@ -28,29 +15,15 @@ import { useViewportMatch } from '@wordpress/compose';
28
15
  /**
29
16
  * Internal dependencies
30
17
  */
31
- import { useBlockBindingsUtils } from '../utils/block-bindings';
18
+ import {
19
+ BlockBindingsAttributeControl,
20
+ useBlockBindingsUtils,
21
+ } from '../components/block-bindings';
32
22
  import { unlock } from '../lock-unlock';
33
23
  import InspectorControls from '../components/inspector-controls';
34
24
  import BlockContext from '../components/block-context';
35
- import { useBlockEditContext } from '../components/block-edit';
36
25
  import { store as blockEditorStore } from '../store';
37
26
 
38
- const { Menu } = unlock( componentsPrivateApis );
39
-
40
- /**
41
- * Get the normalized attribute type for block bindings.
42
- * Converts 'rich-text' to 'string' since rich-text is stored as string.
43
- *
44
- * @param {string} blockName The block name.
45
- * @param {string} attribute The attribute name.
46
- * @return {string} The normalized attribute type.
47
- */
48
- const getAttributeType = ( blockName, attribute ) => {
49
- const _attributeType =
50
- getBlockType( blockName ).attributes?.[ attribute ]?.type;
51
- return _attributeType === 'rich-text' ? 'string' : _attributeType;
52
- };
53
-
54
27
  const useToolsPanelDropdownMenuProps = () => {
55
28
  const isMobile = useViewportMatch( 'medium', '<' );
56
29
  return ! isMobile
@@ -64,295 +37,35 @@ const useToolsPanelDropdownMenuProps = () => {
64
37
  : {};
65
38
  };
66
39
 
67
- function BlockBindingsPanelMenuContent( { attribute, binding, sources } ) {
68
- const { clientId } = useBlockEditContext();
69
- const { updateBlockBindings } = useBlockBindingsUtils();
70
- const isMobile = useViewportMatch( 'medium', '<' );
71
- const blockContext = useContext( BlockContext );
72
- const { attributeType, select } = useSelect(
73
- ( _select ) => {
74
- const { name: blockName } =
75
- _select( blockEditorStore ).getBlock( clientId );
76
- return {
77
- attributeType: getAttributeType( blockName, attribute ),
78
- select: _select,
79
- };
80
- },
81
- [ clientId, attribute ]
82
- );
83
- return (
84
- <Menu placement={ isMobile ? 'bottom-start' : 'left-start' }>
85
- { Object.entries( sources ).map( ( [ sourceKey, data ] ) => {
86
- // Only show sources that have compatible data for this specific attribute.
87
- const sourceDataItems = data.filter(
88
- ( item ) => item.type === attributeType
89
- );
90
-
91
- const noItemsAvailable =
92
- ! sourceDataItems || sourceDataItems.length === 0;
93
-
94
- if ( noItemsAvailable ) {
95
- return null;
96
- }
97
-
98
- const source = getBlockBindingsSource( sourceKey );
99
-
100
- return (
101
- <Menu
102
- key={ sourceKey }
103
- placement={ isMobile ? 'bottom-start' : 'left-start' }
104
- >
105
- <Menu.SubmenuTriggerItem>
106
- <Menu.ItemLabel>{ source.label }</Menu.ItemLabel>
107
- </Menu.SubmenuTriggerItem>
108
- <Menu.Popover gutter={ 8 }>
109
- <Menu.Group>
110
- { sourceDataItems.map( ( item ) => {
111
- const itemBindings = {
112
- source: sourceKey,
113
- args: item.args || {
114
- key: item.key,
115
- },
116
- };
117
- let values = {};
118
- try {
119
- values = source.getValues( {
120
- select,
121
- context: blockContext,
122
- bindings: {
123
- [ attribute ]: itemBindings,
124
- },
125
- } );
126
- } catch ( e ) {}
127
-
128
- return (
129
- <Menu.CheckboxItem
130
- key={
131
- sourceKey +
132
- JSON.stringify(
133
- item.args
134
- ) || item.key
135
- }
136
- onChange={ () => {
137
- const isCurrentlySelected =
138
- fastDeepEqual(
139
- binding?.args,
140
- item.args
141
- ) ??
142
- // Deprecate key dependency in 7.0.
143
- item.key ===
144
- binding?.args?.key;
145
-
146
- if ( isCurrentlySelected ) {
147
- // Unset if the same item is selected again.
148
- updateBlockBindings( {
149
- [ attribute ]:
150
- undefined,
151
- } );
152
- } else {
153
- updateBlockBindings( {
154
- [ attribute ]:
155
- itemBindings,
156
- } );
157
- }
158
- } }
159
- name={ attribute + '-binding' }
160
- value={ values[ attribute ] }
161
- checked={
162
- fastDeepEqual(
163
- binding?.args,
164
- item.args
165
- ) ??
166
- // Deprecate key dependency in 7.0.
167
- item.key === binding?.args?.key
168
- }
169
- >
170
- <Menu.ItemLabel>
171
- { item.label }
172
- </Menu.ItemLabel>
173
- <Menu.ItemHelpText>
174
- { values[ attribute ] }
175
- </Menu.ItemHelpText>
176
- </Menu.CheckboxItem>
177
- );
178
- } ) }
179
- </Menu.Group>
180
- </Menu.Popover>
181
- </Menu>
182
- );
183
- } ) }
184
- </Menu>
185
- );
186
- }
187
-
188
- function BlockBindingsAttribute( { attribute, binding, sources, blockName } ) {
189
- const { source: sourceName, args } = binding || {};
190
- const data = sources?.[ sourceName ];
191
- const source = getBlockBindingsSource( sourceName );
192
-
193
- let displayText;
194
- let isValid = true;
195
- const isNotBound = binding === undefined;
196
-
197
- if ( isNotBound ) {
198
- // Check if there are any compatible sources for this attribute type.
199
- const attributeType = getAttributeType( blockName, attribute );
200
-
201
- const hasCompatibleSources = Object.values( sources ).some( ( items ) =>
202
- items.some( ( item ) => item.type === attributeType )
203
- );
204
-
205
- if ( ! hasCompatibleSources ) {
206
- displayText = __( 'No sources available' );
207
- } else {
208
- displayText = __( 'Not connected' );
209
- }
210
- isValid = true;
211
- } else if ( ! source ) {
212
- // If there's a binding but the source is not found, it's invalid.
213
- isValid = false;
214
- displayText = __( 'Source not registered' );
215
- } else {
216
- displayText =
217
- data?.find( ( item ) => fastDeepEqual( item.args, args ) )?.label ||
218
- source?.label ||
219
- sourceName;
220
- }
221
-
222
- return (
223
- <VStack className="block-editor-bindings__item" spacing={ 0 }>
224
- <Text truncate>{ attribute }</Text>
225
- <Text
226
- truncate
227
- variant={ isValid ? 'muted' : undefined }
228
- isDestructive={ ! isValid }
229
- >
230
- { displayText }
231
- </Text>
232
- </VStack>
233
- );
234
- }
235
-
236
- function ReadOnlyBlockBindingsPanelItem( {
237
- attribute,
238
- binding,
239
- sources,
240
- blockName,
241
- } ) {
242
- const isMobile = useViewportMatch( 'medium', '<' );
243
-
244
- return (
245
- <ToolsPanelItem hasValue={ () => !! binding } label={ attribute }>
246
- <Menu placement={ isMobile ? 'bottom-start' : 'left-start' }>
247
- <Menu.TriggerButton render={ <Item /> } disabled>
248
- <BlockBindingsAttribute
249
- attribute={ attribute }
250
- binding={ binding }
251
- sources={ sources }
252
- blockName={ blockName }
253
- />
254
- </Menu.TriggerButton>
255
- </Menu>
256
- </ToolsPanelItem>
257
- );
258
- }
259
-
260
- function EditableBlockBindingsPanelItem( {
261
- attribute,
262
- binding,
263
- sources,
264
- blockName,
265
- } ) {
266
- const { updateBlockBindings } = useBlockBindingsUtils();
267
- const isMobile = useViewportMatch( 'medium', '<' );
268
-
269
- return (
270
- <ToolsPanelItem
271
- hasValue={ () => !! binding }
272
- label={ attribute }
273
- onDeselect={ () => {
274
- updateBlockBindings( {
275
- [ attribute ]: undefined,
276
- } );
277
- } }
278
- >
279
- <Menu placement={ isMobile ? 'bottom-start' : 'left-start' }>
280
- <Menu.TriggerButton render={ <Item /> }>
281
- <BlockBindingsAttribute
282
- attribute={ attribute }
283
- binding={ binding }
284
- sources={ sources }
285
- blockName={ blockName }
286
- />
287
- </Menu.TriggerButton>
288
- <Menu.Popover gutter={ isMobile ? 8 : 36 }>
289
- <BlockBindingsPanelMenuContent
290
- attribute={ attribute }
291
- binding={ binding }
292
- sources={ sources }
293
- />
294
- </Menu.Popover>
295
- </Menu>
296
- </ToolsPanelItem>
297
- );
298
- }
299
-
300
40
  export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
301
41
  const blockContext = useContext( BlockContext );
302
42
  const { removeAllBlockBindings } = useBlockBindingsUtils();
303
43
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
304
44
 
305
- // Use useSelect to ensure sources are updated whenever there are updates in block context
306
- // or when underlying data changes.
307
- const { canUpdateBlockBindings, bindableAttributes } = useSelect(
45
+ const { bindableAttributes, hasCompatibleFields } = useSelect(
308
46
  ( select ) => {
309
47
  const { __experimentalBlockBindingsSupportedAttributes } =
310
48
  select( blockEditorStore ).getSettings();
49
+ const {
50
+ getAllBlockBindingsSources,
51
+ getBlockBindingsSourceFieldsList,
52
+ } = unlock( select( blocksStore ) );
311
53
 
312
54
  return {
313
- canUpdateBlockBindings:
314
- select( blockEditorStore ).getSettings()
315
- .canUpdateBlockBindings,
316
55
  bindableAttributes:
317
56
  __experimentalBlockBindingsSupportedAttributes?.[
318
57
  blockName
319
58
  ],
59
+ hasCompatibleFields: Object.values(
60
+ getAllBlockBindingsSources()
61
+ ).some(
62
+ ( source ) =>
63
+ getBlockBindingsSourceFieldsList( source, blockContext )
64
+ ?.length > 0
65
+ ),
320
66
  };
321
67
  },
322
- [ blockName ]
323
- );
324
-
325
- const sources = useSelect(
326
- ( select ) => {
327
- const { getAllBlockBindingsSources } = unlock(
328
- select( blockStore )
329
- );
330
- const data = {};
331
- Object.entries( getAllBlockBindingsSources() ).forEach(
332
- ( [ sourceName, source ] ) => {
333
- if ( ! source.getFieldsList ) {
334
- return;
335
- }
336
-
337
- const context = {};
338
- if ( source.usesContext?.length ) {
339
- for ( const key of source.usesContext ) {
340
- context[ key ] = blockContext[ key ];
341
- }
342
- }
343
-
344
- const items = source.getFieldsList( {
345
- select,
346
- context,
347
- } );
348
- if ( items?.length ) {
349
- data[ sourceName ] = items;
350
- }
351
- }
352
- );
353
- return data;
354
- },
355
- [ blockContext ]
68
+ [ blockName, blockContext ]
356
69
  );
357
70
 
358
71
  // Return early if there are no bindable attributes.
@@ -362,12 +75,7 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
362
75
 
363
76
  const { bindings } = metadata || {};
364
77
 
365
- const hasCompatibleData = Object.keys( sources ).length > 0;
366
-
367
- // Lock the UI when the user can't update bindings or there are no fields to connect to.
368
- const readOnly = ! canUpdateBlockBindings || ! hasCompatibleData;
369
-
370
- if ( bindings === undefined && ! hasCompatibleData ) {
78
+ if ( bindings === undefined && ! hasCompatibleFields ) {
371
79
  return null;
372
80
  }
373
81
 
@@ -382,42 +90,14 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
382
90
  className="block-editor-bindings__panel"
383
91
  >
384
92
  <ItemGroup isBordered isSeparated>
385
- { bindableAttributes.map( ( attribute ) => {
386
- const binding = bindings?.[ attribute ];
387
-
388
- // Check if this specific attribute has compatible data from any source.
389
- const attributeType = getAttributeType(
390
- blockName,
391
- attribute
392
- );
393
-
394
- const hasCompatibleDataForAttribute = Object.values(
395
- sources
396
- ).some( ( data ) =>
397
- data.some( ( item ) => item.type === attributeType )
398
- );
399
-
400
- const isAttributeReadOnly =
401
- readOnly || ! hasCompatibleDataForAttribute;
402
-
403
- return isAttributeReadOnly ? (
404
- <ReadOnlyBlockBindingsPanelItem
405
- key={ attribute }
406
- attribute={ attribute }
407
- binding={ binding }
408
- sources={ sources }
409
- blockName={ blockName }
410
- />
411
- ) : (
412
- <EditableBlockBindingsPanelItem
413
- key={ attribute }
414
- attribute={ attribute }
415
- binding={ binding }
416
- sources={ sources }
417
- blockName={ blockName }
418
- />
419
- );
420
- } ) }
93
+ { bindableAttributes.map( ( attribute ) => (
94
+ <BlockBindingsAttributeControl
95
+ key={ attribute }
96
+ attribute={ attribute }
97
+ blockName={ blockName }
98
+ binding={ bindings?.[ attribute ] }
99
+ />
100
+ ) ) }
421
101
  </ItemGroup>
422
102
  { /*
423
103
  Use a div element to make the ToolsPanelHiddenInnerWrapper