From a7a7c6fbd2aa47dc1c6bf813e290d80ee21b5999 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 9 Feb 2024 11:04:46 +0100 Subject: [PATCH] Interactivity API: Allow global configs for namespaces (#58749) * Add `clientNavigation: false` to the router config * Parse store configs from initial data * Expose `getConfig` function * Use router config to disable client navigation * Handle `clientNavigation` cofing inside router's `navigate` * Add a test for `clientNavigation` false * Update changelogs * Bail out in prefetch if clientNavigation is disabled * Change `clientNavigation` config to `clientNavigationDisabled` * Update Query block phpunit tests * Remove console log * Support default namespaces in getConfig --------- Co-authored-by: DAreRodz Co-authored-by: luisherranz Co-authored-by: c4rl0sbr4v0 --- packages/block-library/src/query/index.php | 7 +-- packages/block-library/src/query/view.js | 13 +---- .../router-navigate/render.php | 7 +++ packages/interactivity-router/CHANGELOG.md | 4 ++ packages/interactivity-router/src/index.js | 31 ++++++++-- packages/interactivity/CHANGELOG.md | 4 ++ packages/interactivity/src/hooks.tsx | 2 +- packages/interactivity/src/index.ts | 2 +- packages/interactivity/src/store.ts | 44 +++++++++----- phpunit/blocks/render-query-test.php | 58 ++++++++++++++----- .../interactivity/router-navigate.spec.ts | 31 ++++++++++ 11 files changed, 150 insertions(+), 53 deletions(-) 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' ); + } ); } );