@wordpress/block-editor 8.6.0 → 9.0.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 (152) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -2
  3. package/build/components/block-alignment-control/constants.js +48 -0
  4. package/build/components/block-alignment-control/constants.js.map +1 -0
  5. package/build/components/block-alignment-control/ui.js +9 -40
  6. package/build/components/block-alignment-control/ui.js.map +1 -1
  7. package/build/components/block-alignment-control/ui.native.js +92 -0
  8. package/build/components/block-alignment-control/ui.native.js.map +1 -0
  9. package/build/components/block-list/use-block-props/use-block-class-names.js +1 -7
  10. package/build/components/block-list/use-block-props/use-block-class-names.js.map +1 -1
  11. package/build/components/block-navigation/dropdown.js +11 -5
  12. package/build/components/block-navigation/dropdown.js.map +1 -1
  13. package/build/components/block-popover/index.js +20 -5
  14. package/build/components/block-popover/index.js.map +1 -1
  15. package/build/components/color-style-selector/index.js +9 -0
  16. package/build/components/color-style-selector/index.js.map +1 -1
  17. package/build/components/index.js +0 -9
  18. package/build/components/index.js.map +1 -1
  19. package/build/components/inserter/index.js +21 -7
  20. package/build/components/inserter/index.js.map +1 -1
  21. package/build/components/inserter/index.native.js +1 -1
  22. package/build/components/inserter/index.native.js.map +1 -1
  23. package/build/components/inserter/quick-inserter.js +4 -5
  24. package/build/components/inserter/quick-inserter.js.map +1 -1
  25. package/build/components/list-view/block.js +15 -15
  26. package/build/components/list-view/block.js.map +1 -1
  27. package/build/components/list-view/branch.js +9 -13
  28. package/build/components/list-view/branch.js.map +1 -1
  29. package/build/components/list-view/context.js +1 -4
  30. package/build/components/list-view/context.js.map +1 -1
  31. package/build/components/list-view/index.js +15 -32
  32. package/build/components/list-view/index.js.map +1 -1
  33. package/build/components/url-input/index.js +7 -3
  34. package/build/components/url-input/index.js.map +1 -1
  35. package/build/components/use-setting/index.js +42 -18
  36. package/build/components/use-setting/index.js.map +1 -1
  37. package/build/components/writing-flow/use-click-selection.js +1 -3
  38. package/build/components/writing-flow/use-click-selection.js.map +1 -1
  39. package/build/components/writing-flow/use-selection-observer.js +49 -8
  40. package/build/components/writing-flow/use-selection-observer.js.map +1 -1
  41. package/build/hooks/dimensions.js +2 -2
  42. package/build/hooks/dimensions.js.map +1 -1
  43. package/build/hooks/index.js +2 -0
  44. package/build/hooks/index.js.map +1 -1
  45. package/build/hooks/margin.js +64 -12
  46. package/build/hooks/margin.js.map +1 -1
  47. package/build/hooks/padding.js +60 -12
  48. package/build/hooks/padding.js.map +1 -1
  49. package/build/hooks/settings.js +32 -0
  50. package/build/hooks/settings.js.map +1 -0
  51. package/build/store/defaults.js +0 -1
  52. package/build/store/defaults.js.map +1 -1
  53. package/build/store/selectors.js +14 -12
  54. package/build/store/selectors.js.map +1 -1
  55. package/build-module/components/block-alignment-control/constants.js +36 -0
  56. package/build-module/components/block-alignment-control/constants.js.map +1 -0
  57. package/build-module/components/block-alignment-control/ui.js +4 -35
  58. package/build-module/components/block-alignment-control/ui.js.map +1 -1
  59. package/build-module/components/block-alignment-control/ui.native.js +78 -0
  60. package/build-module/components/block-alignment-control/ui.native.js.map +1 -0
  61. package/build-module/components/block-list/use-block-props/use-block-class-names.js +1 -7
  62. package/build-module/components/block-list/use-block-props/use-block-class-names.js.map +1 -1
  63. package/build-module/components/block-navigation/dropdown.js +10 -5
  64. package/build-module/components/block-navigation/dropdown.js.map +1 -1
  65. package/build-module/components/block-popover/index.js +21 -5
  66. package/build-module/components/block-popover/index.js.map +1 -1
  67. package/build-module/components/color-style-selector/index.js +6 -0
  68. package/build-module/components/color-style-selector/index.js.map +1 -1
  69. package/build-module/components/index.js +0 -1
  70. package/build-module/components/index.js.map +1 -1
  71. package/build-module/components/inserter/index.js +21 -7
  72. package/build-module/components/inserter/index.js.map +1 -1
  73. package/build-module/components/inserter/index.native.js +1 -1
  74. package/build-module/components/inserter/index.native.js.map +1 -1
  75. package/build-module/components/inserter/quick-inserter.js +4 -5
  76. package/build-module/components/inserter/quick-inserter.js.map +1 -1
  77. package/build-module/components/list-view/block.js +15 -16
  78. package/build-module/components/list-view/block.js.map +1 -1
  79. package/build-module/components/list-view/branch.js +9 -13
  80. package/build-module/components/list-view/branch.js.map +1 -1
  81. package/build-module/components/list-view/context.js +1 -4
  82. package/build-module/components/list-view/context.js.map +1 -1
  83. package/build-module/components/list-view/index.js +15 -31
  84. package/build-module/components/list-view/index.js.map +1 -1
  85. package/build-module/components/url-input/index.js +7 -3
  86. package/build-module/components/url-input/index.js.map +1 -1
  87. package/build-module/components/use-setting/index.js +43 -19
  88. package/build-module/components/use-setting/index.js.map +1 -1
  89. package/build-module/components/writing-flow/use-click-selection.js +1 -3
  90. package/build-module/components/writing-flow/use-click-selection.js.map +1 -1
  91. package/build-module/components/writing-flow/use-selection-observer.js +49 -8
  92. package/build-module/components/writing-flow/use-selection-observer.js.map +1 -1
  93. package/build-module/hooks/dimensions.js +5 -5
  94. package/build-module/hooks/dimensions.js.map +1 -1
  95. package/build-module/hooks/index.js +1 -0
  96. package/build-module/hooks/index.js.map +1 -1
  97. package/build-module/hooks/margin.js +61 -13
  98. package/build-module/hooks/margin.js.map +1 -1
  99. package/build-module/hooks/padding.js +57 -13
  100. package/build-module/hooks/padding.js.map +1 -1
  101. package/build-module/hooks/settings.js +29 -0
  102. package/build-module/hooks/settings.js.map +1 -0
  103. package/build-module/store/defaults.js +0 -1
  104. package/build-module/store/defaults.js.map +1 -1
  105. package/build-module/store/selectors.js +14 -12
  106. package/build-module/store/selectors.js.map +1 -1
  107. package/build-style/style-rtl.css +17 -31
  108. package/build-style/style.css +17 -31
  109. package/package.json +28 -28
  110. package/src/components/block-alignment-control/constants.js +45 -0
  111. package/src/components/block-alignment-control/ui.js +69 -109
  112. package/src/components/block-alignment-control/ui.native.js +86 -0
  113. package/src/components/block-content-overlay/style.scss +0 -1
  114. package/src/components/block-list/style.scss +7 -18
  115. package/src/components/block-list/use-block-props/use-block-class-names.js +1 -11
  116. package/src/components/block-navigation/dropdown.js +12 -8
  117. package/src/components/block-popover/index.js +20 -3
  118. package/src/components/block-popover/style.scss +3 -0
  119. package/src/components/color-palette/test/__snapshots__/control.js.snap +1 -1
  120. package/src/components/color-style-selector/index.js +18 -9
  121. package/src/components/image-size-control/README.md +1 -1
  122. package/src/components/index.js +0 -1
  123. package/src/components/inserter/index.js +20 -0
  124. package/src/components/inserter/index.native.js +1 -1
  125. package/src/components/inserter/quick-inserter.js +3 -11
  126. package/src/components/inserter/style.native.scss +1 -0
  127. package/src/components/list-view/block.js +24 -34
  128. package/src/components/list-view/branch.js +10 -20
  129. package/src/components/list-view/context.js +1 -4
  130. package/src/components/list-view/index.js +11 -41
  131. package/src/components/url-input/index.js +6 -3
  132. package/src/components/use-setting/index.js +57 -21
  133. package/src/components/writing-flow/use-click-selection.js +1 -4
  134. package/src/components/writing-flow/use-selection-observer.js +55 -10
  135. package/src/hooks/dimensions.js +44 -38
  136. package/src/hooks/index.js +1 -0
  137. package/src/hooks/margin.js +64 -15
  138. package/src/hooks/padding.js +60 -15
  139. package/src/hooks/padding.scss +12 -0
  140. package/src/hooks/settings.js +32 -0
  141. package/src/hooks/test/settings.js +48 -0
  142. package/src/store/defaults.js +0 -1
  143. package/src/store/selectors.js +14 -12
  144. package/src/store/test/selectors.js +17 -0
  145. package/src/style.scss +1 -1
  146. package/tsconfig.tsbuildinfo +1 -1
  147. package/build/components/border-style-control/index.js +0 -60
  148. package/build/components/border-style-control/index.js.map +0 -1
  149. package/build-module/components/border-style-control/index.js +0 -50
  150. package/build-module/components/border-style-control/index.js.map +0 -1
  151. package/src/components/border-style-control/index.js +0 -47
  152. package/src/components/border-style-control/style.scss +0 -18
@@ -7,7 +7,10 @@ import { get } from 'lodash';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { useSelect } from '@wordpress/data';
10
- import { __EXPERIMENTAL_PATHS_WITH_MERGE as PATHS_WITH_MERGE } from '@wordpress/blocks';
10
+ import {
11
+ __EXPERIMENTAL_PATHS_WITH_MERGE as PATHS_WITH_MERGE,
12
+ hasBlockSupport,
13
+ } from '@wordpress/blocks';
11
14
 
12
15
  /**
13
16
  * Internal dependencies
@@ -91,8 +94,10 @@ const removeCustomPrefixes = ( path ) => {
91
94
  };
92
95
 
93
96
  /**
94
- * Hook that retrieves the editor setting.
95
- * It works with nested objects using by finding the value at path.
97
+ * Hook that retrieves the given setting for the block instance in use.
98
+ *
99
+ * It looks up the settings first in the block instance hierarchy.
100
+ * If none is found, it'll look it up in the block editor store.
96
101
  *
97
102
  * @param {string} path The path to the setting.
98
103
  * @return {any} Returns the value defined for the setting.
@@ -102,7 +107,7 @@ const removeCustomPrefixes = ( path ) => {
102
107
  * ```
103
108
  */
104
109
  export default function useSetting( path ) {
105
- const { name: blockName } = useBlockEditContext();
110
+ const { name: blockName, clientId } = useBlockEditContext();
106
111
 
107
112
  const setting = useSelect(
108
113
  ( select ) => {
@@ -113,28 +118,59 @@ export default function useSetting( path ) {
113
118
  );
114
119
  return undefined;
115
120
  }
116
- const settings = select( blockEditorStore ).getSettings();
117
121
 
118
- // 1 - Use __experimental features, if available.
119
- // We cascade to the all value if the block one is not available.
122
+ let result;
120
123
  const normalizedPath = removeCustomPrefixes( path );
121
- const defaultsPath = `__experimentalFeatures.${ normalizedPath }`;
122
- const blockPath = `__experimentalFeatures.blocks.${ blockName }.${ normalizedPath }`;
123
- const experimentalFeaturesResult =
124
- get( settings, blockPath ) ?? get( settings, defaultsPath );
125
124
 
126
- if ( experimentalFeaturesResult !== undefined ) {
125
+ // 1. Take settings from the block instance or its ancestors.
126
+ const candidates = [
127
+ ...select( blockEditorStore ).getBlockParents( clientId ),
128
+ clientId, // The current block is added last, so it overwrites any ancestor.
129
+ ];
130
+ candidates.forEach( ( candidateClientId ) => {
131
+ const candidateBlockName = select(
132
+ blockEditorStore
133
+ ).getBlockName( candidateClientId );
134
+ if (
135
+ hasBlockSupport(
136
+ candidateBlockName,
137
+ '__experimentalSettings',
138
+ false
139
+ )
140
+ ) {
141
+ const candidateAtts = select(
142
+ blockEditorStore
143
+ ).getBlockAttributes( candidateClientId );
144
+ const candidateResult =
145
+ get(
146
+ candidateAtts,
147
+ `settings.blocks.${ blockName }.${ normalizedPath }`
148
+ ) ??
149
+ get( candidateAtts, `settings.${ normalizedPath }` );
150
+ if ( candidateResult !== undefined ) {
151
+ result = candidateResult;
152
+ }
153
+ }
154
+ } );
155
+
156
+ // 2. Fall back to the settings from the block editor store (__experimentalFeatures).
157
+ const settings = select( blockEditorStore ).getSettings();
158
+ if ( result === undefined ) {
159
+ const defaultsPath = `__experimentalFeatures.${ normalizedPath }`;
160
+ const blockPath = `__experimentalFeatures.blocks.${ blockName }.${ normalizedPath }`;
161
+ result =
162
+ get( settings, blockPath ) ?? get( settings, defaultsPath );
163
+ }
164
+
165
+ // Return if the setting was found in either the block instance or the store.
166
+ if ( result !== undefined ) {
127
167
  if ( PATHS_WITH_MERGE[ normalizedPath ] ) {
128
- return (
129
- experimentalFeaturesResult.custom ??
130
- experimentalFeaturesResult.theme ??
131
- experimentalFeaturesResult.default
132
- );
168
+ return result.custom ?? result.theme ?? result.default;
133
169
  }
134
- return experimentalFeaturesResult;
170
+ return result;
135
171
  }
136
172
 
137
- // 2 - Use deprecated settings, otherwise.
173
+ // 3. Otherwise, use deprecated settings.
138
174
  const deprecatedSettingsValue = deprecatedFlags[ normalizedPath ]
139
175
  ? deprecatedFlags[ normalizedPath ]( settings )
140
176
  : undefined;
@@ -142,13 +178,13 @@ export default function useSetting( path ) {
142
178
  return deprecatedSettingsValue;
143
179
  }
144
180
 
145
- // 3 - Fall back for typography.dropCap:
181
+ // 4. Fallback for typography.dropCap:
146
182
  // This is only necessary to support typography.dropCap.
147
183
  // when __experimentalFeatures are not present (core without plugin).
148
184
  // To remove when __experimentalFeatures are ported to core.
149
185
  return normalizedPath === 'typography.dropCap' ? true : undefined;
150
186
  },
151
- [ blockName, path ]
187
+ [ blockName, clientId, path ]
152
188
  );
153
189
 
154
190
  return setting;
@@ -11,10 +11,9 @@ import { store as blockEditorStore } from '../../store';
11
11
  import { getBlockClientId } from '../../utils/dom';
12
12
 
13
13
  export default function useClickSelection() {
14
- const { multiSelect, selectBlock } = useDispatch( blockEditorStore );
14
+ const { selectBlock } = useDispatch( blockEditorStore );
15
15
  const {
16
16
  isSelectionEnabled,
17
- getBlockParents,
18
17
  getBlockSelectionStart,
19
18
  hasMultiSelection,
20
19
  } = useSelect( blockEditorStore );
@@ -54,10 +53,8 @@ export default function useClickSelection() {
54
53
  };
55
54
  },
56
55
  [
57
- multiSelect,
58
56
  selectBlock,
59
57
  isSelectionEnabled,
60
- getBlockParents,
61
58
  getBlockSelectionStart,
62
59
  hasMultiSelection,
63
60
  ]
@@ -76,36 +76,81 @@ export default function useSelectionObserver() {
76
76
  const { multiSelect, selectBlock, selectionChange } = useDispatch(
77
77
  blockEditorStore
78
78
  );
79
- const { getBlockParents } = useSelect( blockEditorStore );
79
+ const { getBlockParents, getBlockSelectionStart } = useSelect(
80
+ blockEditorStore
81
+ );
80
82
  return useRefEffect(
81
83
  ( node ) => {
82
84
  const { ownerDocument } = node;
83
85
  const { defaultView } = ownerDocument;
84
86
 
85
- function onSelectionChange() {
87
+ function onSelectionChange( event ) {
86
88
  const selection = defaultView.getSelection();
87
-
88
89
  // If no selection is found, end multi selection and disable the
89
90
  // contentEditable wrapper.
90
- if ( ! selection.rangeCount || selection.isCollapsed ) {
91
+ if ( ! selection.rangeCount ) {
92
+ setContentEditableWrapper( node, false );
93
+ return;
94
+ }
95
+ // If selection is collapsed and we haven't used `shift+click`,
96
+ // end multi selection and disable the contentEditable wrapper.
97
+ // We have to check about `shift+click` case because elements
98
+ // that don't support text selection might be involved, and we might
99
+ // update the clientIds to multi-select blocks.
100
+ // For now we check if the event is a `mouse` event.
101
+ const isClickShift = event.shiftKey && event.type === 'mouseup';
102
+ if ( selection.isCollapsed && ! isClickShift ) {
91
103
  setContentEditableWrapper( node, false );
92
104
  return;
93
105
  }
94
106
 
95
- const clientId = getBlockClientId(
107
+ let startClientId = getBlockClientId(
96
108
  extractSelectionStartNode( selection )
97
109
  );
98
- const endClientId = getBlockClientId(
110
+ let endClientId = getBlockClientId(
99
111
  extractSelectionEndNode( selection )
100
112
  );
101
- const isSingularSelection = clientId === endClientId;
113
+ // If the selection has changed and we had pressed `shift+click`,
114
+ // we need to check if in an element that doesn't support
115
+ // text selection has been clicked.
116
+ if ( isClickShift ) {
117
+ const selectedClientId = getBlockSelectionStart();
118
+ const clickedClientId = getBlockClientId( event.target );
119
+ // `endClientId` is not defined if we end the selection by clicking a non-selectable block.
120
+ // We need to check if there was already a selection with a non-selectable focusNode.
121
+ const focusNodeIsNonSelectable =
122
+ clickedClientId !== endClientId;
123
+ if (
124
+ ( startClientId === endClientId &&
125
+ selection.isCollapsed ) ||
126
+ ! endClientId ||
127
+ focusNodeIsNonSelectable
128
+ ) {
129
+ endClientId = clickedClientId;
130
+ }
131
+ // Handle the case when we have a non-selectable block
132
+ // selected and click another one.
133
+ if ( startClientId !== selectedClientId ) {
134
+ startClientId = selectedClientId;
135
+ }
136
+ }
137
+
138
+ // If the selection did not involve a block, return.
139
+ if (
140
+ startClientId === undefined &&
141
+ endClientId === undefined
142
+ ) {
143
+ setContentEditableWrapper( node, false );
144
+ return;
145
+ }
102
146
 
147
+ const isSingularSelection = startClientId === endClientId;
103
148
  if ( isSingularSelection ) {
104
- selectBlock( clientId );
149
+ selectBlock( startClientId );
105
150
  } else {
106
151
  const startPath = [
107
- ...getBlockParents( clientId ),
108
- clientId,
152
+ ...getBlockParents( startClientId ),
153
+ startClientId,
109
154
  ];
110
155
  const endPath = [
111
156
  ...getBlockParents( endClientId ),
@@ -19,6 +19,7 @@ import {
19
19
  } from './gap';
20
20
  import {
21
21
  MarginEdit,
22
+ MarginVisualizer,
22
23
  hasMarginSupport,
23
24
  hasMarginValue,
24
25
  resetMargin,
@@ -26,6 +27,7 @@ import {
26
27
  } from './margin';
27
28
  import {
28
29
  PaddingEdit,
30
+ PaddingVisualizer,
29
31
  hasPaddingSupport,
30
32
  hasPaddingValue,
31
33
  resetPadding,
@@ -71,44 +73,48 @@ export function DimensionsPanel( props ) {
71
73
  } );
72
74
 
73
75
  return (
74
- <InspectorControls __experimentalGroup="dimensions">
75
- { ! isPaddingDisabled && (
76
- <ToolsPanelItem
77
- hasValue={ () => hasPaddingValue( props ) }
78
- label={ __( 'Padding' ) }
79
- onDeselect={ () => resetPadding( props ) }
80
- resetAllFilter={ createResetAllFilter( 'padding' ) }
81
- isShownByDefault={ defaultSpacingControls?.padding }
82
- panelId={ props.clientId }
83
- >
84
- <PaddingEdit { ...props } />
85
- </ToolsPanelItem>
86
- ) }
87
- { ! isMarginDisabled && (
88
- <ToolsPanelItem
89
- hasValue={ () => hasMarginValue( props ) }
90
- label={ __( 'Margin' ) }
91
- onDeselect={ () => resetMargin( props ) }
92
- resetAllFilter={ createResetAllFilter( 'margin' ) }
93
- isShownByDefault={ defaultSpacingControls?.margin }
94
- panelId={ props.clientId }
95
- >
96
- <MarginEdit { ...props } />
97
- </ToolsPanelItem>
98
- ) }
99
- { ! isGapDisabled && (
100
- <ToolsPanelItem
101
- hasValue={ () => hasGapValue( props ) }
102
- label={ __( 'Block spacing' ) }
103
- onDeselect={ () => resetGap( props ) }
104
- resetAllFilter={ createResetAllFilter( 'blockGap' ) }
105
- isShownByDefault={ defaultSpacingControls?.blockGap }
106
- panelId={ props.clientId }
107
- >
108
- <GapEdit { ...props } />
109
- </ToolsPanelItem>
110
- ) }
111
- </InspectorControls>
76
+ <>
77
+ <InspectorControls __experimentalGroup="dimensions">
78
+ { ! isPaddingDisabled && (
79
+ <ToolsPanelItem
80
+ hasValue={ () => hasPaddingValue( props ) }
81
+ label={ __( 'Padding' ) }
82
+ onDeselect={ () => resetPadding( props ) }
83
+ resetAllFilter={ createResetAllFilter( 'padding' ) }
84
+ isShownByDefault={ defaultSpacingControls?.padding }
85
+ panelId={ props.clientId }
86
+ >
87
+ <PaddingEdit { ...props } />
88
+ </ToolsPanelItem>
89
+ ) }
90
+ { ! isMarginDisabled && (
91
+ <ToolsPanelItem
92
+ hasValue={ () => hasMarginValue( props ) }
93
+ label={ __( 'Margin' ) }
94
+ onDeselect={ () => resetMargin( props ) }
95
+ resetAllFilter={ createResetAllFilter( 'margin' ) }
96
+ isShownByDefault={ defaultSpacingControls?.margin }
97
+ panelId={ props.clientId }
98
+ >
99
+ <MarginEdit { ...props } />
100
+ </ToolsPanelItem>
101
+ ) }
102
+ { ! isGapDisabled && (
103
+ <ToolsPanelItem
104
+ hasValue={ () => hasGapValue( props ) }
105
+ label={ __( 'Block spacing' ) }
106
+ onDeselect={ () => resetGap( props ) }
107
+ resetAllFilter={ createResetAllFilter( 'blockGap' ) }
108
+ isShownByDefault={ defaultSpacingControls?.blockGap }
109
+ panelId={ props.clientId }
110
+ >
111
+ <GapEdit { ...props } />
112
+ </ToolsPanelItem>
113
+ ) }
114
+ </InspectorControls>
115
+ { ! isPaddingDisabled && <PaddingVisualizer { ...props } /> }
116
+ { ! isMarginDisabled && <MarginVisualizer { ...props } /> }
117
+ </>
112
118
  );
113
119
  }
114
120
 
@@ -8,6 +8,7 @@ import './anchor';
8
8
  import './custom-class-name';
9
9
  import './generated-class-name';
10
10
  import './style';
11
+ import './settings';
11
12
  import './color';
12
13
  import './duotone';
13
14
  import './font-size';
@@ -2,12 +2,19 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
- import { Platform } from '@wordpress/element';
5
+ import {
6
+ Platform,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ useEffect,
11
+ } from '@wordpress/element';
6
12
  import { getBlockSupport } from '@wordpress/blocks';
7
13
  import {
8
14
  __experimentalUseCustomUnits as useCustomUnits,
9
15
  __experimentalBoxControl as BoxControl,
10
16
  } from '@wordpress/components';
17
+ import isShallowEqual from '@wordpress/is-shallow-equal';
11
18
 
12
19
  /**
13
20
  * Internal dependencies
@@ -20,6 +27,7 @@ import {
20
27
  useIsDimensionsSupportValid,
21
28
  } from './dimensions';
22
29
  import { cleanEmptyObject } from './utils';
30
+ import BlockPopover from '../components/block-popover';
23
31
 
24
32
  /**
25
33
  * Determines if there is margin support.
@@ -124,26 +132,12 @@ export function MarginEdit( props ) {
124
132
  } );
125
133
  };
126
134
 
127
- const onChangeShowVisualizer = ( next ) => {
128
- const newStyle = {
129
- ...style,
130
- visualizers: {
131
- margin: next,
132
- },
133
- };
134
-
135
- setAttributes( {
136
- style: cleanEmptyObject( newStyle ),
137
- } );
138
- };
139
-
140
135
  return Platform.select( {
141
136
  web: (
142
137
  <>
143
138
  <BoxControl
144
139
  values={ style?.spacing?.margin }
145
140
  onChange={ onChange }
146
- onChangeShowVisualizer={ onChangeShowVisualizer }
147
141
  label={ __( 'Margin' ) }
148
142
  sides={ sides }
149
143
  units={ units }
@@ -155,3 +149,58 @@ export function MarginEdit( props ) {
155
149
  native: null,
156
150
  } );
157
151
  }
152
+
153
+ export function MarginVisualizer( { clientId, attributes } ) {
154
+ const margin = attributes?.style?.spacing?.margin;
155
+ const style = useMemo( () => {
156
+ return {
157
+ borderTopWidth: margin?.top ?? 0,
158
+ borderRightWidth: margin?.right ?? 0,
159
+ borderBottomWidth: margin?.bottom ?? 0,
160
+ borderLeftWidth: margin?.left ?? 0,
161
+ top: margin?.top ? `-${ margin.top }` : 0,
162
+ right: margin?.right ? `-${ margin.right }` : 0,
163
+ bottom: margin?.bottom ? `-${ margin.bottom }` : 0,
164
+ left: margin?.left ? `-${ margin.left }` : 0,
165
+ };
166
+ }, [ margin ] );
167
+
168
+ const [ isActive, setIsActive ] = useState( false );
169
+ const valueRef = useRef( margin );
170
+ const timeoutRef = useRef();
171
+
172
+ const clearTimer = () => {
173
+ if ( timeoutRef.current ) {
174
+ window.clearTimeout( timeoutRef.current );
175
+ }
176
+ };
177
+
178
+ useEffect( () => {
179
+ if ( ! isShallowEqual( margin, valueRef.current ) ) {
180
+ setIsActive( true );
181
+ valueRef.current = margin;
182
+
183
+ clearTimer();
184
+
185
+ timeoutRef.current = setTimeout( () => {
186
+ setIsActive( false );
187
+ }, 400 );
188
+ }
189
+
190
+ return () => clearTimer();
191
+ }, [ margin ] );
192
+
193
+ if ( ! isActive ) {
194
+ return null;
195
+ }
196
+
197
+ return (
198
+ <BlockPopover
199
+ clientId={ clientId }
200
+ __unstableCoverTarget
201
+ __unstableRefreshSize={ margin }
202
+ >
203
+ <div className="block-editor__padding-visualizer" style={ style } />
204
+ </BlockPopover>
205
+ );
206
+ }
@@ -2,12 +2,19 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
- import { Platform } from '@wordpress/element';
5
+ import {
6
+ Platform,
7
+ useState,
8
+ useRef,
9
+ useEffect,
10
+ useMemo,
11
+ } from '@wordpress/element';
6
12
  import { getBlockSupport } from '@wordpress/blocks';
7
13
  import {
8
14
  __experimentalUseCustomUnits as useCustomUnits,
9
15
  __experimentalBoxControl as BoxControl,
10
16
  } from '@wordpress/components';
17
+ import isShallowEqual from '@wordpress/is-shallow-equal';
11
18
 
12
19
  /**
13
20
  * Internal dependencies
@@ -20,6 +27,7 @@ import {
20
27
  useIsDimensionsSupportValid,
21
28
  } from './dimensions';
22
29
  import { cleanEmptyObject } from './utils';
30
+ import BlockPopover from '../components/block-popover';
23
31
 
24
32
  /**
25
33
  * Determines if there is padding support.
@@ -124,26 +132,12 @@ export function PaddingEdit( props ) {
124
132
  } );
125
133
  };
126
134
 
127
- const onChangeShowVisualizer = ( next ) => {
128
- const newStyle = {
129
- ...style,
130
- visualizers: {
131
- padding: next,
132
- },
133
- };
134
-
135
- setAttributes( {
136
- style: cleanEmptyObject( newStyle ),
137
- } );
138
- };
139
-
140
135
  return Platform.select( {
141
136
  web: (
142
137
  <>
143
138
  <BoxControl
144
139
  values={ style?.spacing?.padding }
145
140
  onChange={ onChange }
146
- onChangeShowVisualizer={ onChangeShowVisualizer }
147
141
  label={ __( 'Padding' ) }
148
142
  sides={ sides }
149
143
  units={ units }
@@ -155,3 +149,54 @@ export function PaddingEdit( props ) {
155
149
  native: null,
156
150
  } );
157
151
  }
152
+
153
+ export function PaddingVisualizer( { clientId, attributes } ) {
154
+ const padding = attributes?.style?.spacing?.padding;
155
+ const style = useMemo( () => {
156
+ return {
157
+ borderTopWidth: padding?.top ?? 0,
158
+ borderRightWidth: padding?.right ?? 0,
159
+ borderBottomWidth: padding?.bottom ?? 0,
160
+ borderLeftWidth: padding?.left ?? 0,
161
+ };
162
+ }, [ padding ] );
163
+
164
+ const [ isActive, setIsActive ] = useState( false );
165
+ const valueRef = useRef( padding );
166
+ const timeoutRef = useRef();
167
+
168
+ const clearTimer = () => {
169
+ if ( timeoutRef.current ) {
170
+ window.clearTimeout( timeoutRef.current );
171
+ }
172
+ };
173
+
174
+ useEffect( () => {
175
+ if ( ! isShallowEqual( padding, valueRef.current ) ) {
176
+ setIsActive( true );
177
+ valueRef.current = padding;
178
+
179
+ clearTimer();
180
+
181
+ timeoutRef.current = setTimeout( () => {
182
+ setIsActive( false );
183
+ }, 400 );
184
+ }
185
+
186
+ return () => clearTimer();
187
+ }, [ padding ] );
188
+
189
+ if ( ! isActive ) {
190
+ return null;
191
+ }
192
+
193
+ return (
194
+ <BlockPopover
195
+ clientId={ clientId }
196
+ __unstableCoverTarget
197
+ __unstableRefreshSize={ padding }
198
+ >
199
+ <div className="block-editor__padding-visualizer" style={ style } />
200
+ </BlockPopover>
201
+ );
202
+ }
@@ -0,0 +1,12 @@
1
+ .block-editor__padding-visualizer {
2
+ position: absolute;
3
+ top: 0;
4
+ bottom: 0;
5
+ left: 0;
6
+ right: 0;
7
+ opacity: 0.5;
8
+ border-color: var(--wp-admin-theme-color);
9
+ border-style: solid;
10
+ pointer-events: none;
11
+ box-sizing: border-box;
12
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { addFilter } from '@wordpress/hooks';
5
+ import { hasBlockSupport } from '@wordpress/blocks';
6
+
7
+ const hasSettingsSupport = ( blockType ) =>
8
+ hasBlockSupport( blockType, '__experimentalSettings', false );
9
+
10
+ function addAttribute( settings ) {
11
+ if ( ! hasSettingsSupport( settings ) ) {
12
+ return settings;
13
+ }
14
+
15
+ // Allow blocks to specify their own attribute definition with default values if needed.
16
+ if ( ! settings?.attributes?.settings ) {
17
+ settings.attributes = {
18
+ ...settings.attributes,
19
+ settings: {
20
+ type: 'object',
21
+ },
22
+ };
23
+ }
24
+
25
+ return settings;
26
+ }
27
+
28
+ addFilter(
29
+ 'blocks.registerBlockType',
30
+ 'core/settings/addAttribute',
31
+ addAttribute
32
+ );
@@ -0,0 +1,48 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { applyFilters } from '@wordpress/hooks';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import '../settings';
10
+
11
+ describe( 'with settings', () => {
12
+ const blockSettings = {
13
+ save: () => <div className="default" />,
14
+ category: 'text',
15
+ title: 'block title',
16
+ };
17
+
18
+ describe( 'addAttribute', () => {
19
+ const addAttribute = applyFilters.bind(
20
+ null,
21
+ 'blocks.registerBlockType'
22
+ );
23
+
24
+ it( 'does not have settings att if settings block support is not enabled', () => {
25
+ const settings = addAttribute( {
26
+ ...blockSettings,
27
+ supports: {
28
+ __experimentalSettings: false,
29
+ },
30
+ } );
31
+
32
+ expect( settings.attributes ).toBe( undefined );
33
+ } );
34
+
35
+ it( 'has settings att if settings block supports is enabled', () => {
36
+ const settings = addAttribute( {
37
+ ...blockSettings,
38
+ supports: {
39
+ __experimentalSettings: true,
40
+ },
41
+ } );
42
+
43
+ expect( settings.attributes ).toStrictEqual( {
44
+ settings: { type: 'object' },
45
+ } );
46
+ } );
47
+ } );
48
+ } );
@@ -160,7 +160,6 @@ export const SETTINGS_DEFAULTS = {
160
160
  __mobileEnablePageTemplates: false,
161
161
  __experimentalBlockPatterns: [],
162
162
  __experimentalBlockPatternCategories: [],
163
- __experimentalSpotlightEntityBlocks: [],
164
163
  __unstableGalleryWithImageBlocks: false,
165
164
 
166
165
  generateAnchors: false,