diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php
index c065342d9cc43..1a536dd3dcd98 100644
--- a/packages/block-library/src/query/index.php
+++ b/packages/block-library/src/query/index.php
@@ -137,11 +137,8 @@ function block_core_query_disable_enhanced_pagination( $parsed_block ) {
}
if ( isset( $dirty_enhanced_queries[ $block['attrs']['queryId'] ] ) ) {
- $p = new WP_HTML_Tag_Processor( $content );
- if ( $p->next_tag() ) {
- $p->set_attribute( 'data-wp-navigation-disabled', 'true' );
- }
- $content = $p->get_updated_html();
+ // Disable navigation in the router store config.
+ wp_interactivity_config( 'core/router', array( 'clientNavigationDisabled' => true ) );
$dirty_enhanced_queries[ $block['attrs']['queryId'] ] = null;
}
diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js
index 396bf4de64369..e23294a24e02e 100644
--- a/packages/block-library/src/query/view.js
+++ b/packages/block-library/src/query/view.js
@@ -28,13 +28,8 @@ store(
const queryRef = ref.closest(
'.wp-block-query[data-wp-router-region]'
);
- const isDisabled = queryRef?.dataset.wpNavigationDisabled;
- if (
- isValidLink( ref ) &&
- isValidEvent( event ) &&
- ! isDisabled
- ) {
+ if ( isValidLink( ref ) && isValidEvent( event ) ) {
event.preventDefault();
const { actions } = yield import(
@@ -50,11 +45,7 @@ store(
},
*prefetch() {
const { ref } = getElement();
- const queryRef = ref.closest(
- '.wp-block-query[data-wp-router-region]'
- );
- const isDisabled = queryRef?.dataset.wpNavigationDisabled;
- if ( isValidLink( ref ) && ! isDisabled ) {
+ if ( isValidLink( ref ) ) {
const { actions } = yield import(
'@wordpress/interactivity-router'
);
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php
index b3b967164e99a..4abcee3de2c39 100644
--- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php
@@ -8,6 +8,13 @@
*/
wp_enqueue_script_module( 'router-navigate-view' );
+
+if ( $attributes['disableNavigation'] ) {
+ wp_interactivity_config(
+ 'core/router',
+ array( 'clientNavigationDisabled' => true )
+ );
+}
?>
{
}
};
+/**
+ * Load the given page forcing a full page reload.
+ *
+ * The function returns a promise that won't resolve, useful to prevent any
+ * potential feedback indicating that the navigation has finished while the new
+ * page is being loaded.
+ *
+ * @param {string} href The page href.
+ * @return {Promise} Promise that never resolves.
+ */
+const forcePageReload = ( href ) => {
+ window.location.assign( href );
+ return new Promise( () => {} );
+};
+
// Listen to the back and forward buttons and restore the page if it's in the
// cache.
window.addEventListener( 'popstate', async () => {
@@ -113,6 +128,11 @@ export const { state, actions } = store( 'core/router', {
* @return {Promise} Promise that resolves once the navigation is completed or aborted.
*/
*navigate( href, options = {} ) {
+ const { clientNavigationDisabled } = getConfig();
+ if ( clientNavigationDisabled ) {
+ yield forcePageReload( href );
+ }
+
const pagePath = getPagePath( href );
const { navigation } = state;
const {
@@ -183,11 +203,7 @@ export const { state, actions } = store( 'core/router', {
: '' );
}
} else {
- window.location.assign( href );
- // Await a promise that won't resolve to prevent any potential
- // feedback indicating that the navigation has finished while
- // the new page is being loaded.
- yield new Promise( () => {} );
+ yield forcePageReload( href );
}
},
@@ -204,6 +220,9 @@ export const { state, actions } = store( 'core/router', {
* fetching the requested URL.
*/
prefetch( url, options = {} ) {
+ const { clientNavigationDisabled } = getConfig();
+ if ( clientNavigationDisabled ) return;
+
const pagePath = getPagePath( url );
if ( options.force || ! pages.has( pagePath ) ) {
pages.set( pagePath, fetchPage( pagePath, options ) );
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index 6baa1d2662c03..823e9c7370e0c 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### New Features
+
+- Export `getConfig()` to retrieve the server-defined configuration for the passed namespace. ([58749](https://github.com/WordPress/gutenberg/pull/58749))
+
### Breaking changes
- Remove the style prop (`key`) and class name arguments the `data-wp-style` and `data-wp-class` directives. ([#58835](https://github.com/WordPress/gutenberg/pull/58835)).
diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx
index 383bcaa1a4ae8..726579f50176d 100644
--- a/packages/interactivity/src/hooks.tsx
+++ b/packages/interactivity/src/hooks.tsx
@@ -132,7 +132,7 @@ const namespaceStack: string[] = [];
* @return The context content.
*/
export const getContext = < T extends object >( namespace?: string ): T =>
- getScope()?.context[ namespace || namespaceStack.slice( -1 )[ 0 ] ];
+ getScope()?.context[ namespace || getNamespace() ];
/**
* Retrieves a representation of the element where a function from the store
diff --git a/packages/interactivity/src/index.ts b/packages/interactivity/src/index.ts
index 367e612a8dfce..2a98900dfe379 100644
--- a/packages/interactivity/src/index.ts
+++ b/packages/interactivity/src/index.ts
@@ -13,7 +13,7 @@ import { directivePrefix } from './constants';
import { toVdom } from './vdom';
import { directive, getNamespace } from './hooks';
-export { store } from './store';
+export { store, getConfig } from './store';
export { getContext, getElement } from './hooks';
export {
withScope,
diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts
index 78b2fa259f560..b971bbcd1590c 100644
--- a/packages/interactivity/src/store.ts
+++ b/packages/interactivity/src/store.ts
@@ -11,6 +11,7 @@ import {
getScope,
setScope,
resetScope,
+ getNamespace,
setNamespace,
resetNamespace,
} from './hooks';
@@ -34,18 +35,16 @@ const deepMerge = ( target: any, source: any ) => {
}
};
-const parseInitialState = () => {
+const parseInitialData = () => {
const storeTag = document.querySelector(
`script[type="application/json"]#wp-interactivity-data`
);
- if ( ! storeTag?.textContent ) return {};
- try {
- const { state } = JSON.parse( storeTag.textContent );
- if ( isObject( state ) ) return state;
- throw Error( 'Parsed state is not an object' );
- } catch ( e ) {
- // eslint-disable-next-line no-console
- console.log( e );
+ if ( storeTag?.textContent ) {
+ try {
+ return JSON.parse( storeTag.textContent );
+ } catch ( e ) {
+ // Do nothing.
+ }
}
return {};
};
@@ -53,6 +52,7 @@ const parseInitialState = () => {
export const stores = new Map();
const rawStores = new Map();
const storeLocks = new Map();
+const storeConfigs = new Map();
const objToProxy = new WeakMap();
const proxyToNs = new WeakMap();
@@ -164,6 +164,16 @@ const handlers = {
return result;
},
};
+
+/**
+ * Get the defined config for the store with the passed namespace.
+ *
+ * @param namespace Store's namespace from which to retrieve the config.
+ * @return Defined config for the given namespace.
+ */
+export const getConfig = ( namespace: string ) =>
+ storeConfigs.get( namespace || getNamespace() ) || {};
+
interface StoreOptions {
/**
* Property to block/unblock private store namespaces.
@@ -300,7 +310,15 @@ export function store(
return stores.get( namespace );
}
-// Parse and populate the initial state.
-Object.entries( parseInitialState() ).forEach( ( [ namespace, state ] ) => {
- store( namespace, { state }, { lock: universalUnlock } );
-} );
+// Parse and populate the initial state and config.
+const data = parseInitialData();
+if ( isObject( data?.state ) ) {
+ Object.entries( data.state ).forEach( ( [ namespace, state ] ) => {
+ store( namespace, { state }, { lock: universalUnlock } );
+ } );
+}
+if ( isObject( data?.config ) ) {
+ Object.entries( data.config ).forEach( ( [ namespace, config ] ) => {
+ storeConfigs.set( namespace, config );
+ } );
+}
diff --git a/phpunit/blocks/render-query-test.php b/phpunit/blocks/render-query-test.php
index a4dc8a311b44a..a121c490d747c 100644
--- a/phpunit/blocks/render-query-test.php
+++ b/phpunit/blocks/render-query-test.php
@@ -12,6 +12,8 @@ class Tests_Blocks_RenderQueryBlock extends WP_UnitTestCase {
private static $posts;
+ private $original_wp_interactivity;
+
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$posts = $factory->post->create_many( 3 );
@@ -29,6 +31,20 @@ public static function wpTearDownAfterClass() {
unregister_block_type( 'test/plugin-block' );
}
+ public function set_up() {
+ parent::set_up();
+ global $wp_interactivity;
+ $this->original_wp_interactivity = $wp_interactivity;
+ $wp_interactivity = new WP_Interactivity_API();
+ }
+
+ public function tear_down() {
+ global $wp_interactivity;
+ $wp_interactivity = $this->original_wp_interactivity;
+ parent::tear_down();
+ }
+
+
/**
* Tests that the `core/query` block adds the corresponding directives when
* the `enhancedPagination` attribute is set.
@@ -86,11 +102,15 @@ public function test_rendering_query_with_enhanced_pagination() {
$this->assertSame( 'core/query::actions.navigate', $p->get_attribute( 'data-wp-on--click' ) );
$this->assertSame( 'core/query::actions.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) );
$this->assertSame( 'core/query::callbacks.prefetch', $p->get_attribute( 'data-wp-watch' ) );
+
+ $router_config = wp_interactivity_config( 'core/router' );
+ $this->assertEmpty( $router_config );
}
/**
- * Tests that the `core/query` block adds an extra attribute to disable the
- * enhanced pagination in the browser when a plugin block is found inside.
+ * Tests that the `core/query` block sets the option
+ * `clientNavigationDisabled` to `true` in the `core/router` store config
+ * when a plugin block is found inside.
*/
public function test_rendering_query_with_enhanced_pagination_auto_disabled_when_plugins_blocks_are_found() {
global $wp_query, $wp_the_query;
@@ -120,12 +140,15 @@ public function test_rendering_query_with_enhanced_pagination_auto_disabled_when
$p->next_tag( array( 'class_name' => 'wp-block-query' ) );
$this->assertSame( 'query-0', $p->get_attribute( 'data-wp-router-region' ) );
- $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) );
+
+ $router_config = wp_interactivity_config( 'core/router' );
+ $this->assertTrue( $router_config['clientNavigationDisabled'] );
}
/**
- * Tests that the `core/query` block adds an extra attribute to disable the
- * enhanced pagination in the browser when a post content block is found inside.
+ * Tests that the `core/query` block sets the option
+ * `clientNavigationDisabled` to `true` in the `core/router` store config
+ * when a post content block is found inside.
*/
public function test_rendering_query_with_enhanced_pagination_auto_disabled_when_post_content_block_is_found() {
global $wp_query, $wp_the_query;
@@ -155,13 +178,14 @@ public function test_rendering_query_with_enhanced_pagination_auto_disabled_when
$p->next_tag( array( 'class_name' => 'wp-block-query' ) );
$this->assertSame( 'query-0', $p->get_attribute( 'data-wp-router-region' ) );
- $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) );
+ $router_config = wp_interactivity_config( 'core/router' );
+ $this->assertTrue( $router_config['clientNavigationDisabled'] );
}
/**
- * Tests that the correct `core/query` blocks get the attribute that
- * disables enhanced pagination only if they contain a descendant that is
- * not supported (i.e., a plugin block).
+ * Tests that, whenever a `core/query` contains a descendant that is not
+ * supported (i.e., a plugin block), the option `clientNavigationDisabled`
+ * is set to `true` in the `core/router` store config.
*/
public function test_rendering_nested_queries_with_enhanced_pagination_auto_disabled() {
global $wp_query, $wp_the_query;
@@ -204,23 +228,23 @@ public function test_rendering_nested_queries_with_enhanced_pagination_auto_disa
// Query 0 contains a plugin block inside query-2 -> disabled.
$p->next_tag( array( 'class_name' => 'wp-block-query' ) );
$this->assertSame( 'query-0', $p->get_attribute( 'data-wp-router-region' ) );
- $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) );
// Query 1 does not contain a plugin block -> enabled.
$p->next_tag( array( 'class_name' => 'wp-block-query' ) );
$this->assertSame( 'query-1', $p->get_attribute( 'data-wp-router-region' ) );
- $this->assertSame( null, $p->get_attribute( 'data-wp-navigation-disabled' ) );
// Query 2 contains a plugin block -> disabled.
$p->next_tag( array( 'class_name' => 'wp-block-query' ) );
$this->assertSame( 'query-2', $p->get_attribute( 'data-wp-router-region' ) );
- $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) );
+
+ $router_config = wp_interactivity_config( 'core/router' );
+ $this->assertTrue( $router_config['clientNavigationDisabled'] );
}
/**
- * Tests that the `core/query` block adds an extra attribute to disable the
- * enhanced pagination in the browser when a plugin that does not define
- * clientNavigation is found inside.
+ * Tests that the `core/query` block sets the option
+ * `clientNavigationDisabled` to `true` in the `core/router` store config
+ * when a plugin that does not define clientNavigation is found inside.
*/
public function test_rendering_query_with_enhanced_pagination_auto_disabled_when_there_is_a_non_compatible_block() {
global $wp_query, $wp_the_query;
@@ -248,6 +272,8 @@ public function test_rendering_query_with_enhanced_pagination_auto_disabled_when
$p->next_tag( array( 'class_name' => 'wp-block-query' ) );
$this->assertSame( 'query-0', $p->get_attribute( 'data-wp-router-region' ) );
- $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) );
+
+ $router_config = wp_interactivity_config( 'core/router' );
+ $this->assertTrue( $router_config['clientNavigationDisabled'] );
}
}
diff --git a/test/e2e/specs/interactivity/router-navigate.spec.ts b/test/e2e/specs/interactivity/router-navigate.spec.ts
index 552763ea40d5c..fafa31341f463 100644
--- a/test/e2e/specs/interactivity/router-navigate.spec.ts
+++ b/test/e2e/specs/interactivity/router-navigate.spec.ts
@@ -18,6 +18,14 @@ test.describe( 'Router navigate', () => {
alias: 'router navigate - main',
attributes: { title: 'Main', links: [ link1, link2 ] },
} );
+ await utils.addPostWithBlock( 'test/router-navigate', {
+ alias: 'router navigate - disabled',
+ attributes: {
+ title: 'Main (navigation disabled)',
+ links: [ link1, link2 ],
+ disableNavigation: true,
+ },
+ } );
} );
test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
@@ -160,4 +168,27 @@ test.describe( 'Router navigate', () => {
// Make the fetch abort, just in case.
resolver!();
} );
+
+ test( 'should force a page reload when the `clientNavigationDisabled` config is set to true', async ( {
+ page,
+ interactivityUtils: utils,
+ } ) => {
+ await page.goto( utils.getLink( 'router navigate - disabled' ) );
+
+ const navigations = page.getByTestId( 'router navigations' );
+ const status = page.getByTestId( 'router status' );
+ const title = page.getByTestId( 'title' );
+
+ // Check some elements to ensure the page has hydrated.
+ await expect( navigations ).toHaveText( '0' );
+ await expect( status ).toHaveText( 'idle' );
+
+ await page.getByTestId( 'link 1' ).click();
+
+ // Check the page has updated.
+ await expect( title ).toHaveText( 'Link 1' );
+
+ // Check that client-navigations count has not increased.
+ await expect( navigations ).toHaveText( '0' );
+ } );
} );