Product Collection: Disable client side navigation if blocks incompatible with Interactivity API are detected (#45435)
* Add dummy Force Page Reload control to Inspector Advanced Control * Add enhancedPagination attribute that decides if client side navigation is enabled * Consume the enhancedPagination attribute * Force client side navigation when incompatible blocks detected * Dummy util to detect incompatible blocks * Detect incompatible blocks in the Editor * Switch to WordPress Interactivity package in Product Collection * Add initial implementation of the incompatible blocks detection in frontend * Remove leftover * Revert to using internal version of interactivity API * There's no Interactivity store config available in the internal Interactivity implementation so remove it * Disable client side navigation if the incompatible block is detected * Add default attribute value * Switch from enmhancedPagination attribute to forcePageReload * Fixed some misclicked line order change * Switch from enhancedPagination to forcePageReload in PHP code * Apply the correct filter * Fix the incorrect condition to detect incompatible block * Initial implementation of orange dot to bring attention * Cleanup * Remove the orange dot indicator * Refactor checking for unsupported blocks * Add changelog * Fix PHP lint errors * Bring back empty line at the end of pnpm-lock * Bring pnpm-lock.yaml file to original state * Fix incorrect function call * Add visibility description to function * Switch private method to public * More linted fixes
This commit is contained in:
parent
52d300f2a9
commit
5a54dd6527
|
@ -34,6 +34,10 @@
|
||||||
},
|
},
|
||||||
"queryContextIncludes": {
|
"queryContextIncludes": {
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"forcePageReload": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"providesContext": {
|
"providesContext": {
|
||||||
|
|
|
@ -72,6 +72,7 @@ export const DEFAULT_ATTRIBUTES: Partial< ProductCollectionAttributes > = {
|
||||||
shrinkColumns: true,
|
shrinkColumns: true,
|
||||||
},
|
},
|
||||||
queryContextIncludes: [ 'collection' ],
|
queryContextIncludes: [ 'collection' ],
|
||||||
|
forcePageReload: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultQuery = (
|
export const getDefaultQuery = (
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useEffect } from '@wordpress/element';
|
||||||
|
import { ToggleControl } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { useHasUnsupportedBlocks } from './utils';
|
||||||
|
import type { ProductCollectionSetAttributes } from '../../types';
|
||||||
|
|
||||||
|
type ForcePageReloadControlProps = {
|
||||||
|
clientId: string;
|
||||||
|
forcePageReload: boolean;
|
||||||
|
setAttributes: ProductCollectionSetAttributes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpTextIfEnabled = __(
|
||||||
|
'Enforce full page reload on certain interactions, like using paginations controls.',
|
||||||
|
'woocommerce'
|
||||||
|
);
|
||||||
|
const helpTextIfDisabled = __(
|
||||||
|
"Force page reload can't be disabled because there are non-compatible blocks inside the Product Collection block.",
|
||||||
|
'woocommerce'
|
||||||
|
);
|
||||||
|
|
||||||
|
const ForcePageReloadControl = ( props: ForcePageReloadControlProps ) => {
|
||||||
|
const { clientId, forcePageReload, setAttributes } = props;
|
||||||
|
const hasUnsupportedBlocks = useHasUnsupportedBlocks( clientId );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if ( ! forcePageReload && hasUnsupportedBlocks ) {
|
||||||
|
setAttributes( { forcePageReload: true } );
|
||||||
|
}
|
||||||
|
}, [ forcePageReload, hasUnsupportedBlocks, setAttributes ] );
|
||||||
|
|
||||||
|
const helpText = hasUnsupportedBlocks
|
||||||
|
? helpTextIfDisabled
|
||||||
|
: helpTextIfEnabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleControl
|
||||||
|
label={ __( 'Force Page Reload', 'woocommerce' ) }
|
||||||
|
help={ helpText }
|
||||||
|
checked={ forcePageReload }
|
||||||
|
onChange={ () =>
|
||||||
|
setAttributes( { forcePageReload: ! forcePageReload } )
|
||||||
|
}
|
||||||
|
disabled={ hasUnsupportedBlocks }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForcePageReloadControl;
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { InspectorAdvancedControls } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import ForcePageReloadControl from './force-page-reload-control';
|
||||||
|
import type { ProductCollectionEditComponentProps } from '../../types';
|
||||||
|
|
||||||
|
export default function ProductCollectionAdvancedInspectorControls(
|
||||||
|
props: ProductCollectionEditComponentProps
|
||||||
|
) {
|
||||||
|
const { clientId, attributes, setAttributes } = props;
|
||||||
|
const { forcePageReload } = attributes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InspectorAdvancedControls>
|
||||||
|
<ForcePageReloadControl
|
||||||
|
clientId={ clientId }
|
||||||
|
forcePageReload={ forcePageReload }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
</InspectorAdvancedControls>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
const unsupportedBlocks = [
|
||||||
|
'core/post-content',
|
||||||
|
'woocommerce/mini-cart',
|
||||||
|
'woocommerce/featured-product',
|
||||||
|
];
|
||||||
|
const supportedPrefixes = [ 'core/', 'woocommerce/' ];
|
||||||
|
|
||||||
|
const isBlockSupported = ( blockName: string ) => {
|
||||||
|
// Check for explicitly unsupported blocks
|
||||||
|
if ( unsupportedBlocks.includes( blockName ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for supported prefixes
|
||||||
|
if (
|
||||||
|
supportedPrefixes.find( ( prefix ) => blockName.startsWith( prefix ) )
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise block is unsupported
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHasUnsupportedBlocks = ( clientId: string ): boolean =>
|
||||||
|
useSelect(
|
||||||
|
( select ) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore No types for this exist yet
|
||||||
|
const { getClientIdsOfDescendants, getBlockName } =
|
||||||
|
select( blockEditorStore );
|
||||||
|
|
||||||
|
const hasUnsupportedBlocks =
|
||||||
|
getClientIdsOfDescendants( clientId ).find(
|
||||||
|
( blockId: string ) => {
|
||||||
|
const blockName = getBlockName( blockId );
|
||||||
|
const supported = isBlockSupported( blockName );
|
||||||
|
return ! supported;
|
||||||
|
}
|
||||||
|
) || false;
|
||||||
|
|
||||||
|
return hasUnsupportedBlocks;
|
||||||
|
},
|
||||||
|
[ clientId ]
|
||||||
|
);
|
|
@ -16,6 +16,7 @@ import type {
|
||||||
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
|
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
|
||||||
import { getDefaultValueOfInheritQueryFromTemplate } from '../utils';
|
import { getDefaultValueOfInheritQueryFromTemplate } from '../utils';
|
||||||
import InspectorControls from './inspector-controls';
|
import InspectorControls from './inspector-controls';
|
||||||
|
import InspectorAdvancedControls from './inspector-advanced-controls';
|
||||||
import ToolbarControls from './toolbar-controls';
|
import ToolbarControls from './toolbar-controls';
|
||||||
|
|
||||||
const ProductCollectionContent = (
|
const ProductCollectionContent = (
|
||||||
|
@ -69,6 +70,7 @@ const ProductCollectionContent = (
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
<InspectorControls { ...props } />
|
<InspectorControls { ...props } />
|
||||||
|
<InspectorAdvancedControls { ...props } />
|
||||||
<ToolbarControls { ...props } />
|
<ToolbarControls { ...props } />
|
||||||
<div { ...innerBlocksProps } />
|
<div { ...innerBlocksProps } />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,6 +37,13 @@ const isValidEvent = ( event: MouseEvent ) =>
|
||||||
! event.shiftKey &&
|
! event.shiftKey &&
|
||||||
! event.defaultPrevented;
|
! event.defaultPrevented;
|
||||||
|
|
||||||
|
const forcePageReload = ( href: string ) => {
|
||||||
|
window.location.assign( href );
|
||||||
|
// It's function called in generator expecting asyncFunc return.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
return new Promise( () => {} );
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the visibility of the first product in the collection.
|
* Ensures the visibility of the first product in the collection.
|
||||||
* Scrolls the page to the first product if it's not in the viewport.
|
* Scrolls the page to the first product if it's not in the viewport.
|
||||||
|
@ -93,6 +100,13 @@ const productCollectionStore = {
|
||||||
const wcNavigationId = (
|
const wcNavigationId = (
|
||||||
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
||||||
)?.dataset?.wcNavigationId;
|
)?.dataset?.wcNavigationId;
|
||||||
|
const isDisabled = (
|
||||||
|
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
||||||
|
)?.dataset.wcNavigationDisabled;
|
||||||
|
|
||||||
|
if ( isDisabled ) {
|
||||||
|
yield forcePageReload( ref.href );
|
||||||
|
}
|
||||||
|
|
||||||
if ( isValidLink( ref ) && isValidEvent( event ) ) {
|
if ( isValidLink( ref ) && isValidEvent( event ) ) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -130,6 +144,15 @@ const productCollectionStore = {
|
||||||
*/
|
*/
|
||||||
*prefetchOnHover() {
|
*prefetchOnHover() {
|
||||||
const { ref } = getElement();
|
const { ref } = getElement();
|
||||||
|
|
||||||
|
const isDisabled = (
|
||||||
|
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
||||||
|
)?.dataset.wcNavigationDisabled;
|
||||||
|
|
||||||
|
if ( isDisabled ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ( isValidLink( ref ) ) {
|
if ( isValidLink( ref ) ) {
|
||||||
yield prefetch( ref.href );
|
yield prefetch( ref.href );
|
||||||
}
|
}
|
||||||
|
@ -141,8 +164,17 @@ const productCollectionStore = {
|
||||||
* Reduces perceived load times for subsequent page navigations.
|
* Reduces perceived load times for subsequent page navigations.
|
||||||
*/
|
*/
|
||||||
*prefetch() {
|
*prefetch() {
|
||||||
const context = getContext< ProductCollectionStoreContext >();
|
|
||||||
const { ref } = getElement();
|
const { ref } = getElement();
|
||||||
|
const isDisabled = (
|
||||||
|
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
||||||
|
)?.dataset.wcNavigationDisabled;
|
||||||
|
|
||||||
|
if ( isDisabled ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext< ProductCollectionStoreContext >();
|
||||||
|
|
||||||
if ( context?.isPrefetchNextOrPreviousLink && isValidLink( ref ) ) {
|
if ( context?.isPrefetchNextOrPreviousLink && isValidLink( ref ) ) {
|
||||||
yield prefetch( ref.href );
|
yield prefetch( ref.href );
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface ProductCollectionAttributes {
|
||||||
* Contain the list of attributes that should be included in the queryContext
|
* Contain the list of attributes that should be included in the queryContext
|
||||||
*/
|
*/
|
||||||
queryContextIncludes: string[];
|
queryContextIncludes: string[];
|
||||||
|
forcePageReload: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LayoutOptions {
|
export enum LayoutOptions {
|
||||||
|
@ -99,9 +100,13 @@ export type TProductCollectionOrderBy =
|
||||||
| 'popularity'
|
| 'popularity'
|
||||||
| 'rating';
|
| 'rating';
|
||||||
|
|
||||||
|
export type ProductCollectionSetAttributes = (
|
||||||
|
attrs: Partial< ProductCollectionAttributes >
|
||||||
|
) => void;
|
||||||
|
|
||||||
export type DisplayLayoutControlProps = {
|
export type DisplayLayoutControlProps = {
|
||||||
displayLayout: ProductCollectionDisplayLayout;
|
displayLayout: ProductCollectionDisplayLayout;
|
||||||
setAttributes: ( attrs: Partial< ProductCollectionAttributes > ) => void;
|
setAttributes: ProductCollectionSetAttributes;
|
||||||
};
|
};
|
||||||
export type QueryControlProps = {
|
export type QueryControlProps = {
|
||||||
query: ProductCollectionQuery;
|
query: ProductCollectionQuery;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Product Collection: disable client-side navigation if incompatible blocks are detected inside
|
|
@ -81,6 +81,10 @@ class ProductCollection extends AbstractBlock {
|
||||||
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
||||||
|
|
||||||
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
||||||
|
|
||||||
|
// Disable client-side-navigation if incompatible blocks are detected.
|
||||||
|
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,7 +100,8 @@ class ProductCollection extends AbstractBlock {
|
||||||
*/
|
*/
|
||||||
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
|
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
|
||||||
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||||
if ( $is_product_collection_block ) {
|
$is_enhanced_pagination_enabled = ! ( $block['attrs']['forcePageReload'] ?? false );
|
||||||
|
if ( $is_product_collection_block && $is_enhanced_pagination_enabled ) {
|
||||||
// Enqueue the Interactivity API runtime.
|
// Enqueue the Interactivity API runtime.
|
||||||
wp_enqueue_script( 'wc-interactivity' );
|
wp_enqueue_script( 'wc-interactivity' );
|
||||||
|
|
||||||
|
@ -170,9 +175,11 @@ class ProductCollection extends AbstractBlock {
|
||||||
$is_product_collection_block = $query_context['isProductCollectionBlock'] ?? false;
|
$is_product_collection_block = $query_context['isProductCollectionBlock'] ?? false;
|
||||||
$query_id = $instance->context['queryId'] ?? null;
|
$query_id = $instance->context['queryId'] ?? null;
|
||||||
$parsed_query_id = $this->parsed_block['attrs']['queryId'] ?? null;
|
$parsed_query_id = $this->parsed_block['attrs']['queryId'] ?? null;
|
||||||
|
$is_enhanced_pagination_enabled = ! ( $this->parsed_block['attrs']['forcePageReload'] ?? false );
|
||||||
|
|
||||||
// Only proceed if the block is a product collection block and query IDs match.
|
// Only proceed if the block is a product collection block,
|
||||||
if ( $is_product_collection_block && $query_id === $parsed_query_id ) {
|
// enhaced pagination is enabled and query IDs match.
|
||||||
|
if ( $is_product_collection_block && $is_enhanced_pagination_enabled && $query_id === $parsed_query_id ) {
|
||||||
$block_content = $this->process_pagination_links( $block_content );
|
$block_content = $this->process_pagination_links( $block_content );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +238,110 @@ class ProductCollection extends AbstractBlock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if the inner block is compatible with Interactivity API.
|
||||||
|
*
|
||||||
|
* @param string $block_name Name of the block to verify.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
private function is_block_compatible( $block_name ) {
|
||||||
|
// Check for explicitly unsupported blocks.
|
||||||
|
if ( 'core/post-content' === $block_name ||
|
||||||
|
'woocommerce/mini-cart' === $block_name ||
|
||||||
|
'woocommerce/featured-product' === $block_name ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for supported prefixes.
|
||||||
|
if (
|
||||||
|
str_starts_with( $block_name, 'core/' ) ||
|
||||||
|
str_starts_with( $block_name, 'woocommerce/' )
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise block is unsupported.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check inner blocks of Product Collection block if there's one
|
||||||
|
* incompatible with Interactivity API and if so, disable client-side
|
||||||
|
* naviagtion.
|
||||||
|
*
|
||||||
|
* @param array $parsed_block The block being rendered.
|
||||||
|
* @return string Returns the parsed block, unmodified.
|
||||||
|
*/
|
||||||
|
public function disable_enhanced_pagination( $parsed_block ) {
|
||||||
|
static $enhanced_query_stack = array();
|
||||||
|
static $dirty_enhanced_queries = array();
|
||||||
|
static $render_product_collection_callback = null;
|
||||||
|
|
||||||
|
$block_name = $parsed_block['blockName'];
|
||||||
|
$force_page_reload_global =
|
||||||
|
$parsed_block['attrs']['forcePageReload'] ?? false &&
|
||||||
|
isset( $block['attrs']['queryId'] );
|
||||||
|
|
||||||
|
if (
|
||||||
|
'woocommerce/product-collection' === $block_name &&
|
||||||
|
! $force_page_reload_global
|
||||||
|
) {
|
||||||
|
$enhanced_query_stack[] = $parsed_block['attrs']['queryId'];
|
||||||
|
|
||||||
|
if ( ! isset( $render_product_collection_callback ) ) {
|
||||||
|
/**
|
||||||
|
* Filter that disables the enhanced pagination feature during block
|
||||||
|
* rendering when a plugin block has been found inside. It does so
|
||||||
|
* by adding an attribute called `data-wp-navigation-disabled` which
|
||||||
|
* is later handled by the front-end logic.
|
||||||
|
*
|
||||||
|
* @param string $content The block content.
|
||||||
|
* @param array $block The full block, including name and attributes.
|
||||||
|
* @return string Returns the modified output of the query block.
|
||||||
|
*/
|
||||||
|
$render_product_collection_callback = static function ( $content, $block ) use ( &$enhanced_query_stack, &$dirty_enhanced_queries, &$render_product_collection_callback ) {
|
||||||
|
$force_page_reload =
|
||||||
|
$parsed_block['attrs']['forcePageReload'] ?? false &&
|
||||||
|
isset( $block['attrs']['queryId'] );
|
||||||
|
|
||||||
|
if ( $force_page_reload ) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $dirty_enhanced_queries[ $block['attrs']['queryId'] ] ) ) {
|
||||||
|
$p = new \WP_HTML_Tag_Processor( $content );
|
||||||
|
if ( $p->next_tag() ) {
|
||||||
|
$p->set_attribute( 'data-wc-navigation-disabled', 'true' );
|
||||||
|
}
|
||||||
|
$content = $p->get_updated_html();
|
||||||
|
$dirty_enhanced_queries[ $block['attrs']['queryId'] ] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
array_pop( $enhanced_query_stack );
|
||||||
|
|
||||||
|
if ( empty( $enhanced_query_stack ) ) {
|
||||||
|
remove_filter( 'render_block_woocommerce/product-collection', $render_product_collection_callback );
|
||||||
|
$render_product_collection_callback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
};
|
||||||
|
|
||||||
|
add_filter( 'render_block_woocommerce/product-collection', $render_product_collection_callback, 10, 2 );
|
||||||
|
}
|
||||||
|
} elseif (
|
||||||
|
! empty( $enhanced_query_stack ) &&
|
||||||
|
isset( $block_name ) &&
|
||||||
|
! $this->is_block_compatible( $block_name )
|
||||||
|
) {
|
||||||
|
foreach ( $enhanced_query_stack as $query_id ) {
|
||||||
|
$dirty_enhanced_queries[ $query_id ] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parsed_block;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra data passed through from server to client for block.
|
* Extra data passed through from server to client for block.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue