Make WordPress Core

Changeset 61019


Ignore:
Timestamp:
10/21/2025 11:30:43 AM (11 months ago)
Author:
luisherranz
Message:

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.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/blocks.php

    r60999 r61019  
    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;
     189        }
     190
     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 );
    186195        }
    187196
  • trunk/src/wp-includes/interactivity-api/class-wp-interactivity-api.php

    r60953 r61019  
    8787
    8888        /**
     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
     96        /**
    8997         * Stack of namespaces defined by `data-wp-interactive` directives, in
    9098         * the order they are processed.
     
    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
  • trunk/src/wp-includes/script-modules.php

    r60999 r61019  
    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 );
  • trunk/tests/phpunit/tests/blocks/register.php

    r61008 r61019  
    428428                        ) === 0
    429429                );
     430        }
     431
     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' ) );
    430531        }
    431532
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php

    r60953 r61019  
    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
     
    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();
     
    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}
  • trunk/tests/phpunit/tests/script-modules/wpScriptModules.php

    r60999 r61019  
    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,
Note: See TracChangeset for help on using the changeset viewer.