From 5fb6a398565eb56eedd04fd0c397ff994df3ebfc Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 10:58:55 +0400 Subject: [PATCH 1/3] Interactivity API: Resolve the global instance in hook callbacks. `WP_Interactivity_API::add_hooks()` registered its filters as `array( $this, 'method' )`, and `data_wp_router_region_processor()` added `array( $this, 'print_router_markup' )` on `wp_footer`. Those callbacks stay bound to the object that was the global when the hooks were added. Replacing the `$wp_interactivity` global later redirects `wp_interactivity_state()` and `wp_interactivity_config()` to the new instance, but the bound callbacks still read the old one, so the printed script module data comes from the wrong instance. Register procedural functions that delegate to the current global instance, matching how classic scripts and styles register their hooks. See #66100, #64484. --- .../class-wp-interactivity-api.php | 9 ++-- .../interactivity-api/interactivity-api.php | 53 +++++++++++++++++++ .../interactivity-api/wpInteractivityAPI.php | 42 ++++++++++++++- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 8a2bc3467566a..53ce74e8ebb66 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -399,11 +399,12 @@ public function register_script_modules() { * * @since 6.5.0 * @since 6.9.0 Adds support for client-side navigation in script modules. + * @since 7.2.0 The callbacks resolve the current global WP_Interactivity_API instance when they run. */ public function add_hooks() { - add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) ); - add_filter( 'script_module_data_@wordpress/interactivity-router', array( $this, 'filter_script_module_interactivity_router_data' ) ); - add_filter( 'wp_script_attributes', array( $this, 'add_load_on_client_navigation_attribute_to_script_modules' ) ); + add_filter( 'script_module_data_@wordpress/interactivity', 'wp_interactivity_script_module_data' ); + add_filter( 'script_module_data_@wordpress/interactivity-router', 'wp_interactivity_router_script_module_data' ); + add_filter( 'wp_script_attributes', 'wp_interactivity_script_module_attributes' ); } /** @@ -1516,7 +1517,7 @@ private function data_wp_router_region_processor( WP_Interactivity_API_Directive wp_enqueue_style( 'wp-interactivity-router-animations' ); // Adds the necessary markup to the footer. - add_action( 'wp_footer', array( $this, 'print_router_markup' ) ); + add_action( 'wp_footer', 'wp_interactivity_print_router_markup' ); } } diff --git a/src/wp-includes/interactivity-api/interactivity-api.php b/src/wp-includes/interactivity-api/interactivity-api.php index 105f3f6f29c18..315c38225b992 100644 --- a/src/wp-includes/interactivity-api/interactivity-api.php +++ b/src/wp-includes/interactivity-api/interactivity-api.php @@ -138,3 +138,56 @@ function wp_interactivity_get_context( ?string $store_namespace = null ): array function wp_interactivity_get_element(): ?array { return wp_interactivity()->get_element(); } + +/** + * Filters the data for the `@wordpress/interactivity` script module. + * + * @since 7.2.0 + * + * @see WP_Interactivity_API::filter_script_module_interactivity_data() + * + * @param array $data Data to filter. + * @return array Data for the Interactivity API script module. + */ +function wp_interactivity_script_module_data( array $data ): array { + return wp_interactivity()->filter_script_module_interactivity_data( $data ); +} + +/** + * Filters the data for the `@wordpress/interactivity-router` script module. + * + * @since 7.2.0 + * + * @see WP_Interactivity_API::filter_script_module_interactivity_router_data() + * + * @param array $data Data to filter. + * @return array Data for the Interactivity Router script module. + */ +function wp_interactivity_router_script_module_data( array $data ): array { + return wp_interactivity()->filter_script_module_interactivity_router_data( $data ); +} + +/** + * Adds the `data-wp-router-options` attribute to script modules that support client-side navigation. + * + * @since 7.2.0 + * + * @see WP_Interactivity_API::add_load_on_client_navigation_attribute_to_script_modules() + * + * @param array|mixed $attributes The script tag attributes. + * @return array The modified script tag attributes. + */ +function wp_interactivity_script_module_attributes( $attributes ) { + return wp_interactivity()->add_load_on_client_navigation_attribute_to_script_modules( $attributes ); +} + +/** + * Outputs markup for the `@wordpress/interactivity-router` script module. + * + * @since 7.2.0 + * + * @see WP_Interactivity_API::print_router_markup() + */ +function wp_interactivity_print_router_markup() { + wp_interactivity()->print_router_markup(); +} diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index 430fe606f9277..af160e78c8273 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -19,12 +19,28 @@ class Tests_Interactivity_API_WpInteractivityAPI extends WP_UnitTestCase { */ protected $interactivity; + /** + * Original global WP_Interactivity_API instance. + * + * @var WP_Interactivity_API|null + */ + protected $original_wp_interactivity; + /** * Set up. */ public function set_up() { + global $wp_interactivity; parent::set_up(); $this->interactivity = new WP_Interactivity_API(); + + /* + * The hooks added by `add_hooks()` operate on the global instance, so the + * test instance must be the global one for those hooks to see its data. + */ + $this->original_wp_interactivity = $wp_interactivity; + $wp_interactivity = $this->interactivity; + wp_default_script_modules(); $this->interactivity->add_hooks(); } @@ -33,9 +49,10 @@ public function set_up() { * Tear down. */ public function tear_down() { - global $wp_script_modules; + global $wp_script_modules, $wp_interactivity; parent::tear_down(); $wp_script_modules = null; + $wp_interactivity = $this->original_wp_interactivity; } public function charset_iso_8859_1() { @@ -2363,4 +2380,27 @@ public function test_add_client_navigation_support_to_script_module() { $this->assertSame( 'unmarked-module-js-module', $p->get_attribute( 'id' ) ); $this->assertNull( $p->get_attribute( 'data-wp-router-options' ) ); } + + /** + * Tests that the callbacks added by `add_hooks()` read the current global + * WP_Interactivity_API instance when they run. + * + * @ticket 66100 + * @covers WP_Interactivity_API::add_hooks + */ + public function test_add_hooks_callbacks_read_the_current_global_instance() { + global $wp_interactivity; + + // set_up() registered the hooks on the instance that is the global. + $wp_interactivity = new WP_Interactivity_API(); + wp_interactivity_state( 'test-hooks-global', array( 'value' => 'from-current-global' ) ); + + $data = apply_filters( 'script_module_data_@wordpress/interactivity', array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + + $this->assertSame( + array( 'state' => array( 'test-hooks-global' => array( 'value' => 'from-current-global' ) ) ), + $data, + 'The filter callbacks added by add_hooks() must read the current global WP_Interactivity_API instance.' + ); + } } From 628bedf828d8d291b9e52b6c0acb0d8288896079 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 09:37:50 +0200 Subject: [PATCH 2/3] Remove redundant since tag --- src/wp-includes/interactivity-api/class-wp-interactivity-api.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 53ce74e8ebb66..e621bc5c2393a 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -399,7 +399,6 @@ public function register_script_modules() { * * @since 6.5.0 * @since 6.9.0 Adds support for client-side navigation in script modules. - * @since 7.2.0 The callbacks resolve the current global WP_Interactivity_API instance when they run. */ public function add_hooks() { add_filter( 'script_module_data_@wordpress/interactivity', 'wp_interactivity_script_module_data' ); From 455845f73ab22620fdb738a772e3605d4142a443 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 13:41:44 +0400 Subject: [PATCH 3/3] Interactivity API: Declare a void return type on the router markup function. `wp_interactivity_print_router_markup()` returns no value and declared no return type. Declare `: void`. `wp_interactivity_script_module_data()` and `wp_interactivity_router_script_module_data()` already declare `array $data): array`. `wp_interactivity_script_module_attributes()` stays untyped to match `WP_Interactivity_API::add_load_on_client_navigation_attribute_to_script_modules()`, which declares neither a parameter nor a return type. See #66100. --- src/wp-includes/interactivity-api/interactivity-api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/interactivity-api/interactivity-api.php b/src/wp-includes/interactivity-api/interactivity-api.php index 315c38225b992..7de5d77191e41 100644 --- a/src/wp-includes/interactivity-api/interactivity-api.php +++ b/src/wp-includes/interactivity-api/interactivity-api.php @@ -188,6 +188,6 @@ function wp_interactivity_script_module_attributes( $attributes ) { * * @see WP_Interactivity_API::print_router_markup() */ -function wp_interactivity_print_router_markup() { +function wp_interactivity_print_router_markup(): void { wp_interactivity()->print_router_markup(); }