From 584758747ceec45c89784bc23ed7485b293a8ef5 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 13 Jul 2023 13:47:14 +0200 Subject: [PATCH 01/12] Update router to hydrate only interactive regions --- assets/js/interactivity/router.js | 163 +++++++----------------------- 1 file changed, 38 insertions(+), 125 deletions(-) diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index 780371dd764..f4ff36037dc 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -6,10 +6,8 @@ import { csnMetaTagItemprop, directivePrefix } from './constants'; // The root to render the vdom (document.body). let rootFragment; -// The cache of visited and prefetched pages, stylesheets and scripts. +// The cache of visited and prefetched pages. const pages = new Map(); -const stylesheets = new Map(); -const scripts = new Map(); // Helper to remove domain and hash from the URL. We are only interesting in // caching the path and the query. @@ -18,94 +16,24 @@ const cleanUrl = ( url ) => { return u.pathname + u.search; }; -// Helper to check if a page can do client-side navigation. -export const canDoClientSideNavigation = ( dom ) => - dom - .querySelector( `meta[itemprop='${ csnMetaTagItemprop }']` ) - ?.getAttribute( 'content' ) === 'active'; - -/** - * Finds the elements in the document that match the selector and fetch them. - * For each element found, fetch the content and store it in the cache. - * Returns an array of elements to add to the document. - * - * @param {Document} document - * @param {string} selector - CSS selector used to find the elements. - * @param {'href'|'src'} attribute - Attribute that determines where to fetch - * the styles or scripts from. Also used as the key for the cache. - * @param {Map} cache - Cache to use for the elements. Can be `stylesheets` or `scripts`. - * @param {'style'|'script'} elementToCreate - Element to create for each fetched - * item. Can be 'style' or 'script'. - * @return {Promise>} - Array of elements to add to the document. - */ -const fetchScriptOrStyle = async ( - document, - selector, - attribute, - cache, - elementToCreate -) => { - const fetchedItems = await Promise.all( - [].map.call( document.querySelectorAll( selector ), ( el ) => { - const attributeValue = el.getAttribute( attribute ); - if ( ! cache.has( attributeValue ) ) - cache.set( - attributeValue, - fetch( attributeValue ).then( ( r ) => r.text() ) - ); - return cache.get( attributeValue ); - } ) - ); - - return fetchedItems.map( ( item ) => { - const element = document.createElement( elementToCreate ); - element.textContent = item; - return element; - } ); -}; - -// Fetch styles of a new page. -const fetchAssets = async ( document ) => { - const stylesFromSheets = await fetchScriptOrStyle( - document, - 'link[rel=stylesheet]', - 'href', - stylesheets, - 'style' - ); - const scriptTags = await fetchScriptOrStyle( - document, - 'script[src]', - 'src', - scripts, - 'script' - ); - const moduleScripts = await fetchScriptOrStyle( - document, - 'script[type=module]', - 'src', - scripts, - 'script' - ); - moduleScripts.forEach( ( script ) => - script.setAttribute( 'type', 'module' ) - ); - - return [ - ...scriptTags, - document.querySelector( 'title' ), - ...document.querySelectorAll( 'style' ), - ...stylesFromSheets, - ]; -}; - // Fetch a new page and convert it to a static virtual DOM. const fetchPage = async ( url ) => { - const html = await window.fetch( url ).then( ( r ) => r.text() ); - const dom = new window.DOMParser().parseFromString( html, 'text/html' ); - if ( ! canDoClientSideNavigation( dom.head ) ) return false; - const head = await fetchAssets( dom ); - return { head, body: toVdom( dom.body ) }; + let dom; + try { + const res = await window.fetch( url ); + if ( res.status !== 200 ) return false; + const html = await res.text(); + dom = new window.DOMParser().parseFromString( html, 'text/html' ); + } catch ( e ) { + return false; + } + const regions = {}; + dom.querySelectorAll( '[data-wc-navigation-id]' ).forEach( ( region ) => { + const id = region.attributes[ 'data-wc-navigation-id' ]; + regions[ id ] = toVdom( region ); + } ); + + return { regions }; }; // Prefetch a page. We store the promise to avoid triggering a second fetch for @@ -117,14 +45,24 @@ export const prefetch = ( url ) => { } }; +// Render all interactive regions contained in the given page. +const renderRegions = ( page ) => { + document + .querySelectorAll( '[data-wc-navigation-id]' ) + .forEach( ( region ) => { + const id = region.attributes[ 'data-wc-navigation-id' ]; + const fragment = createRootFragment( region.parentElement, region ); + render( page.regions[ id ], fragment ); + } ); +}; + // Navigate to a new page. export const navigate = async ( href, { replace = false } = {} ) => { const url = cleanUrl( href ); prefetch( url ); const page = await pages.get( url ); if ( page ) { - document.head.replaceChildren( ...page.head ); - render( page.body, rootFragment ); + renderRegions( page ); window.history[ replace ? 'replaceState' : 'pushState' ]( {}, '', @@ -141,8 +79,7 @@ window.addEventListener( 'popstate', async () => { const url = cleanUrl( window.location ); // Remove hash. const page = pages.has( url ) && ( await pages.get( url ) ); if ( page ) { - document.head.replaceChildren( ...page.head ); - render( page.body, rootFragment ); + renderRegions( page ); } else { window.location.reload(); } @@ -150,37 +87,13 @@ window.addEventListener( 'popstate', async () => { // Initialize the router with the initial DOM. export const init = async () => { - if ( canDoClientSideNavigation( document.head ) ) { - // Create the root fragment to hydrate everything. - rootFragment = createRootFragment( - document.documentElement, - document.body - ); - const body = toVdom( document.body ); - hydrate( body, rootFragment ); - - // Cache the scripts. Has to be called before fetching the assets. - [].map.call( document.querySelectorAll( 'script[src]' ), ( script ) => { - scripts.set( script.getAttribute( 'src' ), script.textContent ); + document + .querySelectorAll( `[data-${ directivePrefix }-interactive]` ) + .forEach( ( node ) => { + if ( ! hydratedIslands.has( node ) ) { + const fragment = createRootFragment( node.parentNode, node ); + const vdom = toVdom( node ); + hydrate( vdom, fragment ); + } } ); - - const head = await fetchAssets( document ); - pages.set( - cleanUrl( window.location ), - Promise.resolve( { body, head } ) - ); - } else { - document - .querySelectorAll( `[data-${ directivePrefix }-interactive]` ) - .forEach( ( node ) => { - if ( ! hydratedIslands.has( node ) ) { - const fragment = createRootFragment( - node.parentNode, - node - ); - const vdom = toVdom( node ); - hydrate( vdom, fragment ); - } - } ); - } }; From e2b5431c74f5fce1b6ceee9cd1f1f7d2824f4835 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 13 Jul 2023 13:54:14 +0200 Subject: [PATCH 02/12] Rename link directive to navigation-link --- assets/js/interactivity/directives.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/assets/js/interactivity/directives.js b/assets/js/interactivity/directives.js index a306b7df9fe..6b4d55f0475 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -2,10 +2,7 @@ import { useContext, useMemo, useEffect } from 'preact/hooks'; import { deepSignal, peek } from 'deepsignal'; import { useSignalEffect } from './utils'; import { directive } from './hooks'; -import { prefetch, navigate, canDoClientSideNavigation } from './router'; - -// Check if current page can do client-side navigation. -const clientSideNavigation = canDoClientSideNavigation( document.head ); +import { prefetch, navigate } from './router'; const isObject = ( item ) => item && typeof item === 'object' && ! Array.isArray( item ); @@ -151,25 +148,25 @@ export default () => { } ); - // data-wc-link + // data-wc-navigation-link directive( - 'link', + 'navigation-link', ( { directives: { - link: { default: link }, + 'navigation-link': { default: link }, }, props: { href }, element, } ) => { useEffect( () => { // Prefetch the page if it is in the directive options. - if ( clientSideNavigation && link?.prefetch ) { + if ( link?.prefetch ) { prefetch( href ); } } ); // Don't do anything if it's falsy. - if ( clientSideNavigation && link !== false ) { + if ( link !== false ) { element.props.onclick = async ( event ) => { event.preventDefault(); From c0e090c0726998614748ee0ae85e68c141d69cb6 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 13 Jul 2023 17:51:22 +0200 Subject: [PATCH 03/12] Add navigation directives to Query and Pagination blocks --- src/BlockTypes/ProductQuery.php | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 82850a407cb..78254aa36b4 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -79,6 +79,60 @@ protected function initialize() { ); add_filter( 'rest_product_query', array( $this, 'update_rest_query' ), 10, 2 ); add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 ); + add_filter( 'render_block_core/query', array( $this, 'add_navigation_id_directive' ), 10, 3 ); + add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 ); + } + + /** + * Mark the Product Query as an interactive region so it can be updated + * during client-side navigation. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @param \WP_Block $instance The block instance. + */ + public function add_navigation_id_directive( $block_content, $block, $instance ) { + if ( self::is_woocommerce_variation( $block ) ) { + // Enqueue the Interactivity API runtime. + wp_enqueue_script( 'wc-interactivity' ); + + $p = new \WP_HTML_Tag_Processor( $block_content ); + + // Add `data-wc-navigation-id to the query block. + if ( $p->next_tag( array( 'class_name' => 'wp-block-query' ) ) ) { + $p->set_attribute( 'data-wc-interactive', true ); + $p->set_attribute( 'data-wc-navigation-id', $block['attrs']['queryId'] ); + $block_content = $p->get_updated_html(); + } + } + + return $block_content; + } + + /** + * Add interactive links to all anchors inside the Query Pagination block. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @param \WP_Block $instance The block instance. + */ + public function add_navigation_link_directives( $block_content, $block, $instance ) { + if ( + self::is_woocommerce_variation( $this->parsed_block ) && + $instance->context['queryId'] === $this->parsed_block['attrs']['queryId'] + ) { + $p = new \WP_HTML_Tag_Processor( $block_content ); + + while ( $p->next_tag( 'a' ) ) { + $p->set_attribute( + 'data-wc-navigation-link', + '{"prefetch":true,"scroll":false}' + ); + } + $block_content = $p->get_updated_html(); + } + + return $block_content; } /** From 7ab84aafe53730c57ff68e088105434d6554e539 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 13 Jul 2023 17:51:45 +0200 Subject: [PATCH 04/12] Enable the Interactivity API by default --- woocommerce-gutenberg-products-block.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 60f54a154c7..121099bee07 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -309,7 +309,7 @@ function woocommerce_blocks_interactivity_setup() { // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment $is_enabled = apply_filters( 'woocommerce_blocks_enable_interactivity_api', - false + true ); if ( $is_enabled ) { From 91f2432eb25e2ac03f1bb9d028140456d295ffde Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 14 Jul 2023 09:50:44 +0200 Subject: [PATCH 05/12] Remove client-side navigation meta tag --- src/Interactivity/client-side-navigation.php | 9 --------- src/Interactivity/load.php | 1 - 2 files changed, 10 deletions(-) delete mode 100644 src/Interactivity/client-side-navigation.php diff --git a/src/Interactivity/client-side-navigation.php b/src/Interactivity/client-side-navigation.php deleted file mode 100644 index ab550070f61..00000000000 --- a/src/Interactivity/client-side-navigation.php +++ /dev/null @@ -1,9 +0,0 @@ -'; -} -add_action( 'wp_head', 'woocommerce_interactivity_add_client_side_navigation_meta_tag' ); diff --git a/src/Interactivity/load.php b/src/Interactivity/load.php index 8838971f099..59358429242 100644 --- a/src/Interactivity/load.php +++ b/src/Interactivity/load.php @@ -2,4 +2,3 @@ require __DIR__ . '/class-wc-interactivity-store.php'; require __DIR__ . '/store.php'; require __DIR__ . '/scripts.php'; -require __DIR__ . '/client-side-navigation.php'; From 35c0e5f0a96062a2e109903fd379bbbae47d515d Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 19 Jul 2023 12:53:59 +0200 Subject: [PATCH 06/12] Cache initial regions --- assets/js/interactivity/router.js | 36 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index f4ff36037dc..6284b61f4e4 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -1,10 +1,7 @@ import { hydrate, render } from 'preact'; import { toVdom, hydratedIslands } from './vdom'; import { createRootFragment } from './utils'; -import { csnMetaTagItemprop, directivePrefix } from './constants'; - -// The root to render the vdom (document.body). -let rootFragment; +import { directivePrefix } from './constants'; // The cache of visited and prefetched pages. const pages = new Map(); @@ -27,9 +24,17 @@ const fetchPage = async ( url ) => { } catch ( e ) { return false; } + + return regionsToVdom( dom ); +}; + +// Return an object with VDOM trees of those HTML regions marked with a +// `navigation-id` directive. +const regionsToVdom = ( dom ) => { const regions = {}; - dom.querySelectorAll( '[data-wc-navigation-id]' ).forEach( ( region ) => { - const id = region.attributes[ 'data-wc-navigation-id' ]; + const attrName = `data-${ directivePrefix }-navigation-id`; + dom.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { + const id = region.attributes[ attrName ]; regions[ id ] = toVdom( region ); } ); @@ -47,13 +52,12 @@ export const prefetch = ( url ) => { // Render all interactive regions contained in the given page. const renderRegions = ( page ) => { - document - .querySelectorAll( '[data-wc-navigation-id]' ) - .forEach( ( region ) => { - const id = region.attributes[ 'data-wc-navigation-id' ]; - const fragment = createRootFragment( region.parentElement, region ); - render( page.regions[ id ], fragment ); - } ); + const attrName = `data-${ directivePrefix }-navigation-id`; + document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { + const id = region.attributes[ attrName ]; + const fragment = createRootFragment( region.parentElement, region ); + render( page.regions[ id ], fragment ); + } ); }; // Navigate to a new page. @@ -96,4 +100,10 @@ export const init = async () => { hydrate( vdom, fragment ); } } ); + + // Cache the current regions. + pages.set( + cleanUrl( window.location ), + Promise.resolve( regionsToVdom( document ) ) + ); }; From 7b4c991f5d735c511c9d75e3a29be27a96684715 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 19 Jul 2023 13:03:08 +0200 Subject: [PATCH 07/12] Move data-wc-interactive from query to query-pagination --- src/BlockTypes/ProductQuery.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index de61b0543c8..abceff038bc 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -100,7 +100,6 @@ public function add_navigation_id_directive( $block_content, $block, $instance ) // Add `data-wc-navigation-id to the query block. if ( $p->next_tag( array( 'class_name' => 'wp-block-query' ) ) ) { - $p->set_attribute( 'data-wc-interactive', true ); $p->set_attribute( 'data-wc-navigation-id', $block['attrs']['queryId'] ); $block_content = $p->get_updated_html(); } @@ -122,6 +121,8 @@ public function add_navigation_link_directives( $block_content, $block, $instanc $instance->context['queryId'] === $this->parsed_block['attrs']['queryId'] ) { $p = new \WP_HTML_Tag_Processor( $block_content ); + $p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) ); + $p->set_attribute( 'data-wc-interactive', true ); while ( $p->next_tag( 'a' ) ) { $p->set_attribute( From ade05add4196d7e506c55b433f9a74a8b66f79bd Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 19 Jul 2023 16:26:08 +0200 Subject: [PATCH 08/12] Add woo prefix to navigation id --- src/BlockTypes/ProductQuery.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index abceff038bc..ae8f60b3cae 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -100,7 +100,10 @@ public function add_navigation_id_directive( $block_content, $block, $instance ) // Add `data-wc-navigation-id to the query block. if ( $p->next_tag( array( 'class_name' => 'wp-block-query' ) ) ) { - $p->set_attribute( 'data-wc-navigation-id', $block['attrs']['queryId'] ); + $p->set_attribute( + 'data-wc-navigation-id', + 'woo-products-' . $block['attrs']['queryId'] + ); $block_content = $p->get_updated_html(); } } From 967f00d0aa9825213b503cb9668782331a81ae5b Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 19 Jul 2023 18:34:41 +0200 Subject: [PATCH 09/12] Add keys and move wc-interactive back to the query block --- src/BlockTypes/ProductQuery.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index ae8f60b3cae..661a9d40752 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -104,6 +104,7 @@ public function add_navigation_id_directive( $block_content, $block, $instance ) 'data-wc-navigation-id', 'woo-products-' . $block['attrs']['queryId'] ); + $p->set_attribute( 'data-wc-interactive', true ); $block_content = $p->get_updated_html(); } } @@ -125,13 +126,30 @@ public function add_navigation_link_directives( $block_content, $block, $instanc ) { $p = new \WP_HTML_Tag_Processor( $block_content ); $p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) ); - $p->set_attribute( 'data-wc-interactive', true ); while ( $p->next_tag( 'a' ) ) { + $class_attr = $p->get_attribute( 'class' ); + $class_list = preg_split( '/\s+/', $class_attr ); + + $is_previous = in_array( 'wp-block-query-pagination-previous', $class_list, true ); + $is_next = in_array( 'wp-block-query-pagination-next', $class_list, true ); + $is_previous_or_next = $is_previous || $is_next; + + $navigation_link_payload = array( + 'prefetch' => $is_previous_or_next, + 'scroll' => true, + ); + $p->set_attribute( 'data-wc-navigation-link', - '{"prefetch":true,"scroll":false}' + wp_json_encode( $navigation_link_payload ) ); + + if ( $is_previous ) { + $p->set_attribute( 'key', 'pagination-previous' ); + } elseif ( $is_next ) { + $p->set_attribute( 'key', 'pagination-next' ); + } } $block_content = $p->get_updated_html(); } From 8f1c4a314088d41d329d0441ecc0808214144b24 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 19 Jul 2023 18:35:07 +0200 Subject: [PATCH 10/12] Reuse root fragments for each interactive region --- assets/js/interactivity/router.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index 6284b61f4e4..54d5ffe6556 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -6,6 +6,18 @@ import { directivePrefix } from './constants'; // The cache of visited and prefetched pages. const pages = new Map(); +// Keep the same root fragment for each interactive region node. +const regionRootFragments = new WeakMap(); +const getRegionRootFragment = ( region ) => { + if ( ! regionRootFragments.has( region ) ) { + regionRootFragments.set( + region, + createRootFragment( region.parentElement, region ) + ); + } + return regionRootFragments.get( region ); +}; + // Helper to remove domain and hash from the URL. We are only interesting in // caching the path and the query. const cleanUrl = ( url ) => { @@ -55,7 +67,7 @@ const renderRegions = ( page ) => { const attrName = `data-${ directivePrefix }-navigation-id`; document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { const id = region.attributes[ attrName ]; - const fragment = createRootFragment( region.parentElement, region ); + const fragment = getRegionRootFragment( region ); render( page.regions[ id ], fragment ); } ); }; @@ -95,7 +107,7 @@ export const init = async () => { .querySelectorAll( `[data-${ directivePrefix }-interactive]` ) .forEach( ( node ) => { if ( ! hydratedIslands.has( node ) ) { - const fragment = createRootFragment( node.parentNode, node ); + const fragment = getRegionRootFragment( node ); const vdom = toVdom( node ); hydrate( vdom, fragment ); } From 763ddf851a86879e84f1290eba40d9ea2beda2ba Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 25 Jul 2023 18:46:55 +0200 Subject: [PATCH 11/12] Fix navigation-id retrieval --- assets/js/interactivity/router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index 54d5ffe6556..85a9964ed1f 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -46,7 +46,7 @@ const regionsToVdom = ( dom ) => { const regions = {}; const attrName = `data-${ directivePrefix }-navigation-id`; dom.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { - const id = region.attributes[ attrName ]; + const id = region.getAttribute( attrName ); regions[ id ] = toVdom( region ); } ); @@ -66,7 +66,7 @@ export const prefetch = ( url ) => { const renderRegions = ( page ) => { const attrName = `data-${ directivePrefix }-navigation-id`; document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { - const id = region.attributes[ attrName ]; + const id = region.getAttribute( attrName ); const fragment = getRegionRootFragment( region ); render( page.regions[ id ], fragment ); } ); From 2abadbd5ac86b91d7d7792f3751d3e526a47366c Mon Sep 17 00:00:00 2001 From: Manish Menaria Date: Wed, 26 Jul 2023 12:02:10 +0530 Subject: [PATCH 12/12] Enhance navigation with smooth scroll to specific elements This commit introduces changes to the directive.js and ProductQuery.php files. In directives.js, the functionality of fetching a page and updating scroll behavior is enhanced. Now, apart from the smooth or default scroll, the function also considers the 'scrollToSelector' property of a link. It calculates the top position of the element (identified by the selector) in the viewport and uses it for scrolling, providing a more precise and user-friendly scrolling experience. In ProductQuery.php, the navigation link payload is updated to include a 'scrollToSelector' attribute. This attribute uses a data attribute selector that identifies the product block that the navigation link should scroll to. These enhancements improve user navigation by allowing smooth scrolling to specific product blocks rather than just the top of the page. --- assets/js/interactivity/directives.js | 18 ++++++++++++++++-- src/BlockTypes/ProductQuery.php | 5 +++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/assets/js/interactivity/directives.js b/assets/js/interactivity/directives.js index 6b4d55f0475..358f3b2eeb0 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -173,15 +173,29 @@ export default () => { // Fetch the page (or return it from cache). await navigate( href ); + // Scroll to the element if it's defined. + let topPosition = 0; + if ( link?.scrollToSelector ) { + const element = document.querySelector( + link?.scrollToSelector + ); + + if ( element ) { + topPosition = + element.getBoundingClientRect().top + + window.scrollY; + } + } + // Update the scroll, depending on the option. True by default. if ( link?.scroll === 'smooth' ) { window.scrollTo( { - top: 0, + top: topPosition, left: 0, behavior: 'smooth', } ); } else if ( link?.scroll !== false ) { - window.scrollTo( 0, 0 ); + window.scrollTo( 0, topPosition ); } }; } diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 661a9d40752..f4ec5af534c 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -136,8 +136,9 @@ public function add_navigation_link_directives( $block_content, $block, $instanc $is_previous_or_next = $is_previous || $is_next; $navigation_link_payload = array( - 'prefetch' => $is_previous_or_next, - 'scroll' => true, + 'prefetch' => $is_previous_or_next, + 'scroll' => true, + 'scrollToSelector' => '[data-wc-navigation-id="woo-products-' . $this->parsed_block['attrs']['queryId'] . '"]', ); $p->set_attribute(