Skip to content

Commit

Permalink
Experimental: Navigation block with the Interactivity API (#50041)
Browse files Browse the repository at this point in the history
* Add interactivity runtime

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add it to the image block

This is still pretty basic, just to check that it works.

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add a separate webpack config

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Make sure the runtime is imported only once

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Use sideEffects instead of init

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Move script registration to a general file

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add `defer` to the interactivity scripts

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Revert changes of the image block

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Fix init import name

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add experimental setting to use Interactivity API

* Add basic `interactivity.js` file

* Enqueue old script/interactivity API conditionally

* Add `tick()` function until Preact bug is solved

* Add code to handle the overlayMenu

* Add Interactivity API to submenu block

* Add Interactivity API to page list block

* Change code to be more declarative and simple

* Use `__DIR__` instead of `__FILE__`

* Change loading script conditional

* Fix issues momentarily

* Fix some issues with focus

* Add tabindex to modal

* Revert to current navigation block

* Add directives through a filter

* Dont add latest modifications to navigation block

* Ensure focus doesn't get inside menu after closing

* Add conditionals and change function names

* Use `gutenberg_is_experiment_enabled` function

* Add interactivity runtime

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add it to the image block

This is still pretty basic, just to check that it works.

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add a separate webpack config

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Make sure the runtime is imported only once

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Use sideEffects instead of init

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Move script registration to a general file

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add `defer` to the interactivity scripts

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Revert changes of the image block

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Fix init import name

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Move and refactor the interactive scritps registration

* Fix code style violations

* Use `wp-interactivity-` prefix for script handles

* Improve the matcher for side effects in `package.json`

* Enqueue interactivity scripts through hook

* Revert `client-assets.php` file

* Adapt to latest changes in base branch

* Add custom useSignalEffect

* Move role attribute to selectors

* Call `init` after `store` has been initialized

* Change focusable elements

* Add namespaces to context

* Move PHP file to interactivity-api folder

* Add comments with the markup to the PHP filter

* Remove micromodal attributes

* Add interactivity runtime

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add it to the image block

This is still pretty basic, just to check that it works.

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add a separate webpack config

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Make sure the runtime is imported only once

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Use sideEffects instead of init

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Move script registration to a general file

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Add `defer` to the interactivity scripts

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Revert changes of the image block

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Fix init import name

Co-authored-by: David Arenas <david.arenas@automattic.com>

* Move and refactor the interactive scritps registration

* Fix code style violations

* Use `wp-interactivity-` prefix for script handles

* Improve the matcher for side effects in `package.json`

* Add custom useSignalEffect

* Call `init` after `store` has been initialized

* Update lib/experimental/interactivity-api/script-loader.php

Co-authored-by: Weston Ruter <westonruter@google.com>

* Plugin: Ensure that translations are set correctly when overriding scripts
This replicates the same logic in WordPress core:
https://github.com/WordPress/wordpress-develop/blob/a5f3087f6b5d9c52dbe67ed247165dc32427c57d/src/wp-includes/script-loader.php#L306-L308

* Remove unnecessary comment in webpack config

Co-authored-by: Greg Ziółkowski <grzegorz@gziolo.pl>

* Clean up code to fix PHP coding standards issues

* Remove extra space

* Add tests to the navigation block interactivity

* Modify `saveSiteEditorEntities` function to wait

* Fix page-list tests

* Remove e2e tests that are already included in trunk

* Fix the navigation menu when the overlay is not present

* Ensure that all scripts using Intereactivity API have defer attribute

* Remove unused param in the callback used by hook

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>
Co-authored-by: David Arenas <david.arenas@automattic.com>
Co-authored-by: Grzegorz Ziolkowski <grzegorz@gziolo.pl>
Co-authored-by: Weston Ruter <westonruter@google.com>
  • Loading branch information
5 people authored May 10, 2023
1 parent 896ce46 commit a992f2a
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 5 deletions.
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-details-blocks', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableDetailsBlocks = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-interactivity-api-navigation-block', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableNavigationBlockInteractivity = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-theme-previews', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableThemePreviews = true', 'before' );
}
Expand Down
240 changes: 240 additions & 0 deletions lib/experimental/interactivity-api/navigation-block-interactivity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php
/**
* Extend WordPress core navigation block to use the Interactivity API.
* Interactivity API directives are added using the Tag Processor while it is experimental.
*
* @package gutenberg
*/

/**
* Add Interactivity API directives to the navigation block markup using the Tag Processor
* The final HTML of the navigation block will look similar to this:
*
* <nav
* data-wp-island
* data-wp-context='{ "core": { "navigation": { "isMenuOpen": false, "overlay": true, "roleAttribute": "" } } }'
* >
* <button
* class="wp-block-navigation__responsive-container-open"
* data-wp-on.click="actions.core.navigation.openMenu"
* data-wp-on.keydown="actions.core.navigation.handleMenuKeydown"
* >
* <div
* class="wp-block-navigation__responsive-container"
* data-wp-class.has-modal-open="context.core.navigation.isMenuOpen"
* data-wp-class.is-menu-open="context.core.navigation.isMenuOpen"
* data-wp-bind.aria-hidden="!context.core.navigation.isMenuOpen"
* data-wp-effect="effects.core.navigation.initModal"
* data-wp-on.keydow="actions.core.navigation.handleMenuKeydown"
* data-wp-on.focusout="actions.core.navigation.handleMenuFocusout"
* tabindex="-1"
* >
* <div class="wp-block-navigation__responsive-close">
* <div
* class="wp-block-navigation__responsive-dialog"
* data-wp-bind.aria-modal="context.core.navigation.isMenuOpen"
* data-wp-bind.role="selectors.core.navigation.roleAttribute"
* data-wp-effect="effects.core.navigation.focusFirstElement"
* >
* <button
* class="wp-block-navigation__responsive-container-close"
* data-wp-on.click="actions.core.navigation.closeMenu"
* >
* <svg>
* <button>
* MENU ITEMS
* </div>
* </div>
* </div>
* </nav>
*
* @param string $block_content Markup of the navigation block.
*
* @return string Navigation block markup with the proper directives
*/
function gutenberg_block_core_navigation_add_directives_to_markup( $block_content ) {
$w = new WP_HTML_Tag_Processor( $block_content );
// Add directives to the `<nav>` element.
if ( $w->next_tag( 'nav' ) ) {
$w->set_attribute( 'data-wp-island', '' );
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": false, "overlay": true, "roleAttribute": "" } } }' );
};

// Add directives to the open menu button.
if ( $w->next_tag(
array(
'tag_name' => 'BUTTON',
'class_name' => 'wp-block-navigation__responsive-container-open',
)
) ) {
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.openMenu' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
$w->remove_attribute( 'data-micromodal-trigger' );
} else {
// If the open modal button not found, we handle submenus immediately.
$w = new WP_HTML_Tag_Processor( $w->get_updated_html() );

// Add directives to the menu container.
if ( $w->next_tag(
array(
'tag_name' => 'UL',
'class_name' => 'wp-block-navigation__container',
)
) ) {
$w->set_attribute( 'data-wp-class.is-menu-open', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.aria-hidden', '!context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initModal' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
$w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' );
$w->set_attribute( 'tabindex', '-1' );
};

gutenberg_block_core_navigation_add_directives_to_submenu( $w );

return $w->get_updated_html();
}

// Add directives to the menu container.
if ( $w->next_tag(
array(
'tag_name' => 'DIV',
'class_name' => 'wp-block-navigation__responsive-container',
)
) ) {
$w->set_attribute( 'data-wp-class.has-modal-open', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-class.is-menu-open', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.aria-hidden', '!context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initModal' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
$w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' );
$w->set_attribute( 'tabindex', '-1' );
};

// Remove micromodal attribute.
if ( $w->next_tag(
array(
'tag_name' => 'DIV',
'class_name' => 'wp-block-navigation__responsive-close',
)
) ) {
$w->remove_attribute( 'data-micromodal-close' );
};

// Add directives to the dialog container.
if ( $w->next_tag(
array(
'tag_name' => 'DIV',
'class_name' => 'wp-block-navigation__responsive-dialog',
)
) ) {
$w->set_attribute( 'data-wp-bind.aria-modal', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.role', 'selectors.core.navigation.roleAttribute' );
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.focusFirstElement' );
};

// Add directives to the close button.
if ( $w->next_tag(
array(
'tag_name' => 'BUTTON',
'class_name' => 'wp-block-navigation__responsive-container-close',
)
) ) {
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.closeMenu' );
$w->remove_attribute( 'data-micromodal-close' );
};

// Submenus.
gutenberg_block_core_navigation_add_directives_to_submenu( $w );

return $w->get_updated_html();
};

/**
* Add Interactivity API directives to the navigation-submenu and page-list blocks markup using the Tag Processor
* The final HTML of the navigation-submenu and the page-list blocks will look similar to this:
*
* <li
* class="has-child"
* data-wp-context='{ "core": { "navigation": { "isMenuOpen": false, "overlay": false } } }'
* >
* <button
* class="wp-block-navigation-submenu__toggle"
* data-wp-on.click="actions.core.navigation.openMenu"
* data-wp-bind.aria-expanded="context.core.navigation.isMenuOpen"
* data-wp-on.keydown="actions.core.navigation.handleMenuKeydown"
* data-wp-on.focusout="actions.core.navigation.handleMenuFocusout"
* >
* </button>
* <span>Title</span>
* <ul
* class="wp-block-navigation__submenu-container"
* data-wp-effect="effects.core.navigation.initModal"
* data-wp-on.focusout="actions.core.navigation.handleMenuFocusout"
* data-wp-on.keydown="actions.core.navigation.handleMenuKeydown"
* >
* SUBMENU ITEMS
* </ul>
* </li>
*
* @param string $w Markup of the navigation block.
*
* @return void
*/
function gutenberg_block_core_navigation_add_directives_to_submenu( $w ) {
while ( $w->next_tag(
array(
'tag_name' => 'LI',
'class_name' => 'has-child',
)
) ) {
// Add directives to the parent `<li>`.
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": false, "overlay": false } } }' );

// Add directives to the toggle submenu button.
if ( $w->next_tag(
array(
'tag_name' => 'BUTTON',
'class_name' => 'wp-block-navigation-submenu__toggle',
)
) ) {
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.openMenu' );
$w->set_attribute( 'data-wp-bind.aria-expanded', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
$w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' );
};

// Add directives to the `<ul>` containing the subitems.
if ( $w->next_tag(
array(
'tag_name' => 'UL',
'class_name' => 'wp-block-navigation__submenu-container',
)
) ) {
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initModal' );
$w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
};
// Iterate through subitems if exist.
gutenberg_block_core_navigation_add_directives_to_submenu( $w );
}
};

add_filter( 'render_block_core/navigation', 'gutenberg_block_core_navigation_add_directives_to_markup', 10, 1 );

// Enqueue the `interactivity.js` file with the store.
add_filter(
'block_type_metadata',
function ( $metadata ) {
if ( 'core/navigation' === $metadata['name'] ) {
wp_register_script(
'wp-block-navigation-view',
gutenberg_url( 'build/block-library/interactive-blocks/navigation.min.js' ),
array( 'wp-interactivity-runtime' )
);
$metadata['viewScript'] = array( 'wp-block-navigation-view' );
}
return $metadata;
},
10,
1
);
7 changes: 3 additions & 4 deletions lib/experimental/interactivity-api/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@ function gutenberg_register_interactivity_scripts( $scripts ) {
* Adds the "defer" attribute to all the interactivity script tags.
*
* @param string $tag The generated script tag.
* @param string $handle The script's registered handle.
*
* @return string The modified script tag.
*/
function gutenberg_interactivity_scripts_add_defer_attribute( $tag, $handle ) {
if ( str_starts_with( $handle, 'wp-interactivity-' ) ) {
function gutenberg_interactivity_scripts_add_defer_attribute( $tag ) {
if ( str_contains( $tag, '/block-library/interactive-blocks/' ) ) {
$p = new WP_HTML_Tag_Processor( $tag );
$p->next_tag( array( 'tag' => 'script' ) );
$p->set_attribute( 'defer', true );
return $p->get_updated_html();
}
return $tag;
}
add_filter( 'script_loader_tag', 'gutenberg_interactivity_scripts_add_defer_attribute', 10, 2 );
add_filter( 'script_loader_tag', 'gutenberg_interactivity_scripts_add_defer_attribute', 10, 1 );
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-interactivity-api-navigation-block',
__( 'Navigation block', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Test the Navigation block using the Interactivity API', 'gutenberg' ),
'id' => 'gutenberg-interactivity-api-navigation-block',
)
);

register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/kses.php';
require __DIR__ . '/experimental/l10n.php';
require __DIR__ . '/experimental/navigation-fallback.php';
if ( gutenberg_is_experiment_enabled( 'gutenberg-interactivity-api-navigation-block' ) ) {
require __DIR__ . '/experimental/interactivity-api/navigation-block-interactivity.php';
}

// Fonts API.
if ( ! class_exists( 'WP_Fonts' ) ) {
Expand Down
Loading

1 comment on commit a992f2a

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in a992f2a.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4938036405
📝 Reported issues:

Please sign in to comment.