Skip to content

Commit edaed03

Browse files
committed
Interactivity API: Support for loadOnClientNavigation.
Uses the `wp_script_attributes` filter to add a `data-wp-router-options` directive with a `loadOnClientNavigation: true` property for all the interactive blocks that are compatible with client-side navigation to let the Interactivity API router determine which modules it can safely load during client-side navigation. Props luisherranz, westonruter. Fixes #64122. git-svn-id: https://develop.svn.wordpress.org/trunk@61019 602fd350-edb4-49c9-b593-d223f7449a82
1 parent bbbe2e7 commit edaed03

File tree

6 files changed

+220
-7
lines changed

6 files changed

+220
-7
lines changed

src/wp-includes/blocks.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,25 @@ function register_block_script_module_id( $metadata, $field_name, $index = 0 ) {
175175
$block_version = isset( $metadata['version'] ) ? $metadata['version'] : false;
176176
$module_version = isset( $module_asset['version'] ) ? $module_asset['version'] : $block_version;
177177

178-
// Blocks using the Interactivity API are server-side rendered, so they are by design not in the critical rendering path and should be deprioritized.
178+
$supports_interactivity_true = isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity'];
179+
$is_interactive = $supports_interactivity_true || ( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] );
180+
$supports_client_navigation = $supports_interactivity_true || ( isset( $metadata['supports']['interactivity']['clientNavigation'] ) && true === $metadata['supports']['interactivity']['clientNavigation'] );
181+
179182
$args = array();
180-
if (
181-
( isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity'] ) ||
182-
( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] )
183-
) {
183+
184+
// Blocks using the Interactivity API are server-side rendered, so they are
185+
// by design not in the critical rendering path and should be deprioritized.
186+
if ( $is_interactive ) {
184187
$args['fetchpriority'] = 'low';
185188
$args['in_footer'] = true;
186189
}
187190

191+
// Blocks using the Interactivity API that support client-side navigation
192+
// must be marked as such in their script modules.
193+
if ( $is_interactive && $supports_client_navigation ) {
194+
wp_interactivity()->add_client_navigation_support_to_script_module( $module_id );
195+
}
196+
188197
wp_register_script_module(
189198
$module_id,
190199
$module_uri,

src/wp-includes/interactivity-api/class-wp-interactivity-api.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ final class WP_Interactivity_API {
8585
*/
8686
private $has_processed_router_region = false;
8787

88+
/**
89+
* Set of script modules that can be loaded after client-side navigation.
90+
*
91+
* @since 6.9.0
92+
* @var array<string, true>
93+
*/
94+
private $script_modules_that_can_load_on_client_navigation = array();
95+
8896
/**
8997
* Stack of namespaces defined by `data-wp-interactive` directives, in
9098
* the order they are processed.
@@ -371,10 +379,56 @@ public function register_script_modules() {
371379
* Adds the necessary hooks for the Interactivity API.
372380
*
373381
* @since 6.5.0
382+
* @since 6.9.0 Adds support for client-side navigation in script modules.
374383
*/
375384
public function add_hooks() {
376385
add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) );
377386
add_filter( 'script_module_data_@wordpress/interactivity-router', array( $this, 'filter_script_module_interactivity_router_data' ) );
387+
add_filter( 'wp_script_attributes', array( $this, 'add_load_on_client_navigation_attribute_to_script_modules' ), 10, 1 );
388+
}
389+
390+
/**
391+
* Adds the `data-wp-router-options` attribute to script modules that
392+
* support client-side navigation.
393+
*
394+
* This method filters the script attributes to include loading instructions
395+
* for the Interactivity API router, indicating which modules can be loaded
396+
* during client-side navigation.
397+
*
398+
* @since 6.9.0
399+
*
400+
* @param array<string, string|true>|mixed $attributes The script tag attributes.
401+
* @return array The modified script tag attributes.
402+
*/
403+
public function add_load_on_client_navigation_attribute_to_script_modules( $attributes ) {
404+
if (
405+
is_array( $attributes ) &&
406+
isset( $attributes['type'], $attributes['id'] ) &&
407+
'module' === $attributes['type'] &&
408+
array_key_exists(
409+
preg_replace( '/-js-module$/', '', $attributes['id'] ),
410+
$this->script_modules_that_can_load_on_client_navigation
411+
)
412+
) {
413+
$attributes['data-wp-router-options'] = wp_json_encode( array( 'loadOnClientNavigation' => true ) );
414+
}
415+
return $attributes;
416+
}
417+
418+
/**
419+
* Marks a script module as compatible with client-side navigation.
420+
*
421+
* This method registers a script module to be loaded during client-side
422+
* navigation in the Interactivity API router. Script modules marked with
423+
* this method will have the `loadOnClientNavigation` option enabled in the
424+
* `data-wp-router-options` directive.
425+
*
426+
* @since 6.9.0
427+
*
428+
* @param string $script_module_id The script module identifier.
429+
*/
430+
public function add_client_navigation_support_to_script_module( string $script_module_id ) {
431+
$this->script_modules_that_can_load_on_client_navigation[ $script_module_id ] = true;
378432
}
379433

380434
/**

src/wp-includes/script-modules.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ function wp_default_script_modules() {
202202
$args['in_footer'] = true;
203203
}
204204

205+
// Marks all Core blocks as compatible with client-side navigation.
206+
if ( str_starts_with( $script_module_id, '@wordpress/block-library' ) ) {
207+
wp_interactivity()->add_client_navigation_support_to_script_module( $script_module_id );
208+
}
209+
205210
$path = includes_url( "js/dist/script-modules/{$file_name}" );
206211
wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'], $args );
207212
}

tests/phpunit/tests/blocks/register.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,107 @@ public function test_success_register_block_script_module_id() {
429429
);
430430
}
431431

432+
/**
433+
* Tests that blocks with supports.interactivity have the
434+
* `data-wp-router-options` directive.
435+
*
436+
* @ticket 64122
437+
*
438+
* @covers ::register_block_script_module_id
439+
*/
440+
public function test_register_block_script_module_id_with_interactivity_true() {
441+
$metadata = array(
442+
'file' => DIR_TESTDATA . '/blocks/notice/block.json',
443+
'viewScriptModule' => 'file:./block.js',
444+
);
445+
446+
$interactivity_true = array_merge(
447+
$metadata,
448+
array(
449+
'name' => 'tests/interactivity-true',
450+
'supports' => array( 'interactivity' => true ),
451+
)
452+
);
453+
$interactive_and_client_navigation = array_merge(
454+
$metadata,
455+
array(
456+
'name' => 'tests/interactive-and-client-navigation',
457+
'supports' => array(
458+
'interactivity' => array(
459+
'interactive' => true,
460+
'clientNavigation' => true,
461+
),
462+
),
463+
)
464+
);
465+
$interactive_and_not_client_navigation = array_merge(
466+
$metadata,
467+
array(
468+
'name' => 'tests/interactive-and-not-client-navigation',
469+
'supports' => array(
470+
'interactivity' => array(
471+
'interactive' => true,
472+
'clientNavigation' => false,
473+
),
474+
),
475+
)
476+
);
477+
$not_interactive_and_client_navigation = array_merge(
478+
$metadata,
479+
array(
480+
'name' => 'tests/not-interactive-and-client-navigation',
481+
'supports' => array(
482+
'interactivity' => array(
483+
'interactive' => false,
484+
'clientNavigation' => true,
485+
),
486+
),
487+
)
488+
);
489+
$no_interactivity = array_merge(
490+
$metadata,
491+
array(
492+
'name' => 'tests/no-interactivity',
493+
'supports' => array(),
494+
)
495+
);
496+
497+
$interactivity_true_module_id = register_block_script_module_id( $interactivity_true, 'viewScriptModule' );
498+
$interactive_and_client_navigation_module_id = register_block_script_module_id( $interactive_and_client_navigation, 'viewScriptModule' );
499+
$interactive_and_not_client_navigation_module_id = register_block_script_module_id( $interactive_and_not_client_navigation, 'viewScriptModule' );
500+
$not_interactive_and_client_navigation_module_id = register_block_script_module_id( $not_interactive_and_client_navigation, 'viewScriptModule' );
501+
$no_interactivity_module_id = register_block_script_module_id( $no_interactivity, 'viewScriptModule' );
502+
wp_enqueue_script_module( $interactivity_true_module_id );
503+
wp_enqueue_script_module( $interactive_and_client_navigation_module_id );
504+
wp_enqueue_script_module( $interactive_and_not_client_navigation_module_id );
505+
wp_enqueue_script_module( $not_interactive_and_client_navigation_module_id );
506+
wp_enqueue_script_module( $no_interactivity_module_id );
507+
508+
$output = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
509+
510+
$p = new WP_HTML_Tag_Processor( $output );
511+
512+
$this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' );
513+
$this->assertSame( 'tests-interactivity-true-view-script-module-js-module', $p->get_attribute( 'id' ) );
514+
$this->assertSame( '{"loadOnClientNavigation":true}', $p->get_attribute( 'data-wp-router-options' ) );
515+
516+
$this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' );
517+
$this->assertSame( 'tests-interactive-and-client-navigation-view-script-module-js-module', $p->get_attribute( 'id' ) );
518+
$this->assertSame( '{"loadOnClientNavigation":true}', $p->get_attribute( 'data-wp-router-options' ) );
519+
520+
$this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' );
521+
$this->assertSame( 'tests-interactive-and-not-client-navigation-view-script-module-js-module', $p->get_attribute( 'id' ) );
522+
$this->assertNull( $p->get_attribute( 'data-wp-router-options' ) );
523+
524+
$this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' );
525+
$this->assertSame( 'tests-not-interactive-and-client-navigation-view-script-module-js-module', $p->get_attribute( 'id' ) );
526+
$this->assertNull( $p->get_attribute( 'data-wp-router-options' ) );
527+
528+
$this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' );
529+
$this->assertSame( 'tests-no-interactivity-view-script-module-js-module', $p->get_attribute( 'id' ) );
530+
$this->assertNull( $p->get_attribute( 'data-wp-router-options' ) );
531+
}
532+
432533
/**
433534
* @ticket 50263
434535
*/

tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ public function set_up() {
2626
parent::set_up();
2727
$this->interactivity = new WP_Interactivity_API();
2828
wp_default_script_modules();
29+
$this->interactivity->add_hooks();
30+
}
31+
32+
/**
33+
* Tear down.
34+
*/
35+
public function tear_down() {
36+
global $wp_script_modules;
37+
parent::tear_down();
38+
$wp_script_modules = null;
2939
}
3040

3141
public function charset_iso_8859_1() {
@@ -236,7 +246,6 @@ public function test_register_script_modules_deprecated() {
236246
* @return MockAction
237247
*/
238248
private function get_script_data_filter_result( ?Closure $callback = null ): MockAction {
239-
$this->interactivity->add_hooks();
240249
wp_enqueue_script_module( '@wordpress/interactivity' );
241250
$filter = new MockAction();
242251
add_filter( 'script_module_data_@wordpress/interactivity', array( $filter, 'filter' ) );
@@ -1723,4 +1732,39 @@ public function test_invalid_directive_names_are_ignored() {
17231732
$this->assertStringNotContainsString( 'class="dis:allowed"', $processed_html );
17241733
$this->assertStringNotContainsString( 'class="[disallowed]"', $processed_html );
17251734
}
1735+
1736+
/**
1737+
* Tests that add_client_navigation_support_to_script_module marks a
1738+
* script module for client navigation.
1739+
*
1740+
* @ticket 64122
1741+
*
1742+
* @covers WP_Interactivity_API::add_client_navigation_support_to_script_module
1743+
* @covers WP_Interactivity_API::add_load_on_client_navigation_attribute_to_script_modules
1744+
*/
1745+
public function test_add_client_navigation_support_to_script_module() {
1746+
$this->interactivity->add_client_navigation_support_to_script_module( 'marked-module' );
1747+
1748+
wp_register_script_module( 'marked-module', '/marked.js' );
1749+
wp_register_script_module( 'unmarked-module', '/unmarked.js' );
1750+
wp_enqueue_script_module( 'marked-module' );
1751+
wp_enqueue_script_module( 'unmarked-module' );
1752+
1753+
$output = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
1754+
1755+
$p = new WP_HTML_Tag_Processor( $output );
1756+
1757+
// First module: marked-module should have the attribute.
1758+
$p->next_tag( array( 'tag_name' => 'SCRIPT' ) );
1759+
$this->assertSame( 'marked-module-js-module', $p->get_attribute( 'id' ) );
1760+
$this->assertSame(
1761+
'{"loadOnClientNavigation":true}',
1762+
$p->get_attribute( 'data-wp-router-options' )
1763+
);
1764+
1765+
// Second module: unmarked-module should NOT have the attribute.
1766+
$p->next_tag( array( 'tag_name' => 'SCRIPT' ) );
1767+
$this->assertSame( 'unmarked-module-js-module', $p->get_attribute( 'id' ) );
1768+
$this->assertNull( $p->get_attribute( 'data-wp-router-options' ) );
1769+
}
17261770
}

tests/phpunit/tests/script-modules/wpScriptModules.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1806,7 +1806,7 @@ public function test_default_script_modules() {
18061806
$this->assertEqualHTML(
18071807
'
18081808
<script type="module" src="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
1809-
<script type="module" src="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
1809+
<script type="module" src="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low" data-wp-router-options="{&quot;loadOnClientNavigation&quot;:true}"></script>
18101810
',
18111811
$actual_footer_script_modules,
18121812
'<body>',

0 commit comments

Comments
 (0)