@wordpress/block-library 8.15.0 → 8.16.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 (156) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/avatar/index.js +3 -0
  3. package/build/avatar/index.js.map +1 -1
  4. package/build/block/edit.js +2 -30
  5. package/build/block/edit.js.map +1 -1
  6. package/build/cover/index.js +2 -1
  7. package/build/cover/index.js.map +1 -1
  8. package/build/footnotes/edit.js +11 -0
  9. package/build/footnotes/edit.js.map +1 -1
  10. package/build/footnotes/format.js +101 -8
  11. package/build/footnotes/format.js.map +1 -1
  12. package/build/footnotes/index.js +45 -3
  13. package/build/footnotes/index.js.map +1 -1
  14. package/build/gallery/edit.js +7 -5
  15. package/build/gallery/edit.js.map +1 -1
  16. package/build/image/deprecated.js +106 -2
  17. package/build/image/deprecated.js.map +1 -1
  18. package/build/image/image.js +2 -2
  19. package/build/image/image.js.map +1 -1
  20. package/build/image/index.js +2 -1
  21. package/build/image/index.js.map +1 -1
  22. package/build/index.js +3 -1
  23. package/build/index.js.map +1 -1
  24. package/build/list-item/hooks/use-merge.js +10 -1
  25. package/build/list-item/hooks/use-merge.js.map +1 -1
  26. package/build/navigation/edit/menu-inspector-controls.js +1 -1
  27. package/build/navigation/edit/menu-inspector-controls.js.map +1 -1
  28. package/build/navigation/edit/navigation-menu-selector.js +4 -4
  29. package/build/navigation/edit/navigation-menu-selector.js.map +1 -1
  30. package/build/navigation/view-modal.js +93 -32
  31. package/build/navigation/view-modal.js.map +1 -1
  32. package/build/navigation/view.js +63 -31
  33. package/build/navigation/view.js.map +1 -1
  34. package/build/pattern/edit.js +28 -4
  35. package/build/pattern/edit.js.map +1 -1
  36. package/build/preformatted/index.js +4 -0
  37. package/build/preformatted/index.js.map +1 -1
  38. package/build/search/view.js +166 -62
  39. package/build/search/view.js.map +1 -1
  40. package/build/social-link/icons/index.js +13 -0
  41. package/build/social-link/icons/index.js.map +1 -1
  42. package/build/social-link/icons/threads.js +25 -0
  43. package/build/social-link/icons/threads.js.map +1 -0
  44. package/build/social-link/variations.js +7 -0
  45. package/build/social-link/variations.js.map +1 -1
  46. package/build/template-part/edit/import-controls.js +1 -1
  47. package/build/template-part/edit/import-controls.js.map +1 -1
  48. package/build-module/avatar/index.js +3 -0
  49. package/build-module/avatar/index.js.map +1 -1
  50. package/build-module/block/edit.js +4 -29
  51. package/build-module/block/edit.js.map +1 -1
  52. package/build-module/cover/index.js +2 -1
  53. package/build-module/cover/index.js.map +1 -1
  54. package/build-module/footnotes/edit.js +11 -0
  55. package/build-module/footnotes/edit.js.map +1 -1
  56. package/build-module/footnotes/format.js +102 -10
  57. package/build-module/footnotes/format.js.map +1 -1
  58. package/build-module/footnotes/index.js +45 -3
  59. package/build-module/footnotes/index.js.map +1 -1
  60. package/build-module/gallery/edit.js +7 -5
  61. package/build-module/gallery/edit.js.map +1 -1
  62. package/build-module/image/deprecated.js +107 -3
  63. package/build-module/image/deprecated.js.map +1 -1
  64. package/build-module/image/image.js +2 -2
  65. package/build-module/image/image.js.map +1 -1
  66. package/build-module/image/index.js +2 -1
  67. package/build-module/image/index.js.map +1 -1
  68. package/build-module/index.js +3 -1
  69. package/build-module/index.js.map +1 -1
  70. package/build-module/list-item/hooks/use-merge.js +10 -1
  71. package/build-module/list-item/hooks/use-merge.js.map +1 -1
  72. package/build-module/navigation/edit/menu-inspector-controls.js +1 -1
  73. package/build-module/navigation/edit/menu-inspector-controls.js.map +1 -1
  74. package/build-module/navigation/edit/navigation-menu-selector.js +4 -4
  75. package/build-module/navigation/edit/navigation-menu-selector.js.map +1 -1
  76. package/build-module/navigation/view-modal.js +93 -31
  77. package/build-module/navigation/view-modal.js.map +1 -1
  78. package/build-module/navigation/view.js +63 -31
  79. package/build-module/navigation/view.js.map +1 -1
  80. package/build-module/pattern/edit.js +27 -4
  81. package/build-module/pattern/edit.js.map +1 -1
  82. package/build-module/preformatted/index.js +4 -0
  83. package/build-module/preformatted/index.js.map +1 -1
  84. package/build-module/search/view.js +166 -62
  85. package/build-module/search/view.js.map +1 -1
  86. package/build-module/social-link/icons/index.js +1 -0
  87. package/build-module/social-link/icons/index.js.map +1 -1
  88. package/build-module/social-link/icons/threads.js +15 -0
  89. package/build-module/social-link/icons/threads.js.map +1 -0
  90. package/build-module/social-link/variations.js +8 -1
  91. package/build-module/social-link/variations.js.map +1 -1
  92. package/build-module/template-part/edit/import-controls.js +2 -2
  93. package/build-module/template-part/edit/import-controls.js.map +1 -1
  94. package/build-style/preformatted/style-rtl.css +2 -1
  95. package/build-style/preformatted/style.css +2 -1
  96. package/build-style/social-links/style-rtl.css +7 -0
  97. package/build-style/social-links/style.css +7 -0
  98. package/build-style/style-rtl.css +10 -1
  99. package/build-style/style.css +10 -1
  100. package/build-style/video/style-rtl.css +1 -0
  101. package/build-style/video/style.css +1 -0
  102. package/package.json +32 -32
  103. package/src/audio/test/__snapshots__/edit.native.js.snap +60 -0
  104. package/src/avatar/block.json +3 -0
  105. package/src/block/edit.js +1 -39
  106. package/src/buttons/test/edit.native.js +4 -0
  107. package/src/columns/test/edit.native.js +5 -0
  108. package/src/comment-template/index.php +2 -0
  109. package/src/cover/block.json +2 -1
  110. package/src/cover/test/edit.native.js +8 -0
  111. package/src/embed/test/index.native.js +8 -0
  112. package/src/file/index.php +1 -1
  113. package/src/file/test/__snapshots__/edit.native.js.snap +61 -0
  114. package/src/footnotes/block.json +44 -1
  115. package/src/footnotes/edit.js +12 -0
  116. package/src/footnotes/format.js +70 -7
  117. package/src/footnotes/index.js +0 -1
  118. package/src/footnotes/index.php +207 -0
  119. package/src/gallery/edit.js +41 -37
  120. package/src/gallery/test/index.native.js +15 -3
  121. package/src/heading/test/index.native.js +4 -0
  122. package/src/image/block.json +2 -1
  123. package/src/image/deprecated.js +109 -3
  124. package/src/image/image.js +2 -2
  125. package/src/image/index.php +1 -3
  126. package/src/image/test/edit.native.js +0 -1
  127. package/src/index.js +5 -1
  128. package/src/list/test/edit.native.js +5 -0
  129. package/src/list-item/hooks/use-merge.js +12 -5
  130. package/src/missing/test/__snapshots__/edit.native.js.snap +21 -0
  131. package/src/navigation/edit/menu-inspector-controls.js +1 -1
  132. package/src/navigation/edit/navigation-menu-selector.js +8 -4
  133. package/src/navigation/index.php +27 -13
  134. package/src/navigation/view-modal.js +88 -39
  135. package/src/navigation/view.js +69 -36
  136. package/src/paragraph/test/edit.native.js +55 -35
  137. package/src/pattern/edit.js +21 -0
  138. package/src/pattern/index.php +13 -1
  139. package/src/post-template/index.php +2 -0
  140. package/src/post-title/index.php +2 -0
  141. package/src/preformatted/block.json +4 -0
  142. package/src/preformatted/style.scss +4 -1
  143. package/src/pullquote/test/edit.native.js +12 -4
  144. package/src/quote/test/edit.native.js +12 -4
  145. package/src/search/index.php +4 -0
  146. package/src/search/test/__snapshots__/edit.native.js.snap +63 -0
  147. package/src/search/view.js +171 -67
  148. package/src/social-link/icons/index.js +1 -0
  149. package/src/social-link/icons/threads.js +10 -0
  150. package/src/social-link/index.php +4 -0
  151. package/src/social-link/socials-with-bg.scss +5 -0
  152. package/src/social-link/socials-without-bg.scss +4 -0
  153. package/src/social-link/variations.js +7 -0
  154. package/src/template-part/edit/import-controls.js +2 -2
  155. package/src/template-part/index.php +6 -9
  156. package/src/video/style.scss +1 -0
package/src/index.js CHANGED
@@ -282,7 +282,11 @@ export const registerCoreBlocks = (
282
282
  blocks.forEach( ( { init } ) => init() );
283
283
 
284
284
  setDefaultBlockName( paragraph.name );
285
- if ( window.wp && window.wp.oldEditor ) {
285
+ if (
286
+ window.wp &&
287
+ window.wp.oldEditor &&
288
+ blocks.some( ( { name } ) => name === classic.name )
289
+ ) {
286
290
  setFreeformContentHandlerName( classic.name );
287
291
  }
288
292
  setUnregisteredTypeHandlerName( missing.name );
@@ -2,6 +2,7 @@
2
2
  * External dependencies
3
3
  */
4
4
  import {
5
+ act,
5
6
  selectRangeInRichText,
6
7
  typeInRichText,
7
8
  fireEvent,
@@ -577,6 +578,10 @@ describe( 'List block', () => {
577
578
  preventDefault() {},
578
579
  keyCode: BACKSPACE,
579
580
  } );
581
+ // Inner blocks batch store updates with microtasks.
582
+ // To avoid `act` warnings, we let queued microtasks to be executed.
583
+ // Reference: https://t.ly/b95nA
584
+ await act( async () => {} );
580
585
 
581
586
  expect( getEditorHtml() ).toMatchInlineSnapshot( `
582
587
  "<!-- wp:paragraph -->
@@ -107,11 +107,18 @@ export default function useMerge( clientId, onMerge ) {
107
107
  } else if ( previousBlockClientId ) {
108
108
  const trailingId = getTrailingId( previousBlockClientId );
109
109
  registry.batch( () => {
110
- moveBlocksToPosition(
111
- getBlockOrder( clientId ),
112
- clientId,
113
- previousBlockClientId
114
- );
110
+ // When merging a list item with a previous trailing list
111
+ // item, we also need to move any nested list items. First,
112
+ // check if there's a listed list. If there's a nested list,
113
+ // append its nested list items to the trailing list.
114
+ const [ nestedListClientId ] = getBlockOrder( clientId );
115
+ if ( nestedListClientId ) {
116
+ moveBlocksToPosition(
117
+ getBlockOrder( nestedListClientId ),
118
+ nestedListClientId,
119
+ getBlockRootClientId( trailingId )
120
+ );
121
+ }
115
122
  mergeBlocks( trailingId, clientId );
116
123
  } );
117
124
  } else {
@@ -7,7 +7,11 @@ exports[`Missing block renders without crashing 1`] = `
7
7
  accessibilityRole="button"
8
8
  accessibilityState={
9
9
  {
10
+ "busy": undefined,
11
+ "checked": undefined,
10
12
  "disabled": true,
13
+ "expanded": undefined,
14
+ "selected": undefined,
11
15
  }
12
16
  }
13
17
  accessible={true}
@@ -24,6 +28,23 @@ exports[`Missing block renders without crashing 1`] = `
24
28
  accessibilityHint="Tap here to show help"
25
29
  accessibilityLabel="Help button"
26
30
  accessibilityRole="button"
31
+ accessibilityState={
32
+ {
33
+ "busy": undefined,
34
+ "checked": undefined,
35
+ "disabled": undefined,
36
+ "expanded": undefined,
37
+ "selected": undefined,
38
+ }
39
+ }
40
+ accessibilityValue={
41
+ {
42
+ "max": undefined,
43
+ "min": undefined,
44
+ "now": undefined,
45
+ "text": undefined,
46
+ }
47
+ }
27
48
  accessible={true}
28
49
  collapsable={false}
29
50
  focusable={true}
@@ -103,7 +103,7 @@ const MainContent = ( {
103
103
  ? sprintf(
104
104
  /* translators: %s: The name of a menu. */
105
105
  __( 'Structure for navigation menu: %s' ),
106
- navigationMenu?.title?.rendered || __( 'Untitled menu' )
106
+ navigationMenu?.title || __( 'Untitled menu' )
107
107
  )
108
108
  : __(
109
109
  'You have not yet created any menus. Displaying a list of your Pages'
@@ -20,19 +20,19 @@ import useNavigationMenu from '../use-navigation-menu';
20
20
  import useNavigationEntities from '../use-navigation-entities';
21
21
 
22
22
  function buildMenuLabel( title, id, status ) {
23
- if ( ! title?.rendered ) {
23
+ if ( ! title ) {
24
24
  /* translators: %s is the index of the menu in the list of menus. */
25
25
  return sprintf( __( '(no title %s)' ), id );
26
26
  }
27
27
 
28
28
  if ( status === 'publish' ) {
29
- return decodeEntities( title?.rendered );
29
+ return decodeEntities( title );
30
30
  }
31
31
 
32
32
  return sprintf(
33
33
  // translators: %1s: title of the menu; %2s: status of the menu (draft, pending, etc.).
34
34
  __( '%1$s (%2$s)' ),
35
- decodeEntities( title?.rendered ),
35
+ decodeEntities( title ),
36
36
  status
37
37
  );
38
38
  }
@@ -72,7 +72,11 @@ function NavigationMenuSelector( {
72
72
  const menuChoices = useMemo( () => {
73
73
  return (
74
74
  navigationMenus?.map( ( { id, title, status }, index ) => {
75
- const label = buildMenuLabel( title, index + 1, status );
75
+ const label = buildMenuLabel(
76
+ title?.rendered,
77
+ index + 1,
78
+ status
79
+ );
76
80
 
77
81
  return {
78
82
  value: id,
@@ -671,19 +671,33 @@ function render_block_core_navigation( $attributes, $content, $block ) {
671
671
  $inner_blocks_html .= '</ul>';
672
672
  }
673
673
 
674
- // If the script already exists, there is no point in removing it from viewScript.
675
- $should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) );
676
- $view_js_file = 'wp-block-navigation-view';
677
- if ( ! wp_script_is( $view_js_file ) ) {
678
- $script_handles = $block->block_type->view_script_handles;
679
-
680
- // If the script is not needed, and it is still in the `view_script_handles`, remove it.
681
- if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) {
682
- $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file, 'wp-block-navigation-view-2' ) );
683
- }
684
- // If the script is needed, but it was previously removed, add it again.
685
- if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) {
686
- $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file, 'wp-block-navigation-view-2' ) );
674
+ $needed_script_map = array(
675
+ 'wp-block-navigation-view' => ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ),
676
+ 'wp-block-navigation-view-2' => $is_responsive_menu,
677
+ );
678
+
679
+ $should_load_view_script = false;
680
+ if ( gutenberg_should_block_use_interactivity_api( 'core/navigation' ) ) {
681
+ // TODO: The script is still loaded even when it isn't needed when the Interactivity API is used.
682
+ $should_load_view_script = count( array_filter( $needed_script_map ) ) > 0;
683
+ } else {
684
+ foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) {
685
+
686
+ // If the script already exists, there is no point in removing it from viewScript.
687
+ if ( wp_script_is( $view_script_handle ) ) {
688
+ continue;
689
+ }
690
+
691
+ $script_handles = $block->block_type->view_script_handles;
692
+
693
+ // If the script is not needed, and it is still in the `view_script_handles`, remove it.
694
+ if ( ! $is_view_script_needed && in_array( $view_script_handle, $script_handles, true ) ) {
695
+ $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_script_handle ) );
696
+ }
697
+ // If the script is needed, but it was previously removed, add it again.
698
+ if ( $is_view_script_needed && ! in_array( $view_script_handle, $script_handles, true ) ) {
699
+ $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_script_handle ) );
700
+ }
687
701
  }
688
702
  }
689
703
 
@@ -1,16 +1,22 @@
1
+ /*eslint-env browser*/
1
2
  /**
2
3
  * External dependencies
3
4
  */
4
5
  import MicroModal from 'micromodal';
5
6
 
6
7
  // Responsive navigation toggle.
7
- function navigationToggleModal( modal ) {
8
+
9
+ /**
10
+ * Toggles responsive navigation.
11
+ *
12
+ * @param {HTMLDivElement} modal
13
+ * @param {boolean} isHidden
14
+ */
15
+ function navigationToggleModal( modal, isHidden ) {
8
16
  const dialogContainer = modal.querySelector(
9
17
  `.wp-block-navigation__responsive-dialog`
10
18
  );
11
19
 
12
- const isHidden = 'true' === modal.getAttribute( 'aria-hidden' );
13
-
14
20
  modal.classList.toggle( 'has-modal-open', ! isHidden );
15
21
  dialogContainer.toggleAttribute( 'aria-modal', ! isHidden );
16
22
 
@@ -23,10 +29,15 @@ function navigationToggleModal( modal ) {
23
29
  }
24
30
 
25
31
  // Add a class to indicate the modal is open.
26
- const htmlElement = document.documentElement;
27
- htmlElement.classList.toggle( 'has-modal-open' );
32
+ document.documentElement.classList.toggle( 'has-modal-open' );
28
33
  }
29
34
 
35
+ /**
36
+ * Checks whether the provided link is an anchor on the current page.
37
+ *
38
+ * @param {HTMLAnchorElement} node
39
+ * @return {boolean} Is anchor.
40
+ */
30
41
  function isLinkToAnchorOnCurrentPage( node ) {
31
42
  return (
32
43
  node.hash &&
@@ -37,42 +48,80 @@ function isLinkToAnchorOnCurrentPage( node ) {
37
48
  );
38
49
  }
39
50
 
40
- window.addEventListener( 'load', () => {
41
- MicroModal.init( {
42
- onShow: navigationToggleModal,
43
- onClose: navigationToggleModal,
44
- openClass: 'is-menu-open',
51
+ /**
52
+ * Handles effects after opening the modal.
53
+ *
54
+ * @param {HTMLDivElement} modal
55
+ */
56
+ function onShow( modal ) {
57
+ navigationToggleModal( modal, false );
58
+ modal.addEventListener( 'click', handleAnchorLinkClicksInsideModal, {
59
+ passive: true,
45
60
  } );
61
+ }
46
62
 
47
- // Close modal automatically on clicking anchor links inside modal.
48
- const navigationLinks = document.querySelectorAll(
49
- '.wp-block-navigation-item__content'
50
- );
63
+ /**
64
+ * Handles effects after closing the modal.
65
+ *
66
+ * @param {HTMLDivElement} modal
67
+ */
68
+ function onClose( modal ) {
69
+ navigationToggleModal( modal, true );
70
+ modal.removeEventListener( 'click', handleAnchorLinkClicksInsideModal, {
71
+ passive: true,
72
+ } );
73
+ }
51
74
 
52
- navigationLinks.forEach( function ( link ) {
53
- // Ignore non-anchor links and anchor links which open on a new tab.
54
- if (
55
- ! isLinkToAnchorOnCurrentPage( link ) ||
56
- link.attributes?.target === '_blank'
57
- ) {
58
- return;
59
- }
75
+ /**
76
+ * Handle clicks to anchor links in modal using event delegation by closing modal automatically
77
+ *
78
+ * @param {UIEvent} event
79
+ */
80
+ function handleAnchorLinkClicksInsideModal( event ) {
81
+ const link = event.target.closest( '.wp-block-navigation-item__content' );
82
+ if ( ! ( link instanceof HTMLAnchorElement ) ) {
83
+ return;
84
+ }
60
85
 
61
- // Find the specific parent modal for this link
62
- // since .close() won't work without an ID if there are
63
- // multiple navigation menus in a post/page.
64
- const modal = link.closest(
65
- '.wp-block-navigation__responsive-container'
66
- );
67
- const modalId = modal?.getAttribute( 'id' );
86
+ // Ignore non-anchor links and anchor links which open on a new tab.
87
+ if (
88
+ ! isLinkToAnchorOnCurrentPage( link ) ||
89
+ link.attributes?.target === '_blank'
90
+ ) {
91
+ return;
92
+ }
68
93
 
69
- link.addEventListener( 'click', () => {
70
- // check if modal exists and is open before trying to close it
71
- // otherwise Micromodal will toggle the `has-modal-open` class
72
- // on the html tag which prevents scrolling
73
- if ( modalId && modal.classList.contains( 'has-modal-open' ) ) {
74
- MicroModal.close( modalId );
75
- }
76
- } );
77
- } );
78
- } );
94
+ // Find the specific parent modal for this link
95
+ // since .close() won't work without an ID if there are
96
+ // multiple navigation menus in a post/page.
97
+ const modal = link.closest( '.wp-block-navigation__responsive-container' );
98
+ const modalId = modal?.getAttribute( 'id' );
99
+ if ( ! modalId ) {
100
+ return;
101
+ }
102
+
103
+ // check if modal exists and is open before trying to close it
104
+ // otherwise Micromodal will toggle the `has-modal-open` class
105
+ // on the html tag which prevents scrolling
106
+ if ( modalId && modal.classList.contains( 'has-modal-open' ) ) {
107
+ MicroModal.close( modalId );
108
+ }
109
+ }
110
+
111
+ // MicroModal.init() does not support event delegation for the open trigger, so here MicroModal.show() is called manually.
112
+ document.addEventListener(
113
+ 'click',
114
+ ( event ) => {
115
+ /** @type {HTMLElement} */
116
+ const target = event.target;
117
+
118
+ if ( target.dataset.micromodalTrigger ) {
119
+ MicroModal.show( target.dataset.micromodalTrigger, {
120
+ onShow,
121
+ onClose,
122
+ openClass: 'is-menu-open',
123
+ } );
124
+ }
125
+ },
126
+ { passive: true }
127
+ );
@@ -1,62 +1,94 @@
1
+ /*eslint-env browser*/
1
2
  // Open on click functionality.
2
- function closeSubmenus( element ) {
3
- element
3
+
4
+ /**
5
+ * Keep track of whether a submenu is open to short-circuit delegated event listeners.
6
+ *
7
+ * @type {boolean}
8
+ */
9
+ let hasOpenSubmenu = false;
10
+
11
+ /**
12
+ * Close submenu items for a navigation item.
13
+ *
14
+ * @param {HTMLElement} navigationItem - Either a NAV or LI element.
15
+ */
16
+ function closeSubmenus( navigationItem ) {
17
+ navigationItem
4
18
  .querySelectorAll( '[aria-expanded="true"]' )
5
19
  .forEach( function ( toggle ) {
6
20
  toggle.setAttribute( 'aria-expanded', 'false' );
7
21
  } );
22
+ hasOpenSubmenu = false;
8
23
  }
9
24
 
10
- function toggleSubmenuOnClick( event ) {
11
- const buttonToggle = event.target.closest( '[aria-expanded]' );
12
- const isSubmenuOpen = buttonToggle.getAttribute( 'aria-expanded' );
25
+ /**
26
+ * Toggle submenu on click.
27
+ *
28
+ * @param {HTMLButtonElement} buttonToggle
29
+ */
30
+ function toggleSubmenuOnClick( buttonToggle ) {
31
+ const isSubmenuOpen =
32
+ buttonToggle.getAttribute( 'aria-expanded' ) === 'true';
33
+ const navigationItem = buttonToggle.closest( '.wp-block-navigation-item' );
13
34
 
14
- if ( isSubmenuOpen === 'true' ) {
15
- closeSubmenus( buttonToggle.closest( '.wp-block-navigation-item' ) );
35
+ if ( isSubmenuOpen ) {
36
+ closeSubmenus( navigationItem );
16
37
  } else {
17
38
  // Close all sibling submenus.
18
- const parentElement = buttonToggle.closest(
19
- '.wp-block-navigation-item'
20
- );
21
39
  const navigationParent = buttonToggle.closest(
22
40
  '.wp-block-navigation__submenu-container, .wp-block-navigation__container, .wp-block-page-list'
23
41
  );
24
42
  navigationParent
25
43
  .querySelectorAll( '.wp-block-navigation-item' )
26
- .forEach( function ( child ) {
27
- if ( child !== parentElement ) {
44
+ .forEach( ( child ) => {
45
+ if ( child !== navigationItem ) {
28
46
  closeSubmenus( child );
29
47
  }
30
48
  } );
49
+
31
50
  // Open submenu.
32
51
  buttonToggle.setAttribute( 'aria-expanded', 'true' );
52
+ hasOpenSubmenu = true;
33
53
  }
34
54
  }
35
55
 
36
- // Necessary for some themes such as TT1 Blocks, where
37
- // scripts could be loaded before the body.
38
- window.addEventListener( 'load', () => {
39
- const submenuButtons = document.querySelectorAll(
40
- '.wp-block-navigation-submenu__toggle'
41
- );
56
+ // Open on button click or close on click outside.
57
+ document.addEventListener(
58
+ 'click',
59
+ function ( event ) {
60
+ const target = event.target;
61
+ const button = target.closest( '.wp-block-navigation-submenu__toggle' );
42
62
 
43
- submenuButtons.forEach( function ( button ) {
44
- button.addEventListener( 'click', toggleSubmenuOnClick );
45
- } );
63
+ // Close any other open submenus.
64
+ if ( hasOpenSubmenu ) {
65
+ const navigationBlocks = document.querySelectorAll(
66
+ '.wp-block-navigation'
67
+ );
68
+ navigationBlocks.forEach( function ( block ) {
69
+ if ( ! block.contains( target ) ) {
70
+ closeSubmenus( block );
71
+ }
72
+ } );
73
+ }
74
+
75
+ // Now open the submenu if one was clicked.
76
+ if ( button instanceof HTMLButtonElement ) {
77
+ toggleSubmenuOnClick( button );
78
+ }
79
+ },
80
+ { passive: true }
81
+ );
82
+
83
+ // Close on focus outside or escape key.
84
+ document.addEventListener(
85
+ 'keyup',
86
+ function ( event ) {
87
+ // Abort if there aren't any submenus open anyway.
88
+ if ( ! hasOpenSubmenu ) {
89
+ return;
90
+ }
46
91
 
47
- // Close on click outside.
48
- document.addEventListener( 'click', function ( event ) {
49
- const navigationBlocks = document.querySelectorAll(
50
- '.wp-block-navigation'
51
- );
52
- navigationBlocks.forEach( function ( block ) {
53
- if ( ! block.contains( event.target ) ) {
54
- closeSubmenus( block );
55
- }
56
- } );
57
- } );
58
- // Close on focus outside or escape key.
59
- document.addEventListener( 'keyup', function ( event ) {
60
92
  const submenuBlocks = document.querySelectorAll(
61
93
  '.wp-block-navigation-item.has-child'
62
94
  );
@@ -70,5 +102,6 @@ window.addEventListener( 'load', () => {
70
102
  toggle?.focus();
71
103
  }
72
104
  } );
73
- } );
74
- } );
105
+ },
106
+ { passive: true }
107
+ );
@@ -13,6 +13,8 @@ import {
13
13
  setupCoreBlocks,
14
14
  waitFor,
15
15
  within,
16
+ withFakeTimers,
17
+ waitForElementToBeRemoved,
16
18
  } from 'test/helpers';
17
19
  import Clipboard from '@react-native-clipboard/clipboard';
18
20
 
@@ -26,6 +28,19 @@ import { ENTER } from '@wordpress/keycodes';
26
28
  */
27
29
  import Paragraph from '../edit';
28
30
 
31
+ // Mock debounce to prevent potentially belated state updates.
32
+ jest.mock( '@wordpress/compose/src/utils/debounce', () => ( {
33
+ debounce: ( fn ) => {
34
+ fn.cancel = jest.fn();
35
+ return fn;
36
+ },
37
+ } ) );
38
+ // Mock link suggestions that are fetched by the link picker
39
+ // when typing a search query.
40
+ jest.mock( '@wordpress/core-data/src/fetch', () => ( {
41
+ __experimentalFetchLinkSuggestions: jest.fn().mockResolvedValue( [ {} ] ),
42
+ } ) );
43
+
29
44
  setupCoreBlocks();
30
45
 
31
46
  const getTestComponentWithContent = ( content ) => {
@@ -238,26 +253,27 @@ describe( 'Paragraph block', () => {
238
253
  // Act
239
254
  const paragraphBlock = getBlock( screen, 'Paragraph' );
240
255
  fireEvent.press( paragraphBlock );
241
- // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931
242
- await act( () => fireEvent.press( screen.getByLabelText( 'Link' ) ) );
243
- // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931
244
- await act( () =>
245
- fireEvent.press(
246
- screen.getByLabelText( 'Link to, Search or type URL' )
247
- )
248
- );
249
- fireEvent.changeText(
250
- screen.getByPlaceholderText( 'Search or type URL' ),
251
- 'wordpress.org'
252
- );
256
+ fireEvent.press( screen.getByLabelText( 'Link' ) );
257
+
253
258
  fireEvent.changeText(
254
- screen.getByPlaceholderText( 'Add link text', { hidden: true } ),
259
+ screen.getByPlaceholderText( 'Add link text' ),
255
260
  'WordPress'
256
261
  );
257
- jest.useFakeTimers();
258
- fireEvent.press( screen.getByLabelText( 'Apply' ) );
259
- // Await link picker navigation delay
260
- act( () => jest.runOnlyPendingTimers() );
262
+ fireEvent.press(
263
+ screen.getByLabelText( 'Link to, Search or type URL' )
264
+ );
265
+ const typeURLInput = await waitFor( () =>
266
+ screen.getByPlaceholderText( 'Search or type URL' )
267
+ );
268
+ fireEvent.changeText( typeURLInput, 'wordpress.org' );
269
+ await waitForElementToBeRemoved( () =>
270
+ screen.getByTestId( 'link-picker-loading' )
271
+ );
272
+ // Back navigation from link picker uses `setTimeout`
273
+ await withFakeTimers( () => {
274
+ fireEvent.press( screen.getByLabelText( 'Apply' ) );
275
+ act( () => jest.runOnlyPendingTimers() );
276
+ } );
261
277
 
262
278
  // Assert
263
279
  expect( getEditorHtml() ).toMatchInlineSnapshot( `
@@ -265,8 +281,6 @@ describe( 'Paragraph block', () => {
265
281
  <p><a href="http://wordpress.org">WordPress</a></p>
266
282
  <!-- /wp:paragraph -->"
267
283
  ` );
268
-
269
- jest.useRealTimers();
270
284
  } );
271
285
 
272
286
  it( 'should link text with selection', async () => {
@@ -287,22 +301,22 @@ describe( 'Paragraph block', () => {
287
301
  finalSelectionEnd: 7,
288
302
  }
289
303
  );
290
- // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931
291
- await act( () => fireEvent.press( screen.getByLabelText( 'Link' ) ) );
292
- // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931
293
- await act( () =>
294
- fireEvent.press(
295
- screen.getByLabelText( 'Link to, Search or type URL' )
296
- )
304
+ fireEvent.press( screen.getByLabelText( 'Link' ) );
305
+ fireEvent.press(
306
+ screen.getByLabelText( 'Link to, Search or type URL' )
297
307
  );
298
- fireEvent.changeText(
299
- screen.getByPlaceholderText( 'Search or type URL' ),
300
- 'wordpress.org'
308
+ const typeURLInput = await waitFor( () =>
309
+ screen.getByPlaceholderText( 'Search or type URL' )
301
310
  );
302
- jest.useFakeTimers();
303
- fireEvent.press( screen.getByLabelText( 'Apply' ) );
304
- // Await link picker navigation delay
305
- act( () => jest.runOnlyPendingTimers() );
311
+ fireEvent.changeText( typeURLInput, 'wordpress.org' );
312
+ await waitForElementToBeRemoved( () =>
313
+ screen.getByTestId( 'link-picker-loading' )
314
+ );
315
+ // Back navigation from link picker uses `setTimeout`
316
+ await withFakeTimers( () => {
317
+ fireEvent.press( screen.getByLabelText( 'Apply' ) );
318
+ act( () => jest.runOnlyPendingTimers() );
319
+ } );
306
320
 
307
321
  // Assert
308
322
  expect( getEditorHtml() ).toMatchInlineSnapshot( `
@@ -310,8 +324,6 @@ describe( 'Paragraph block', () => {
310
324
  <p>A <a href="http://wordpress.org">quick</a> brown fox jumps over the lazy dog.</p>
311
325
  <!-- /wp:paragraph -->"
312
326
  ` );
313
-
314
- jest.useRealTimers();
315
327
  } );
316
328
 
317
329
  it( 'should link text with clipboard contents', async () => {
@@ -402,6 +414,10 @@ describe( 'Paragraph block', () => {
402
414
 
403
415
  // Tap one color
404
416
  fireEvent.press( screen.getByLabelText( 'Pale pink' ) );
417
+ // TODO(jest-console): Fix the warning and remove the expect below.
418
+ expect( console ).toHaveWarnedWith(
419
+ `Non-serializable values were found in the navigation state. Check:\n\nColor > params.onColorChange (Function)\n\nThis can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`
420
+ );
405
421
 
406
422
  // Dismiss the Block Settings modal.
407
423
  fireEvent( blockSettingsModal, 'backdropPress' );
@@ -657,6 +673,10 @@ describe( 'Paragraph block', () => {
657
673
  );
658
674
  fireEvent.press( screen.getByLabelText( 'Text color' ) );
659
675
  fireEvent.press( await screen.findByLabelText( 'Tertiary' ) );
676
+ // TODO(jest-console): Fix the warning and remove the expect below.
677
+ expect( console ).toHaveWarnedWith(
678
+ `Non-serializable values were found in the navigation state. Check:\n\ntext-color > Palette > params.onColorChange (Function)\n\nThis can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`
679
+ );
660
680
 
661
681
  // Assert
662
682
  expect( getEditorHtml() ).toMatchInlineSnapshot( `