From 7e469ec61486e4d984d8aa87170c4f3675da85c3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 11 Jul 2023 15:41:55 -0700 Subject: [PATCH 01/24] Load interactivity API scripts with defer loading strategy --- lib/experimental/interactivity-api/scripts.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/experimental/interactivity-api/scripts.php b/lib/experimental/interactivity-api/scripts.php index e95bf518c75f7..545d984f6f76a 100644 --- a/lib/experimental/interactivity-api/scripts.php +++ b/lib/experimental/interactivity-api/scripts.php @@ -7,13 +7,13 @@ */ /** - * Move interactive scripts to the footer. This is a temporary measure to make - * it work with `wp_store` and it should be replaced with deferred scripts or - * modules. + * Move interactive scripts to the footer and make them load with the defer strategy. + * This ensures they work with `wp_store`. */ function gutenberg_interactivity_move_interactive_scripts_to_the_footer() { // Move the @wordpress/interactivity package to the footer. wp_script_add_data( 'wp-interactivity', 'group', 1 ); + wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' ); // Move all the view scripts of the interactive blocks to the footer. $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); @@ -21,6 +21,7 @@ function gutenberg_interactivity_move_interactive_scripts_to_the_footer() { if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) { foreach ( $block->view_script_handles as $handle ) { wp_script_add_data( $handle, 'group', 1 ); + wp_script_add_data( $handle, 'strategy', 'defer' ); } } } From 23b7aad26201c18b6704bf46a97ad59acc024afc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 11 Jul 2023 15:42:15 -0700 Subject: [PATCH 02/24] Load comment-reply script with defer loading strategy --- packages/block-library/src/comments/index.php | 1 + packages/block-library/src/post-comments-form/index.php | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/block-library/src/comments/index.php b/packages/block-library/src/comments/index.php index 48aac15af6fbb..fbcff6217c0e3 100644 --- a/packages/block-library/src/comments/index.php +++ b/packages/block-library/src/comments/index.php @@ -76,6 +76,7 @@ function render_block_core_comments( $attributes, $content, $block ) { * why they are not defined in `block.json`. */ wp_enqueue_script( 'comment-reply' ); + wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); enqueue_legacy_post_comments_block_styles( $block->name ); return sprintf( '
%2$s
', $wrapper_attributes, $output ); diff --git a/packages/block-library/src/post-comments-form/index.php b/packages/block-library/src/post-comments-form/index.php index 644b02ae0f149..4f4f7b6ee8774 100644 --- a/packages/block-library/src/post-comments-form/index.php +++ b/packages/block-library/src/post-comments-form/index.php @@ -48,6 +48,7 @@ function render_block_core_post_comments_form( $attributes, $content, $block ) { // Enqueue the comment-reply script. wp_enqueue_script( 'comment-reply' ); + wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); return $form; } From aefd67ac8ee281ef605184967c5dbb46e512344e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 11 Jul 2023 15:42:39 -0700 Subject: [PATCH 03/24] Load search view script with defer loading strategy --- packages/block-library/src/search/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index ce76587dbbb44..02a766c76ecc2 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -74,6 +74,7 @@ function render_block_core_search( $attributes ) { $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); wp_enqueue_script( 'wp-block--search-view', plugins_url( 'search/view.min.js', __FILE__ ) ); + wp_script_add_data( 'wp-block--search-view', 'strategy', 'defer' ); } } From 3dd37c225810a54bc6c014aad7df5e6f9d3990b5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Jul 2023 10:58:41 -0700 Subject: [PATCH 04/24] Defer the view scripts for the Navigation and File blocks --- packages/block-library/src/file/index.php | 1 + packages/block-library/src/navigation/index.php | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 172bb5d836343..0dd249cdfae8a 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -34,6 +34,7 @@ function gutenberg_block_core_file_update_interactive_view_script( $metadata ) { function render_block_core_file( $attributes, $content, $block ) { $should_load_view_script = ! empty( $attributes['displayPreview'] ); $view_js_file = 'wp-block-file-view'; + wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // If the script already exists, there is no point in removing it from viewScript. if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index cfdd20100af81..656d7156d889a 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -671,16 +671,19 @@ function render_block_core_navigation( $attributes, $content, $block ) { // If the script already exists, there is no point in removing it from viewScript. $should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) ); $view_js_file = 'wp-block-navigation-view'; + $view_js_file2 = 'wp-block-navigation-view-2'; + wp_script_add_data( $view_js_file, 'strategy', 'defer' ); + wp_script_add_data( $view_js_file2, 'strategy', 'defer' ); if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; // If the script is not needed, and it is still in the `view_script_handles`, remove it. if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file, 'wp-block-navigation-view-2' ) ); + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file, $view_js_file2 ) ); } // If the script is needed, but it was previously removed, add it again. if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file, 'wp-block-navigation-view-2' ) ); + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file, $view_js_file2 ) ); } } From 4f7301b20e34b26386259965f5f674ac08c2f052 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Jul 2023 17:42:06 -0700 Subject: [PATCH 05/24] Use DCA event instead of window load event for Navigation viewScripts --- packages/block-library/src/navigation/view-modal.js | 2 +- packages/block-library/src/navigation/view.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/navigation/view-modal.js b/packages/block-library/src/navigation/view-modal.js index 9477d262816d9..6ec8acd8744a5 100644 --- a/packages/block-library/src/navigation/view-modal.js +++ b/packages/block-library/src/navigation/view-modal.js @@ -37,7 +37,7 @@ function isLinkToAnchorOnCurrentPage( node ) { ); } -window.addEventListener( 'load', () => { +document.addEventListener( 'DOMContentLoaded', () => { MicroModal.init( { onShow: navigationToggleModal, onClose: navigationToggleModal, diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 19805a44ae4ae..6302923da8b2c 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -35,7 +35,7 @@ function toggleSubmenuOnClick( event ) { // Necessary for some themes such as TT1 Blocks, where // scripts could be loaded before the body. -window.addEventListener( 'load', () => { +document.addEventListener( 'DOMContentLoaded', () => { const submenuButtons = document.querySelectorAll( '.wp-block-navigation-submenu__toggle' ); From 34faf8f4b0f4a117bc35f47445d51646331c57fd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Jul 2023 13:44:14 -0700 Subject: [PATCH 06/24] Optimize submenu-on-click view script * Fully leverage event delegation to allow async loading strategy. * Keep track of whether a submenu is open to short-circuit event handlers. * Use passive event listeners. --- .../block-library/src/navigation/index.php | 2 +- packages/block-library/src/navigation/view.js | 99 ++++++++++++------- 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 0aa6c97b41f9e..bd52db38a5dd7 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -675,7 +675,7 @@ function render_block_core_navigation( $attributes, $content, $block ) { $should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) ); $view_js_file = 'wp-block-navigation-view'; $view_js_file2 = 'wp-block-navigation-view-2'; - wp_script_add_data( $view_js_file, 'strategy', 'defer' ); + wp_script_add_data( $view_js_file, 'strategy', 'async' ); wp_script_add_data( $view_js_file2, 'strategy', 'defer' ); if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 6302923da8b2c..5d6adf52eae93 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -1,62 +1,94 @@ +/*eslint-env browser*/ // Open on click functionality. -function closeSubmenus( element ) { - element + +/** + * Keep track of whether a submenu is open to short-circuit delegated event listeners. + * + * @type {boolean} + */ +let hasOpenSubmenu = false; + +/** + * Close submenu items for a navigation item. + * + * @param {HTMLElement} navigationItem - Either a NAV or LI element. + */ +function closeSubmenus( navigationItem ) { + navigationItem .querySelectorAll( '[aria-expanded="true"]' ) .forEach( function ( toggle ) { toggle.setAttribute( 'aria-expanded', 'false' ); } ); + hasOpenSubmenu = false; } -function toggleSubmenuOnClick( event ) { - const buttonToggle = event.target.closest( '[aria-expanded]' ); +/** + * Toggle submenu on click. + * + * @param {HTMLButtonElement} buttonToggle + */ +function toggleSubmenuOnClick( buttonToggle ) { const isSubmenuOpen = buttonToggle.getAttribute( 'aria-expanded' ); + const navigationItem = buttonToggle.closest( '.wp-block-navigation-item' ); if ( isSubmenuOpen === 'true' ) { - closeSubmenus( buttonToggle.closest( '.wp-block-navigation-item' ) ); + closeSubmenus( navigationItem ); } else { // Close all sibling submenus. - const parentElement = buttonToggle.closest( - '.wp-block-navigation-item' - ); const navigationParent = buttonToggle.closest( '.wp-block-navigation__submenu-container, .wp-block-navigation__container, .wp-block-page-list' ); navigationParent .querySelectorAll( '.wp-block-navigation-item' ) .forEach( function ( child ) { - if ( child !== parentElement ) { + if ( child !== navigationItem ) { closeSubmenus( child ); } } ); // Open submenu. buttonToggle.setAttribute( 'aria-expanded', 'true' ); + hasOpenSubmenu = true; } } -// Necessary for some themes such as TT1 Blocks, where -// scripts could be loaded before the body. -document.addEventListener( 'DOMContentLoaded', () => { - const submenuButtons = document.querySelectorAll( - '.wp-block-navigation-submenu__toggle' - ); +// Open on button click or close on click outside. +document.addEventListener( + 'click', + function ( event ) { + const target = event.target; + if ( ! ( target instanceof Element ) ) { + return; + } - submenuButtons.forEach( function ( button ) { - button.addEventListener( 'click', toggleSubmenuOnClick ); - } ); + const button = target.closest( '.wp-block-navigation-submenu__toggle' ); + if ( button instanceof HTMLButtonElement ) { + toggleSubmenuOnClick( button ); + } + + // Close any other open submenus. + if ( hasOpenSubmenu ) { + const navigationBlocks = document.querySelectorAll( + '.wp-block-navigation' + ); + navigationBlocks.forEach( function ( block ) { + if ( ! block.contains( event.target ) ) { + closeSubmenus( block ); + } + } ); + } + }, + { passive: true } +); + +// Close on focus outside or escape key. +document.addEventListener( + 'keyup', + function ( event ) { + // Abort if there aren't any submenus open anyway. + if ( ! hasOpenSubmenu ) { + return; + } - // Close on click outside. - document.addEventListener( 'click', function ( event ) { - const navigationBlocks = document.querySelectorAll( - '.wp-block-navigation' - ); - navigationBlocks.forEach( function ( block ) { - if ( ! block.contains( event.target ) ) { - closeSubmenus( block ); - } - } ); - } ); - // Close on focus outside or escape key. - document.addEventListener( 'keyup', function ( event ) { const submenuBlocks = document.querySelectorAll( '.wp-block-navigation-item.has-child' ); @@ -70,5 +102,6 @@ document.addEventListener( 'DOMContentLoaded', () => { toggle?.focus(); } } ); - } ); -} ); + }, + { passive: true } +); From 5329284c5596e9c1728b2170b0635c30123e735f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Jul 2023 14:16:40 -0700 Subject: [PATCH 07/24] Optimize modal view script * Fully leverage event delegation to allow async loading strategy. * Remove event listener for anchor clicks in modal when modal is closed. * Use passive event listeners. --- .../block-library/src/navigation/index.php | 2 +- .../src/navigation/view-modal.js | 117 ++++++++++++------ 2 files changed, 80 insertions(+), 39 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index bd52db38a5dd7..0bc9f8205d1d2 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -676,7 +676,7 @@ function render_block_core_navigation( $attributes, $content, $block ) { $view_js_file = 'wp-block-navigation-view'; $view_js_file2 = 'wp-block-navigation-view-2'; wp_script_add_data( $view_js_file, 'strategy', 'async' ); - wp_script_add_data( $view_js_file2, 'strategy', 'defer' ); + wp_script_add_data( $view_js_file2, 'strategy', 'async' ); if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/navigation/view-modal.js b/packages/block-library/src/navigation/view-modal.js index 6ec8acd8744a5..89539cf1e6975 100644 --- a/packages/block-library/src/navigation/view-modal.js +++ b/packages/block-library/src/navigation/view-modal.js @@ -1,16 +1,22 @@ +/*eslint-env browser*/ /** * External dependencies */ import MicroModal from 'micromodal'; // Responsive navigation toggle. -function navigationToggleModal( modal ) { + +/** + * Toggles responsive navigation. + * + * @param {HTMLDivElement} modal + * @param {boolean} isHidden + */ +function navigationToggleModal( modal, isHidden ) { const dialogContainer = modal.querySelector( `.wp-block-navigation__responsive-dialog` ); - const isHidden = 'true' === modal.getAttribute( 'aria-hidden' ); - modal.classList.toggle( 'has-modal-open', ! isHidden ); dialogContainer.toggleAttribute( 'aria-modal', ! isHidden ); @@ -23,10 +29,15 @@ function navigationToggleModal( modal ) { } // Add a class to indicate the modal is open. - const htmlElement = document.documentElement; - htmlElement.classList.toggle( 'has-modal-open' ); + document.documentElement.classList.toggle( 'has-modal-open' ); } +/** + * Checks whether the provided link is an anchor on the current page. + * + * @param {HTMLAnchorElement} node + * @return {boolean} Is anchor. + */ function isLinkToAnchorOnCurrentPage( node ) { return ( node.hash && @@ -37,42 +48,72 @@ function isLinkToAnchorOnCurrentPage( node ) { ); } -document.addEventListener( 'DOMContentLoaded', () => { - MicroModal.init( { - onShow: navigationToggleModal, - onClose: navigationToggleModal, - openClass: 'is-menu-open', +/** + * Handles effects after opening the modal. + * + * @param {HTMLDivElement} modal + */ +function onShow( modal ) { + navigationToggleModal( modal, false ); + modal.addEventListener( 'click', handleAnchorLinkClicksInsideModal, { + passive: true, } ); +} - // Close modal automatically on clicking anchor links inside modal. - const navigationLinks = document.querySelectorAll( - '.wp-block-navigation-item__content' - ); +/** + * Handles effects after closing the modal. + * + * @param {HTMLDivElement} modal + */ +function onClose( modal ) { + navigationToggleModal( modal, true ); + modal.removeEventListener( 'click', handleAnchorLinkClicksInsideModal, { + passive: true, + } ); +} - navigationLinks.forEach( function ( link ) { - // Ignore non-anchor links and anchor links which open on a new tab. - if ( - ! isLinkToAnchorOnCurrentPage( link ) || - link.attributes?.target === '_blank' - ) { - return; - } +/** + * Handle clicks to anchor links in modal using event delegation by closing modal automatically + * + * @param {UIEvent} event + */ +function handleAnchorLinkClicksInsideModal( event ) { + if ( ! event.target.closest ) { + return; + } - // Find the specific parent modal for this link - // since .close() won't work without an ID if there are - // multiple navigation menus in a post/page. - const modal = link.closest( - '.wp-block-navigation__responsive-container' - ); - const modalId = modal?.getAttribute( 'id' ); + const link = event.target.closest( '.wp-block-navigation-item__content' ); + if ( ! ( link instanceof HTMLAnchorElement ) ) { + return; + } - link.addEventListener( 'click', () => { - // check if modal exists and is open before trying to close it - // otherwise Micromodal will toggle the `has-modal-open` class - // on the html tag which prevents scrolling - if ( modalId && modal.classList.contains( 'has-modal-open' ) ) { - MicroModal.close( modalId ); - } - } ); - } ); + // Ignore non-anchor links and anchor links which open on a new tab. + if ( + ! isLinkToAnchorOnCurrentPage( link ) || + link.attributes?.target === '_blank' + ) { + return; + } + + // Find the specific parent modal for this link + // since .close() won't work without an ID if there are + // multiple navigation menus in a post/page. + const modal = link.closest( '.wp-block-navigation__responsive-container' ); + const modalId = modal?.getAttribute( 'id' ); + if ( ! modalId ) { + return; + } + + // check if modal exists and is open before trying to close it + // otherwise Micromodal will toggle the `has-modal-open` class + // on the html tag which prevents scrolling + if ( modalId && modal.classList.contains( 'has-modal-open' ) ) { + MicroModal.close( modalId ); + } +} + +MicroModal.init( { + onShow, + onClose, + openClass: 'is-menu-open', } ); From 2bafd3797025a1b2daafd5a85e2ae848981d757c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Jul 2023 14:30:56 -0700 Subject: [PATCH 08/24] Utilize asset file for wp-block--search-view --- packages/block-library/src/search/index.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 02a766c76ecc2..3a8ff5d918ec1 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -73,7 +73,15 @@ function render_block_core_search( $attributes ) { if ( 'button-only' === $button_position && 'expand-searchfield' === $button_behavior ) { $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); - wp_enqueue_script( 'wp-block--search-view', plugins_url( 'search/view.min.js', __FILE__ ) ); + + // See logic in gutenberg_register_packages_scripts(). + $asset_file = plugin_dir_path( __FILE__ ) . '/search/view.min.asset.php'; + $script_url = plugins_url( 'search/view.min.js', __FILE__ ); + $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); + $asset = file_exists( $asset_file ) ? require $asset_file : null; + $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); + $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; + wp_enqueue_script( 'wp-block--search-view', $script_url, $dependencies, $version ); wp_script_add_data( 'wp-block--search-view', 'strategy', 'defer' ); } } From 4b86ce45f2d354683fa78d0b4e91b3c0cf384087 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Jul 2023 14:43:28 -0700 Subject: [PATCH 09/24] Conditionally enqueue navigation view scripts only if needed --- .../block-library/src/navigation/block.json | 1 - .../block-library/src/navigation/index.php | 47 ++++++++++++------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index e45d053536778..e4610275ea517 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -133,7 +133,6 @@ } } }, - "viewScript": [ "file:./view.min.js", "file:./view-modal.min.js" ], "editorStyle": "wp-block-navigation-editor", "style": "wp-block-navigation" } diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 0bc9f8205d1d2..b78a82981309c 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -671,25 +671,38 @@ function render_block_core_navigation( $attributes, $content, $block ) { $inner_blocks_html .= ''; } - // If the script already exists, there is no point in removing it from viewScript. - $should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) ); - $view_js_file = 'wp-block-navigation-view'; - $view_js_file2 = 'wp-block-navigation-view-2'; - wp_script_add_data( $view_js_file, 'strategy', 'async' ); - wp_script_add_data( $view_js_file2, 'strategy', 'async' ); - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file, $view_js_file2 ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file, $view_js_file2 ) ); - } + // Enqueue the view script for submenus-on-click if enabled. + if ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) { + $submenu_onclick_script_handle = 'wp-block-navigation-view'; + + // See logic in gutenberg_register_packages_scripts(). + $asset_file = plugin_dir_path( __FILE__ ) . '/navigation/view.min.asset.php'; + $script_url = plugins_url( 'navigation/view.min.js', __FILE__ ); + $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); + $asset = file_exists( $asset_file ) ? require $asset_file : null; + $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); + $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; + wp_enqueue_script( $submenu_onclick_script_handle, $script_url, $dependencies, $version ); + wp_script_add_data( $submenu_onclick_script_handle, 'strategy', 'async' ); } + // Enqueue the view script for responsive modal if enabled. + if ( $is_responsive_menu ) { + $responsive_menu_script_handle = 'wp-block-navigation-view-2'; + + // See logic in gutenberg_register_packages_scripts(). + $asset_file = plugin_dir_path( __FILE__ ) . '/navigation/view-modal.min.asset.php'; + $script_url = plugins_url( 'navigation/view-modal.min.js', __FILE__ ); + $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); + $asset = file_exists( $asset_file ) ? require $asset_file : null; + $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); + $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; + wp_enqueue_script( $responsive_menu_script_handle, $script_url, $dependencies, $version ); + wp_script_add_data( $responsive_menu_script_handle, 'strategy', 'async' ); + } + + $should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) ); + // Add directives to the submenu if needed. if ( gutenberg_should_block_use_interactivity_api( 'core/navigation' ) && $has_submenus && $should_load_view_script ) { $w = new WP_HTML_Tag_Processor( $inner_blocks_html ); From 95beb24177c61b9b0f144e2186f133d3ec920205 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Jul 2023 15:15:29 -0700 Subject: [PATCH 10/24] Update Navigation block to remove/add view scripts in the same way the File block does --- .../block-library/src/navigation/block.json | 1 + .../block-library/src/navigation/index.php | 57 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index e4610275ea517..e45d053536778 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -133,6 +133,7 @@ } } }, + "viewScript": [ "file:./view.min.js", "file:./view-modal.min.js" ], "editorStyle": "wp-block-navigation-editor", "style": "wp-block-navigation" } diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index b78a82981309c..49bdfaff7e5aa 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -671,37 +671,36 @@ function render_block_core_navigation( $attributes, $content, $block ) { $inner_blocks_html .= ''; } - // Enqueue the view script for submenus-on-click if enabled. - if ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) { - $submenu_onclick_script_handle = 'wp-block-navigation-view'; - - // See logic in gutenberg_register_packages_scripts(). - $asset_file = plugin_dir_path( __FILE__ ) . '/navigation/view.min.asset.php'; - $script_url = plugins_url( 'navigation/view.min.js', __FILE__ ); - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - $asset = file_exists( $asset_file ) ? require $asset_file : null; - $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); - $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; - wp_enqueue_script( $submenu_onclick_script_handle, $script_url, $dependencies, $version ); - wp_script_add_data( $submenu_onclick_script_handle, 'strategy', 'async' ); - } + $needed_script_map = array( + 'wp-block-navigation-view' => ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ), + 'wp-block-navigation-view-2' => $is_responsive_menu, + ); - // Enqueue the view script for responsive modal if enabled. - if ( $is_responsive_menu ) { - $responsive_menu_script_handle = 'wp-block-navigation-view-2'; - - // See logic in gutenberg_register_packages_scripts(). - $asset_file = plugin_dir_path( __FILE__ ) . '/navigation/view-modal.min.asset.php'; - $script_url = plugins_url( 'navigation/view-modal.min.js', __FILE__ ); - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - $asset = file_exists( $asset_file ) ? require $asset_file : null; - $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); - $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; - wp_enqueue_script( $responsive_menu_script_handle, $script_url, $dependencies, $version ); - wp_script_add_data( $responsive_menu_script_handle, 'strategy', 'async' ); - } + $should_load_view_script = false; + if ( gutenberg_should_block_use_interactivity_api( 'core/navigation' ) ) { + // TODO: The script is still loaded even when it isn't needed when the Interactivity API is used. + $should_load_view_script = count( array_filter( $needed_script_map ) ) > 0; + } else { + foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) { + wp_script_add_data( $view_script_handle, 'strategy', 'async' ); + + // If the script already exists, there is no point in removing it from viewScript. + if ( wp_script_is( $view_script_handle ) ) { + continue; + } - $should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) ); + $script_handles = $block->block_type->view_script_handles; + + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $is_view_script_needed && in_array( $view_script_handle, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_script_handle ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $is_view_script_needed && ! in_array( $view_script_handle, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_script_handle ) ); + } + } + } // Add directives to the submenu if needed. if ( gutenberg_should_block_use_interactivity_api( 'core/navigation' ) && $has_submenus && $should_load_view_script ) { From 404128f419a1cf5dbc8c6d43a28728588dab00b4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Jul 2023 15:28:04 -0700 Subject: [PATCH 11/24] Update Search block to include view script in same way as File block and Navigation block --- packages/block-library/src/file/index.php | 4 +-- .../block-library/src/navigation/index.php | 2 +- packages/block-library/src/search/block.json | 1 + packages/block-library/src/search/index.php | 36 ++++++++++++------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index aaf223290566d..1b641b2afe4be 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -24,7 +24,7 @@ function gutenberg_block_core_file_update_interactive_view_script( $metadata ) { } /** - * When the `core/file` block is rendering, check if we need to enqueue the `'wp-block-file-view` script. + * When the `core/file` block is rendering, check if we need to enqueue the `wp-block-file-view` script. * * @param array $attributes The block attributes. * @param string $content The block content. @@ -35,7 +35,7 @@ function gutenberg_block_core_file_update_interactive_view_script( $metadata ) { function render_block_core_file( $attributes, $content, $block ) { $should_load_view_script = ! empty( $attributes['displayPreview'] ); $view_js_file = 'wp-block-file-view'; - wp_script_add_data( $view_js_file, 'strategy', 'defer' ); + wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be specified in block.json. // If the script already exists, there is no point in removing it from viewScript. if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 49bdfaff7e5aa..0f129be6c7d85 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -682,7 +682,7 @@ function render_block_core_navigation( $attributes, $content, $block ) { $should_load_view_script = count( array_filter( $needed_script_map ) ) > 0; } else { foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) { - wp_script_add_data( $view_script_handle, 'strategy', 'async' ); + wp_script_add_data( $view_script_handle, 'strategy', 'async' ); // TODO: This should be specified in block.json. // If the script already exists, there is no point in removing it from viewScript. if ( wp_script_is( $view_script_handle ) ) { diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index 1a23590699af4..b2873bfa8e572 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -90,6 +90,7 @@ }, "html": false }, + "viewScript": "file:./view.min.js", "editorStyle": "wp-block-search-editor", "style": "wp-block-search" } diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 3a8ff5d918ec1..286dbfe3e12cc 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -8,11 +8,13 @@ /** * Dynamically renders the `core/search` block. * - * @param array $attributes The block attributes. + * @param array $attributes The block attributes. + * @param string $content The block content. + * @param WP_Block $block The parsed block. * - * @return string The search block markup. + * @return string Returns the block content. */ -function render_block_core_search( $attributes ) { +function render_block_core_search( $attributes, $content, $block ) { // Older versions of the Search block defaulted the label and buttonText // attributes to `__( 'Search' )` meaning that many posts contain ``. Support these by defaulting an undefined label and @@ -65,6 +67,11 @@ function render_block_core_search( $attributes ) { if ( ! empty( $typography_classes ) ) { $input_classes[] = $typography_classes; } + + $view_js_file = 'wp-block-search-view'; + wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be specified in block.json. + $should_load_view_script = false; + if ( $input->next_tag() ) { $input->add_class( implode( ' ', $input_classes ) ); $input->set_attribute( 'id', $input_id ); @@ -73,16 +80,21 @@ function render_block_core_search( $attributes ) { if ( 'button-only' === $button_position && 'expand-searchfield' === $button_behavior ) { $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); + $should_load_view_script = true; + } + } + + // If the script already exists, there is no point in removing it from viewScript. + if ( ! wp_script_is( $view_js_file ) ) { + $script_handles = $block->block_type->view_script_handles; - // See logic in gutenberg_register_packages_scripts(). - $asset_file = plugin_dir_path( __FILE__ ) . '/search/view.min.asset.php'; - $script_url = plugins_url( 'search/view.min.js', __FILE__ ); - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - $asset = file_exists( $asset_file ) ? require $asset_file : null; - $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); - $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; - wp_enqueue_script( 'wp-block--search-view', $script_url, $dependencies, $version ); - wp_script_add_data( 'wp-block--search-view', 'strategy', 'defer' ); + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); } } From 4ccd709f5f539a8e3b6ae434ce962b8a49d0feaa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 11:29:55 -0700 Subject: [PATCH 12/24] Refactor Search block view script * Adopt asynchronous loading strategy. * Use event delegation. * Collapse expanded blocks when tabbing out of expanded Search block. * Only attach keydown/keyup event handlers while Search block is expanded. * Ensure search button's aria-label is translated. * Ensure Search button's type is restored to 'button' instead of deleting type (since no type is same as 'submit'). * Use passive event listeners. --- packages/block-library/src/navigation/view.js | 2 + packages/block-library/src/search/index.php | 4 +- packages/block-library/src/search/view.js | 237 +++++++++++++----- 3 files changed, 175 insertions(+), 68 deletions(-) diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 5d6adf52eae93..4432f005500f8 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -4,6 +4,8 @@ /** * Keep track of whether a submenu is open to short-circuit delegated event listeners. * + * TODO: What if there are multiple nav menus? Can multiple submenus be expanded at once from separate Navigation blocks? + * * @type {boolean} */ let hasOpenSubmenu = false; diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 286dbfe3e12cc..369502df44fe7 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -69,7 +69,7 @@ function render_block_core_search( $attributes, $content, $block ) { } $view_js_file = 'wp-block-search-view'; - wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be specified in block.json. + wp_script_add_data( $view_js_file, 'strategy', 'async' ); // TODO: This should be specified in block.json. $should_load_view_script = false; if ( $input->next_tag() ) { @@ -141,8 +141,10 @@ function render_block_core_search( $attributes, $content, $block ) { $button->add_class( implode( ' ', $button_classes ) ); if ( 'expand-searchfield' === $attributes['buttonBehavior'] && 'button-only' === $attributes['buttonPosition'] ) { $button->set_attribute( 'aria-label', __( 'Expand search field' ) ); + $button->set_attribute( 'data-toggled-aria-label', __( 'Submit Search' ) ); $button->set_attribute( 'aria-controls', 'wp-block-search__input-' . $input_id ); $button->set_attribute( 'aria-expanded', 'false' ); + $button->set_attribute( 'type', 'button' ); // Will be set to submit after clicking. } else { $button->set_attribute( 'aria-label', wp_strip_all_tags( $attributes['buttonText'] ) ); } diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 0909121b25bf0..f17c30091e8b2 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -1,68 +1,171 @@ -window.addEventListener( 'DOMContentLoaded', () => { - const hiddenClass = 'wp-block-search__searchfield-hidden'; - - Array.from( - document.getElementsByClassName( - 'wp-block-search__button-behavior-expand' - ) - ).forEach( ( block ) => { - const searchField = block.querySelector( '.wp-block-search__input' ); - const searchButton = block.querySelector( '.wp-block-search__button' ); - const searchLabel = block.querySelector( '.wp-block-search__label' ); - const ariaLabel = searchButton.getAttribute( 'aria-label' ); - const id = searchField.getAttribute( 'id' ); - - const toggleSearchField = ( showSearchField ) => { - if ( showSearchField ) { - searchField.removeAttribute( 'aria-hidden' ); - searchField.removeAttribute( 'tabindex' ); - searchButton.removeAttribute( 'aria-expanded' ); - searchButton.removeAttribute( 'aria-controls' ); - searchButton.setAttribute( 'type', 'submit' ); - searchButton.setAttribute( 'aria-label', 'Submit Search' ); - - return block.classList.remove( hiddenClass ); - } - - searchButton.removeAttribute( 'type' ); - searchField.setAttribute( 'aria-hidden', 'true' ); - searchField.setAttribute( 'tabindex', '-1' ); - searchButton.setAttribute( 'aria-expanded', 'false' ); - searchButton.setAttribute( 'aria-controls', id ); - searchButton.setAttribute( 'aria-label', ariaLabel ); - return block.classList.add( hiddenClass ); - }; - - const hideSearchField = ( e ) => { - if ( ! e.target.closest( '.wp-block-search' ) ) { - return toggleSearchField( false ); - } - - if ( e.key === 'Escape' ) { - searchButton.focus(); - return toggleSearchField( false ); - } - }; - - const handleButtonClick = ( e ) => { - if ( block.classList.contains( hiddenClass ) ) { - e.preventDefault(); - searchField.focus(); - toggleSearchField( true ); - } - }; - - searchButton.removeAttribute( 'type' ); - searchField.addEventListener( 'keydown', ( e ) => { - hideSearchField( e ); - } ); - searchButton.addEventListener( 'click', handleButtonClick ); - searchButton.addEventListener( 'keydown', ( e ) => { - hideSearchField( e ); - } ); - if ( searchLabel ) { - searchLabel.addEventListener( 'click', handleButtonClick ); - } - document.body.addEventListener( 'click', hideSearchField ); +/*eslint-env browser*/ + +/** @type {?HTMLFormElement} */ +let expandedSearchBlock = null; + +const hiddenClass = 'wp-block-search__searchfield-hidden'; + +/** + * Toggles aria-label with data-toggled-aria-label. + * + * @param {HTMLElement} element + */ +function toggleAriaLabel( element ) { + if ( ! ( 'toggledAriaLabel' in element.dataset ) ) { + throw new Error( 'Element lacks toggledAriaLabel in dataset.' ); + } + + const ariaLabel = element.dataset.toggledAriaLabel; + element.dataset.toggledAriaLabel = element.ariaLabel; + element.ariaLabel = ariaLabel; +} + +/** + * Gets search input. + * + * @param {HTMLFormElement} block Search block. + * @return {HTMLInputElement} Search input. + */ +function getSearchInput( block ) { + return block.querySelector( '.wp-block-search__input' ); +} + +/** + * Gets search button. + * + * @param {HTMLFormElement} block Search block. + * @return {HTMLButtonElement} Search button. + */ +function getSearchButton( block ) { + return block.querySelector( '.wp-block-search__button' ); +} + +/** + * Handles keydown event to collapse an expanded Search block (when pressing Escape key). + * + * @param {KeyboardEvent} event + */ +function handleKeydownEvent( event ) { + if ( ! expandedSearchBlock ) { + // In case the event listener wasn't removed in time. + return; + } + + if ( event.key === 'Escape' ) { + const block = expandedSearchBlock; // This is nullified by collapseExpandedSearchBlock(). + collapseExpandedSearchBlock(); + getSearchButton( block ).focus(); + } +} + +/** + * Handles keyup event to collapse an expanded Search block (e.g. when tabbing out of expanded Search block). + * + * @param {KeyboardEvent} event + */ +function handleKeyupEvent( event ) { + if ( ! expandedSearchBlock ) { + // In case the event listener wasn't removed in time. + return; + } + + if ( event.target.closest( '.wp-block-search' ) !== expandedSearchBlock ) { + collapseExpandedSearchBlock(); + } +} + +/** + * Expands search block. + * + * Inverse of what is done in collapseExpandedSearchBlock(). + * + * @param {HTMLFormElement} block Search block. + */ +function expandSearchBlock( block ) { + // Make sure only one is open at a time. + if ( expandedSearchBlock ) { + collapseExpandedSearchBlock(); + } + + const searchField = getSearchInput( block ); + const searchButton = getSearchButton( block ); + + searchField.removeAttribute( 'aria-hidden' ); + searchField.removeAttribute( 'tabindex' ); + searchButton.removeAttribute( 'aria-expanded' ); + searchButton.removeAttribute( 'aria-controls' ); + searchButton.type = 'submit'; + toggleAriaLabel( searchButton ); + block.classList.remove( hiddenClass ); + + searchField.focus(); // Note that Chrome seems to do this automatically. + + // The following two must be inverse of what is done in collapseExpandedSearchBlock(). + document.addEventListener( 'keydown', handleKeydownEvent, { + passive: true, + } ); + document.addEventListener( 'keyup', handleKeyupEvent, { + passive: true, + } ); + + expandedSearchBlock = block; +} + +/** + * Collapses the expanded search block. + * + * Inverse of what is done in expandSearchBlock(). + */ +function collapseExpandedSearchBlock() { + if ( ! expandedSearchBlock ) { + throw new Error( 'Expected expandedSearchBlock to be defined.' ); + } + const block = expandedSearchBlock; + const searchField = getSearchInput( block ); + const searchButton = getSearchButton( block ); + searchButton.type = 'button'; + searchField.ariaHidden = 'true'; + searchField.tabIndex = -1; + searchButton.ariaExpanded = 'false'; + searchButton.ariaControls = searchField.getAttribute( 'id' ); + toggleAriaLabel( searchButton ); + block.classList.add( hiddenClass ); + + // The following two must be inverse of what is done in expandSearchBlock(). + document.removeEventListener( 'keydown', handleKeydownEvent, { + passive: true, } ); -} ); + document.removeEventListener( 'keyup', handleKeyupEvent, { + passive: true, + } ); + + expandedSearchBlock = null; +} + +// Listen for click events anywhere on the document so this script can be loaded asynchronously in the head. +document.addEventListener( + 'click', + ( event ) => { + // Get the ancestor expandable Search block of the clicked element. + const block = event.target.closest( + '.wp-block-search__button-behavior-expand' + ); + + /* + * If there is already an expanded search block and either the current click was not for a Search block or it was + * for another block, then collapse the currently-expanded block. + */ + if ( expandedSearchBlock && block !== expandedSearchBlock ) { + collapseExpandedSearchBlock(); + } + + // If the click was on or inside a collapsed Search block, expand it. + if ( + block instanceof HTMLFormElement && + block.classList.contains( hiddenClass ) + ) { + expandSearchBlock( block ); + } + }, + { passive: true } +); From b7863e9c2bcdc9e4a0dc78a3a6769436be0fc167 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 11:38:41 -0700 Subject: [PATCH 13/24] Use more DOM properties instead of attributes --- packages/block-library/src/search/view.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index f17c30091e8b2..4df62a475a19b 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -90,11 +90,11 @@ function expandSearchBlock( block ) { const searchField = getSearchInput( block ); const searchButton = getSearchButton( block ); - searchField.removeAttribute( 'aria-hidden' ); - searchField.removeAttribute( 'tabindex' ); - searchButton.removeAttribute( 'aria-expanded' ); - searchButton.removeAttribute( 'aria-controls' ); searchButton.type = 'submit'; + searchField.ariaHidden = 'false'; + searchField.tabIndex = 0; + searchButton.ariaExpanded = 'true'; + searchButton.removeAttribute( 'aria-controls' ); // Note: Seemingly not mirrored with searchButton.ariaControls. toggleAriaLabel( searchButton ); block.classList.remove( hiddenClass ); @@ -123,11 +123,12 @@ function collapseExpandedSearchBlock() { const block = expandedSearchBlock; const searchField = getSearchInput( block ); const searchButton = getSearchButton( block ); + searchButton.type = 'button'; searchField.ariaHidden = 'true'; searchField.tabIndex = -1; searchButton.ariaExpanded = 'false'; - searchButton.ariaControls = searchField.getAttribute( 'id' ); + searchButton.setAttribute( 'aria-controls', searchField.id ); // Note: Seemingly not mirrored with searchButton.ariaControls. toggleAriaLabel( searchButton ); block.classList.add( hiddenClass ); From c41d35b6c4f3ef4d68b21007ae274de9858adc14 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 11:40:24 -0700 Subject: [PATCH 14/24] Use more precise reflected terminology --- packages/block-library/src/search/view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 4df62a475a19b..5aaf1dd1ef3ad 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -94,7 +94,7 @@ function expandSearchBlock( block ) { searchField.ariaHidden = 'false'; searchField.tabIndex = 0; searchButton.ariaExpanded = 'true'; - searchButton.removeAttribute( 'aria-controls' ); // Note: Seemingly not mirrored with searchButton.ariaControls. + searchButton.removeAttribute( 'aria-controls' ); // Note: Seemingly not reflected with searchButton.ariaControls. toggleAriaLabel( searchButton ); block.classList.remove( hiddenClass ); @@ -128,7 +128,7 @@ function collapseExpandedSearchBlock() { searchField.ariaHidden = 'true'; searchField.tabIndex = -1; searchButton.ariaExpanded = 'false'; - searchButton.setAttribute( 'aria-controls', searchField.id ); // Note: Seemingly not mirrored with searchButton.ariaControls. + searchButton.setAttribute( 'aria-controls', searchField.id ); // Note: Seemingly not reflected with searchButton.ariaControls. toggleAriaLabel( searchButton ); block.classList.add( hiddenClass ); From b50a2b859167d63aaa83d24b5596f5e81ab36c41 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 12:31:32 -0700 Subject: [PATCH 15/24] Remove resolved TODO comment --- packages/block-library/src/navigation/view.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 4432f005500f8..5d6adf52eae93 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -4,8 +4,6 @@ /** * Keep track of whether a submenu is open to short-circuit delegated event listeners. * - * TODO: What if there are multiple nav menus? Can multiple submenus be expanded at once from separate Navigation blocks? - * * @type {boolean} */ let hasOpenSubmenu = false; From a18d055868c34a5d160a711104db29c7b207a6f5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 14:34:06 -0700 Subject: [PATCH 16/24] Use event delegation to open MicroModal --- .../src/navigation/view-modal.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/navigation/view-modal.js b/packages/block-library/src/navigation/view-modal.js index 89539cf1e6975..8abf1c05e3ee4 100644 --- a/packages/block-library/src/navigation/view-modal.js +++ b/packages/block-library/src/navigation/view-modal.js @@ -112,8 +112,20 @@ function handleAnchorLinkClicksInsideModal( event ) { } } -MicroModal.init( { - onShow, - onClose, - openClass: 'is-menu-open', -} ); +// MicroModal.init() does not support event delegation for the open trigger, so here MicroModal.show() is called manually. +document.addEventListener( + 'click', + ( event ) => { + /** @type {HTMLElement} */ + const target = event.target; + + if ( target.dataset.micromodalTrigger ) { + MicroModal.show( target.dataset.micromodalTrigger, { + onShow, + onClose, + openClass: 'is-menu-open', + } ); + } + }, + { passive: true } +); From 712c4cf8db59b786cb0ac95b1c16f9238619b871 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 14:59:27 -0700 Subject: [PATCH 17/24] Remove excessive type check --- packages/block-library/src/navigation/view-modal.js | 4 ---- packages/block-library/src/navigation/view.js | 3 --- 2 files changed, 7 deletions(-) diff --git a/packages/block-library/src/navigation/view-modal.js b/packages/block-library/src/navigation/view-modal.js index 8abf1c05e3ee4..62de6e8808bf0 100644 --- a/packages/block-library/src/navigation/view-modal.js +++ b/packages/block-library/src/navigation/view-modal.js @@ -78,10 +78,6 @@ function onClose( modal ) { * @param {UIEvent} event */ function handleAnchorLinkClicksInsideModal( event ) { - if ( ! event.target.closest ) { - return; - } - const link = event.target.closest( '.wp-block-navigation-item__content' ); if ( ! ( link instanceof HTMLAnchorElement ) ) { return; diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 5d6adf52eae93..7f2dbf8b721b1 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -56,9 +56,6 @@ document.addEventListener( 'click', function ( event ) { const target = event.target; - if ( ! ( target instanceof Element ) ) { - return; - } const button = target.closest( '.wp-block-navigation-submenu__toggle' ); if ( button instanceof HTMLButtonElement ) { From b2db9323b394032da52962ed8bd748cf13ad32e0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 15:10:59 -0700 Subject: [PATCH 18/24] Fix closing submenus when there are multiple Navigation blocks on a page --- packages/block-library/src/navigation/view.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 7f2dbf8b721b1..d808d1707d5bf 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -28,10 +28,11 @@ function closeSubmenus( navigationItem ) { * @param {HTMLButtonElement} buttonToggle */ function toggleSubmenuOnClick( buttonToggle ) { - const isSubmenuOpen = buttonToggle.getAttribute( 'aria-expanded' ); + const isSubmenuOpen = + buttonToggle.getAttribute( 'aria-expanded' ) === 'true'; const navigationItem = buttonToggle.closest( '.wp-block-navigation-item' ); - if ( isSubmenuOpen === 'true' ) { + if ( isSubmenuOpen ) { closeSubmenus( navigationItem ); } else { // Close all sibling submenus. @@ -40,11 +41,12 @@ function toggleSubmenuOnClick( buttonToggle ) { ); navigationParent .querySelectorAll( '.wp-block-navigation-item' ) - .forEach( function ( child ) { + .forEach( ( child ) => { if ( child !== navigationItem ) { closeSubmenus( child ); } } ); + // Open submenu. buttonToggle.setAttribute( 'aria-expanded', 'true' ); hasOpenSubmenu = true; @@ -56,11 +58,7 @@ document.addEventListener( 'click', function ( event ) { const target = event.target; - const button = target.closest( '.wp-block-navigation-submenu__toggle' ); - if ( button instanceof HTMLButtonElement ) { - toggleSubmenuOnClick( button ); - } // Close any other open submenus. if ( hasOpenSubmenu ) { @@ -68,11 +66,16 @@ document.addEventListener( '.wp-block-navigation' ); navigationBlocks.forEach( function ( block ) { - if ( ! block.contains( event.target ) ) { + if ( ! block.contains( target ) ) { closeSubmenus( block ); } } ); } + + // Now open the submenu if one was clicked. + if ( button instanceof HTMLButtonElement ) { + toggleSubmenuOnClick( button ); + } }, { passive: true } ); From 4a7fc468c45abe66f3566578b117df028159e7ee Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 14 Jul 2023 15:15:40 -0700 Subject: [PATCH 19/24] Clarify block.json comment --- packages/block-library/src/file/index.php | 2 +- packages/block-library/src/navigation/index.php | 2 +- packages/block-library/src/search/index.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 1b641b2afe4be..f6cfca6700728 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -35,7 +35,7 @@ function gutenberg_block_core_file_update_interactive_view_script( $metadata ) { function render_block_core_file( $attributes, $content, $block ) { $should_load_view_script = ! empty( $attributes['displayPreview'] ); $view_js_file = 'wp-block-file-view'; - wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be specified in block.json. + wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be able to be specified in block.json. See Core-54018. // If the script already exists, there is no point in removing it from viewScript. if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 0f129be6c7d85..2b0d967a6c8e9 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -682,7 +682,7 @@ function render_block_core_navigation( $attributes, $content, $block ) { $should_load_view_script = count( array_filter( $needed_script_map ) ) > 0; } else { foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) { - wp_script_add_data( $view_script_handle, 'strategy', 'async' ); // TODO: This should be specified in block.json. + wp_script_add_data( $view_script_handle, 'strategy', 'async' ); // TODO: This should be able to be specified in block.json. See Core-54018. // If the script already exists, there is no point in removing it from viewScript. if ( wp_script_is( $view_script_handle ) ) { diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 369502df44fe7..8ce35f3b766ea 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -69,7 +69,7 @@ function render_block_core_search( $attributes, $content, $block ) { } $view_js_file = 'wp-block-search-view'; - wp_script_add_data( $view_js_file, 'strategy', 'async' ); // TODO: This should be specified in block.json. + wp_script_add_data( $view_js_file, 'strategy', 'async' ); // TODO: This should be able to be specified in block.json. See Core-54018. $should_load_view_script = false; if ( $input->next_tag() ) { From ffd9e5cc6ab64b7b0c40441950de2de0c420d64e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 18 Jul 2023 13:00:19 -0700 Subject: [PATCH 20/24] Add core merge note for comment-reply script strategy --- packages/block-library/src/comments/index.php | 2 +- packages/block-library/src/post-comments-form/index.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/comments/index.php b/packages/block-library/src/comments/index.php index fbcff6217c0e3..392e66d091bb0 100644 --- a/packages/block-library/src/comments/index.php +++ b/packages/block-library/src/comments/index.php @@ -76,7 +76,7 @@ function render_block_core_comments( $attributes, $content, $block ) { * why they are not defined in `block.json`. */ wp_enqueue_script( 'comment-reply' ); - wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); + wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); // TODO: For core merge, this would rather be done in wp-includes/script-loader.php. enqueue_legacy_post_comments_block_styles( $block->name ); return sprintf( '
%2$s
', $wrapper_attributes, $output ); diff --git a/packages/block-library/src/post-comments-form/index.php b/packages/block-library/src/post-comments-form/index.php index 4f4f7b6ee8774..58792069ac011 100644 --- a/packages/block-library/src/post-comments-form/index.php +++ b/packages/block-library/src/post-comments-form/index.php @@ -48,7 +48,7 @@ function render_block_core_post_comments_form( $attributes, $content, $block ) { // Enqueue the comment-reply script. wp_enqueue_script( 'comment-reply' ); - wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); + wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); // TODO: For core merge, this would rather be done in wp-includes/script-loader.php. return $form; } From 26c06ab71e7833066ab2711d796dd9665b65edfa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 19 Jul 2023 20:33:38 -0700 Subject: [PATCH 21/24] Use defer strategy instead of async for Navigation and Search blocks --- packages/block-library/src/navigation/index.php | 2 +- packages/block-library/src/search/index.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 2b0d967a6c8e9..958c7e93a1924 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -682,7 +682,7 @@ function render_block_core_navigation( $attributes, $content, $block ) { $should_load_view_script = count( array_filter( $needed_script_map ) ) > 0; } else { foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) { - wp_script_add_data( $view_script_handle, 'strategy', 'async' ); // TODO: This should be able to be specified in block.json. See Core-54018. + wp_script_add_data( $view_script_handle, 'strategy', 'defer' ); // TODO: This should be able to be specified in block.json. See Core-54018. // If the script already exists, there is no point in removing it from viewScript. if ( wp_script_is( $view_script_handle ) ) { diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 5da11ffc883d2..9588476ddc5eb 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -81,7 +81,7 @@ function render_block_core_search( $attributes, $content, $block ) { // If the script already exists, there is no point in removing it from viewScript. $view_js_file = 'wp-block-search-view'; - wp_script_add_data( $view_js_file, 'strategy', 'async' ); // TODO: This should be able to be specified in block.json. See Core-54018. + wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be able to be specified in block.json. See Core-54018. if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; From 86b8e367411540e9fafd0d16521c2b5898364b9b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 19 Jul 2023 22:12:07 -0700 Subject: [PATCH 22/24] Defer all block view scripts --- lib/blocks.php | 34 +++++++++++++++++++ packages/block-library/src/file/index.php | 1 - .../block-library/src/navigation/index.php | 1 - packages/block-library/src/search/index.php | 1 - 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index e98f711b5c85a..f160d2a7080d3 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -172,6 +172,40 @@ function gutenberg_reregister_core_block_types() { add_action( 'init', 'gutenberg_reregister_core_block_types' ); +/** + * Adds the defer loading strategy to all registered blocks. + * + * This function would not be part of core merge. Instead, the register_block_script_handle() function would be patched + * as follows. + * + * ``` + * --- a/wp-includes/blocks.php + * +++ b/wp-includes/blocks.php + * @ @ -153,7 +153,8 @ @ function register_block_script_handle( $metadata, $field_name, $index = 0 ) { + * $script_handle, + * $script_uri, + * $script_dependencies, + * - isset( $script_asset['version'] ) ? $script_asset['version'] : false + * + isset( $script_asset['version'] ) ? $script_asset['version'] : false, + * + array( 'strategy' => 'defer' ) + * ); + * if ( ! $result ) { + * return false; + * ``` + * + * @see register_block_script_handle() + */ +function gutenberg_defer_block_view_scripts() { + $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); + foreach ( $block_types as $block_type ) { + foreach ( $block_type->view_script_handles as $view_script_handle ) { + wp_script_add_data( $view_script_handle, 'strategy', 'defer' ); + } + } +} + +add_action( 'init', 'gutenberg_defer_block_view_scripts', 100 ); + /** * Deregisters the existing core block type and its assets. * diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index f6cfca6700728..7dd77b20f466c 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -35,7 +35,6 @@ function gutenberg_block_core_file_update_interactive_view_script( $metadata ) { function render_block_core_file( $attributes, $content, $block ) { $should_load_view_script = ! empty( $attributes['displayPreview'] ); $view_js_file = 'wp-block-file-view'; - wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be able to be specified in block.json. See Core-54018. // If the script already exists, there is no point in removing it from viewScript. if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 958c7e93a1924..52376aba0d44e 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -682,7 +682,6 @@ function render_block_core_navigation( $attributes, $content, $block ) { $should_load_view_script = count( array_filter( $needed_script_map ) ) > 0; } else { foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) { - wp_script_add_data( $view_script_handle, 'strategy', 'defer' ); // TODO: This should be able to be specified in block.json. See Core-54018. // If the script already exists, there is no point in removing it from viewScript. if ( wp_script_is( $view_script_handle ) ) { diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 9588476ddc5eb..892b5163cfab0 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -81,7 +81,6 @@ function render_block_core_search( $attributes, $content, $block ) { // If the script already exists, there is no point in removing it from viewScript. $view_js_file = 'wp-block-search-view'; - wp_script_add_data( $view_js_file, 'strategy', 'defer' ); // TODO: This should be able to be specified in block.json. See Core-54018. if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; From aab0b4bc93a9742e2e3c43fc3556e966aa00241d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 20 Jul 2023 10:05:35 -0700 Subject: [PATCH 23/24] Ensure interactivity scripts remain in footer in WP<6.3 --- .../interactivity-api/scripts.php | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/experimental/interactivity-api/scripts.php b/lib/experimental/interactivity-api/scripts.php index 545d984f6f76a..ed1fca8550070 100644 --- a/lib/experimental/interactivity-api/scripts.php +++ b/lib/experimental/interactivity-api/scripts.php @@ -7,21 +7,32 @@ */ /** - * Move interactive scripts to the footer and make them load with the defer strategy. - * This ensures they work with `wp_store`. + * Makes sure that interactivity scripts execute after all `wp_store` directives have been printed to the page. + * + * In WordPress 6.3+ this is achieved by printing in the head but marking the scripts with defer. This has the benefit + * of early discovery so the script is loaded by the browser, while at the same time not blocking rendering. In older + * versions of WordPress, this is achieved by loading the scripts in the footer. + * + * @link https://make.wordpress.org/core/2023/07/14/registering-scripts-with-async-and-defer-attributes-in-wordpress-6-3/ */ function gutenberg_interactivity_move_interactive_scripts_to_the_footer() { - // Move the @wordpress/interactivity package to the footer. - wp_script_add_data( 'wp-interactivity', 'group', 1 ); - wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' ); + $supports_defer = version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' ); + if ( $supports_defer ) { + // Defer execution of @wordpress/interactivity package but continue loading in head. + wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' ); + wp_script_add_data( 'wp-interactivity', 'group', 0 ); + } else { + // Move the @wordpress/interactivity package to the footer. + wp_script_add_data( 'wp-interactivity', 'group', 1 ); + } // Move all the view scripts of the interactive blocks to the footer. $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); foreach ( array_values( $registered_blocks ) as $block ) { if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) { foreach ( $block->view_script_handles as $handle ) { - wp_script_add_data( $handle, 'group', 1 ); - wp_script_add_data( $handle, 'strategy', 'defer' ); + // Note that all block view scripts are already made defer by default. + wp_script_add_data( $handle, 'group', $supports_defer ? 0 : 1 ); } } } From 73e356444986422075246cb2bce6e3b53423f0f9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 20 Jul 2023 11:06:00 -0700 Subject: [PATCH 24/24] Remove defer from comment-reply for now Co-authored-by: Felix Arntz --- packages/block-library/src/comments/index.php | 1 - packages/block-library/src/post-comments-form/index.php | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/block-library/src/comments/index.php b/packages/block-library/src/comments/index.php index 392e66d091bb0..48aac15af6fbb 100644 --- a/packages/block-library/src/comments/index.php +++ b/packages/block-library/src/comments/index.php @@ -76,7 +76,6 @@ function render_block_core_comments( $attributes, $content, $block ) { * why they are not defined in `block.json`. */ wp_enqueue_script( 'comment-reply' ); - wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); // TODO: For core merge, this would rather be done in wp-includes/script-loader.php. enqueue_legacy_post_comments_block_styles( $block->name ); return sprintf( '
%2$s
', $wrapper_attributes, $output ); diff --git a/packages/block-library/src/post-comments-form/index.php b/packages/block-library/src/post-comments-form/index.php index 58792069ac011..644b02ae0f149 100644 --- a/packages/block-library/src/post-comments-form/index.php +++ b/packages/block-library/src/post-comments-form/index.php @@ -48,7 +48,6 @@ function render_block_core_post_comments_form( $attributes, $content, $block ) { // Enqueue the comment-reply script. wp_enqueue_script( 'comment-reply' ); - wp_script_add_data( 'comment-reply', 'strategy', 'defer' ); // TODO: For core merge, this would rather be done in wp-includes/script-loader.php. return $form; }