woocommerce/plugins/woocommerce-blocks/assets/js/blocks/product-collection/frontend.tsx

140 lines
3.5 KiB
TypeScript

/**
* External dependencies
*/
import {
store,
navigate,
prefetch,
getElement,
getContext,
} from '@woocommerce/interactivity';
/**
* Internal dependencies
*/
import './style.scss';
export type ProductCollectionStoreContext = {
isPrefetchNextOrPreviousLink: boolean;
animation: 'start' | 'finish';
};
const isValidLink = ( ref: HTMLAnchorElement ) =>
ref &&
ref instanceof window.HTMLAnchorElement &&
ref.href &&
( ! ref.target || ref.target === '_self' ) &&
ref.origin === window.location.origin;
const isValidEvent = ( event: MouseEvent ) =>
event.button === 0 && // Left clicks only.
! event.metaKey && // Open in new tab (Mac).
! event.ctrlKey && // Open in new tab (Windows).
! event.altKey && // Download.
! event.shiftKey &&
! event.defaultPrevented;
/**
* Ensures the visibility of the first product in the collection.
* Scrolls the page to the first product if it's not in the viewport.
*
* @param {string} wcNavigationId Unique ID for each Product Collection block on page/post.
*/
function scrollToFirstProductIfNotVisible( wcNavigationId?: string ) {
if ( ! wcNavigationId ) {
return;
}
const productSelector = `[data-wc-navigation-id=${ wcNavigationId }] .wc-block-product-template .wc-block-product`;
const product = document.querySelector( productSelector );
if ( product ) {
const rect = product.getBoundingClientRect();
const isVisible =
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
( window.innerHeight ||
document.documentElement.clientHeight ) &&
rect.right <=
( window.innerWidth || document.documentElement.clientWidth );
// If the product is not visible, scroll to it.
if ( ! isVisible ) {
product.scrollIntoView( {
behavior: 'smooth',
block: 'start',
} );
}
}
}
const productCollectionStore = {
state: {
get startAnimation() {
return (
getContext< ProductCollectionStoreContext >().animation ===
'start'
);
},
get finishAnimation() {
return (
getContext< ProductCollectionStoreContext >().animation ===
'finish'
);
},
},
actions: {
*navigate( event: MouseEvent ) {
const ctx = getContext< ProductCollectionStoreContext >();
const { ref } = getElement();
const wcNavigationId = (
ref?.closest( '[data-wc-navigation-id]' ) as HTMLDivElement
)?.dataset?.wcNavigationId;
if ( isValidLink( ref ) && isValidEvent( event ) ) {
event.preventDefault();
// Don't start animation if it doesn't take long to navigate.
const timeout = setTimeout( () => {
ctx.animation = 'start';
}, 400 );
yield navigate( ref.href );
// Clear the timeout if the navigation is fast.
clearTimeout( timeout );
ctx.animation = 'finish';
ctx.isPrefetchNextOrPreviousLink = !! ref.href;
scrollToFirstProductIfNotVisible( wcNavigationId );
}
},
/**
* We prefetch the next or previous button page on hover.
* Optimizes user experience by preloading content for faster access.
*/
*prefetchOnHover() {
const { ref } = getElement();
if ( isValidLink( ref ) ) {
yield prefetch( ref.href );
}
},
},
callbacks: {
/**
* Prefetches content for next or previous links after initial user interaction.
* Reduces perceived load times for subsequent page navigations.
*/
*prefetch() {
const context = getContext< ProductCollectionStoreContext >();
const { ref } = getElement();
if ( context?.isPrefetchNextOrPreviousLink && isValidLink( ref ) ) {
yield prefetch( ref.href );
}
},
},
};
store( 'woocommerce/product-collection', productCollectionStore );