Make WordPress Core

Changeset 61019


Ignore:
Timestamp:
10/21/2025 11:30:43 AM (4 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.