@wordpress/block-editor 11.3.5 → 11.3.7

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 (105) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/build/components/block-inspector/index.js +11 -10
  3. package/build/components/block-inspector/index.js.map +1 -1
  4. package/build/components/block-inspector/useBlockInspectorAnimationSettings.js +46 -0
  5. package/build/components/block-inspector/useBlockInspectorAnimationSettings.js.map +1 -0
  6. package/build/components/block-settings-menu/block-settings-dropdown.js +2 -2
  7. package/build/components/block-settings-menu/block-settings-dropdown.js.map +1 -1
  8. package/build/components/inserter/index.js +29 -17
  9. package/build/components/inserter/index.js.map +1 -1
  10. package/build/components/inserter/media-tab/hooks.js +10 -11
  11. package/build/components/inserter/media-tab/hooks.js.map +1 -1
  12. package/build/components/inserter/media-tab/media-list.js +5 -108
  13. package/build/components/inserter/media-tab/media-list.js.map +1 -1
  14. package/build/components/inserter/media-tab/media-preview.js +242 -0
  15. package/build/components/inserter/media-tab/media-preview.js.map +1 -0
  16. package/build/components/inserter/menu.js +4 -7
  17. package/build/components/inserter/menu.js.map +1 -1
  18. package/build/components/inserter/quick-inserter.js +4 -2
  19. package/build/components/inserter/quick-inserter.js.map +1 -1
  20. package/build/components/inserter/search-results.js +10 -3
  21. package/build/components/inserter/search-results.js.map +1 -1
  22. package/build/components/link-control/index.js +17 -44
  23. package/build/components/link-control/index.js.map +1 -1
  24. package/build/components/off-canvas-editor/appender.js +28 -3
  25. package/build/components/off-canvas-editor/appender.js.map +1 -1
  26. package/build/components/off-canvas-editor/block-contents.js +1 -1
  27. package/build/components/off-canvas-editor/block-contents.js.map +1 -1
  28. package/build/components/off-canvas-editor/branch.js +5 -3
  29. package/build/components/off-canvas-editor/branch.js.map +1 -1
  30. package/build/components/off-canvas-editor/index.js +4 -1
  31. package/build/components/off-canvas-editor/index.js.map +1 -1
  32. package/build/private-apis.js +4 -1
  33. package/build/private-apis.js.map +1 -1
  34. package/build/store/actions.js +28 -14
  35. package/build/store/actions.js.map +1 -1
  36. package/build/store/defaults.js +28 -1
  37. package/build/store/defaults.js.map +1 -1
  38. package/build/store/selectors.js +1 -1
  39. package/build/store/selectors.js.map +1 -1
  40. package/build-module/components/block-inspector/index.js +9 -9
  41. package/build-module/components/block-inspector/index.js.map +1 -1
  42. package/build-module/components/block-inspector/useBlockInspectorAnimationSettings.js +37 -0
  43. package/build-module/components/block-inspector/useBlockInspectorAnimationSettings.js.map +1 -0
  44. package/build-module/components/block-settings-menu/block-settings-dropdown.js +2 -2
  45. package/build-module/components/block-settings-menu/block-settings-dropdown.js.map +1 -1
  46. package/build-module/components/inserter/index.js +28 -16
  47. package/build-module/components/inserter/index.js.map +1 -1
  48. package/build-module/components/inserter/media-tab/hooks.js +10 -11
  49. package/build-module/components/inserter/media-tab/hooks.js.map +1 -1
  50. package/build-module/components/inserter/media-tab/media-list.js +6 -105
  51. package/build-module/components/inserter/media-tab/media-list.js.map +1 -1
  52. package/build-module/components/inserter/media-tab/media-preview.js +222 -0
  53. package/build-module/components/inserter/media-tab/media-preview.js.map +1 -0
  54. package/build-module/components/inserter/menu.js +4 -7
  55. package/build-module/components/inserter/menu.js.map +1 -1
  56. package/build-module/components/inserter/quick-inserter.js +4 -2
  57. package/build-module/components/inserter/quick-inserter.js.map +1 -1
  58. package/build-module/components/inserter/search-results.js +10 -3
  59. package/build-module/components/inserter/search-results.js.map +1 -1
  60. package/build-module/components/link-control/index.js +16 -44
  61. package/build-module/components/link-control/index.js.map +1 -1
  62. package/build-module/components/off-canvas-editor/appender.js +28 -4
  63. package/build-module/components/off-canvas-editor/appender.js.map +1 -1
  64. package/build-module/components/off-canvas-editor/block-contents.js +1 -1
  65. package/build-module/components/off-canvas-editor/block-contents.js.map +1 -1
  66. package/build-module/components/off-canvas-editor/branch.js +5 -3
  67. package/build-module/components/off-canvas-editor/branch.js.map +1 -1
  68. package/build-module/components/off-canvas-editor/index.js +4 -1
  69. package/build-module/components/off-canvas-editor/index.js.map +1 -1
  70. package/build-module/private-apis.js +3 -1
  71. package/build-module/private-apis.js.map +1 -1
  72. package/build-module/store/actions.js +28 -14
  73. package/build-module/store/actions.js.map +1 -1
  74. package/build-module/store/defaults.js +28 -1
  75. package/build-module/store/defaults.js.map +1 -1
  76. package/build-module/store/selectors.js +1 -1
  77. package/build-module/store/selectors.js.map +1 -1
  78. package/build-style/style-rtl.css +46 -8
  79. package/build-style/style.css +46 -8
  80. package/package.json +4 -4
  81. package/src/components/block-inspector/index.js +11 -14
  82. package/src/components/block-inspector/useBlockInspectorAnimationSettings.js +53 -0
  83. package/src/components/block-settings-menu/block-settings-dropdown.js +4 -1
  84. package/src/components/inserter/index.js +30 -11
  85. package/src/components/inserter/media-tab/hooks.js +9 -8
  86. package/src/components/inserter/media-tab/media-list.js +3 -122
  87. package/src/components/inserter/media-tab/media-preview.js +268 -0
  88. package/src/components/inserter/menu.js +14 -20
  89. package/src/components/inserter/quick-inserter.js +2 -0
  90. package/src/components/inserter/search-results.js +7 -1
  91. package/src/components/inserter/style.scss +25 -0
  92. package/src/components/link-control/index.js +23 -58
  93. package/src/components/link-control/style.scss +23 -7
  94. package/src/components/link-control/test/index.js +5 -134
  95. package/src/components/media-replace-flow/test/index.js +1 -1
  96. package/src/components/off-canvas-editor/appender.js +30 -4
  97. package/src/components/off-canvas-editor/block-contents.js +1 -1
  98. package/src/components/off-canvas-editor/branch.js +3 -1
  99. package/src/components/off-canvas-editor/index.js +3 -0
  100. package/src/components/spacing-sizes-control/style.scss +1 -1
  101. package/src/private-apis.js +2 -0
  102. package/src/store/actions.js +16 -6
  103. package/src/store/defaults.js +14 -1
  104. package/src/store/selectors.js +4 -1
  105. package/src/store/test/actions.js +4 -2
@@ -0,0 +1,268 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import {
10
+ __unstableCompositeItem as CompositeItem,
11
+ Tooltip,
12
+ DropdownMenu,
13
+ MenuGroup,
14
+ MenuItem,
15
+ Spinner,
16
+ Modal,
17
+ Flex,
18
+ FlexItem,
19
+ Button,
20
+ __experimentalVStack as VStack,
21
+ } from '@wordpress/components';
22
+ import { __, sprintf } from '@wordpress/i18n';
23
+ import { useMemo, useCallback, useState } from '@wordpress/element';
24
+ import { cloneBlock } from '@wordpress/blocks';
25
+ import { moreVertical, external } from '@wordpress/icons';
26
+ import { useSelect, useDispatch } from '@wordpress/data';
27
+ import { store as noticesStore } from '@wordpress/notices';
28
+ import { isBlobURL } from '@wordpress/blob';
29
+
30
+ /**
31
+ * Internal dependencies
32
+ */
33
+ import InserterDraggableBlocks from '../../inserter-draggable-blocks';
34
+ import { getBlockAndPreviewFromMedia } from './utils';
35
+ import { store as blockEditorStore } from '../../../store';
36
+
37
+ const ALLOWED_MEDIA_TYPES = [ 'image' ];
38
+ const MAXIMUM_TITLE_LENGTH = 25;
39
+ const MEDIA_OPTIONS_POPOVER_PROPS = {
40
+ position: 'bottom left',
41
+ className:
42
+ 'block-editor-inserter__media-list__item-preview-options__popover',
43
+ };
44
+
45
+ function MediaPreviewOptions( { category, media } ) {
46
+ if ( ! category.getReportUrl ) {
47
+ return null;
48
+ }
49
+ const reportUrl = category.getReportUrl( media );
50
+ return (
51
+ <DropdownMenu
52
+ className="block-editor-inserter__media-list__item-preview-options"
53
+ label={ __( 'Options' ) }
54
+ popoverProps={ MEDIA_OPTIONS_POPOVER_PROPS }
55
+ icon={ moreVertical }
56
+ >
57
+ { () => (
58
+ <MenuGroup>
59
+ <MenuItem
60
+ onClick={ () =>
61
+ window.open( reportUrl, '_blank' ).focus()
62
+ }
63
+ icon={ external }
64
+ >
65
+ { sprintf(
66
+ /* translators: %s: The media type to report e.g: "image", "video", "audio" */
67
+ __( 'Report %s' ),
68
+ category.mediaType
69
+ ) }
70
+ </MenuItem>
71
+ </MenuGroup>
72
+ ) }
73
+ </DropdownMenu>
74
+ );
75
+ }
76
+
77
+ function InsertExternalImageModal( { onClose, onSubmit } ) {
78
+ return (
79
+ <Modal
80
+ title={ __( 'Insert external image' ) }
81
+ onRequestClose={ onClose }
82
+ className="block-editor-inserter-media-tab-media-preview-inserter-external-image-modal"
83
+ >
84
+ <VStack spacing={ 3 }>
85
+ <p>
86
+ { __(
87
+ 'This image cannot be uploaded to your Media Library, but it can still be inserted as an external image.'
88
+ ) }
89
+ </p>
90
+ <p>
91
+ { __(
92
+ 'External images can be removed by the external provider without warning and could even have legal compliance issues related to privacy legislation.'
93
+ ) }
94
+ </p>
95
+ </VStack>
96
+ <Flex
97
+ className="block-editor-block-lock-modal__actions"
98
+ justify="flex-end"
99
+ expanded={ false }
100
+ >
101
+ <FlexItem>
102
+ <Button variant="tertiary" onClick={ onClose }>
103
+ { __( 'Cancel' ) }
104
+ </Button>
105
+ </FlexItem>
106
+ <FlexItem>
107
+ <Button variant="primary" onClick={ onSubmit }>
108
+ { __( 'Insert' ) }
109
+ </Button>
110
+ </FlexItem>
111
+ </Flex>
112
+ </Modal>
113
+ );
114
+ }
115
+
116
+ export function MediaPreview( { media, onClick, composite, category } ) {
117
+ const [ showExternalUploadModal, setShowExternalUploadModal ] =
118
+ useState( false );
119
+ const [ isHovered, setIsHovered ] = useState( false );
120
+ const [ isInserting, setIsInserting ] = useState( false );
121
+ const [ block, preview ] = useMemo(
122
+ () => getBlockAndPreviewFromMedia( media, category.mediaType ),
123
+ [ media, category.mediaType ]
124
+ );
125
+ const { createErrorNotice, createSuccessNotice } =
126
+ useDispatch( noticesStore );
127
+ const mediaUpload = useSelect(
128
+ ( select ) => select( blockEditorStore ).getSettings().mediaUpload,
129
+ []
130
+ );
131
+ const onMediaInsert = useCallback(
132
+ ( previewBlock ) => {
133
+ // Prevent multiple uploads when we're in the process of inserting.
134
+ if ( isInserting ) {
135
+ return;
136
+ }
137
+ const clonedBlock = cloneBlock( previewBlock );
138
+ const { id, url, caption } = clonedBlock.attributes;
139
+ // Media item already exists in library, so just insert it.
140
+ if ( !! id ) {
141
+ onClick( clonedBlock );
142
+ return;
143
+ }
144
+ setIsInserting( true );
145
+ // Media item does not exist in library, so try to upload it.
146
+ // Fist fetch the image data. This may fail if the image host
147
+ // doesn't allow CORS with the domain.
148
+ // If this happens, we insert the image block using the external
149
+ // URL and let the user know about the possible implications.
150
+ window
151
+ .fetch( url )
152
+ .then( ( response ) => response.blob() )
153
+ .then( ( blob ) => {
154
+ mediaUpload( {
155
+ filesList: [ blob ],
156
+ additionalData: { caption },
157
+ onFileChange( [ img ] ) {
158
+ if ( isBlobURL( img.url ) ) {
159
+ return;
160
+ }
161
+ onClick( {
162
+ ...clonedBlock,
163
+ attributes: {
164
+ ...clonedBlock.attributes,
165
+ id: img.id,
166
+ url: img.url,
167
+ },
168
+ } );
169
+ createSuccessNotice(
170
+ __( 'Image uploaded and inserted.' ),
171
+ { type: 'snackbar' }
172
+ );
173
+ setIsInserting( false );
174
+ },
175
+ allowedTypes: ALLOWED_MEDIA_TYPES,
176
+ onError( message ) {
177
+ createErrorNotice( message, { type: 'snackbar' } );
178
+ setIsInserting( false );
179
+ },
180
+ } );
181
+ } )
182
+ .catch( () => {
183
+ setShowExternalUploadModal( true );
184
+ setIsInserting( false );
185
+ } );
186
+ },
187
+ [
188
+ isInserting,
189
+ onClick,
190
+ mediaUpload,
191
+ createErrorNotice,
192
+ createSuccessNotice,
193
+ ]
194
+ );
195
+ const title = media.title?.rendered || media.title;
196
+ let truncatedTitle;
197
+ if ( title.length > MAXIMUM_TITLE_LENGTH ) {
198
+ const omission = '...';
199
+ truncatedTitle =
200
+ title.slice( 0, MAXIMUM_TITLE_LENGTH - omission.length ) + omission;
201
+ }
202
+ const onMouseEnter = useCallback( () => setIsHovered( true ), [] );
203
+ const onMouseLeave = useCallback( () => setIsHovered( false ), [] );
204
+ return (
205
+ <>
206
+ <InserterDraggableBlocks isEnabled={ true } blocks={ [ block ] }>
207
+ { ( { draggable, onDragStart, onDragEnd } ) => (
208
+ <div
209
+ className={ classnames(
210
+ 'block-editor-inserter__media-list__list-item',
211
+ {
212
+ 'is-hovered': isHovered,
213
+ }
214
+ ) }
215
+ draggable={ draggable }
216
+ onDragStart={ onDragStart }
217
+ onDragEnd={ onDragEnd }
218
+ >
219
+ <Tooltip text={ truncatedTitle || title }>
220
+ { /* Adding `is-hovered` class to the wrapper element is needed
221
+ because the options Popover is rendered outside of this node. */ }
222
+ <div
223
+ onMouseEnter={ onMouseEnter }
224
+ onMouseLeave={ onMouseLeave }
225
+ >
226
+ <CompositeItem
227
+ role="option"
228
+ as="div"
229
+ { ...composite }
230
+ className="block-editor-inserter__media-list__item"
231
+ onClick={ () => onMediaInsert( block ) }
232
+ aria-label={ title }
233
+ >
234
+ <div className="block-editor-inserter__media-list__item-preview">
235
+ { preview }
236
+ { isInserting && (
237
+ <div className="block-editor-inserter__media-list__item-preview-spinner">
238
+ <Spinner />
239
+ </div>
240
+ ) }
241
+ </div>
242
+ </CompositeItem>
243
+ { ! isInserting && (
244
+ <MediaPreviewOptions
245
+ category={ category }
246
+ media={ media }
247
+ />
248
+ ) }
249
+ </div>
250
+ </Tooltip>
251
+ </div>
252
+ ) }
253
+ </InserterDraggableBlocks>
254
+ { showExternalUploadModal && (
255
+ <InsertExternalImageModal
256
+ onClose={ () => setShowExternalUploadModal( false ) }
257
+ onSubmit={ () => {
258
+ onClick( cloneBlock( block ) );
259
+ createSuccessNotice( __( 'Image inserted.' ), {
260
+ type: 'snackbar',
261
+ } );
262
+ setShowExternalUploadModal( false );
263
+ } }
264
+ />
265
+ ) }
266
+ </>
267
+ );
268
+ }
@@ -67,25 +67,19 @@ function InserterMenu(
67
67
  insertionIndex: __experimentalInsertionIndex,
68
68
  shouldFocusBlock,
69
69
  } );
70
- const { showPatterns, inserterItems, enableOpenverseMediaCategory } =
71
- useSelect(
72
- ( select ) => {
73
- const {
74
- __experimentalGetAllowedPatterns,
75
- getInserterItems,
76
- getSettings,
77
- } = select( blockEditorStore );
78
- return {
79
- showPatterns: !! __experimentalGetAllowedPatterns(
80
- destinationRootClientId
81
- ).length,
82
- inserterItems: getInserterItems( destinationRootClientId ),
83
- enableOpenverseMediaCategory:
84
- getSettings().enableOpenverseMediaCategory,
85
- };
86
- },
87
- [ destinationRootClientId ]
88
- );
70
+ const { showPatterns, inserterItems } = useSelect(
71
+ ( select ) => {
72
+ const { __experimentalGetAllowedPatterns, getInserterItems } =
73
+ select( blockEditorStore );
74
+ return {
75
+ showPatterns: !! __experimentalGetAllowedPatterns(
76
+ destinationRootClientId
77
+ ).length,
78
+ inserterItems: getInserterItems( destinationRootClientId ),
79
+ };
80
+ },
81
+ [ destinationRootClientId ]
82
+ );
89
83
  const hasReusableBlocks = useMemo( () => {
90
84
  return inserterItems.some(
91
85
  ( { category } ) => category === 'reusable'
@@ -93,7 +87,7 @@ function InserterMenu(
93
87
  }, [ inserterItems ] );
94
88
 
95
89
  const mediaCategories = useMediaCategories( destinationRootClientId );
96
- const showMedia = !! mediaCategories.length || enableOpenverseMediaCategory;
90
+ const showMedia = !! mediaCategories.length;
97
91
 
98
92
  const onInsert = useCallback(
99
93
  ( blocks, meta, shouldForceFocusBlock ) => {
@@ -32,6 +32,7 @@ export default function QuickInserter( {
32
32
  isAppender,
33
33
  prioritizePatterns,
34
34
  selectBlockOnInsert,
35
+ orderInitialBlockItems,
35
36
  } ) {
36
37
  const [ filterValue, setFilterValue ] = useState( '' );
37
38
  const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( {
@@ -124,6 +125,7 @@ export default function QuickInserter( {
124
125
  isDraggable={ false }
125
126
  prioritizePatterns={ prioritizePatterns }
126
127
  selectBlockOnInsert={ selectBlockOnInsert }
128
+ orderInitialBlockItems={ orderInitialBlockItems }
127
129
  />
128
130
  </div>
129
131
 
@@ -46,6 +46,7 @@ function InserterSearchResults( {
46
46
  shouldFocusBlock = true,
47
47
  prioritizePatterns,
48
48
  selectBlockOnInsert,
49
+ orderInitialBlockItems,
49
50
  } ) {
50
51
  const debouncedSpeak = useDebounce( speak, 500 );
51
52
 
@@ -88,8 +89,12 @@ function InserterSearchResults( {
88
89
  if ( maxBlockTypesToShow === 0 ) {
89
90
  return [];
90
91
  }
92
+ let orderedItems = orderBy( blockTypes, 'frecency', 'desc' );
93
+ if ( ! filterValue && orderInitialBlockItems ) {
94
+ orderedItems = orderInitialBlockItems( orderedItems );
95
+ }
91
96
  const results = searchBlockItems(
92
- orderBy( blockTypes, 'frecency', 'desc' ),
97
+ orderedItems,
93
98
  blockTypeCategories,
94
99
  blockTypeCollections,
95
100
  filterValue
@@ -104,6 +109,7 @@ function InserterSearchResults( {
104
109
  blockTypeCategories,
105
110
  blockTypeCollections,
106
111
  maxBlockTypes,
112
+ orderInitialBlockItems,
107
113
  ] );
108
114
 
109
115
  // Announce search results on change.
@@ -35,6 +35,7 @@ $block-inserter-tabs-height: 44px;
35
35
  .components-popover__content {
36
36
  border: none;
37
37
  outline: none;
38
+ box-shadow: $shadow-popover;
38
39
 
39
40
  .block-editor-inserter__quick-inserter > * {
40
41
  border-left: $border-width solid $gray-400;
@@ -42,10 +43,12 @@ $block-inserter-tabs-height: 44px;
42
43
 
43
44
  &:first-child {
44
45
  border-top: $border-width solid $gray-400;
46
+ border-radius: $radius-block-ui $radius-block-ui 0 0;
45
47
  }
46
48
 
47
49
  &:last-child {
48
50
  border-bottom: $border-width solid $gray-400;
51
+ border-radius: 0 0 $radius-block-ui $radius-block-ui;
49
52
  }
50
53
 
51
54
  &.components-button {
@@ -660,6 +663,17 @@ $block-inserter-tabs-height: 44px;
660
663
  margin: 0 auto;
661
664
  max-width: 100%;
662
665
  }
666
+
667
+ .block-editor-inserter__media-list__item-preview-spinner {
668
+ display: flex;
669
+ height: 100%;
670
+ width: 100%;
671
+ position: absolute;
672
+ justify-content: center;
673
+ background: rgba($white, 0.7);
674
+ align-items: center;
675
+ pointer-events: none;
676
+ }
663
677
  }
664
678
 
665
679
  &:focus .block-editor-inserter__media-list__item-preview {
@@ -686,3 +700,14 @@ $block-inserter-tabs-height: 44px;
686
700
  height: 100%;
687
701
  }
688
702
  }
703
+
704
+
705
+ .block-editor-inserter-media-tab-media-preview-inserter-external-image-modal {
706
+ @include break-small() {
707
+ max-width: $break-mobile;
708
+ }
709
+
710
+ p {
711
+ margin: 0;
712
+ }
713
+ }
@@ -7,6 +7,7 @@ import classnames from 'classnames';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { Button, Spinner, Notice, TextControl } from '@wordpress/components';
10
+ import { keyboardReturn } from '@wordpress/icons';
10
11
  import { __ } from '@wordpress/i18n';
11
12
  import { useRef, useState, useEffect } from '@wordpress/element';
12
13
  import { focus } from '@wordpress/dom';
@@ -112,7 +113,6 @@ function LinkControl( {
112
113
  settings = DEFAULT_LINK_SETTINGS,
113
114
  onChange = noop,
114
115
  onRemove,
115
- onCancel,
116
116
  noDirectEntry = false,
117
117
  showSuggestions = true,
118
118
  showInitialSuggestions,
@@ -190,8 +190,6 @@ function LinkControl( {
190
190
  isEndingEditWithFocus.current = false;
191
191
  }, [ isEditingLink, isCreatingPage ] );
192
192
 
193
- const hasLinkValue = value?.url?.trim()?.length > 0;
194
-
195
193
  /**
196
194
  * Cancels editing state and marks that focus may need to be restored after
197
195
  * the next render, if focus was within the wrapper when editing finished.
@@ -237,29 +235,6 @@ function LinkControl( {
237
235
  }
238
236
  };
239
237
 
240
- const resetInternalValues = () => {
241
- setInternalUrlInputValue( value?.url );
242
- setInternalTextInputValue( value?.title );
243
- };
244
-
245
- const handleCancel = ( event ) => {
246
- event.preventDefault();
247
- event.stopPropagation();
248
-
249
- // Ensure that any unsubmitted input changes are reset.
250
- resetInternalValues();
251
-
252
- if ( hasLinkValue ) {
253
- // If there is a link then exist editing mode and show preview.
254
- stopEditing();
255
- } else {
256
- // If there is no link value, then remove the link entirely.
257
- onRemove?.();
258
- }
259
-
260
- onCancel?.();
261
- };
262
-
263
238
  const currentUrlInputValue = propInputValue || internalUrlInputValue;
264
239
 
265
240
  const currentInputIsEmpty = ! currentUrlInputValue?.trim()?.length;
@@ -272,9 +247,8 @@ function LinkControl( {
272
247
  // Only show text control once a URL value has been committed
273
248
  // and it isn't just empty whitespace.
274
249
  // See https://github.com/WordPress/gutenberg/pull/33849/#issuecomment-932194927.
275
- const showTextControl = hasLinkValue && hasTextControl;
250
+ const showTextControl = value?.url?.trim()?.length > 0 && hasTextControl;
276
251
 
277
- const isEditing = ( isEditingLink || ! value ) && ! isCreatingPage;
278
252
  return (
279
253
  <div
280
254
  tabIndex={ -1 }
@@ -287,7 +261,7 @@ function LinkControl( {
287
261
  </div>
288
262
  ) }
289
263
 
290
- { isEditing && (
264
+ { ( isEditingLink || ! value ) && ! isCreatingPage && (
291
265
  <>
292
266
  <div
293
267
  className={ classnames( {
@@ -325,7 +299,17 @@ function LinkControl( {
325
299
  createSuggestionButtonText
326
300
  }
327
301
  useLabel={ showTextControl }
328
- />
302
+ >
303
+ <div className="block-editor-link-control__search-actions">
304
+ <Button
305
+ onClick={ handleSubmit }
306
+ label={ __( 'Submit' ) }
307
+ icon={ keyboardReturn }
308
+ className="block-editor-link-control__search-submit"
309
+ disabled={ currentInputIsEmpty } // Disallow submitting empty values.
310
+ />
311
+ </div>
312
+ </LinkControlSearchInput>
329
313
  </div>
330
314
  { errorMessage && (
331
315
  <Notice
@@ -350,34 +334,15 @@ function LinkControl( {
350
334
  />
351
335
  ) }
352
336
 
353
- <div className="block-editor-link-control__drawer">
354
- { showSettingsDrawer && (
355
- <div className="block-editor-link-control__tools">
356
- <LinkControlSettingsDrawer
357
- value={ value }
358
- settings={ settings }
359
- onChange={ onChange }
360
- />
361
- </div>
362
- ) }
363
-
364
- { isEditing && (
365
- <div className="block-editor-link-control__search-actions">
366
- <Button
367
- variant="primary"
368
- onClick={ handleSubmit }
369
- className="xblock-editor-link-control__search-submit"
370
- disabled={ currentInputIsEmpty } // Disallow submitting empty values.
371
- >
372
- { __( 'Apply' ) }
373
- </Button>
374
- <Button variant="tertiary" onClick={ handleCancel }>
375
- { __( 'Cancel' ) }
376
- </Button>
377
- </div>
378
- ) }
379
- </div>
380
-
337
+ { showSettingsDrawer && (
338
+ <div className="block-editor-link-control__tools">
339
+ <LinkControlSettingsDrawer
340
+ value={ value }
341
+ settings={ settings }
342
+ onChange={ onChange }
343
+ />
344
+ </div>
345
+ ) }
381
346
  { renderControlBottom && renderControlBottom() }
382
347
  </div>
383
348
  );
@@ -64,6 +64,7 @@ $preview-image-height: 140px;
64
64
  width: calc(100% - #{$grid-unit-20 * 2});
65
65
  display: block;
66
66
  padding: 11px $grid-unit-20;
67
+ padding-right: ( $button-size * $block-editor-link-control-number-of-actions ); // width of reset and submit buttons
67
68
  margin: 0;
68
69
  position: relative;
69
70
  border: 1px solid $gray-300;
@@ -76,10 +77,20 @@ $preview-image-height: 140px;
76
77
  }
77
78
 
78
79
  .block-editor-link-control__search-actions {
79
- display: flex;
80
- flex-direction: row-reverse; // put "Cancel" on the left but retain DOM order.
81
- justify-content: flex-start;
82
- gap: $grid-unit-10;
80
+ position: absolute;
81
+ /*
82
+ * Actions must be positioned on top of URLInput, since the input will grow
83
+ * when suggestions are rendered.
84
+ *
85
+ * Compensate for:
86
+ * - Border (1px)
87
+ * - Vertically, for the difference in height between the input (40px) and
88
+ * the icon buttons.
89
+ * - Horizontally, pad to the minimum of: default input padding, or the
90
+ * equivalent of the vertical padding.
91
+ */
92
+ top: 1px + ( ( 40px - $button-size ) * 0.5 );
93
+ right: $grid-unit-20 + 1px + min($grid-unit-10, ( 40px - $button-size ) * 0.5);
83
94
  }
84
95
 
85
96
  .components-button .block-editor-link-control__search-submit .has-icon {
@@ -426,10 +437,9 @@ $preview-image-height: 140px;
426
437
  padding: 10px;
427
438
  }
428
439
 
429
- .block-editor-link-control__drawer {
440
+ .block-editor-link-control__tools {
430
441
  display: flex;
431
442
  align-items: center;
432
- justify-content: space-between;
433
443
  border-top: $border-width solid $gray-300;
434
444
  margin: 0;
435
445
  padding: $grid-unit-20;
@@ -469,8 +479,14 @@ $preview-image-height: 140px;
469
479
  position: absolute;
470
480
  left: auto;
471
481
  bottom: auto;
482
+ /*
483
+ * Position spinner to the left of the actions.
484
+ *
485
+ * Compensate for:
486
+ * - Input padding right ($button-size)
487
+ */
472
488
  top: calc(50% - #{$spinner-size} / 2);
473
- right: $grid-unit-20;
489
+ right: $button-size;
474
490
  }
475
491
  }
476
492