@wordpress/block-editor 11.3.3 → 11.4.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 (192) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/components/block-popover/inbetween.js +10 -33
  3. package/build/components/block-popover/inbetween.js.map +1 -1
  4. package/build/components/block-types-list/index.native.js +2 -0
  5. package/build/components/block-types-list/index.native.js.map +1 -1
  6. package/build/components/colors-gradients/control.js +6 -3
  7. package/build/components/colors-gradients/control.js.map +1 -1
  8. package/build/components/font-appearance-control/index.js +0 -3
  9. package/build/components/font-appearance-control/index.js.map +1 -1
  10. package/build/components/global-styles/hooks.js +106 -45
  11. package/build/components/global-styles/hooks.js.map +1 -1
  12. package/build/components/global-styles/index.js +24 -0
  13. package/build/components/global-styles/index.js.map +1 -1
  14. package/build/components/global-styles/typography-panel.js +421 -0
  15. package/build/components/global-styles/typography-panel.js.map +1 -0
  16. package/build/components/global-styles/use-global-styles-output.js +6 -2
  17. package/build/components/global-styles/use-global-styles-output.js.map +1 -1
  18. package/build/components/iframe/index.js +17 -11
  19. package/build/components/iframe/index.js.map +1 -1
  20. package/build/components/image-editor/aspect-ratio-dropdown.js +2 -1
  21. package/build/components/image-editor/aspect-ratio-dropdown.js.map +1 -1
  22. package/build/components/image-size-control/index.js +6 -11
  23. package/build/components/image-size-control/index.js.map +1 -1
  24. package/build/components/inserter/block-patterns-tab.js +9 -15
  25. package/build/components/inserter/block-patterns-tab.js.map +1 -1
  26. package/build/components/inserter/block-types-tab.native.js +4 -1
  27. package/build/components/inserter/block-types-tab.native.js.map +1 -1
  28. package/build/components/inserter/reusable-blocks-tab.native.js +4 -1
  29. package/build/components/inserter/reusable-blocks-tab.native.js.map +1 -1
  30. package/build/components/inserter/search-results.native.js +4 -1
  31. package/build/components/inserter/search-results.native.js.map +1 -1
  32. package/build/components/link-control/index.js +15 -15
  33. package/build/components/link-control/index.js.map +1 -1
  34. package/build/components/link-control/settings-drawer.js +72 -30
  35. package/build/components/link-control/settings-drawer.js.map +1 -1
  36. package/build/components/link-control/settings.js +52 -0
  37. package/build/components/link-control/settings.js.map +1 -0
  38. package/build/components/list-view/index.js +6 -1
  39. package/build/components/list-view/index.js.map +1 -1
  40. package/build/components/off-canvas-editor/block-contents.js +3 -2
  41. package/build/components/off-canvas-editor/block-contents.js.map +1 -1
  42. package/build/components/off-canvas-editor/leaf-more-menu.js +116 -0
  43. package/build/components/off-canvas-editor/leaf-more-menu.js.map +1 -0
  44. package/build/components/off-canvas-editor/link-ui.js +1 -0
  45. package/build/components/off-canvas-editor/link-ui.js.map +1 -1
  46. package/build/components/rich-text/index.js +0 -2
  47. package/build/components/rich-text/index.js.map +1 -1
  48. package/build/components/url-input/index.js +2 -2
  49. package/build/components/url-input/index.js.map +1 -1
  50. package/build/hooks/dimensions.js +6 -0
  51. package/build/hooks/dimensions.js.map +1 -1
  52. package/build/hooks/font-family.js +2 -76
  53. package/build/hooks/font-family.js.map +1 -1
  54. package/build/hooks/font-size.js +3 -51
  55. package/build/hooks/font-size.js.map +1 -1
  56. package/build/hooks/gap.js +2 -1
  57. package/build/hooks/gap.js.map +1 -1
  58. package/build/hooks/index.js +2 -0
  59. package/build/hooks/index.js.map +1 -1
  60. package/build/hooks/line-height.js +0 -42
  61. package/build/hooks/line-height.js.map +1 -1
  62. package/build/hooks/typography.js +112 -127
  63. package/build/hooks/typography.js.map +1 -1
  64. package/build/private-apis.js +3 -0
  65. package/build/private-apis.js.map +1 -1
  66. package/build/store/selectors.js +18 -1
  67. package/build/store/selectors.js.map +1 -1
  68. package/build-module/components/block-popover/inbetween.js +10 -33
  69. package/build-module/components/block-popover/inbetween.js.map +1 -1
  70. package/build-module/components/block-types-list/index.native.js +2 -0
  71. package/build-module/components/block-types-list/index.native.js.map +1 -1
  72. package/build-module/components/colors-gradients/control.js +6 -3
  73. package/build-module/components/colors-gradients/control.js.map +1 -1
  74. package/build-module/components/font-appearance-control/index.js +2 -1
  75. package/build-module/components/font-appearance-control/index.js.map +1 -1
  76. package/build-module/components/global-styles/hooks.js +100 -46
  77. package/build-module/components/global-styles/hooks.js.map +1 -1
  78. package/build-module/components/global-styles/index.js +2 -1
  79. package/build-module/components/global-styles/index.js.map +1 -1
  80. package/build-module/components/global-styles/typography-panel.js +400 -0
  81. package/build-module/components/global-styles/typography-panel.js.map +1 -0
  82. package/build-module/components/global-styles/use-global-styles-output.js +6 -2
  83. package/build-module/components/global-styles/use-global-styles-output.js.map +1 -1
  84. package/build-module/components/iframe/index.js +17 -11
  85. package/build-module/components/iframe/index.js.map +1 -1
  86. package/build-module/components/image-editor/aspect-ratio-dropdown.js +2 -1
  87. package/build-module/components/image-editor/aspect-ratio-dropdown.js.map +1 -1
  88. package/build-module/components/image-size-control/index.js +7 -12
  89. package/build-module/components/image-size-control/index.js.map +1 -1
  90. package/build-module/components/inserter/block-patterns-tab.js +9 -15
  91. package/build-module/components/inserter/block-patterns-tab.js.map +1 -1
  92. package/build-module/components/inserter/block-types-tab.native.js +3 -1
  93. package/build-module/components/inserter/block-types-tab.native.js.map +1 -1
  94. package/build-module/components/inserter/reusable-blocks-tab.native.js +3 -1
  95. package/build-module/components/inserter/reusable-blocks-tab.native.js.map +1 -1
  96. package/build-module/components/inserter/search-results.native.js +3 -1
  97. package/build-module/components/inserter/search-results.native.js.map +1 -1
  98. package/build-module/components/link-control/index.js +16 -16
  99. package/build-module/components/link-control/index.js.map +1 -1
  100. package/build-module/components/link-control/settings-drawer.js +68 -30
  101. package/build-module/components/link-control/settings-drawer.js.map +1 -1
  102. package/build-module/components/link-control/settings.js +43 -0
  103. package/build-module/components/link-control/settings.js.map +1 -0
  104. package/build-module/components/list-view/index.js +6 -1
  105. package/build-module/components/list-view/index.js.map +1 -1
  106. package/build-module/components/off-canvas-editor/block-contents.js +3 -2
  107. package/build-module/components/off-canvas-editor/block-contents.js.map +1 -1
  108. package/build-module/components/off-canvas-editor/leaf-more-menu.js +99 -0
  109. package/build-module/components/off-canvas-editor/leaf-more-menu.js.map +1 -0
  110. package/build-module/components/off-canvas-editor/link-ui.js +1 -0
  111. package/build-module/components/off-canvas-editor/link-ui.js.map +1 -1
  112. package/build-module/components/rich-text/index.js +0 -2
  113. package/build-module/components/rich-text/index.js.map +1 -1
  114. package/build-module/components/url-input/index.js +2 -2
  115. package/build-module/components/url-input/index.js.map +1 -1
  116. package/build-module/hooks/dimensions.js +6 -0
  117. package/build-module/hooks/dimensions.js.map +1 -1
  118. package/build-module/hooks/font-family.js +3 -69
  119. package/build-module/hooks/font-family.js.map +1 -1
  120. package/build-module/hooks/font-size.js +6 -47
  121. package/build-module/hooks/font-size.js.map +1 -1
  122. package/build-module/hooks/gap.js +3 -2
  123. package/build-module/hooks/gap.js.map +1 -1
  124. package/build-module/hooks/index.js +1 -0
  125. package/build-module/hooks/index.js.map +1 -1
  126. package/build-module/hooks/line-height.js +0 -38
  127. package/build-module/hooks/line-height.js.map +1 -1
  128. package/build-module/hooks/typography.js +110 -123
  129. package/build-module/hooks/typography.js.map +1 -1
  130. package/build-module/private-apis.js +2 -0
  131. package/build-module/private-apis.js.map +1 -1
  132. package/build-module/store/selectors.js +18 -1
  133. package/build-module/store/selectors.js.map +1 -1
  134. package/build-style/style-rtl.css +35 -22
  135. package/build-style/style.css +35 -22
  136. package/package.json +31 -31
  137. package/src/components/block-popover/inbetween.js +21 -53
  138. package/src/components/block-types-list/index.native.js +2 -0
  139. package/src/components/colors-gradients/control.js +3 -0
  140. package/src/components/font-appearance-control/index.js +1 -1
  141. package/src/components/global-styles/hooks.js +186 -66
  142. package/src/components/global-styles/index.js +5 -0
  143. package/src/components/global-styles/typography-panel.js +403 -0
  144. package/src/components/global-styles/use-global-styles-output.js +6 -2
  145. package/src/components/iframe/index.js +20 -18
  146. package/src/components/image-editor/aspect-ratio-dropdown.js +1 -0
  147. package/src/components/image-size-control/index.js +10 -12
  148. package/src/components/image-size-control/style.scss +3 -21
  149. package/src/components/inserter/block-patterns-tab.js +9 -23
  150. package/src/components/inserter/block-types-tab.native.js +2 -0
  151. package/src/components/inserter/reusable-blocks-tab.native.js +2 -0
  152. package/src/components/inserter/search-results.native.js +2 -0
  153. package/src/components/link-control/index.js +22 -22
  154. package/src/components/link-control/settings-drawer.js +85 -30
  155. package/src/components/link-control/settings.js +41 -0
  156. package/src/components/link-control/style.scss +39 -7
  157. package/src/components/link-control/test/index.js +213 -4
  158. package/src/components/list-view/index.js +5 -0
  159. package/src/components/off-canvas-editor/block-contents.js +2 -1
  160. package/src/components/off-canvas-editor/leaf-more-menu.js +115 -0
  161. package/src/components/off-canvas-editor/link-ui.js +1 -0
  162. package/src/components/rich-text/index.js +0 -2
  163. package/src/components/url-input/index.js +3 -2
  164. package/src/hooks/dimensions.js +12 -0
  165. package/src/hooks/font-family.js +0 -58
  166. package/src/hooks/font-size.js +1 -36
  167. package/src/hooks/gap.js +9 -2
  168. package/src/hooks/index.js +1 -0
  169. package/src/hooks/line-height.js +0 -33
  170. package/src/hooks/typography.js +133 -212
  171. package/src/private-apis.js +2 -0
  172. package/src/store/selectors.js +16 -1
  173. package/build/hooks/font-appearance.js +0 -188
  174. package/build/hooks/font-appearance.js.map +0 -1
  175. package/build/hooks/letter-spacing.js +0 -129
  176. package/build/hooks/letter-spacing.js.map +0 -1
  177. package/build/hooks/text-decoration.js +0 -130
  178. package/build/hooks/text-decoration.js.map +0 -1
  179. package/build/hooks/text-transform.js +0 -130
  180. package/build/hooks/text-transform.js.map +0 -1
  181. package/build-module/hooks/font-appearance.js +0 -161
  182. package/build-module/hooks/font-appearance.js.map +0 -1
  183. package/build-module/hooks/letter-spacing.js +0 -107
  184. package/build-module/hooks/letter-spacing.js.map +0 -1
  185. package/build-module/hooks/text-decoration.js +0 -108
  186. package/build-module/hooks/text-decoration.js.map +0 -1
  187. package/build-module/hooks/text-transform.js +0 -108
  188. package/build-module/hooks/text-transform.js.map +0 -1
  189. package/src/hooks/font-appearance.js +0 -146
  190. package/src/hooks/letter-spacing.js +0 -101
  191. package/src/hooks/text-decoration.js +0 -102
  192. package/src/hooks/text-transform.js +0 -101
@@ -6,7 +6,7 @@ import classnames from 'classnames';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { Button, Spinner, Notice, TextControl } from '@wordpress/components';
9
+ import { Button, Spinner, Notice } from '@wordpress/components';
10
10
  import { __ } from '@wordpress/i18n';
11
11
  import { useRef, useState, useEffect } from '@wordpress/element';
12
12
  import { focus } from '@wordpress/dom';
@@ -136,6 +136,8 @@ function LinkControl( {
136
136
  const textInputRef = useRef();
137
137
  const isEndingEditWithFocus = useRef( false );
138
138
 
139
+ const [ settingsOpen, setSettingsOpen ] = useState( false );
140
+
139
141
  const [ internalUrlInputValue, setInternalUrlInputValue ] =
140
142
  useInternalInputValue( value?.url || '' );
141
143
 
@@ -201,6 +203,7 @@ function LinkControl( {
201
203
  wrapperNode.current.ownerDocument.activeElement
202
204
  );
203
205
 
206
+ setSettingsOpen( false );
204
207
  setIsEditingLink( false );
205
208
  };
206
209
 
@@ -267,7 +270,7 @@ function LinkControl( {
267
270
  const shownUnlinkControl =
268
271
  onRemove && value && ! isEditingLink && ! isCreatingPage;
269
272
 
270
- const showSettingsDrawer = !! settings?.length;
273
+ const showSettings = !! settings?.length;
271
274
 
272
275
  // Only show text control once a URL value has been committed
273
276
  // and it isn't just empty whitespace.
@@ -275,6 +278,7 @@ function LinkControl( {
275
278
  const showTextControl = hasLinkValue && hasTextControl;
276
279
 
277
280
  const isEditing = ( isEditingLink || ! value ) && ! isCreatingPage;
281
+
278
282
  return (
279
283
  <div
280
284
  tabIndex={ -1 }
@@ -295,18 +299,6 @@ function LinkControl( {
295
299
  'has-text-control': showTextControl,
296
300
  } ) }
297
301
  >
298
- { showTextControl && (
299
- <TextControl
300
- __nextHasNoMarginBottom
301
- ref={ textInputRef }
302
- className="block-editor-link-control__field block-editor-link-control__text-content"
303
- label="Text"
304
- value={ internalTextInputValue }
305
- onChange={ setInternalTextInputValue }
306
- onKeyDown={ handleSubmitWithEnter }
307
- />
308
- ) }
309
-
310
302
  <LinkControlSearchInput
311
303
  currentLink={ value }
312
304
  className="block-editor-link-control__field block-editor-link-control__search-input"
@@ -350,18 +342,26 @@ function LinkControl( {
350
342
  />
351
343
  ) }
352
344
 
353
- <div className="block-editor-link-control__drawer">
354
- { showSettingsDrawer && (
355
- <div className="block-editor-link-control__tools">
345
+ { isEditing && (
346
+ <div className="block-editor-link-control__tools">
347
+ { ( showSettings || showTextControl ) && (
356
348
  <LinkControlSettingsDrawer
349
+ settingsOpen={ settingsOpen }
350
+ setSettingsOpen={ setSettingsOpen }
351
+ showTextControl={ showTextControl }
352
+ showSettings={ showSettings }
353
+ textInputRef={ textInputRef }
354
+ internalTextInputValue={ internalTextInputValue }
355
+ setInternalTextInputValue={
356
+ setInternalTextInputValue
357
+ }
358
+ handleSubmitWithEnter={ handleSubmitWithEnter }
357
359
  value={ value }
358
360
  settings={ settings }
359
361
  onChange={ onChange }
360
362
  />
361
- </div>
362
- ) }
363
+ ) }
363
364
 
364
- { isEditing && (
365
365
  <div className="block-editor-link-control__search-actions">
366
366
  <Button
367
367
  variant="primary"
@@ -375,8 +375,8 @@ function LinkControl( {
375
375
  { __( 'Cancel' ) }
376
376
  </Button>
377
377
  </div>
378
- ) }
379
- </div>
378
+ </div>
379
+ ) }
380
380
 
381
381
  { renderControlBottom && renderControlBottom() }
382
382
  </div>
@@ -1,41 +1,96 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
+ import {
5
+ Button,
6
+ TextControl,
7
+ __unstableMotion as motion,
8
+ __unstableAnimatePresence as AnimatePresence,
9
+ } from '@wordpress/components';
10
+ import { settings as settingsIcon } from '@wordpress/icons';
11
+ import { useReducedMotion, useInstanceId } from '@wordpress/compose';
4
12
  import { __ } from '@wordpress/i18n';
5
- import { ToggleControl, VisuallyHidden } from '@wordpress/components';
6
-
7
- const noop = () => {};
13
+ import { Fragment } from '@wordpress/element';
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import Settings from './settings';
8
18
 
9
- const LinkControlSettingsDrawer = ( { value, onChange = noop, settings } ) => {
10
- if ( ! settings || ! settings.length ) {
11
- return null;
12
- }
19
+ function LinkSettingsDrawer( {
20
+ settingsOpen,
21
+ setSettingsOpen,
22
+ showTextControl,
23
+ showSettings,
24
+ textInputRef,
25
+ internalTextInputValue,
26
+ setInternalTextInputValue,
27
+ handleSubmitWithEnter,
28
+ value,
29
+ settings,
30
+ onChange,
31
+ } ) {
32
+ const prefersReducedMotion = useReducedMotion();
33
+ const MaybeAnimatePresence = prefersReducedMotion
34
+ ? Fragment
35
+ : AnimatePresence;
36
+ const MaybeMotionDiv = prefersReducedMotion ? 'div' : motion.div;
13
37
 
14
- const handleSettingChange = ( setting ) => ( newValue ) => {
15
- onChange( {
16
- ...value,
17
- [ setting.id ]: newValue,
18
- } );
19
- };
38
+ const id = useInstanceId( LinkSettingsDrawer );
20
39
 
21
- const theSettings = settings.map( ( setting ) => (
22
- <ToggleControl
23
- className="block-editor-link-control__setting"
24
- key={ setting.id }
25
- label={ setting.title }
26
- onChange={ handleSettingChange( setting ) }
27
- checked={ value ? !! value[ setting.id ] : false }
28
- />
29
- ) );
40
+ const settingsDrawerId = `link-control-settings-drawer-${ id }`;
30
41
 
31
42
  return (
32
- <fieldset className="block-editor-link-control__settings">
33
- <VisuallyHidden as="legend">
34
- { __( 'Currently selected link settings' ) }
35
- </VisuallyHidden>
36
- { theSettings }
37
- </fieldset>
43
+ <>
44
+ <Button
45
+ className="block-editor-link-control__drawer-toggle"
46
+ aria-expanded={ settingsOpen }
47
+ onClick={ () => setSettingsOpen( ! settingsOpen ) }
48
+ icon={ settingsIcon }
49
+ label={ __( 'Toggle link settings' ) }
50
+ aria-controls={ settingsDrawerId }
51
+ />
52
+ <MaybeAnimatePresence>
53
+ { settingsOpen && (
54
+ <MaybeMotionDiv
55
+ className="block-editor-link-control__drawer"
56
+ hidden={ ! settingsOpen }
57
+ id={ settingsDrawerId }
58
+ initial="collapsed"
59
+ animate="open"
60
+ exit="collapsed"
61
+ variants={ {
62
+ open: { opacity: 1, height: 'auto' },
63
+ collapsed: { opacity: 0, height: 0 },
64
+ } }
65
+ transition={ {
66
+ duration: 0.1,
67
+ } }
68
+ >
69
+ <div className="block-editor-link-control__drawer-inner">
70
+ { showTextControl && (
71
+ <TextControl
72
+ __nextHasNoMarginBottom
73
+ ref={ textInputRef }
74
+ className="block-editor-link-control__setting block-editor-link-control__text-content"
75
+ label="Text"
76
+ value={ internalTextInputValue }
77
+ onChange={ setInternalTextInputValue }
78
+ onKeyDown={ handleSubmitWithEnter }
79
+ />
80
+ ) }
81
+ { showSettings && (
82
+ <Settings
83
+ value={ value }
84
+ settings={ settings }
85
+ onChange={ onChange }
86
+ />
87
+ ) }
88
+ </div>
89
+ </MaybeMotionDiv>
90
+ ) }
91
+ </MaybeAnimatePresence>
92
+ </>
38
93
  );
39
- };
94
+ }
40
95
 
41
- export default LinkControlSettingsDrawer;
96
+ export default LinkSettingsDrawer;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { ToggleControl, VisuallyHidden } from '@wordpress/components';
6
+
7
+ const noop = () => {};
8
+
9
+ const LinkControlSettings = ( { value, onChange = noop, settings } ) => {
10
+ if ( ! settings || ! settings.length ) {
11
+ return null;
12
+ }
13
+
14
+ const handleSettingChange = ( setting ) => ( newValue ) => {
15
+ onChange( {
16
+ ...value,
17
+ [ setting.id ]: newValue,
18
+ } );
19
+ };
20
+
21
+ const theSettings = settings.map( ( setting ) => (
22
+ <ToggleControl
23
+ className="block-editor-link-control__setting"
24
+ key={ setting.id }
25
+ label={ setting.title }
26
+ onChange={ handleSettingChange( setting ) }
27
+ checked={ value ? !! value[ setting.id ] : false }
28
+ />
29
+ ) );
30
+
31
+ return (
32
+ <fieldset className="block-editor-link-control__settings">
33
+ <VisuallyHidden as="legend">
34
+ { __( 'Currently selected link settings' ) }
35
+ </VisuallyHidden>
36
+ { theSettings }
37
+ </fieldset>
38
+ );
39
+ };
40
+
41
+ export default LinkControlSettings;
@@ -80,6 +80,7 @@ $preview-image-height: 140px;
80
80
  flex-direction: row-reverse; // put "Cancel" on the left but retain DOM order.
81
81
  justify-content: flex-start;
82
82
  gap: $grid-unit-10;
83
+ order: 20;
83
84
  }
84
85
 
85
86
  .components-button .block-editor-link-control__search-submit .has-icon {
@@ -427,12 +428,48 @@ $preview-image-height: 140px;
427
428
  }
428
429
 
429
430
  .block-editor-link-control__drawer {
431
+ display: flex; // allow for ordering.
432
+ order: 30;
433
+ flex-direction: column;
434
+ flex-basis: 100%; // occupy full width.
435
+ }
436
+
437
+ // Inner div required to avoid padding/margin
438
+ // causing janky animation.
439
+ .block-editor-link-control__drawer-inner {
440
+ display: flex; // allow for ordering.
441
+ flex-direction: column;
442
+ flex-basis: 100%; // occupy full width.
443
+ margin-top: $grid-unit-20;
444
+ padding-top: $grid-unit-20;
445
+ position: relative;
446
+
447
+ &::after {
448
+ content: "";
449
+ display: block;
450
+ height: 1px;
451
+ background-color: $gray-300;
452
+ position: absolute;
453
+ left: -$grid-unit-20;
454
+ right: -$grid-unit-20;
455
+ top: 0;
456
+ }
457
+ }
458
+
459
+ .block-editor-link-control__tools {
430
460
  display: flex;
461
+ flex-wrap: wrap;
431
462
  align-items: center;
432
463
  justify-content: space-between;
433
- border-top: $border-width solid $gray-300;
434
464
  margin: 0;
435
465
  padding: $grid-unit-20;
466
+
467
+ // To hide the horizontal scrollbar on toggle.
468
+ // Margin and padding are needed to prevent cutoff of the toggle button focus outline.
469
+ // See: https://github.com/WordPress/gutenberg/pull/47986
470
+ margin-top: calc(var(--wp-admin-border-width-focus) * -1);
471
+ padding-top: var(--wp-admin-border-width-focus);
472
+ overflow: hidden;
436
473
  }
437
474
 
438
475
  .block-editor-link-control__unlink {
@@ -444,11 +481,6 @@ $preview-image-height: 140px;
444
481
  flex: 1;
445
482
  margin: 0;
446
483
 
447
-
448
- :last-child {
449
- margin-bottom: 0;
450
- }
451
-
452
484
  .is-alternate & {
453
485
  border-top: $border-width solid $gray-900;
454
486
  }
@@ -457,7 +489,7 @@ $preview-image-height: 140px;
457
489
  .block-editor-link-control__setting {
458
490
  margin-bottom: $grid-unit-20;
459
491
 
460
- :last-child {
492
+ &.block-editor-link-control__setting:last-child {
461
493
  margin-bottom: 0;
462
494
  }
463
495
  }
@@ -53,6 +53,11 @@ jest.mock( '@wordpress/data/src/components/use-dispatch', () => ( {
53
53
 
54
54
  jest.useRealTimers();
55
55
 
56
+ jest.mock( '@wordpress/compose', () => ( {
57
+ ...jest.requireActual( '@wordpress/compose' ),
58
+ useReducedMotion: jest.fn( () => true ),
59
+ } ) );
60
+
56
61
  beforeEach( () => {
57
62
  // Setup a DOM element as a render target.
58
63
  mockFetchSearchSuggestions.mockImplementation( fetchFauxEntitySuggestions );
@@ -137,7 +142,122 @@ describe( 'Basic rendering', () => {
137
142
  // Search Input UI.
138
143
  const searchInput = screen.getByRole( 'combobox', { name: 'URL' } );
139
144
 
140
- expect( searchInput ).toBeInTheDocument();
145
+ expect( searchInput ).toBeVisible();
146
+ } );
147
+
148
+ it( 'should have aria-owns attribute to follow the ARIA 1.0 pattern', () => {
149
+ render( <LinkControl /> );
150
+
151
+ // Search Input UI.
152
+ const searchInput = screen.getByRole( 'combobox', { name: 'URL' } );
153
+
154
+ expect( searchInput ).toBeVisible();
155
+ // Make sure we use the ARIA 1.0 pattern with aria-owns.
156
+ // See https://github.com/WordPress/gutenberg/issues/47147
157
+ expect( searchInput ).not.toHaveAttribute( 'aria-controls' );
158
+ expect( searchInput ).toHaveAttribute( 'aria-owns' );
159
+ } );
160
+
161
+ it( 'should have aria-selected attribute only on the highlighted item', async () => {
162
+ const user = userEvent.setup();
163
+
164
+ let resolver;
165
+ mockFetchSearchSuggestions.mockImplementation(
166
+ () =>
167
+ new Promise( ( resolve ) => {
168
+ resolver = resolve;
169
+ } )
170
+ );
171
+
172
+ render( <LinkControl /> );
173
+
174
+ // Search Input UI.
175
+ const searchInput = screen.getByRole( 'combobox', { name: 'URL' } );
176
+
177
+ // Simulate searching for a term.
178
+ await user.type( searchInput, 'Hello' );
179
+
180
+ // Wait for the spinner SVG icon to be rendered.
181
+ expect( await screen.findByRole( 'presentation' ) ).toBeVisible();
182
+ // Check the suggestions list is not rendered yet.
183
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
184
+
185
+ // Make the search suggestions fetch return a response.
186
+ resolver( fauxEntitySuggestions );
187
+
188
+ const resultsList = await screen.findByRole( 'listbox', {
189
+ name: 'Search results for "Hello"',
190
+ } );
191
+
192
+ // Check the suggestions list is rendered.
193
+ expect( resultsList ).toBeVisible();
194
+ // Check the spinner SVG icon is not rendered any longer.
195
+ expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
196
+
197
+ const searchResultElements =
198
+ within( resultsList ).getAllByRole( 'option' );
199
+
200
+ expect( searchResultElements ).toHaveLength(
201
+ // The fauxEntitySuggestions length plus the 'Press ENTER to add this link' button.
202
+ fauxEntitySuggestions.length + 1
203
+ );
204
+
205
+ // Step down into the search results, highlighting the first result item.
206
+ triggerArrowDown( searchInput );
207
+
208
+ const firstSearchSuggestion = searchResultElements[ 0 ];
209
+ const secondSearchSuggestion = searchResultElements[ 1 ];
210
+
211
+ let selectedSearchResultElement = screen.getByRole( 'option', {
212
+ selected: true,
213
+ } );
214
+
215
+ // We should have highlighted the first item using the keyboard.
216
+ expect( selectedSearchResultElement ).toEqual( firstSearchSuggestion );
217
+
218
+ // Check the aria-selected attribute is set only on the highlighted item.
219
+ expect( firstSearchSuggestion ).toHaveAttribute(
220
+ 'aria-selected',
221
+ 'true'
222
+ );
223
+ // Check the aria-selected attribute is omitted on the non-highlighted items.
224
+ expect( secondSearchSuggestion ).not.toHaveAttribute( 'aria-selected' );
225
+
226
+ // Step down into the search results, highlighting the second result item.
227
+ triggerArrowDown( searchInput );
228
+
229
+ selectedSearchResultElement = screen.getByRole( 'option', {
230
+ selected: true,
231
+ } );
232
+
233
+ // We should have highlighted the first item using the keyboard.
234
+ expect( selectedSearchResultElement ).toEqual( secondSearchSuggestion );
235
+
236
+ // Check the aria-selected attribute is omitted on non-highlighted items.
237
+ expect( firstSearchSuggestion ).not.toHaveAttribute( 'aria-selected' );
238
+ // Check the aria-selected attribute is set only on the highlighted item.
239
+ expect( secondSearchSuggestion ).toHaveAttribute(
240
+ 'aria-selected',
241
+ 'true'
242
+ );
243
+
244
+ // Step up into the search results, highlighting the first result item.
245
+ triggerArrowUp( searchInput );
246
+
247
+ selectedSearchResultElement = screen.getByRole( 'option', {
248
+ selected: true,
249
+ } );
250
+
251
+ // We should be back to highlighting the first search result again.
252
+ expect( selectedSearchResultElement ).toEqual( firstSearchSuggestion );
253
+
254
+ // Check the aria-selected attribute is set only on the highlighted item.
255
+ expect( firstSearchSuggestion ).toHaveAttribute(
256
+ 'aria-selected',
257
+ 'true'
258
+ );
259
+ // Check the aria-selected attribute is omitted on non-highlighted items.
260
+ expect( secondSearchSuggestion ).not.toHaveAttribute( 'aria-selected' );
141
261
  } );
142
262
 
143
263
  it( 'should not render protocol in links', async () => {
@@ -559,7 +679,7 @@ describe( 'Manual link entry', () => {
559
679
  } );
560
680
 
561
681
  // Verify the UI hasn't allowed submission.
562
- expect( searchInput ).toBeInTheDocument();
682
+ expect( searchInput ).toBeVisible();
563
683
  expect( submitButton ).toBeDisabled();
564
684
  expect( submitButton ).toBeVisible();
565
685
  }
@@ -601,7 +721,7 @@ describe( 'Manual link entry', () => {
601
721
  } );
602
722
 
603
723
  // Verify the UI hasn't allowed submission.
604
- expect( searchInput ).toBeInTheDocument();
724
+ expect( searchInput ).toBeVisible();
605
725
  expect( submitButton ).toBeDisabled();
606
726
  expect( submitButton ).toBeVisible();
607
727
  }
@@ -674,6 +794,8 @@ describe( 'Manual link entry', () => {
674
794
 
675
795
  await user.click( editButton );
676
796
 
797
+ await toggleSettingsDrawer( user );
798
+
677
799
  let searchInput = screen.getByRole( 'combobox', {
678
800
  name: 'URL',
679
801
  } );
@@ -706,6 +828,8 @@ describe( 'Manual link entry', () => {
706
828
 
707
829
  await user.click( editButton );
708
830
 
831
+ await toggleSettingsDrawer( user );
832
+
709
833
  // Re-query the inputs as they have been replaced.
710
834
  searchInput = screen.getByRole( 'combobox', {
711
835
  name: 'URL',
@@ -1534,6 +1658,62 @@ describe( 'Selecting links', () => {
1534
1658
  } );
1535
1659
 
1536
1660
  describe( 'Addition Settings UI', () => {
1661
+ it( 'should not show a means to toggle the link settings when not editing a link', async () => {
1662
+ const selectedLink = fauxEntitySuggestions[ 0 ];
1663
+
1664
+ const LinkControlConsumer = () => {
1665
+ const [ link ] = useState( selectedLink );
1666
+
1667
+ return <LinkControl value={ link } />;
1668
+ };
1669
+
1670
+ render( <LinkControlConsumer /> );
1671
+
1672
+ const settingsToggle = screen.queryByRole( 'button', {
1673
+ name: 'Toggle link settings',
1674
+ ariaControls: 'link-settings-1',
1675
+ } );
1676
+
1677
+ expect( settingsToggle ).not.toBeInTheDocument();
1678
+ } );
1679
+ it( 'should provides a means to toggle the link settings', async () => {
1680
+ const selectedLink = fauxEntitySuggestions[ 0 ];
1681
+
1682
+ const LinkControlConsumer = () => {
1683
+ const [ link ] = useState( selectedLink );
1684
+
1685
+ return <LinkControl value={ link } forceIsEditingLink />;
1686
+ };
1687
+
1688
+ render( <LinkControlConsumer /> );
1689
+
1690
+ const user = userEvent.setup();
1691
+
1692
+ const settingsToggle = screen.queryByRole( 'button', {
1693
+ name: 'Toggle link settings',
1694
+ ariaControls: 'link-settings-1',
1695
+ } );
1696
+
1697
+ expect( settingsToggle ).toHaveAttribute( 'aria-expanded', 'false' );
1698
+
1699
+ expect( settingsToggle ).toBeVisible();
1700
+
1701
+ await user.click( settingsToggle );
1702
+
1703
+ expect( settingsToggle ).toHaveAttribute( 'aria-expanded', 'true' );
1704
+
1705
+ const newTabSettingInput = screen.getByRole( 'checkbox', {
1706
+ name: 'Open in new tab',
1707
+ } );
1708
+
1709
+ expect( newTabSettingInput ).toBeVisible();
1710
+
1711
+ await user.click( settingsToggle );
1712
+
1713
+ expect( settingsToggle ).toHaveAttribute( 'aria-expanded', 'false' );
1714
+ expect( newTabSettingInput ).not.toBeVisible();
1715
+ } );
1716
+
1537
1717
  it( 'should display "New Tab" setting (in "off" mode) by default when a link is selected', async () => {
1538
1718
  const selectedLink = fauxEntitySuggestions[ 0 ];
1539
1719
  const expectedSettingText = 'Open in new tab';
@@ -1541,11 +1721,15 @@ describe( 'Addition Settings UI', () => {
1541
1721
  const LinkControlConsumer = () => {
1542
1722
  const [ link ] = useState( selectedLink );
1543
1723
 
1544
- return <LinkControl value={ link } />;
1724
+ return <LinkControl value={ link } forceIsEditingLink />;
1545
1725
  };
1546
1726
 
1547
1727
  render( <LinkControlConsumer /> );
1548
1728
 
1729
+ const user = userEvent.setup();
1730
+
1731
+ await toggleSettingsDrawer( user );
1732
+
1549
1733
  const newTabSettingLabel = screen.getByText( expectedSettingText );
1550
1734
  expect( newTabSettingLabel ).toBeVisible();
1551
1735
 
@@ -1578,12 +1762,17 @@ describe( 'Addition Settings UI', () => {
1578
1762
  <LinkControl
1579
1763
  value={ { ...link, newTab: false, noFollow: true } }
1580
1764
  settings={ customSettings }
1765
+ forceIsEditingLink
1581
1766
  />
1582
1767
  );
1583
1768
  };
1584
1769
 
1585
1770
  render( <LinkControlConsumer /> );
1586
1771
 
1772
+ const user = userEvent.setup();
1773
+
1774
+ await toggleSettingsDrawer( user );
1775
+
1587
1776
  expect( screen.queryAllByRole( 'checkbox' ) ).toHaveLength( 2 );
1588
1777
 
1589
1778
  expect(
@@ -1932,6 +2121,10 @@ describe( 'Controlling link title text', () => {
1932
2121
  />
1933
2122
  );
1934
2123
 
2124
+ const user = userEvent.setup();
2125
+
2126
+ await toggleSettingsDrawer( user );
2127
+
1935
2128
  expect(
1936
2129
  screen.queryByRole( 'textbox', { name: 'Text' } )
1937
2130
  ).toBeVisible();
@@ -1958,6 +2151,10 @@ describe( 'Controlling link title text', () => {
1958
2151
  />
1959
2152
  );
1960
2153
 
2154
+ const user = userEvent.setup();
2155
+
2156
+ await toggleSettingsDrawer( user );
2157
+
1961
2158
  const textInput = screen.queryByRole( 'textbox', {
1962
2159
  name: 'Text',
1963
2160
  } );
@@ -1980,6 +2177,8 @@ describe( 'Controlling link title text', () => {
1980
2177
  />
1981
2178
  );
1982
2179
 
2180
+ await toggleSettingsDrawer( user );
2181
+
1983
2182
  const textInput = screen.queryByRole( 'textbox', { name: 'Text' } );
1984
2183
 
1985
2184
  await user.clear( textInput );
@@ -2014,6 +2213,8 @@ describe( 'Controlling link title text', () => {
2014
2213
  />
2015
2214
  );
2016
2215
 
2216
+ await toggleSettingsDrawer( user );
2217
+
2017
2218
  const textInput = screen.queryByRole( 'textbox', { name: 'Text' } );
2018
2219
 
2019
2220
  expect( textInput ).toBeVisible();
@@ -2037,3 +2238,11 @@ describe( 'Controlling link title text', () => {
2037
2238
  ).not.toBeInTheDocument();
2038
2239
  } );
2039
2240
  } );
2241
+
2242
+ async function toggleSettingsDrawer( user ) {
2243
+ const settingsToggle = screen.queryByRole( 'button', {
2244
+ name: 'Toggle link settings',
2245
+ } );
2246
+
2247
+ await user.click( settingsToggle );
2248
+ }
@@ -174,6 +174,11 @@ function ListView(
174
174
  [ isMounted.current, draggedClientIds, expandedState, expand, collapse ]
175
175
  );
176
176
 
177
+ // If there are no blocks to show, do not render the list view.
178
+ if ( ! clientIdsTree.length ) {
179
+ return null;
180
+ }
181
+
177
182
  return (
178
183
  <AsyncModeProvider value={ true }>
179
184
  <ListViewDropIndicator
@@ -73,7 +73,7 @@ const ListViewBlockContents = forwardRef(
73
73
  setInsertedBlockAttributes,
74
74
  } = useInsertedBlock( lastInsertedBlockClientId );
75
75
 
76
- const hasExistingLinkValue = insertedBlockAttributes?.id;
76
+ const hasExistingLinkValue = insertedBlockAttributes?.url;
77
77
 
78
78
  useEffect( () => {
79
79
  if (
@@ -121,6 +121,7 @@ const ListViewBlockContents = forwardRef(
121
121
  );
122
122
  setIsLinkUIOpen( false );
123
123
  } }
124
+ onCancel={ () => setIsLinkUIOpen( false ) }
124
125
  />
125
126
  ) }
126
127
  <BlockDraggable clientIds={ draggableClientIds }>