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": {
|
||||
"type": "array"
|
||||
},
|
||||
"forcePageReload": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"providesContext": {
|
||||
|
|
|
@ -72,6 +72,7 @@ export const DEFAULT_ATTRIBUTES: Partial< ProductCollectionAttributes > = {
|
|||
shrinkColumns: true,
|
||||
},
|
||||
queryContextIncludes: [ 'collection' ],
|
||||
forcePageReload: false,
|
||||
};
|
||||
|
||||
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 { getDefaultValueOfInheritQueryFromTemplate } from '../utils';
|
||||
import InspectorControls from './inspector-controls';
|
||||
import InspectorAdvancedControls from './inspector-advanced-controls';
|
||||
import ToolbarControls from './toolbar-controls';
|
||||
|
||||
const ProductCollectionContent = (
|
||||
|
@ -69,6 +70,7 @@ const ProductCollectionContent = (
|
|||
return (
|
||||
<div { ...blockProps }>
|
||||
<InspectorControls { ...props } />
|
||||
<InspectorAdvancedControls { ...props } />
|
||||
<ToolbarControls { ...props } />
|
||||
<div { ...innerBlocksProps } />
|
||||
</div>
|
||||
|
|
|
@ -37,6 +37,13 @@ const isValidEvent = ( event: MouseEvent ) =>
|
|||
! event.shiftKey &&
|
||||
! 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.
|
||||
* Scrolls the page to the first product if it's not in the viewport.
|
||||
|
@ -93,6 +100,13 @@ const productCollectionStore = {
|
|||
const wcNavigationId = (
|
||||
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
||||
)?.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 ) ) {
|
||||
event.preventDefault();
|
||||
|
@ -130,6 +144,15 @@ const productCollectionStore = {
|
|||
*/
|
||||
*prefetchOnHover() {
|
||||
const { ref } = getElement();
|
||||
|
||||
const isDisabled = (
|
||||
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
|
||||
)?.dataset.wcNavigationDisabled;
|
||||
|
||||
if ( isDisabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isValidLink( ref ) ) {
|
||||
yield prefetch( ref.href );
|
||||
}
|
||||
|
@ -141,8 +164,17 @@ const productCollectionStore = {
|
|||
* Reduces perceived load times for subsequent page navigations.
|
||||
*/
|
||||
*prefetch() {
|
||||
const context = getContext< ProductCollectionStoreContext >();
|
||||
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 ) ) {
|
||||
yield prefetch( ref.href );
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface ProductCollectionAttributes {
|
|||
* Contain the list of attributes that should be included in the queryContext
|
||||
*/
|
||||
queryContextIncludes: string[];
|
||||
forcePageReload: boolean;
|
||||
}
|
||||
|
||||
export enum LayoutOptions {
|
||||
|
@ -99,9 +100,13 @@ export type TProductCollectionOrderBy =
|
|||
| 'popularity'
|
||||
| 'rating';
|
||||
|
||||
export type ProductCollectionSetAttributes = (
|
||||
attrs: Partial< ProductCollectionAttributes >
|
||||
) => void;
|
||||
|
||||
export type DisplayLayoutControlProps = {
|
||||
displayLayout: ProductCollectionDisplayLayout;
|
||||
setAttributes: ( attrs: Partial< ProductCollectionAttributes > ) => void;
|
||||
setAttributes: ProductCollectionSetAttributes;
|
||||
};
|
||||
export type QueryControlProps = {
|
||||
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( '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 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,8 +99,9 @@ class ProductCollection extends AbstractBlock {
|
|||
* @return string Updated block content with added interactivity attributes.
|
||||
*/
|
||||
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
|
||||
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||
if ( $is_product_collection_block ) {
|
||||
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||
$is_enhanced_pagination_enabled = ! ( $block['attrs']['forcePageReload'] ?? false );
|
||||
if ( $is_product_collection_block && $is_enhanced_pagination_enabled ) {
|
||||
// Enqueue the Interactivity API runtime.
|
||||
wp_enqueue_script( 'wc-interactivity' );
|
||||
|
||||
|
@ -166,13 +171,15 @@ class ProductCollection extends AbstractBlock {
|
|||
* @param \WP_Block $instance The block instance.
|
||||
*/
|
||||
public function add_navigation_link_directives( $block_content, $block, $instance ) {
|
||||
$query_context = $instance->context['query'] ?? array();
|
||||
$is_product_collection_block = $query_context['isProductCollectionBlock'] ?? false;
|
||||
$query_id = $instance->context['queryId'] ?? null;
|
||||
$parsed_query_id = $this->parsed_block['attrs']['queryId'] ?? null;
|
||||
$query_context = $instance->context['query'] ?? array();
|
||||
$is_product_collection_block = $query_context['isProductCollectionBlock'] ?? false;
|
||||
$query_id = $instance->context['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.
|
||||
if ( $is_product_collection_block && $query_id === $parsed_query_id ) {
|
||||
// Only proceed if the block is a product collection block,
|
||||
// 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 );
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue