initial commit
This commit is contained in:
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import Drawer from '@woocommerce/base-components/drawer';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import {
|
||||
formatPrice,
|
||||
getCurrencyFromPriceResponse,
|
||||
} from '@woocommerce/price-format';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CartLineItemsTable from '../cart/full-cart/cart-line-items-table';
|
||||
import './style.scss';
|
||||
|
||||
interface MiniCartBlockProps {
|
||||
isPlaceholderOpen?: boolean;
|
||||
}
|
||||
|
||||
const MiniCartBlock = ( {
|
||||
isPlaceholderOpen = false,
|
||||
}: MiniCartBlockProps ): JSX.Element => {
|
||||
const {
|
||||
cartItems,
|
||||
cartItemsCount,
|
||||
cartIsLoading,
|
||||
cartTotals,
|
||||
} = useStoreCart();
|
||||
const [ isOpen, setIsOpen ] = useState< boolean >( isPlaceholderOpen );
|
||||
const emptyCartRef = useRef< HTMLDivElement | null >( null );
|
||||
// We already rendered the HTML drawer placeholder, so we want to skip the
|
||||
// slide in animation.
|
||||
const [ skipSlideIn, setSkipSlideIn ] = useState< boolean >(
|
||||
isPlaceholderOpen
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
const openMiniCartAndRefreshData = ( e ) => {
|
||||
const eventDetail = e.detail;
|
||||
if ( ! eventDetail || ! eventDetail.preserveCartData ) {
|
||||
dispatch( storeKey ).invalidateResolutionForStore();
|
||||
}
|
||||
setSkipSlideIn( false );
|
||||
setIsOpen( true );
|
||||
};
|
||||
|
||||
// Make it so we can read jQuery events triggered by WC Core elements.
|
||||
const removeJQueryAddedToCartEvent = translateJQueryEventToNative(
|
||||
'added_to_cart',
|
||||
'wc-blocks_added_to_cart'
|
||||
);
|
||||
|
||||
document.body.addEventListener(
|
||||
'wc-blocks_added_to_cart',
|
||||
openMiniCartAndRefreshData
|
||||
);
|
||||
|
||||
return () => {
|
||||
removeJQueryAddedToCartEvent();
|
||||
|
||||
document.body.removeEventListener(
|
||||
'wc-blocks_added_to_cart',
|
||||
openMiniCartAndRefreshData
|
||||
);
|
||||
};
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
// If the cart has been completely emptied, move focus to empty cart
|
||||
// element.
|
||||
if ( isOpen && ! cartIsLoading && cartItems.length === 0 ) {
|
||||
if ( emptyCartRef.current instanceof HTMLElement ) {
|
||||
emptyCartRef.current.focus();
|
||||
}
|
||||
}
|
||||
}, [ isOpen, cartIsLoading, cartItems.length, emptyCartRef ] );
|
||||
|
||||
const subTotal = getSetting( 'displayCartPricesIncludingTax', false )
|
||||
? parseInt( cartTotals.total_items, 10 ) +
|
||||
parseInt( cartTotals.total_items_tax, 10 )
|
||||
: cartTotals.total_items;
|
||||
|
||||
const ariaLabel = sprintf(
|
||||
/* translators: %1$d is the number of products in the cart. %2$s is the cart total */
|
||||
_n(
|
||||
'%1$d item in cart, total price of %2$s',
|
||||
'%1$d items in cart, total price of %2$s',
|
||||
cartItemsCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
cartItemsCount,
|
||||
formatPrice( subTotal, getCurrencyFromPriceResponse( cartTotals ) )
|
||||
);
|
||||
|
||||
const contents =
|
||||
! cartIsLoading && cartItems.length === 0 ? (
|
||||
<div
|
||||
className="wc-block-mini-cart__empty-cart"
|
||||
tabIndex={ -1 }
|
||||
ref={ emptyCartRef }
|
||||
>
|
||||
{ __( 'Cart is empty', 'woo-gutenberg-products-block' ) }
|
||||
</div>
|
||||
) : (
|
||||
<CartLineItemsTable
|
||||
lineItems={ cartItems }
|
||||
isLoading={ cartIsLoading }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="wc-block-mini-cart__button"
|
||||
onClick={ () => {
|
||||
if ( ! isOpen ) {
|
||||
setIsOpen( true );
|
||||
setSkipSlideIn( false );
|
||||
}
|
||||
} }
|
||||
aria-label={ ariaLabel }
|
||||
>
|
||||
{ sprintf(
|
||||
/* translators: %d is the count of items in the cart. */
|
||||
_n(
|
||||
'%d item',
|
||||
'%d items',
|
||||
cartItemsCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
cartItemsCount
|
||||
) }
|
||||
</button>
|
||||
<Drawer
|
||||
className={ classNames(
|
||||
'wc-block-mini-cart__drawer',
|
||||
'is-mobile',
|
||||
{
|
||||
'is-loading': cartIsLoading,
|
||||
}
|
||||
) }
|
||||
title={
|
||||
cartIsLoading
|
||||
? __( 'Your cart', 'woo-gutenberg-products-block' )
|
||||
: sprintf(
|
||||
/* translators: %d is the count of items in the cart. */
|
||||
_n(
|
||||
'Your cart (%d item)',
|
||||
'Your cart (%d items)',
|
||||
cartItemsCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
cartItemsCount
|
||||
)
|
||||
}
|
||||
isOpen={ isOpen }
|
||||
onClose={ () => {
|
||||
setIsOpen( false );
|
||||
} }
|
||||
slideIn={ ! skipSlideIn }
|
||||
>
|
||||
{ contents }
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiniCartBlock;
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { renderFrontend } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withMiniCartConditionalHydration from './with-mini-cart-conditional-hydration';
|
||||
import MiniCartBlock from './block';
|
||||
import './style.scss';
|
||||
|
||||
const renderMiniCartFrontend = () => {
|
||||
// Check if button is focused. In that case, we want to refocus it after we
|
||||
// replace it with the React equivalent.
|
||||
let focusedMiniCartBlock: HTMLElement | null = null;
|
||||
/* eslint-disable @wordpress/no-global-active-element */
|
||||
if (
|
||||
document.activeElement &&
|
||||
document.activeElement.classList.contains(
|
||||
'wc-block-mini-cart__button'
|
||||
) &&
|
||||
document.activeElement.parentNode instanceof HTMLElement
|
||||
) {
|
||||
focusedMiniCartBlock = document.activeElement.parentNode;
|
||||
}
|
||||
/* eslint-enable @wordpress/no-global-active-element */
|
||||
|
||||
renderFrontend( {
|
||||
selector: '.wc-block-mini-cart',
|
||||
Block: withMiniCartConditionalHydration( MiniCartBlock ),
|
||||
getProps: ( el: HTMLElement ) => ( {
|
||||
isDataOutdated: el.dataset.isDataOutdated,
|
||||
isPlaceholderOpen: el.dataset.isPlaceholderOpen === 'true',
|
||||
} ),
|
||||
} );
|
||||
|
||||
// Refocus previously focused button if drawer is not open.
|
||||
if (
|
||||
focusedMiniCartBlock instanceof HTMLElement &&
|
||||
! focusedMiniCartBlock.dataset.isPlaceholderOpen
|
||||
) {
|
||||
const innerButton = focusedMiniCartBlock.querySelector(
|
||||
'.wc-block-mini-cart__button'
|
||||
);
|
||||
if ( innerButton instanceof HTMLElement ) {
|
||||
innerButton.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderMiniCartFrontend();
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
const MiniCartBlock = (): ReactElement => {
|
||||
const blockProps = useBlockProps( {
|
||||
className: 'wc-block-mini-cart',
|
||||
} );
|
||||
|
||||
const productCount = 0;
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<button className="wc-block-mini-cart__button">
|
||||
{ sprintf(
|
||||
/* translators: %d is the number of products in the cart. */
|
||||
_n(
|
||||
'%d product',
|
||||
'%d products',
|
||||
productCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
productCount
|
||||
) }
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiniCartBlock;
|
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import preloadScript from '@woocommerce/base-utils/preload-script';
|
||||
import lazyLoadScript from '@woocommerce/base-utils/lazy-load-script';
|
||||
import { translateJQueryEventToNative } from '@woocommerce/base-utils/legacy-events';
|
||||
|
||||
interface dependencyData {
|
||||
src: string;
|
||||
version?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
translations?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @wordpress/no-global-event-listener
|
||||
window.onload = () => {
|
||||
const miniCartBlocks = document.querySelectorAll( '.wc-block-mini-cart' );
|
||||
let wasLoadScriptsCalled = false;
|
||||
|
||||
if ( miniCartBlocks.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dependencies = getSetting(
|
||||
'mini_cart_block_frontend_dependencies',
|
||||
{}
|
||||
) as Record< string, dependencyData >;
|
||||
|
||||
// Preload scripts
|
||||
for ( const dependencyHandle in dependencies ) {
|
||||
const dependency = dependencies[ dependencyHandle ];
|
||||
preloadScript( {
|
||||
handle: dependencyHandle,
|
||||
...dependency,
|
||||
} );
|
||||
}
|
||||
|
||||
// Make it so we can read jQuery events triggered by WC Core elements.
|
||||
const removeJQueryAddingToCartEvent = translateJQueryEventToNative(
|
||||
'adding_to_cart',
|
||||
'wc-blocks_adding_to_cart'
|
||||
);
|
||||
const removeJQueryAddedToCartEvent = translateJQueryEventToNative(
|
||||
'added_to_cart',
|
||||
'wc-blocks_added_to_cart'
|
||||
);
|
||||
|
||||
const loadScripts = async () => {
|
||||
// Ensure we only call loadScripts once.
|
||||
if ( wasLoadScriptsCalled ) {
|
||||
return;
|
||||
}
|
||||
wasLoadScriptsCalled = true;
|
||||
|
||||
// Remove adding to cart event handler.
|
||||
document.body.removeEventListener(
|
||||
'wc-blocks_adding_to_cart',
|
||||
loadScripts
|
||||
);
|
||||
removeJQueryAddingToCartEvent();
|
||||
|
||||
// Lazy load scripts.
|
||||
for ( const dependencyHandle in dependencies ) {
|
||||
const dependency = dependencies[ dependencyHandle ];
|
||||
await lazyLoadScript( {
|
||||
handle: dependencyHandle,
|
||||
...dependency,
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener( 'wc-blocks_adding_to_cart', loadScripts );
|
||||
|
||||
miniCartBlocks.forEach( ( miniCartBlock, i ) => {
|
||||
if ( ! ( miniCartBlock instanceof HTMLElement ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const miniCartButton = miniCartBlock.querySelector(
|
||||
'.wc-block-mini-cart__button'
|
||||
);
|
||||
const miniCartDrawerPlaceholderOverlay = miniCartBlock.querySelector(
|
||||
'.wc-block-components-drawer__screen-overlay'
|
||||
);
|
||||
|
||||
if ( ! miniCartButton || ! miniCartDrawerPlaceholderOverlay ) {
|
||||
// Markup is not correct, abort.
|
||||
return;
|
||||
}
|
||||
|
||||
const showContents = () => {
|
||||
if ( ! wasLoadScriptsCalled ) {
|
||||
loadScripts();
|
||||
}
|
||||
document.body.removeEventListener(
|
||||
'wc-blocks_added_to_cart',
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
showContentsAndUpdate
|
||||
);
|
||||
miniCartBlock.dataset.isPlaceholderOpen = 'true';
|
||||
miniCartDrawerPlaceholderOverlay.classList.add(
|
||||
'wc-block-components-drawer__screen-overlay--with-slide-in'
|
||||
);
|
||||
miniCartDrawerPlaceholderOverlay.classList.remove(
|
||||
'wc-block-components-drawer__screen-overlay--is-hidden'
|
||||
);
|
||||
removeJQueryAddedToCartEvent();
|
||||
};
|
||||
|
||||
const showContentsAndUpdate = () => {
|
||||
miniCartBlock.dataset.isDataOutdated = 'true';
|
||||
showContents();
|
||||
};
|
||||
|
||||
miniCartButton.addEventListener( 'mouseover', loadScripts );
|
||||
miniCartButton.addEventListener( 'focus', loadScripts );
|
||||
miniCartButton.addEventListener( 'click', showContents );
|
||||
|
||||
// There might be more than one Mini Cart block in the page. Make sure
|
||||
// only one opens when adding a product to the cart.
|
||||
if ( i === 0 ) {
|
||||
document.body.addEventListener(
|
||||
'wc-blocks_added_to_cart',
|
||||
showContentsAndUpdate
|
||||
);
|
||||
}
|
||||
} );
|
||||
};
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, cart } from '@woocommerce/icons';
|
||||
import { registerExperimentalBlockType } from '@woocommerce/block-settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import edit from './edit';
|
||||
|
||||
const settings = {
|
||||
apiVersion: 2,
|
||||
title: __( 'Mini Cart', 'woo-gutenberg-products-block' ),
|
||||
icon: {
|
||||
src: <Icon srcElement={ cart } />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
category: 'woocommerce',
|
||||
keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
|
||||
description: __(
|
||||
'Display a mini cart widget.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
supports: {
|
||||
html: false,
|
||||
multiple: false,
|
||||
},
|
||||
example: {
|
||||
attributes: {
|
||||
isPreview: true,
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
isPreview: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
save: false,
|
||||
},
|
||||
},
|
||||
|
||||
edit,
|
||||
|
||||
save() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
registerExperimentalBlockType( 'woocommerce/mini-cart', settings );
|
@ -0,0 +1,8 @@
|
||||
.modal-open .wc-block-mini-cart__button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Reset font size so it doesn't depend on drawer's ancestors.
|
||||
.wc-block-mini-cart__drawer {
|
||||
font-size: 1rem;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
withStoreCartApiHydration,
|
||||
withRestApiHydration,
|
||||
} from '@woocommerce/block-hocs';
|
||||
|
||||
interface MiniCartBlockInterface {
|
||||
// Signals whether the cart data is outdated. That happens when
|
||||
// opening the mini cart after adding a product to the cart.
|
||||
isDataOutdated?: boolean;
|
||||
// Signals that the HTML placeholder drawer has been opened. Needed
|
||||
// to know whether we have to skip the slide in animation.
|
||||
isPlaceholderOpen?: boolean;
|
||||
}
|
||||
|
||||
// Custom HOC to conditionally hydrate API data depending on the isDataOutdated
|
||||
// prop.
|
||||
export default (
|
||||
OriginalComponent: ( component: MiniCartBlockInterface ) => JSX.Element
|
||||
) => {
|
||||
return ( {
|
||||
isDataOutdated,
|
||||
...props
|
||||
}: MiniCartBlockInterface ): JSX.Element => {
|
||||
const Component = isDataOutdated
|
||||
? OriginalComponent
|
||||
: withStoreCartApiHydration(
|
||||
withRestApiHydration( OriginalComponent )
|
||||
);
|
||||
return <Component { ...props } />;
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user