rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -0,0 +1,65 @@
{
"name": "woocommerce/mini-cart",
"version": "1.0.0",
"title": "Mini-Cart",
"icon": "miniCartAlt",
"description": "Display a button for shoppers to quickly view their cart.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"textdomain": "woocommerce",
"supports": {
"html": false,
"multiple": false,
"typography": {
"fontSize": true
}
},
"example": {
"attributes": {
"isPreview": true,
"className": "wc-block-mini-cart--preview"
}
},
"attributes": {
"isPreview": {
"type": "boolean",
"default": false
},
"miniCartIcon": {
"type": "string",
"default": "cart"
},
"addToCartBehaviour": {
"type": "string",
"default": "none"
},
"hasHiddenPrice": {
"type": "boolean",
"default": false
},
"cartAndCheckoutRenderStyle": {
"type": "string",
"default": "hidden"
},
"priceColor": {
"type": "object"
},
"priceColorValue": {
"type": "string"
},
"iconColor": {
"type": "object"
},
"iconColorValue": {
"type": "string"
},
"productCountColor": {
"type": "object"
},
"productCountColorValue": {
"type": "string"
}
},
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,310 @@
/**
* External dependencies
*/
import { renderParentBlock } from '@woocommerce/atomic-utils';
import Drawer from '@woocommerce/base-components/drawer';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import {
getValidBlockAttributes,
translateJQueryEventToNative,
} from '@woocommerce/base-utils';
import { getRegisteredBlockComponents } from '@woocommerce/blocks-registry';
import {
formatPrice,
getCurrencyFromPriceResponse,
} from '@woocommerce/price-format';
import { getSettingWithCoercion } from '@woocommerce/settings';
import {
isBoolean,
isString,
isCartResponseTotals,
isNumber,
} from '@woocommerce/types';
import {
unmountComponentAtNode,
useCallback,
useEffect,
useRef,
useState,
} from '@wordpress/element';
import { sprintf, _n } from '@wordpress/i18n';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import type { BlockAttributes } from './types';
import QuantityBadge from './quantity-badge';
import { MiniCartContentsBlock } from './mini-cart-contents/block';
import './style.scss';
import {
blockName,
attributes as miniCartContentsAttributes,
} from './mini-cart-contents/attributes';
import { defaultColorItem } from './utils/defaults';
type Props = BlockAttributes;
function getScrollbarWidth() {
return window.innerWidth - document.documentElement.clientWidth;
}
const MiniCartBlock = ( attributes: Props ): JSX.Element => {
const {
initialCartItemsCount,
initialCartTotals,
isInitiallyOpen = false,
colorClassNames,
contents = '',
miniCartIcon,
addToCartBehaviour = 'none',
hasHiddenPrice = false,
priceColor = defaultColorItem,
iconColor = defaultColorItem,
productCountColor = defaultColorItem,
} = attributes;
const {
cartItemsCount: cartItemsCountFromApi,
cartIsLoading,
cartTotals: cartTotalsFromApi,
} = useStoreCart();
const cartIsLoadingForTheFirstTime = useRef( cartIsLoading );
useEffect( () => {
if ( cartIsLoadingForTheFirstTime.current && ! cartIsLoading ) {
cartIsLoadingForTheFirstTime.current = false;
}
}, [ cartIsLoading, cartIsLoadingForTheFirstTime ] );
useEffect( () => {
if (
! cartIsLoading &&
isCartResponseTotals( cartTotalsFromApi ) &&
isNumber( cartItemsCountFromApi )
) {
// Save server data to local storage, so we can re-fetch it faster
// on the next page load.
localStorage.setItem(
'wc-blocks_mini_cart_totals',
JSON.stringify( {
totals: cartTotalsFromApi,
itemsCount: cartItemsCountFromApi,
} )
);
}
} );
const [ isOpen, setIsOpen ] = useState< boolean >( isInitiallyOpen );
// We already rendered the HTML drawer placeholder, so we want to skip the
// slide in animation.
const [ skipSlideIn, setSkipSlideIn ] =
useState< boolean >( isInitiallyOpen );
const [ contentsNode, setContentsNode ] = useState< HTMLDivElement | null >(
null
);
const contentsRef = useCallback( ( node ) => {
setContentsNode( node );
}, [] );
useEffect( () => {
const body = document.querySelector( 'body' );
if ( body ) {
const scrollBarWidth = getScrollbarWidth();
if ( isOpen ) {
Object.assign( body.style, {
overflow: 'hidden',
paddingRight: scrollBarWidth + 'px',
} );
} else {
Object.assign( body.style, { overflow: '', paddingRight: 0 } );
}
}
}, [ isOpen ] );
useEffect( () => {
if ( contentsNode instanceof Element ) {
const container = contentsNode.querySelector(
'.wp-block-woocommerce-mini-cart-contents'
);
if ( ! container ) {
return;
}
if ( isOpen ) {
renderParentBlock( {
Block: MiniCartContentsBlock,
blockName,
getProps: ( el: Element ) => {
return {
attributes: getValidBlockAttributes(
miniCartContentsAttributes,
/* eslint-disable @typescript-eslint/no-explicit-any */
( el instanceof HTMLElement
? el.dataset
: {} ) as any
),
};
},
selector: '.wp-block-woocommerce-mini-cart-contents',
blockMap: getRegisteredBlockComponents( blockName ),
} );
}
}
return () => {
if ( contentsNode instanceof Element && isOpen ) {
const container = contentsNode.querySelector(
'.wp-block-woocommerce-mini-cart-contents'
);
if ( container ) {
unmountComponentAtNode( container );
}
}
};
}, [ isOpen, contentsNode ] );
useEffect( () => {
const openMiniCart = () => {
if ( addToCartBehaviour === 'open_drawer' ) {
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',
openMiniCart
);
return () => {
removeJQueryAddedToCartEvent();
document.body.removeEventListener(
'wc-blocks_added_to_cart',
openMiniCart
);
};
}, [ addToCartBehaviour ] );
const showIncludingTax = getSettingWithCoercion(
'displayCartPricesIncludingTax',
false,
isBoolean
);
const taxLabel = getSettingWithCoercion( 'taxLabel', '', isString );
const cartTotals =
cartIsLoadingForTheFirstTime.current &&
isCartResponseTotals( initialCartTotals )
? initialCartTotals
: cartTotalsFromApi;
const cartItemsCount =
cartIsLoadingForTheFirstTime.current &&
isNumber( initialCartItemsCount )
? initialCartItemsCount
: cartItemsCountFromApi;
const subTotal = showIncludingTax
? parseInt( cartTotals.total_items, 10 ) +
parseInt( cartTotals.total_items_tax, 10 )
: parseInt( cartTotals.total_items, 10 );
const ariaLabel = hasHiddenPrice
? sprintf(
/* translators: %1$d is the number of products in the cart. */
_n(
'%1$d item in cart',
'%1$d items in cart',
cartItemsCount,
'woo-gutenberg-products-block'
),
cartItemsCount
)
: 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 )
)
);
return (
<>
<button
className={ `wc-block-mini-cart__button ${ colorClassNames }` }
onClick={ () => {
if ( ! isOpen ) {
setIsOpen( true );
setSkipSlideIn( false );
}
} }
aria-label={ ariaLabel }
>
{ ! hasHiddenPrice && (
<span
className="wc-block-mini-cart__amount"
style={ { color: priceColor.color } }
>
{ formatPrice(
subTotal,
getCurrencyFromPriceResponse( cartTotals )
) }
</span>
) }
{ taxLabel !== '' && subTotal !== 0 && ! hasHiddenPrice && (
<small
className="wc-block-mini-cart__tax-label"
style={ { color: priceColor.color } }
>
{ taxLabel }
</small>
) }
<QuantityBadge
count={ cartItemsCount }
icon={ miniCartIcon }
iconColor={ iconColor }
productCountColor={ productCountColor }
/>
</button>
<Drawer
className={ classnames(
'wc-block-mini-cart__drawer',
'is-mobile',
{
'is-loading': cartIsLoading,
}
) }
isOpen={ isOpen }
onClose={ () => {
setIsOpen( false );
} }
slideIn={ ! skipSlideIn }
>
<div
className="wc-block-mini-cart__template-part"
ref={ contentsRef }
dangerouslySetInnerHTML={ { __html: contents } }
></div>
</Drawer>
</>
);
};
export default MiniCartBlock;

View File

@@ -0,0 +1,83 @@
/**
* External dependencies
*/
import { renderFrontend } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
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: MiniCartBlock,
getProps: ( el ) => {
let colorClassNames = '';
const button = el.querySelector( '.wc-block-mini-cart__button' );
if ( button instanceof HTMLButtonElement ) {
colorClassNames = button.classList
.toString()
.replace( 'wc-block-mini-cart__button', '' );
}
return {
initialCartTotals: el.dataset.cartTotals
? JSON.parse( el.dataset.cartTotals )
: null,
initialCartItemsCount: el.dataset.cartItemsCount
? parseInt( el.dataset.cartItemsCount, 10 )
: 0,
isInitiallyOpen: el.dataset.isInitiallyOpen === 'true',
colorClassNames,
style: el.dataset.style ? JSON.parse( el.dataset.style ) : {},
miniCartIcon: el.dataset.miniCartIcon,
addToCartBehaviour: el.dataset.addToCartBehaviour || 'none',
hasHiddenPrice: el.dataset.hasHiddenPrice,
priceColor: el.dataset.priceColor
? JSON.parse( el.dataset.priceColor )
: {},
iconColor: el.dataset.iconColor
? JSON.parse( el.dataset.iconColor )
: {},
productCountColor: el.dataset.productCountColor
? JSON.parse( el.dataset.productCountColor )
: {},
contents:
el.querySelector( '.wc-block-mini-cart__template-part' )
?.innerHTML ?? '',
};
},
} );
// Refocus previously focused button if drawer is not open.
if (
focusedMiniCartBlock instanceof HTMLElement &&
! focusedMiniCartBlock.dataset.isInitiallyOpen
) {
const innerButton = focusedMiniCartBlock.querySelector(
'.wc-block-mini-cart__button'
);
if ( innerButton instanceof HTMLElement ) {
innerButton.focus();
}
}
};
renderMiniCartFrontend();

View File

@@ -0,0 +1,268 @@
/**
* External dependencies
*/
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { formatPrice } from '@woocommerce/price-format';
import {
PanelBody,
ExternalLink,
ToggleControl,
BaseControl,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
__experimentalToggleGroupControl as ToggleGroupControl,
} from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
import { __, isRTL } from '@wordpress/i18n';
import Noninteractive from '@woocommerce/base-components/noninteractive';
import { isSiteEditorPage } from '@woocommerce/utils';
import type { ReactElement } from 'react';
import { select } from '@wordpress/data';
import { cartOutline, bag, bagAlt } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings';
import { ColorPanel } from '@woocommerce/editor-components/color-panel';
import type { ColorPaletteOption } from '@woocommerce/editor-components/color-panel/types';
/**
* Internal dependencies
*/
import QuantityBadge from './quantity-badge';
import { defaultColorItem } from './utils/defaults';
import { migrateAttributesToColorPanel } from './utils/data';
import './editor.scss';
export interface Attributes {
miniCartIcon: 'cart' | 'bag' | 'bag-alt';
addToCartBehaviour: string;
hasHiddenPrice: boolean;
cartAndCheckoutRenderStyle: boolean;
priceColor: ColorPaletteOption;
iconColor: ColorPaletteOption;
productCountColor: ColorPaletteOption;
}
interface Props {
attributes: Attributes;
setAttributes: ( attributes: Record< string, unknown > ) => void;
clientId: number;
setPriceColor: ( colorValue: string | undefined ) => void;
setIconColor: ( colorValue: string | undefined ) => void;
setProductCountColor: ( colorValue: string | undefined ) => void;
}
const Edit = ( { attributes, setAttributes }: Props ): ReactElement => {
const {
cartAndCheckoutRenderStyle,
addToCartBehaviour,
hasHiddenPrice,
priceColor = defaultColorItem,
iconColor = defaultColorItem,
productCountColor = defaultColorItem,
miniCartIcon,
} = migrateAttributesToColorPanel( attributes );
const miniCartColorAttributes = {
priceColor: {
label: __( 'Price', 'woo-gutenberg-products-block' ),
context: 'price-color',
},
iconColor: {
label: __( 'Icon', 'woo-gutenberg-products-block' ),
context: 'icon-color',
},
productCountColor: {
label: __( 'Product Count', 'woo-gutenberg-products-block' ),
context: 'product-count-color',
},
};
const blockProps = useBlockProps( {
className: 'wc-block-mini-cart',
} );
const isSiteEditor = isSiteEditorPage( select( 'core/edit-site' ) );
const templatePartEditUri = getSetting(
'templatePartEditUri',
''
) as string;
const productCount = 0;
const productTotal = 0;
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody
title={ __( 'Settings', 'woo-gutenberg-products-block' ) }
>
<ToggleGroupControl
className="wc-block-editor-mini-cart__cart-icon-toggle"
isBlock={ true }
label={ __(
'Cart Icon',
'woo-gutenberg-products-block'
) }
value={ miniCartIcon }
onChange={ ( value: 'cart' | 'bag' | 'bag-alt' ) => {
setAttributes( {
miniCartIcon: value,
} );
} }
>
<ToggleGroupControlOption
value={ 'cart' }
label={ <Icon size={ 32 } icon={ cartOutline } /> }
/>
<ToggleGroupControlOption
value={ 'bag' }
label={ <Icon size={ 32 } icon={ bag } /> }
/>
<ToggleGroupControlOption
value={ 'bag-alt' }
label={ <Icon size={ 32 } icon={ bagAlt } /> }
/>
</ToggleGroupControl>
<BaseControl
id="wc-block-mini-cart__display-toggle"
label={ __(
'Display',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Display total price',
'woo-gutenberg-products-block'
) }
help={ __(
'Toggle to display the total price of products in the shopping cart. If no products have been added, the price will not display.',
'woo-gutenberg-products-block'
) }
checked={ ! hasHiddenPrice }
onChange={ () =>
setAttributes( {
hasHiddenPrice: ! hasHiddenPrice,
} )
}
/>
</BaseControl>
{ isSiteEditor && (
<ToggleGroupControl
className="wc-block-editor-mini-cart__render-in-cart-and-checkout-toggle"
label={ __(
'Mini-Cart in cart and checkout pages',
'woo-gutenberg-products-block'
) }
value={ cartAndCheckoutRenderStyle }
onChange={ ( value: boolean ) => {
setAttributes( {
cartAndCheckoutRenderStyle: value,
} );
} }
help={ __(
'Select how the Mini-Cart behaves in the Cart and Checkout pages. This might affect the header layout.',
'woo-gutenberg-products-block'
) }
>
<ToggleGroupControlOption
value={ 'hidden' }
label={ __(
'Hide',
'woo-gutenberg-products-block'
) }
/>
<ToggleGroupControlOption
value={ 'removed' }
label={ __(
'Remove',
'woo-gutenberg-products-block'
) }
/>
</ToggleGroupControl>
) }
</PanelBody>
<PanelBody
title={ __(
'Cart Drawer',
'woo-gutenberg-products-block'
) }
>
{ templatePartEditUri && (
<>
<img
className="wc-block-editor-mini-cart__drawer-image"
src={
isRTL()
? `${ WC_BLOCKS_IMAGE_URL }blocks/mini-cart/cart-drawer-rtl.svg`
: `${ WC_BLOCKS_IMAGE_URL }blocks/mini-cart/cart-drawer.svg`
}
alt=""
/>
<p>
{ __(
'When opened, the Mini-Cart drawer gives shoppers quick access to view their selected products and checkout.',
'woo-gutenberg-products-block'
) }
</p>
<p className="wc-block-editor-mini-cart__drawer-link">
<ExternalLink href={ templatePartEditUri }>
{ __(
'Edit Mini-Cart Drawer template',
'woo-gutenberg-products-block'
) }
</ExternalLink>
</p>
</>
) }
<BaseControl
id="wc-block-mini-cart__add-to-cart-behaviour-toggle"
label={ __(
'Behavior',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Open drawer when adding',
'woo-gutenberg-products-block'
) }
onChange={ ( value ) => {
setAttributes( {
addToCartBehaviour: value
? 'open_drawer'
: 'none',
} );
} }
help={ __(
'Toggle to open the Mini-Cart drawer when a shopper adds a product to their cart.',
'woo-gutenberg-products-block'
) }
checked={ addToCartBehaviour === 'open_drawer' }
/>
</BaseControl>
</PanelBody>
</InspectorControls>
<ColorPanel colorTypes={ miniCartColorAttributes } />
<Noninteractive>
<button className="wc-block-mini-cart__button">
{ ! hasHiddenPrice && (
<span
className="wc-block-mini-cart__amount"
style={ { color: priceColor.color } }
>
{ formatPrice( productTotal ) }
</span>
) }
<QuantityBadge
count={ productCount }
iconColor={ iconColor }
productCountColor={ productCountColor }
icon={ miniCartIcon }
/>
</button>
</Noninteractive>
</div>
);
};
export default Edit;

View File

@@ -0,0 +1,17 @@
.wc-block-editor-mini-cart__render-in-cart-and-checkout-toggle {
width: 100%;
}
.wc-block-editor-mini-cart__drawer-image {
margin-bottom: 6px;
}
.wc-block-editor-mini-cart__drawer-link {
margin-bottom: 24px;
}
.wc-block-editor-mini-cart__icon {
html[dir="rtl"] & {
transform: scaleX(-1);
}
}

View File

@@ -0,0 +1,183 @@
/**
* External dependencies
*/
import preloadScript from '@woocommerce/base-utils/preload-script';
import lazyLoadScript from '@woocommerce/base-utils/lazy-load-script';
import getNavigationType from '@woocommerce/base-utils/get-navigation-type';
import { translateJQueryEventToNative } from '@woocommerce/base-utils/legacy-events';
/**
* Internal dependencies
*/
import {
getMiniCartTotalsFromLocalStorage,
getMiniCartTotalsFromServer,
updateTotals,
} from './utils/data';
import setStyles from './utils/set-styles';
interface dependencyData {
src: string;
version?: string;
after?: string;
before?: string;
translations?: string;
}
updateTotals( getMiniCartTotalsFromLocalStorage() );
getMiniCartTotalsFromServer().then( updateTotals );
setStyles();
window.addEventListener( 'load', () => {
const miniCartBlocks = document.querySelectorAll( '.wc-block-mini-cart' );
let wasLoadScriptsCalled = false;
if ( miniCartBlocks.length === 0 ) {
return;
}
const dependencies = window.wcBlocksMiniCartFrontendDependencies 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 removeJQueryRemovedFromCartEvent = translateJQueryEventToNative(
'removed_from_cart',
'wc-blocks_removed_from_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 );
// Load scripts if a page is reloaded via the back button (potentially out of date cart data).
// Based on refreshCachedCartData() in assets/js/base/context/cart-checkout/cart/index.js.
window.addEventListener(
'pageshow',
( event: PageTransitionEvent ): void => {
if ( event?.persisted || getNavigationType() === 'back_forward' ) {
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 loadContents = () => {
if ( ! wasLoadScriptsCalled ) {
loadScripts();
}
document.body.removeEventListener(
'wc-blocks_added_to_cart',
// eslint-disable-next-line @typescript-eslint/no-use-before-define
funcOnAddToCart
);
document.body.removeEventListener(
'wc-blocks_removed_from_cart',
// eslint-disable-next-line @typescript-eslint/no-use-before-define
loadContentsWithRefresh
);
removeJQueryAddedToCartEvent();
removeJQueryRemovedFromCartEvent();
};
const openDrawer = () => {
miniCartBlock.dataset.isInitiallyOpen = 'true';
miniCartDrawerPlaceholderOverlay.classList.add(
'wc-block-components-drawer__screen-overlay--with-slide-in'
);
miniCartDrawerPlaceholderOverlay.classList.remove(
'wc-block-components-drawer__screen-overlay--is-hidden'
);
loadContents();
};
const openDrawerWithRefresh = () => {
openDrawer();
};
const loadContentsWithRefresh = () => {
miniCartBlock.dataset.isInitiallyOpen = 'false';
loadContents();
};
miniCartButton.addEventListener( 'mouseover', loadScripts );
miniCartButton.addEventListener( 'focus', loadScripts );
miniCartButton.addEventListener( 'click', openDrawer );
const funcOnAddToCart =
miniCartBlock.dataset.addToCartBehaviour === 'open_drawer'
? openDrawerWithRefresh
: loadContentsWithRefresh;
// 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',
funcOnAddToCart
);
document.body.addEventListener(
'wc-blocks_removed_from_cart',
loadContentsWithRefresh
);
}
} );
} );

View File

@@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { miniCartAlt } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import './style.scss';
const featurePluginSupport = {
...metadata.supports,
...( isFeaturePluginBuild() && {
typography: {
...metadata.supports.typography,
__experimentalFontFamily: true,
__experimentalFontWeight: true,
},
} ),
};
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ miniCartAlt }
className="wc-block-editor-components-block-icon wc-block-editor-mini-cart__icon"
/>
),
},
supports: {
...featurePluginSupport,
},
example: {
...metadata.example,
},
attributes: {
...metadata.attributes,
},
edit,
save() {
return null;
},
} );
// Remove the Mini Cart template part from the block inserter.
addFilter(
'blocks.registerBlockType',
'woocommerce/mini-cart',
function ( blockSettings, blockName ) {
if ( blockName === 'core/template-part' ) {
return {
...blockSettings,
variations: blockSettings.variations.map(
( variation: { name: string } ) => {
if ( variation.name === 'mini-cart' ) {
return {
...variation,
scope: [],
};
}
return variation;
}
),
};
}
return blockSettings;
}
);

View File

@@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Icon } from '@wordpress/icons';
import { filledCart, removeCart } from '@woocommerce/icons';
export const blockName = 'woocommerce/mini-cart-contents';
export const attributes = {
isPreview: {
type: 'boolean',
default: false,
},
lock: {
type: 'object',
default: {
remove: true,
move: true,
},
},
currentView: {
type: 'string',
default: 'woocommerce/filled-mini-cart-contents-block',
source: 'readonly', // custom source to prevent saving to post content
},
editorViews: {
type: 'object',
default: [
{
view: 'woocommerce/filled-mini-cart-contents-block',
label: __( 'Filled Mini-Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ filledCart } />,
},
{
view: 'woocommerce/empty-mini-cart-contents-block',
label: __( 'Empty Mini-Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ removeCart } />,
},
],
},
width: {
type: 'string',
default: '480px',
},
};

View File

@@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { DrawerCloseButton } from '@woocommerce/base-components/drawer';
import { CartEventsProvider } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import './inner-blocks/register-components';
type MiniCartContentsBlockProps = {
attributes: Record< string, unknown >;
children: JSX.Element | JSX.Element[];
};
export const MiniCartContentsBlock = (
props: MiniCartContentsBlockProps
): JSX.Element => {
const { children } = props;
return (
<>
<CartEventsProvider>
<DrawerCloseButton />
{ children }
</CartEventsProvider>
</>
);
};

View File

@@ -0,0 +1,184 @@
/* eslint-disable jsdoc/check-alignment */
/**
* External dependencies
*/
import {
useBlockProps,
InnerBlocks,
InspectorControls,
} from '@wordpress/block-editor';
import { EditorProvider } from '@woocommerce/base-context';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import type { TemplateArray } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
import type { FocusEvent, ReactElement } from 'react';
import { __ } from '@wordpress/i18n';
import {
PanelBody,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalUnitControl as UnitControl,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { useForcedLayout } from '../../cart-checkout-shared';
import { MiniCartInnerBlocksStyle } from './inner-blocks-style';
import './editor.scss';
import { attributes as defaultAttributes } from './attributes';
// Array of allowed block names.
const ALLOWED_BLOCKS = [
'woocommerce/filled-mini-cart-contents-block',
'woocommerce/empty-mini-cart-contents-block',
];
const MIN_WIDTH = 300;
interface Props {
clientId: string;
attributes: Record< string, unknown >;
setAttributes: ( attributes: Record< string, unknown > ) => void;
}
const Edit = ( {
clientId,
attributes,
setAttributes,
}: Props ): ReactElement => {
const { currentView, width } = attributes;
const blockProps = useBlockProps();
const defaultTemplate = [
[ 'woocommerce/filled-mini-cart-contents-block', {}, [] ],
[ 'woocommerce/empty-mini-cart-contents-block', {}, [] ],
] as TemplateArray;
useForcedLayout( {
clientId,
registeredBlocks: ALLOWED_BLOCKS,
defaultTemplate,
} );
/**
* This is a workaround for the Site Editor to set the correct
* background color of the Mini-Cart Contents block base on
* the main background color set by the theme.
*/
useEffect( () => {
const canvasEl = document.querySelector(
'.edit-site-visual-editor__editor-canvas'
);
if ( ! ( canvasEl instanceof HTMLIFrameElement ) ) {
return;
}
const canvas =
canvasEl.contentDocument || canvasEl.contentWindow?.document;
if ( ! canvas ) {
return;
}
if ( canvas.getElementById( 'mini-cart-contents-background-color' ) ) {
return;
}
const styles = canvas.querySelectorAll( 'style' );
const [ cssRule ] = Array.from( styles )
.map( ( style ) => Array.from( style.sheet?.cssRules || [] ) )
.flatMap( ( style ) => style )
.filter( Boolean )
.filter(
( rule ) =>
rule.selectorText === '.editor-styles-wrapper' &&
rule.style.backgroundColor
);
if ( ! cssRule ) {
return;
}
const backgroundColor = cssRule.style.backgroundColor;
if ( ! backgroundColor ) {
return;
}
const style = document.createElement( 'style' );
style.id = 'mini-cart-contents-background-color';
style.appendChild(
document.createTextNode(
`:where(.wp-block-woocommerce-mini-cart-contents) {
background-color: ${ backgroundColor };
}`
)
);
const body = canvas.querySelector( '.editor-styles-wrapper' );
if ( ! body ) {
return;
}
body.appendChild( style );
}, [] );
return (
<>
{ isFeaturePluginBuild() && (
<InspectorControls key="inspector">
<PanelBody
title={ __(
'Dimensions',
'woo-gutenberg-products-block'
) }
initialOpen
>
<UnitControl
onChange={ ( value ) => {
setAttributes( { width: value } );
} }
onBlur={ ( e: FocusEvent< HTMLInputElement > ) => {
if ( e.target.value === '' ) {
setAttributes( {
width: defaultAttributes.width.default,
} );
} else if (
Number( e.target.value ) < MIN_WIDTH
) {
setAttributes( {
width: MIN_WIDTH + 'px',
} );
}
} }
value={ width }
units={ [
{
value: 'px',
label: 'px',
default: defaultAttributes.width.default,
},
] }
/>
</PanelBody>
</InspectorControls>
) }
<div
className="wc-block-components-drawer__screen-overlay"
aria-hidden="true"
></div>
<div className="wc-block-editor-mini-cart-contents__wrapper">
<div { ...blockProps }>
<EditorProvider currentView={ currentView }>
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={ defaultTemplate }
templateLock={ false }
/>
</EditorProvider>
<MiniCartInnerBlocksStyle style={ blockProps.style } />
</div>
</div>
</>
);
};
export default Edit;
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,99 @@
// Extra classes added for specificity, so we get rid of a top margin added by GB.
.editor-styles-wrapper .wc-block-editor-mini-cart-contents__wrapper.wc-block-editor-mini-cart-contents__wrapper {
display: flex;
justify-content: center;
margin: 0;
position: relative;
z-index: 9999;
}
.editor-styles-wrapper .wp-block-woocommerce-mini-cart-contents {
.wp-block-woocommerce-empty-mini-cart-contents-block[hidden],
.wp-block-woocommerce-filled-mini-cart-contents-block[hidden] {
display: none;
}
.wp-block-woocommerce-filled-mini-cart-contents-block > .block-editor-inner-blocks > .block-editor-block-list__layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.wp-block-woocommerce-mini-cart-items-block {
display: grid;
flex-grow: 1;
margin-bottom: $gap;
padding: 0 $gap;
> .block-editor-inner-blocks > .block-editor-block-list__layout {
display: flex;
flex-direction: column;
height: 100%;
}
// Temporary fix after the appender button was positioned absolute
// See https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5742#issuecomment-1032804168
.block-list-appender {
position: relative;
}
}
.wp-block-woocommerce-mini-cart-products-table-block {
margin-bottom: auto;
margin-top: $gap;
}
h2.wc-block-mini-cart__title {
@include font-size(larger);
.block-editor-block-list__layout {
display: flex;
align-items: baseline;
}
}
table.wc-block-cart-items {
color: inherit;
}
.block-editor-button-block-appender {
box-shadow: inset 0 0 0 1px;
color: inherit;
}
.wp-block-woocommerce-empty-mini-cart-contents-block {
min-height: 100vh;
overflow-y: unset;
padding: 0;
> .block-editor-inner-blocks {
box-sizing: border-box;
max-height: 100vh;
overflow-y: auto;
padding: $gap-largest $gap $gap;
}
// Temporary fix after the appender button was positioned absolute
// See https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5742#issuecomment-1032804168
.block-list-appender {
margin-top: $gap;
position: relative;
}
}
.wc-block-mini-cart__shopping-button a {
color: currentColor;
}
}
/* Site Editor preview */
.block-editor-block-preview__content-iframe .editor-styles-wrapper {
.wp-block-woocommerce-mini-cart-contents,
.wp-block-woocommerce-filled-mini-cart-contents-block,
.wp-block-woocommerce-empty-mini-cart-contents-block {
height: 800px;
min-height: none;
}
}

View File

@@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { cart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
import type { BlockConfiguration } from '@wordpress/blocks';
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import edit, { Save as save } from './edit';
import { blockName, attributes } from './attributes';
import './inner-blocks';
const settings: BlockConfiguration = {
apiVersion: 2,
title: __( 'Mini-Cart Contents', 'woo-gutenberg-products-block' ),
icon: {
src: (
<Icon
icon={ cart }
className="wc-block-editor-components-block-icon"
/>
),
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
description: __(
'Display a Mini-Cart widget.',
'woo-gutenberg-products-block'
),
supports: {
align: false,
html: false,
multiple: false,
reusable: false,
inserter: false,
color: {
link: true,
},
lock: false,
...( isFeaturePluginBuild() && {
__experimentalBorder: {
color: true,
width: true,
},
} ),
},
attributes,
example: {
attributes: {
isPreview: true,
},
},
edit,
save,
};
registerBlockType( blockName, settings );

View File

@@ -0,0 +1,55 @@
/**
* This is a workaround to style inner blocks using the color
* settings of the Mini-Cart Contents block. It's possible to get
* the Mini-Cart Contents block's attributes inside the inner blocks
* components, but we have 4 out of 7 inner blocks that inherit
* style from the Mini-Cart Contents block, so we need to apply the
* styles here to avoid duplication.
*
* We only use this hack for the Site Editor. On the frontend, we
* manipulate the style using block attributes and inject the CSS
* via `wp_add_inline_style()` function.
*/
export const MiniCartInnerBlocksStyle = ( {
style,
}: {
style: Record< string, unknown >;
} ): JSX.Element => {
const innerStyles = [
{
selector:
'.wc-block-mini-cart__footer .wc-block-mini-cart__footer-actions .wc-block-mini-cart__footer-checkout',
properties: [
{
property: 'color',
value: style.backgroundColor,
},
{
property: 'background-color',
value: style.color,
},
{
property: 'border-color',
value: style.color,
},
],
},
]
.map( ( { selector, properties } ) => {
const rules = properties
.filter( ( { value } ) => value )
.map( ( { property, value } ) => `${ property }: ${ value };` )
.join( '' );
if ( rules ) return `${ selector } { ${ rules } }`;
return '';
} )
.join( '' )
.trim();
if ( ! innerStyles ) {
return <></>;
}
return <style>{ innerStyles } </style>;
};

View File

@@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { getBlockTypes } from '@wordpress/blocks';
const EXCLUDED_BLOCKS: readonly string[] = [
'woocommerce/mini-cart',
'woocommerce/checkout',
'woocommerce/cart',
'woocommerce/single-product',
'woocommerce/cart-totals-block',
'woocommerce/checkout-fields-block',
'core/post-template',
'core/comment-template',
'core/query-pagination',
'core/comments-query-loop',
'core/post-comments-form',
'core/post-comments-link',
'core/post-comments-count',
'core/comments-pagination',
'core/post-navigation-link',
'core/button',
];
export const getMiniCartAllowedBlocks = (): string[] =>
getBlockTypes()
.filter( ( block ) => {
if ( EXCLUDED_BLOCKS.includes( block.name ) ) {
return false;
}
// Exclude child blocks of EXCLUDED_BLOCKS.
if (
block.parent &&
block.parent.filter( ( value ) =>
EXCLUDED_BLOCKS.includes( value )
).length > 0
) {
return false;
}
return true;
} )
.map( ( { name } ) => name );

View File

@@ -0,0 +1,28 @@
{
"name": "woocommerce/empty-mini-cart-contents-block",
"version": "1.0.0",
"title": "Empty Mini-Cart view",
"description": "Blocks that are displayed when the Mini-Cart is empty.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/mini-cart-contents" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { useEditorContext } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import { getMiniCartAllowedBlocks } from '../allowed-blocks';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
const { currentView } = useEditorContext();
return (
<div
{ ...blockProps }
hidden={
currentView !== 'woocommerce/empty-mini-cart-contents-block'
}
>
<InnerBlocks
allowedBlocks={ getMiniCartAllowedBlocks() }
renderAppender={ InnerBlocks.ButtonBlockAppender }
/>
</div>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
type EmptyMiniCartContentsBlockProps = {
children: JSX.Element | JSX.Element[];
className: string;
};
const EmptyMiniCartContentsBlock = ( {
children,
className,
}: EmptyMiniCartContentsBlockProps ): JSX.Element | null => {
const { cartItems, cartIsLoading } = useStoreCart();
const elementRef = useRef< HTMLDivElement >( null );
useEffect( () => {
if ( cartItems.length === 0 && ! cartIsLoading ) {
elementRef.current?.focus();
}
}, [ cartItems, cartIsLoading ] );
if ( cartIsLoading || cartItems.length > 0 ) {
return null;
}
return (
<div tabIndex={ -1 } ref={ elementRef } className={ className }>
<div className="wc-block-mini-cart__empty-cart-wrapper">
{ children }
</div>
</div>
);
};
export default EmptyMiniCartContentsBlock;

View File

@@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { removeCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/empty-mini-cart-contents-block', {
icon: {
src: (
<Icon
icon={ removeCart }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,28 @@
{
"name": "woocommerce/filled-mini-cart-contents-block",
"version": "1.0.0",
"title": "Filled Mini-Cart view",
"description": "Contains blocks that display the content of the Mini-Cart.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/mini-cart-contents" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import type { TemplateArray } from '@wordpress/blocks';
import { EditorProvider, useEditorContext } from '@woocommerce/base-context';
import { previewCart } from '@woocommerce/resource-previews';
/**
* Internal dependencies
*/
import {
useForcedLayout,
getAllowedBlocks,
} from '../../../../cart-checkout-shared';
export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => {
const blockProps = useBlockProps();
const allowedBlocks = getAllowedBlocks( innerBlockAreas.FILLED_MINI_CART );
const { currentView } = useEditorContext();
const defaultTemplate = [
[ 'woocommerce/mini-cart-title-block', {} ],
[ 'woocommerce/mini-cart-items-block', {} ],
[ 'woocommerce/mini-cart-footer-block', {} ],
].filter( Boolean ) as unknown as TemplateArray;
useForcedLayout( {
clientId,
registeredBlocks: allowedBlocks,
defaultTemplate,
} );
return (
<div
{ ...blockProps }
hidden={
currentView !== 'woocommerce/filled-mini-cart-contents-block'
}
>
<EditorProvider
currentView={ currentView }
previewData={ { previewCart } }
>
<InnerBlocks
template={ defaultTemplate }
allowedBlocks={ allowedBlocks }
templateLock="insert"
/>
</EditorProvider>
</div>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { useStoreCart } from '@woocommerce/base-context/hooks';
type FilledMiniCartContentsBlockProps = {
children: JSX.Element;
className: string;
};
const FilledMiniCartContentsBlock = ( {
children,
className,
}: FilledMiniCartContentsBlockProps ): JSX.Element | null => {
const { cartItems } = useStoreCart();
if ( cartItems.length === 0 ) {
return null;
}
return (
<div className={ className }>
<StoreNoticesContainer context="wc/cart" />
{ children }
</div>
);
};
export default FilledMiniCartContentsBlock;

View File

@@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { filledCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/filled-mini-cart-contents-block', {
icon: {
src: (
<Icon
icon={ filledCart }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import './empty-mini-cart-contents-block';
import './filled-mini-cart-contents-block';
import './mini-cart-title-block';
import './mini-cart-title-items-counter-block';
import './mini-cart-title-label-block';
import './mini-cart-items-block';
import './mini-cart-products-table-block';
import './mini-cart-footer-block';
import './mini-cart-shopping-button-block';
import './mini-cart-cart-button-block';
import './mini-cart-checkout-button-block';

View File

@@ -0,0 +1,11 @@
/**
* Internal dependencies
*/
import { defaultCartButtonLabel } from './constants';
export default {
cartButtonLabel: {
type: 'string',
default: defaultCartButtonLabel,
},
};

View File

@@ -0,0 +1,42 @@
{
"name": "woocommerce/mini-cart-cart-button-block",
"version": "1.0.0",
"title": "Mini-Cart View Cart Button",
"description": "Block that displays the cart button when the Mini-Cart has products.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": true,
"color": {
"text": true,
"background": true
}
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": false,
"move": false
}
}
},
"styles": [
{
"name": "fill",
"label": "Fill"
},
{
"name": "outline",
"label": "Outline",
"isDefault": true
}
],
"parent": [ "woocommerce/mini-cart-footer-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { CART_URL } from '@woocommerce/block-settings';
import Button from '@woocommerce/base-components/button';
import classNames from 'classnames';
import { useStyleProps } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import { defaultCartButtonLabel } from './constants';
import { getVariant } from '../utils';
type MiniCartCartButtonBlockProps = {
cartButtonLabel?: string;
className?: string;
style?: string;
};
const Block = ( {
className,
cartButtonLabel,
style,
}: MiniCartCartButtonBlockProps ): JSX.Element | null => {
const styleProps = useStyleProps( { style } );
if ( ! CART_URL ) {
return null;
}
return (
<Button
className={ classNames(
className,
styleProps.className,
'wc-block-mini-cart__footer-cart'
) }
style={ styleProps.style }
href={ CART_URL }
variant={ getVariant( className, 'outlined' ) }
>
{ cartButtonLabel || defaultCartButtonLabel }
</Button>
);
};
export default Block;

View File

@@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const defaultCartButtonLabel = __(
'View my cart',
'woo-gutenberg-products-block'
);

View File

@@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import EditableButton from '@woocommerce/editor-components/editable-button';
/**
* Internal dependencies
*/
import { defaultCartButtonLabel } from './constants';
import { getVariant } from '../utils';
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
cartButtonLabel: string;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const blockProps = useBlockProps( {
className: 'wc-block-mini-cart__footer-cart',
} );
const { cartButtonLabel } = attributes;
return (
<div { ...blockProps }>
<EditableButton
variant={ getVariant( blockProps.className, 'outlined' ) }
value={ cartButtonLabel }
placeholder={ defaultCartButtonLabel }
onChange={ ( content ) => {
setAttributes( {
cartButtonLabel: content,
} );
} }
style={ blockProps.style }
/>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { Icon, button } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-cart-button-block', {
icon: {
src: (
<Icon
icon={ button }
className="wc-block-editor-components-block-icon"
/>
),
},
attributes,
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,11 @@
/**
* Internal dependencies
*/
import { defaultCheckoutButtonLabel } from './constants';
export default {
checkoutButtonLabel: {
type: 'string',
default: defaultCheckoutButtonLabel,
},
};

View File

@@ -0,0 +1,44 @@
{
"name": "woocommerce/mini-cart-checkout-button-block",
"version": "1.0.0",
"title": "Mini-Cart Proceed to Checkout Button",
"description": "Block that displays the checkout button when the Mini-Cart has products.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": true,
"color": {
"text": true,
"background": true
}
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": false,
"move": false
}
}
},
"styles": [
{
"name": "fill",
"label": "Fill",
"isDefault": true
},
{
"name": "outline",
"label": "Outline"
}
],
"parent": [
"woocommerce/mini-cart-footer-block"
],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import { CHECKOUT_URL } from '@woocommerce/block-settings';
import Button from '@woocommerce/base-components/button';
import classNames from 'classnames';
import { useStyleProps } from '@woocommerce/base-hooks';
import {
isErrorResponse,
useCartEventsContext,
} from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import { defaultCheckoutButtonLabel } from './constants';
import { getVariant } from '../utils';
type MiniCartCheckoutButtonBlockProps = {
checkoutButtonLabel?: string;
className?: string;
style?: string;
};
const Block = ( {
className,
checkoutButtonLabel,
style,
}: MiniCartCheckoutButtonBlockProps ): JSX.Element | null => {
const styleProps = useStyleProps( { style } );
const { dispatchOnProceedToCheckout } = useCartEventsContext();
if ( ! CHECKOUT_URL ) {
return null;
}
return (
<Button
className={ classNames(
className,
styleProps.className,
'wc-block-mini-cart__footer-checkout'
) }
variant={ getVariant( className, 'contained' ) }
style={ styleProps.style }
href={ CHECKOUT_URL }
onClick={ ( e ) => {
dispatchOnProceedToCheckout().then( ( observerResponses ) => {
if ( observerResponses.some( isErrorResponse ) ) {
e.preventDefault();
}
} );
} }
>
{ checkoutButtonLabel || defaultCheckoutButtonLabel }
</Button>
);
};
export default Block;

View File

@@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const defaultCheckoutButtonLabel = __(
'Go to checkout',
'woo-gutenberg-products-block'
);

View File

@@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import EditableButton from '@woocommerce/editor-components/editable-button';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { defaultCheckoutButtonLabel } from './constants';
import { getVariant } from '../utils';
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
checkoutButtonLabel: string;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const blockProps = useBlockProps( {
className: classNames( 'wc-block-mini-cart__footer-checkout' ),
} );
const { checkoutButtonLabel } = attributes;
return (
<div { ...blockProps }>
<EditableButton
variant={ getVariant( blockProps.className, 'contained' ) }
value={ checkoutButtonLabel }
placeholder={ defaultCheckoutButtonLabel }
onChange={ ( content ) => {
setAttributes( {
checkoutButtonLabel: content,
} );
} }
style={ blockProps.style }
/>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { Icon, button } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-checkout-button-block', {
icon: {
src: (
<Icon
icon={ button }
className="wc-block-editor-components-block-icon"
/>
),
},
attributes,
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,48 @@
/**
* External dependencies
*/
import {
CartEventsProvider,
useCartEventsContext,
} from '@woocommerce/base-context';
import { useEffect } from '@wordpress/element';
import { render, screen, waitFor } from '@testing-library/react';
/**
* Internal dependencies
*/
import Block from '../block';
describe( 'Mini Cart Checkout Button Block', () => {
it( 'dispatches the onProceedToCheckout event when the button is clicked', async () => {
const mockObserver = jest.fn().mockReturnValue( { type: 'error' } );
const MockObserverComponent = () => {
const { onProceedToCheckout } = useCartEventsContext();
useEffect( () => {
return onProceedToCheckout( mockObserver );
}, [ onProceedToCheckout ] );
return <div>Mock observer</div>;
};
render(
<CartEventsProvider>
<div>
<MockObserverComponent />
<Block
checkoutButtonLabel={ 'Proceed to Checkout' }
className="test-block"
/>
</div>
</CartEventsProvider>
);
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
const button = screen.getByText( 'Proceed to Checkout' );
// Forcibly set the button URL to # to prevent JSDOM error: `["Error: Not implemented: navigation (except hash changes)`
button.parentElement?.removeAttribute( 'href' );
button.click();
await waitFor( () => {
expect( mockObserver ).toHaveBeenCalled();
} );
} );
} );

View File

@@ -0,0 +1,18 @@
/**
* Internal dependencies
*/
import {
defaultCartButtonLabel,
defaultCheckoutButtonLabel,
} from './constants';
export default {
cartButtonLabel: {
type: 'string',
default: defaultCartButtonLabel,
},
checkoutButtonLabel: {
type: 'string',
default: defaultCheckoutButtonLabel,
},
};

View File

@@ -0,0 +1,28 @@
{
"name": "woocommerce/mini-cart-footer-block",
"version": "1.0.0",
"title": "Mini-Cart Footer",
"description": "Block that displays the footer of the Mini-Cart block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/filled-mini-cart-contents-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { TotalsItem } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
usePaymentMethods,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons';
import { getIconsFromPaymentMethods } from '@woocommerce/base-utils';
import { getSetting } from '@woocommerce/settings';
import { PaymentEventsProvider } from '@woocommerce/base-context';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import CartButton from '../mini-cart-cart-button-block/block';
import CheckoutButton from '../mini-cart-checkout-button-block/block';
import { hasChildren } from '../utils';
const PaymentMethodIconsElement = (): JSX.Element => {
const { paymentMethods } = usePaymentMethods();
return (
<PaymentMethodIcons
icons={ getIconsFromPaymentMethods( paymentMethods ) }
/>
);
};
interface Props {
children: JSX.Element | JSX.Element[];
className?: string;
cartButtonLabel: string;
checkoutButtonLabel: string;
}
const Block = ( {
children,
className,
cartButtonLabel,
checkoutButtonLabel,
}: Props ): JSX.Element => {
const { cartTotals } = useStoreCart();
const subTotal = getSetting( 'displayCartPricesIncludingTax', false )
? parseInt( cartTotals.total_items, 10 ) +
parseInt( cartTotals.total_items_tax, 10 )
: parseInt( cartTotals.total_items, 10 );
// The `Cart` and `Checkout` buttons were converted to inner blocks, but we still need to render the buttons
// for themes that have the old `mini-cart.html` template. So we check if there are any inner blocks (buttons) and
// if not, render the buttons.
const hasButtons = hasChildren( children );
return (
<div
className={ classNames( className, 'wc-block-mini-cart__footer' ) }
>
<TotalsItem
className="wc-block-mini-cart__footer-subtotal"
currency={ getCurrencyFromPriceResponse( cartTotals ) }
label={ __( 'Subtotal', 'woo-gutenberg-products-block' ) }
value={ subTotal }
description={ __(
'Shipping, taxes, and discounts calculated at checkout.',
'woo-gutenberg-products-block'
) }
/>
<div className="wc-block-mini-cart__footer-actions">
{ hasButtons ? (
children
) : (
<>
<CartButton cartButtonLabel={ cartButtonLabel } />
<CheckoutButton
checkoutButtonLabel={ checkoutButtonLabel }
/>
</>
) }
</div>
<PaymentEventsProvider>
<PaymentMethodIconsElement />
</PaymentEventsProvider>
</div>
);
};
export default Block;

View File

@@ -0,0 +1,14 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const defaultCartButtonLabel = __(
'View my cart',
'woo-gutenberg-products-block'
);
export const defaultCheckoutButtonLabel = __(
'Go to checkout',
'woo-gutenberg-products-block'
);

View File

@@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { TotalsItem } from '@woocommerce/blocks-components';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
usePaymentMethods,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons';
import { getIconsFromPaymentMethods } from '@woocommerce/base-utils';
import { getSetting } from '@woocommerce/settings';
import { PaymentEventsProvider } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import './editor.scss';
const PaymentMethodIconsElement = (): JSX.Element => {
const { paymentMethods } = usePaymentMethods();
return (
<PaymentMethodIcons
icons={ getIconsFromPaymentMethods( paymentMethods ) }
/>
);
};
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
const { cartTotals } = useStoreCart();
const subTotal = getSetting( 'displayCartPricesIncludingTax', false )
? parseInt( cartTotals.total_items, 10 ) +
parseInt( cartTotals.total_items_tax, 10 )
: parseInt( cartTotals.total_items, 10 );
const TEMPLATE = [
[ 'woocommerce/mini-cart-cart-button-block', {} ],
[ 'woocommerce/mini-cart-checkout-button-block', {} ],
];
return (
<div { ...blockProps }>
<div className="wc-block-mini-cart__footer">
<TotalsItem
className="wc-block-mini-cart__footer-subtotal"
currency={ getCurrencyFromPriceResponse( cartTotals ) }
label={ __( 'Subtotal', 'woo-gutenberg-products-block' ) }
value={ subTotal }
description={ __(
'Shipping, taxes, and discounts calculated at checkout.',
'woo-gutenberg-products-block'
) }
/>
<div className="wc-block-mini-cart__footer-actions">
<InnerBlocks template={ TEMPLATE } />
</div>
<PaymentEventsProvider>
<PaymentMethodIconsElement />
</PaymentEventsProvider>
</div>
</div>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,3 @@
.editor-styles-wrapper .wc-block-mini-cart__footer .block-editor-inner-blocks {
width: 100%;
}

View File

@@ -0,0 +1,66 @@
/**
* External dependencies
*/
import { Icon, payment } from '@wordpress/icons';
import { createBlock, registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import deprecatedAttributes from './attributes';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-footer-block', {
icon: {
src: (
<Icon
icon={ payment }
className="wc-block-editor-components-block-icon"
/>
),
},
deprecated: [
{
attributes: deprecatedAttributes,
migrate( attributes, innerBlocks ) {
const {
cartButtonLabel,
checkoutButtonLabel,
...restAttributes
} = attributes;
return [
restAttributes,
[
createBlock(
'woocommerce/mini-cart-cart-button-block',
{
cartButtonLabel,
}
),
createBlock(
'woocommerce/mini-cart-checkout-button-block',
{
checkoutButtonLabel,
}
),
...innerBlocks,
],
];
},
isEligible: ( attributes, innerBlocks ) => {
return ! innerBlocks.length;
},
save: (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
},
},
],
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,28 @@
{
"name": "woocommerce/mini-cart-items-block",
"version": "1.0.0",
"title": "Mini-Cart Items",
"description": "Contains the products table and other custom blocks of filled mini-cart.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/filled-mini-cart-contents-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import type { TemplateArray } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { getMiniCartAllowedBlocks } from '../allowed-blocks';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps( {
className: 'wc-block-mini-cart__items',
} );
const defaultTemplate = [
[ 'woocommerce/mini-cart-products-table-block', {} ],
].filter( Boolean ) as unknown as TemplateArray;
return (
<div { ...blockProps }>
<InnerBlocks
template={ defaultTemplate }
renderAppender={ InnerBlocks.ButtonBlockAppender }
templateLock={ false }
allowedBlocks={ getMiniCartAllowedBlocks() }
/>
</div>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import classNames from 'classnames';
type MiniCartItemsBlockProps = {
children: JSX.Element;
className: string;
};
const Block = ( {
children,
className,
}: MiniCartItemsBlockProps ): JSX.Element => {
return (
<div
className={ classNames( className, 'wc-block-mini-cart__items' ) }
tabIndex={ -1 }
>
{ children }
</div>
);
};
export default Block;

View File

@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { Icon, grid } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-items-block', {
icon: {
src: (
<Icon
icon={ grid }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,28 @@
{
"name": "woocommerce/mini-cart-products-table-block",
"version": "1.0.0",
"title": "Mini-Cart Products Table",
"description": "Block that displays the products table of the Mini-Cart block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": false
}
}
},
"parent": [ "woocommerce/mini-cart-items-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,32 @@
/**
* External dependencies
*/
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { CartLineItemsTable } from '@woocommerce/base-components/cart-checkout';
import classNames from 'classnames';
type MiniCartProductsTableBlockProps = {
className: string;
};
const Block = ( {
className,
}: MiniCartProductsTableBlockProps ): JSX.Element => {
const { cartItems, cartIsLoading } = useStoreCart();
return (
<div
className={ classNames(
className,
'wc-block-mini-cart__products-table'
) }
>
<CartLineItemsTable
lineItems={ cartItems }
isLoading={ cartIsLoading }
className="wc-block-mini-cart-items"
/>
</div>
);
};
export default Block;

View File

@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
*/
import Block from './block';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<Noninteractive>
<Block className="is-mobile" />
</Noninteractive>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { Icon, list } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-products-table-block', {
icon: (
<Icon icon={ list } className="wc-block-editor-components-block-icon" />
),
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,11 @@
/**
* Internal dependencies
*/
import { defaultStartShoppingButtonLabel } from './constants';
export default {
startShoppingButtonLabel: {
type: 'string',
default: defaultStartShoppingButtonLabel,
},
};

View File

@@ -0,0 +1,42 @@
{
"name": "woocommerce/mini-cart-shopping-button-block",
"version": "1.0.0",
"title": "Mini-Cart Shopping Button",
"description": "Block that displays the shopping button when the Mini-Cart is empty.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": true,
"color": {
"text": true,
"background": true
}
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": false,
"move": false
}
}
},
"styles": [
{
"name": "fill",
"label": "Fill",
"isDefault": true
},
{
"name": "outline",
"label": "Outline"
}
],
"parent": [ "woocommerce/empty-mini-cart-contents-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { SHOP_URL } from '@woocommerce/block-settings';
import Button from '@woocommerce/base-components/button';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { defaultStartShoppingButtonLabel } from './constants';
import { getVariant } from '../utils';
type MiniCartShoppingButtonBlockProps = {
className: string;
startShoppingButtonLabel: string;
};
const Block = ( {
className,
startShoppingButtonLabel,
}: MiniCartShoppingButtonBlockProps ): JSX.Element | null => {
if ( ! SHOP_URL ) {
return null;
}
return (
<div className="wp-block-button has-text-align-center">
<Button
className={ classNames(
className,
'wp-block-button__link',
'wc-block-mini-cart__shopping-button'
) }
variant={ getVariant( className, 'contained' ) }
href={ SHOP_URL }
>
{ startShoppingButtonLabel || defaultStartShoppingButtonLabel }
</Button>
</div>
);
};
export default Block;

View File

@@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const defaultStartShoppingButtonLabel = __(
'Start shopping',
'woo-gutenberg-products-block'
);

View File

@@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import EditableButton from '@woocommerce/editor-components/editable-button';
/**
* Internal dependencies
*/
import { defaultStartShoppingButtonLabel } from './constants';
import { getVariant } from '../utils';
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
startShoppingButtonLabel: string;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const blockProps = useBlockProps( {
className: 'wp-block-button aligncenter',
} );
const { startShoppingButtonLabel } = attributes;
return (
<div { ...blockProps }>
<EditableButton
className="wc-block-mini-cart__shopping-button"
value={ startShoppingButtonLabel }
placeholder={ defaultStartShoppingButtonLabel }
onChange={ ( content ) => {
setAttributes( {
startShoppingButtonLabel: content,
} );
} }
variant={ getVariant( blockProps.className, 'contained' ) }
/>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { Icon, button } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-shopping-button-block', {
icon: {
src: (
<Icon
icon={ button }
className="wc-block-editor-components-block-icon"
/>
),
},
attributes,
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,35 @@
{
"name": "woocommerce/mini-cart-title-block",
"version": "1.0.0",
"title": "Mini-Cart Title",
"description": "Block that displays the title of the Mini-Cart block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false,
"color": {
"text": true,
"background": false
},
"typography": {
"fontSize": true
}
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/filled-mini-cart-contents-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { useStoreCart } from '@woocommerce/base-context/hooks';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import TitleItemsCounter from '../mini-cart-title-items-counter-block/block';
import TitleYourCart from '../mini-cart-title-label-block/block';
import { hasChildren } from '../utils';
type MiniCartTitleBlockProps = {
className: string;
children: JSX.Element;
};
const Block = ( {
children,
className,
}: MiniCartTitleBlockProps ): JSX.Element | null => {
const { cartIsLoading } = useStoreCart();
if ( cartIsLoading ) {
return null;
}
// The `Mini-Cart Title` was converted to two inner blocks, but we still need to render the old title for
// themes that have the old `mini-cart.html` template. So we check if there are any inner blocks and if
// not, render the title blocks.
const hasTitleInnerBlocks = hasChildren( children );
return (
<h2 className={ classNames( className, 'wc-block-mini-cart__title' ) }>
{ hasTitleInnerBlocks ? (
children
) : (
<>
<TitleYourCart />
<TitleItemsCounter />
</>
) }
</h2>
);
};
export default Block;

View File

@@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps( {
className: 'wc-block-mini-cart__title',
} );
const TEMPLATE = [
[ 'woocommerce/mini-cart-title-label-block', {} ],
[ 'woocommerce/mini-cart-title-items-counter-block', {} ],
];
return (
<h2 { ...blockProps }>
<InnerBlocks
allowedBlocks={ [
'woocommerce/mini-cart-title-label-block',
'woocommerce/mini-cart-title-items-counter-block',
] }
template={ TEMPLATE }
templateLock="all"
/>
</h2>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { Icon, heading } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-title-block', {
icon: {
src: (
<Icon
icon={ heading }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,29 @@
{
"name": "woocommerce/mini-cart-title-items-counter-block",
"version": "1.0.0",
"title": "Mini-Cart Title Items Counter",
"description": "Block that displays the items counter part of the Mini-Cart Title block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false,
"color": {
"text": true,
"background": true
},
"typography": {
"fontSize": true
},
"spacing": {
"padding": true
}
},
"parent": [ "woocommerce/mini-cart-title-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { useStoreCart } from '@woocommerce/base-context';
import classNames from 'classnames';
import { _n, sprintf } from '@wordpress/i18n';
import { useStyleProps } from '@woocommerce/base-hooks';
type Props = {
className?: string;
};
const Block = ( props: Props ): JSX.Element => {
const { cartItemsCount } = useStoreCart();
const styleProps = useStyleProps( props );
return (
<span
className={ classNames( props.className, styleProps.className ) }
style={ styleProps.style }
>
{ sprintf(
/* translators: %d is the count of items in the cart. */
_n(
'(%d item)',
'(%d items)',
cartItemsCount,
'woo-gutenberg-products-block'
),
cartItemsCount
) }
</span>
);
};
export default Block;

View File

@@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { _n, sprintf } from '@wordpress/i18n';
import { useStoreCart } from '@woocommerce/base-context';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
const { cartItemsCount } = useStoreCart();
return (
<span { ...blockProps }>
{ sprintf(
/* translators: %d is the count of items in the cart. */
_n(
'(%d item)',
'(%d items)',
cartItemsCount,
'woo-gutenberg-products-block'
),
cartItemsCount
) }
</span>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { Icon, heading } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-title-items-counter-block', {
icon: {
src: (
<Icon
icon={ heading }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,11 @@
/**
* Internal dependencies
*/
import { defaultYourCartLabel } from './constants';
export default {
label: {
type: 'string',
default: defaultYourCartLabel,
},
};

View File

@@ -0,0 +1,34 @@
{
"name": "woocommerce/mini-cart-title-label-block",
"version": "1.0.0",
"title": "Mini-Cart Title Label",
"description": "Block that displays the 'Your cart' part of the Mini-Cart Title block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false,
"color": {
"text": true,
"background": true
},
"typography": {
"fontSize": true
},
"spacing": {
"padding": true
}
},
"attributes": {
"label": {
"type": "string"
}
},
"parent": [ "woocommerce/mini-cart-title-block" ],
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { useStyleProps } from '@woocommerce/base-hooks';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import { defaultYourCartLabel } from './constants';
type Props = {
label?: string;
className?: string;
};
const Block = ( props: Props ): JSX.Element => {
const styleProps = useStyleProps( props );
return (
<span
className={ classnames( props.className, styleProps.className ) }
style={ styleProps.style }
>
{ props.label || defaultYourCartLabel }
</span>
);
};
export default Block;

View File

@@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const defaultYourCartLabel = __(
'Your cart',
'woo-gutenberg-products-block'
);

View File

@@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { RichText, useBlockProps } from '@wordpress/block-editor';
interface Attributes {
attributes: {
label: string;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
}
export const Edit = ( {
attributes: { label },
setAttributes,
}: Attributes ): JSX.Element => {
const blockProps = useBlockProps();
return (
<span { ...blockProps }>
<RichText
allowedFormats={ [] }
value={ label }
onChange={ ( newLabel ) =>
setAttributes( { label: newLabel } )
}
/>
</span>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { Icon, heading } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- TypeScript expects some required properties which we already
// registered in PHP.
registerBlockType( 'woocommerce/mini-cart-title-label-block', {
icon: {
src: (
<Icon
icon={ heading }
className="wc-block-editor-components-block-icon"
/>
),
},
attributes,
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,137 @@
/**
* External dependencies
*/
import { WC_BLOCKS_BUILD_URL } from '@woocommerce/block-settings';
import { registerCheckoutBlock } from '@woocommerce/blocks-checkout';
import { lazy } from '@wordpress/element';
/**
* Internal dependencies
*/
import emptyMiniCartContentsMetadata from './empty-mini-cart-contents-block/block.json';
import filledMiniCartMetadata from './filled-mini-cart-contents-block/block.json';
import miniCartTitleMetadata from './mini-cart-title-block/block.json';
import miniCartTitleItemsCounterMetadata from './mini-cart-title-items-counter-block/block.json';
import miniCartTitleLabelBlockMetadata from './mini-cart-title-label-block/block.json';
import miniCartProductsTableMetadata from './mini-cart-products-table-block/block.json';
import miniCartFooterMetadata from './mini-cart-footer-block/block.json';
import miniCartItemsMetadata from './mini-cart-items-block/block.json';
import miniCartShoppingButtonMetadata from './mini-cart-shopping-button-block/block.json';
import miniCartCartButtonMetadata from './mini-cart-cart-button-block/block.json';
import miniCartCheckoutButtonMetadata from './mini-cart-checkout-button-block/block.json';
// Modify webpack publicPath at runtime based on location of WordPress Plugin.
// eslint-disable-next-line no-undef,camelcase
__webpack_public_path__ = WC_BLOCKS_BUILD_URL;
registerCheckoutBlock( {
metadata: filledMiniCartMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/filled-cart" */ './filled-mini-cart-contents-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: emptyMiniCartContentsMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/empty-cart" */ './empty-mini-cart-contents-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartTitleMetadata,
force: false,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/title" */ './mini-cart-title-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartTitleItemsCounterMetadata,
force: false,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/title-items-counter" */ './mini-cart-title-items-counter-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartTitleLabelBlockMetadata,
force: false,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/title-label" */ './mini-cart-title-label-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartItemsMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/items" */ './mini-cart-items-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartProductsTableMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/products-table" */ './mini-cart-products-table-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartFooterMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/footer" */ './mini-cart-footer-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartShoppingButtonMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/shopping-button" */ './mini-cart-shopping-button-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartCartButtonMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/cart-button" */ './mini-cart-cart-button-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: miniCartCheckoutButtonMetadata,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/checkout-button" */ './mini-cart-checkout-button-block/block'
)
),
} );

View File

@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { isObject } from '@woocommerce/types';
type Variant = 'text' | 'contained' | 'outlined';
export const getVariant = (
className = '',
defaultVariant: Variant
): Variant => {
if ( className.includes( 'is-style-outline' ) ) {
return 'outlined';
}
if ( className.includes( 'is-style-fill' ) ) {
return 'contained';
}
return defaultVariant;
};
/**
* Checks if there are any children that are blocks.
*/
export const hasChildren = ( children ): boolean => {
return children.some( ( child ) => {
if ( Array.isArray( child ) ) {
return hasChildren( child );
}
return isObject( child ) && child.key !== null;
} );
};

View File

@@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { cartOutline, bag, bagAlt } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './style.scss';
import { IconType, ColorItem } from '.././types';
interface Props {
count: number;
icon?: IconType;
iconColor: ColorItem | { color: undefined };
productCountColor: ColorItem | { color: undefined };
}
const QuantityBadge = ( {
count,
icon,
iconColor,
productCountColor,
}: Props ): JSX.Element => {
function getIcon( iconName?: 'cart' | 'bag' | 'bag-alt' ) {
switch ( iconName ) {
case 'cart':
return cartOutline;
case 'bag':
return bag;
case 'bag-alt':
return bagAlt;
default:
return cartOutline;
}
}
return (
<span className="wc-block-mini-cart__quantity-badge">
<Icon
className="wc-block-mini-cart__icon"
color={ iconColor.color }
size={ 20 }
icon={ getIcon( icon ) }
/>
<span
className="wc-block-mini-cart__badge"
style={ { background: productCountColor.color } }
>
{ count > 0 ? count : '' }
</span>
</span>
);
};
export default QuantityBadge;

View File

@@ -0,0 +1,45 @@
.wc-block-mini-cart__quantity-badge {
align-items: center;
display: flex;
position: relative;
}
.wc-block-mini-cart__badge {
align-items: center;
border-radius: 1em;
box-sizing: border-box;
display: flex;
font-size: 0.875em;
font-weight: 600;
height: math.div(em(20px), 0.875);
justify-content: center;
left: 100%;
margin-left: -44%;
min-width: math.div(em(20px), 0.875);
padding: 0 em($gap-smallest);
position: absolute;
transform: translateY(-50%);
white-space: nowrap;
z-index: 1;
}
:where(.wc-block-mini-cart__badge) {
// These values will be overridden in JS.
background-color: transparent;
color: transparent;
}
.wc-block-mini-cart__badge:empty {
opacity: 0;
}
.wc-block-mini-cart__icon {
display: block;
height: em(32px);
width: em(32px);
margin: -0.25em;
html[dir="rtl"] & {
transform: scaleX(-1);
}
}

View File

@@ -0,0 +1,209 @@
.wc-block-mini-cart {
display: inline-block;
}
.wc-block-mini-cart__template-part,
.wp-block-woocommerce-mini-cart-contents {
height: 100%;
}
.wc-block-mini-cart__button {
align-items: center;
background-color: transparent;
border: none;
color: inherit;
display: flex;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
padding: em($gap-small) em($gap-smaller);
&:hover:not([disabled]) {
opacity: 0.6;
}
}
.wc-block-mini-cart__amount {
margin-right: 0.5em;
}
.wc-block-mini-cart--preview {
.wc-block-mini-cart__amount {
display: initial;
}
}
.wc-block-mini-cart__tax-label {
margin-right: em($gap-smaller);
}
@media screen and (min-width: 768px) {
.wc-block-mini-cart__amount {
display: initial;
font-size: inherit;
font-weight: inherit;
margin-right: $gap-smaller;
}
}
.drawer-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;
.wp-block-woocommerce-mini-cart-contents {
box-sizing: border-box;
padding: 0;
position: relative;
justify-content: center;
.wc-block-components-notices {
margin: #{$gap} #{$gap-largest} -#{$gap} #{$gap};
margin-bottom: unset;
.wc-block-components-notices__notice {
margin-bottom: unset;
}
&:empty {
display: none;
}
}
}
}
:where(.wp-block-woocommerce-mini-cart-contents) {
background: #fff;
}
.wp-block-woocommerce-empty-mini-cart-contents-block,
.wp-block-woocommerce-filled-mini-cart-contents-block {
background: inherit;
height: 100%;
max-height: -webkit-fill-available;
max-height: -moz-available;
max-height: fill-available;
display: flex;
flex-direction: column;
}
.wp-block-woocommerce-empty-mini-cart-contents-block {
justify-content: center;
}
.wp-block-woocommerce-filled-mini-cart-contents-block {
justify-content: space-between;
}
.wp-block-woocommerce-empty-mini-cart-contents-block
.wc-block-mini-cart__empty-cart-wrapper {
overflow-y: auto;
padding: $gap-largest $gap $gap;
}
h2.wc-block-mini-cart__title {
display: flex;
align-items: baseline;
background: inherit;
padding-bottom: $gap * 2;
mask-image: linear-gradient(#000 calc(100% - #{$gap * 1.5}), transparent);
z-index: 1;
margin: $gap $gap $gap * -2;
@include font-size(larger);
span:first-child {
margin-right: $gap-smaller;
}
}
.wc-block-mini-cart__items {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
padding: $gap $gap 0;
.wc-block-mini-cart__products-table {
margin-bottom: auto;
.wc-block-cart-items__row {
padding-top: $gap-smaller;
padding-bottom: $gap-smaller;
&:last-child::after {
content: none;
}
}
}
}
.wc-block-mini-cart__footer {
@include with-translucent-border( 1px 0 0 );
padding: $gap-large $gap;
.wc-block-components-totals-item.wc-block-mini-cart__footer-subtotal {
font-weight: 600;
margin-bottom: $gap;
.wc-block-components-totals-item__description {
display: none;
font-size: 0.75em;
font-weight: 400;
@media only screen and (min-width: 480px) {
display: unset;
}
}
}
// First selector for the frontend, second selector for the editor.
.wc-block-mini-cart__footer-actions,
.wc-block-mini-cart__footer-actions > .block-editor-inner-blocks > .block-editor-block-list__layout {
display: flex;
gap: $gap;
.wc-block-components-button,
.wp-block-button,
.wp-block-woocommerce-mini-cart-cart-button-block,
.wp-block-woocommerce-mini-cart-checkout-button-block {
flex-grow: 1;
display: inline-flex;
}
.wp-block-woocommerce-mini-cart-cart-button-block {
@media only screen and (min-width: 480px) {
display: inline-flex;
}
}
@media only screen and (max-width: 480px) {
flex-direction: column;
}
}
.wc-block-components-payment-method-icons {
margin-top: $gap;
}
}
.wc-block-mini-cart__shopping-button {
display: flex;
justify-content: center;
a {
border: 2px solid;
color: currentColor;
font-weight: 600;
padding: $gap-small $gap-large;
text-decoration: none;
&:hover,
&:focus {
background-color: $gray-900;
border-color: $gray-900;
color: $white;
}
}
}

View File

@@ -0,0 +1,224 @@
/**
* External dependencies
*/
import {
act,
render,
screen,
queryByText,
waitFor,
waitForElementToBeRemoved,
} from '@testing-library/react';
import { previewCart } from '@woocommerce/resource-previews';
import { dispatch } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import { default as fetchMock } from 'jest-fetch-mock';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import Block from '../block';
import { defaultCartState } from '../../../data/cart/default-state';
const MiniCartBlock = ( props ) => (
<SlotFillProvider>
<Block
contents='<div data-block-name="woocommerce/mini-cart-contents" class="wp-block-woocommerce-mini-cart-contents"><div data-block-name="woocommerce/filled-mini-cart-contents-block" class="wp-block-woocommerce-filled-mini-cart-contents-block"><div data-block-name="woocommerce/mini-cart-title-block" class="wp-block-woocommerce-mini-cart-title-block"><div data-block-name="woocommerce/mini-cart-title-label-block" class="wp-block-woocommerce-mini-cart-title-label-block"></div>
<div data-block-name="woocommerce/mini-cart-title-items-counter-block" class="wp-block-woocommerce-mini-cart-title-items-counter-block"></div></div>
<div data-block-name="woocommerce/mini-cart-items-block" class="wp-block-woocommerce-mini-cart-items-block"><div data-block-name="woocommerce/mini-cart-products-table-block" class="wp-block-woocommerce-mini-cart-products-table-block"></div></div>
<div data-block-name="woocommerce/mini-cart-footer-block" class="wp-block-woocommerce-mini-cart-footer-block"><div data-block-name="woocommerce/mini-cart-cart-button-block" class="wp-block-woocommerce-mini-cart-cart-button-block"></div>
<div data-block-name="woocommerce/mini-cart-checkout-button-block" class="wp-block-woocommerce-mini-cart-checkout-button-block"></div></div></div>
<div data-block-name="woocommerce/empty-mini-cart-contents-block" class="wp-block-woocommerce-empty-mini-cart-contents-block">
<p class="has-text-align-center"><strong>Your cart is currently empty!</strong></p>
<div data-block-name="woocommerce/mini-cart-shopping-button-block" class="wp-block-woocommerce-mini-cart-shopping-button-block"></div></div></div>'
{ ...props }
/>
</SlotFillProvider>
);
const mockEmptyCart = () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/v1\/cart/ ) ) {
return Promise.resolve(
JSON.stringify( defaultCartState.cartData )
);
}
return Promise.resolve( '' );
} );
};
const mockFullCart = () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/v1\/cart/ ) ) {
return Promise.resolve( JSON.stringify( previewCart ) );
}
return Promise.resolve( '' );
} );
};
const initializeLocalStorage = () => {
Object.defineProperty( window, 'localStorage', {
value: {
setItem: jest.fn(),
},
writable: true,
} );
};
describe( 'Testing Mini-Cart', () => {
beforeEach( () => {
act( () => {
mockFullCart();
// need to clear the store resolution state between tests.
dispatch( storeKey ).invalidateResolutionForStore();
dispatch( storeKey ).receiveCart( defaultCartState.cartData );
} );
} );
afterEach( () => {
fetchMock.resetMocks();
} );
it( 'shows Mini-Cart count badge when there are items in the cart', async () => {
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
await waitFor( () =>
expect( screen.getByText( '3' ) ).toBeInTheDocument()
);
} );
it( "doesn't show Mini-Cart count badge when cart is empty", async () => {
mockEmptyCart();
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
const badgeWith0Count = screen.queryByText( '0' );
expect( badgeWith0Count ).toBeNull();
} );
it( 'opens Mini-Cart drawer when clicking on button', async () => {
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
userEvent.click( screen.getByLabelText( /items/i ) );
await waitFor( () =>
expect( screen.getByText( /your cart/i ) ).toBeInTheDocument()
);
} );
it( 'closes the drawer when clicking on the close button', async () => {
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
// Open drawer.
userEvent.click( screen.getByLabelText( /items/i ) );
// Close drawer.
let closeButton = null;
await waitFor( () => {
closeButton = screen.getByLabelText( /close/i );
} );
if ( closeButton ) {
userEvent.click( closeButton );
}
await waitFor( () => {
expect(
screen.queryByText( /your cart/i )
).not.toBeInTheDocument();
} );
} );
it( 'renders empty cart if there are no items in the cart', async () => {
mockEmptyCart();
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
userEvent.click( screen.getByLabelText( /items/i ) );
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
} );
it( 'updates contents when removed from cart event is triggered', async () => {
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
mockEmptyCart();
// eslint-disable-next-line no-undef
const removedFromCartEvent = new Event( 'wc-blocks_removed_from_cart' );
act( () => {
document.body.dispatchEvent( removedFromCartEvent );
} );
await waitForElementToBeRemoved( () =>
screen.queryByLabelText( /3 items in cart/i )
);
await waitFor( () =>
expect(
screen.getByLabelText( /0 items in cart/i )
).toBeInTheDocument()
);
} );
it( 'updates contents when added to cart event is triggered', async () => {
mockEmptyCart();
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
mockFullCart();
// eslint-disable-next-line no-undef
const addedToCartEvent = new Event( 'wc-blocks_added_to_cart' );
act( () => {
document.body.dispatchEvent( addedToCartEvent );
} );
await waitForElementToBeRemoved( () =>
screen.queryByLabelText( /0 items in cart/i )
);
await waitFor( () =>
expect(
screen.getByLabelText( /3 items in cart/i )
).toBeInTheDocument()
);
} );
it( 'updates local storage when cart finishes loading', async () => {
initializeLocalStorage();
mockFullCart();
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
// Assert we saved the values returned to the localStorage.
await waitFor( () =>
expect(
JSON.parse( window.localStorage.setItem.mock.calls[ 0 ][ 1 ] )
.itemsCount
).toEqual( 3 )
);
} );
it( 'renders cart price if "Hide Cart Price" setting is not enabled', async () => {
mockEmptyCart();
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
await waitFor( () =>
expect( screen.getByText( '$0.00' ) ).toBeInTheDocument()
);
} );
it( 'does not render cart price if "Hide Cart Price" setting is enabled', async () => {
mockEmptyCart();
const { container } = render(
<MiniCartBlock hasHiddenPrice={ true } />
);
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
await waitFor( () =>
expect( queryByText( container, '$0.00' ) ).not.toBeInTheDocument()
);
} );
} );

View File

@@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { CartResponseTotals } from '@woocommerce/types';
export type IconType = 'cart' | 'bag' | 'bag-alt' | undefined;
export interface ColorItem {
color: string;
name?: string;
slug?: string;
class?: string;
}
export interface BlockAttributes {
initialCartItemsCount?: number;
initialCartTotals?: CartResponseTotals;
isInitiallyOpen?: boolean;
colorClassNames?: string;
style?: Record< string, Record< string, string > >;
contents: string;
miniCartIcon?: IconType;
addToCartBehaviour: string;
hasHiddenPrice: boolean;
priceColor: ColorItem;
iconColor: ColorItem;
productCountColor: ColorItem;
}

View File

@@ -0,0 +1,197 @@
/**
* External dependencies
*/
import { _n, sprintf } from '@wordpress/i18n';
import {
getCurrencyFromPriceResponse,
formatPrice,
} from '@woocommerce/price-format';
import {
CartResponse,
CartResponseTotals,
isBoolean,
} from '@woocommerce/types';
import { getSettingWithCoercion } from '@woocommerce/settings';
import type { ColorPaletteOption } from '@woocommerce/editor-components/color-panel/types';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { Attributes } from '../edit';
const getPrice = ( totals: CartResponseTotals, showIncludingTax: boolean ) => {
const currency = getCurrencyFromPriceResponse( totals );
const subTotal = showIncludingTax
? parseInt( totals.total_items, 10 ) +
parseInt( totals.total_items_tax, 10 )
: parseInt( totals.total_items, 10 );
return formatPrice( subTotal, currency );
};
export const updateTotals = (
cartData: [ CartResponseTotals, number ] | undefined
) => {
if ( ! cartData ) {
return;
}
const [ totals, quantity ] = cartData;
const showIncludingTax = getSettingWithCoercion(
'displayCartPricesIncludingTax',
false,
isBoolean
);
const amount = getPrice( totals, showIncludingTax );
const miniCartBlocks = document.querySelectorAll( '.wc-block-mini-cart' );
const miniCartQuantities = document.querySelectorAll(
'.wc-block-mini-cart__badge'
);
const miniCartAmounts = document.querySelectorAll(
'.wc-block-mini-cart__amount'
);
miniCartBlocks.forEach( ( miniCartBlock ) => {
if ( ! ( miniCartBlock instanceof HTMLElement ) ) {
return;
}
const miniCartButton = miniCartBlock.querySelector(
'.wc-block-mini-cart__button'
);
miniCartButton?.setAttribute(
'aria-label',
miniCartBlock.dataset.hasHiddenPrice
? sprintf(
/* translators: %s number of products in cart. */
_n(
'%1$d item in cart',
'%1$d items in cart',
quantity,
'woo-gutenberg-products-block'
),
quantity
)
: 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',
quantity,
'woo-gutenberg-products-block'
),
quantity,
amount
)
);
miniCartBlock.dataset.cartTotals = JSON.stringify( totals );
miniCartBlock.dataset.cartItemsCount = quantity.toString();
} );
miniCartQuantities.forEach( ( miniCartQuantity ) => {
if ( quantity > 0 || miniCartQuantity.textContent !== '' ) {
miniCartQuantity.textContent = quantity.toString();
}
} );
miniCartAmounts.forEach( ( miniCartAmount ) => {
miniCartAmount.textContent = amount;
} );
// Show the tax label only if there are products in the cart.
if ( quantity > 0 ) {
const miniCartTaxLabels = document.querySelectorAll(
'.wc-block-mini-cart__tax-label'
);
miniCartTaxLabels.forEach( ( miniCartTaxLabel ) => {
miniCartTaxLabel.removeAttribute( 'hidden' );
} );
}
};
export const getMiniCartTotalsFromLocalStorage = ():
| [ CartResponseTotals, number ]
| undefined => {
const rawMiniCartTotals = localStorage.getItem(
'wc-blocks_mini_cart_totals'
);
if ( ! rawMiniCartTotals ) {
return undefined;
}
const cartData = JSON.parse( rawMiniCartTotals );
return [ cartData.totals, cartData.itemsCount ] as [
CartResponseTotals,
number
];
};
export const getMiniCartTotalsFromServer = async (): Promise<
[ CartResponseTotals, number ] | undefined
> => {
return apiFetch< CartResponse >( {
path: '/wc/store/v1/cart',
} )
.then( ( data: CartResponse ) => {
// Save server data to local storage, so we can re-fetch it faster
// on the next page load.
localStorage.setItem(
'wc-blocks_mini_cart_totals',
JSON.stringify( {
totals: data.totals,
itemsCount: data.items_count,
} )
);
return [ data.totals, data.items_count ] as [
CartResponseTotals,
number
];
} )
.catch( ( error ) => {
// eslint-disable-next-line no-console
console.error( error );
return undefined;
} );
};
interface MaybeInCompatibleAttributes
extends Omit<
Attributes,
'priceColor' | 'iconColor' | 'productCountColor'
> {
priceColorValue?: string;
iconColorValue?: string;
productCountColorValue?: string;
priceColor: Partial< ColorPaletteOption > | string;
iconColor: Partial< ColorPaletteOption > | string;
productCountColor: Partial< ColorPaletteOption > | string;
}
export function migrateAttributesToColorPanel(
attributes: MaybeInCompatibleAttributes
): Attributes {
const attrs = { ...attributes };
if ( attrs.priceColorValue && ! attrs.priceColor ) {
attrs.priceColor = {
color: attributes.priceColorValue as string,
};
delete attrs.priceColorValue;
}
if ( attrs.iconColorValue && ! attrs.iconColor ) {
attrs.iconColor = {
color: attributes.iconColorValue as string,
};
delete attrs.iconColorValue;
}
if ( attrs.productCountColorValue && ! attrs.productCountColor ) {
attrs.productCountColor = {
color: attributes.productCountColorValue as string,
};
delete attrs.productCountColorValue;
}
return <Attributes>attrs;
}

View File

@@ -0,0 +1,5 @@
export const defaultColorItem = {
name: undefined,
color: undefined,
slug: undefined,
};

View File

@@ -0,0 +1,51 @@
function getClosestColor(
element: Element | null,
colorType: 'color' | 'backgroundColor'
): string | null {
if ( ! element ) {
return null;
}
const color = window.getComputedStyle( element )[ colorType ];
if ( color !== 'rgba(0, 0, 0, 0)' && color !== 'transparent' ) {
return color;
}
return getClosestColor( element.parentElement, colorType );
}
function setStyles() {
/**
* Get the background color of the body then set it as the background color
* of the Mini-Cart Contents block.
*
* We only set the background color, instead of the whole background. As
* we only provide the option to customize the background color.
*/
const style = document.createElement( 'style' );
const backgroundColor = getComputedStyle( document.body ).backgroundColor;
// For simplicity, we only consider the background color of the first Mini-Cart button.
const firstMiniCartButton = document.querySelector(
'.wc-block-mini-cart__button'
);
const badgeTextColor =
getClosestColor( firstMiniCartButton, 'backgroundColor' ) || '#fff';
const badgeBackgroundColor =
getClosestColor( firstMiniCartButton, 'color' ) || '#000';
// We use :where here to reduce specificity so customized colors and theme
// CSS take priority.
style.appendChild(
document.createTextNode(
`:where(.wp-block-woocommerce-mini-cart-contents) {
background-color: ${ backgroundColor };
}
:where(.wc-block-mini-cart__badge) {
background-color: ${ badgeBackgroundColor };
color: ${ badgeTextColor };
}`
)
);
document.head.appendChild( style );
}
export default setStyles;

View File

@@ -0,0 +1,240 @@
// eslint-disable testing-library/no-dom-import
/**
* External dependencies
*/
import { getByTestId, waitFor } from '@testing-library/dom';
import { getSettingWithCoercion } from '@woocommerce/settings';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import {
getMiniCartTotalsFromLocalStorage,
getMiniCartTotalsFromServer,
updateTotals,
migrateAttributesToColorPanel,
} from '../data';
// This is a simplified version of the response of the Cart API endpoint.
const responseMock = {
totals: {
total_price: '1800',
total_items: '1400',
total_items_tax: '200',
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
items_count: 2,
};
const localStorageMock = {
totals: {
total_price: '1800',
total_items: '1400',
total_items_tax: '200',
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
itemsCount: 2,
};
const initializeLocalStorage = () => {
Object.defineProperty( window, 'localStorage', {
value: {
getItem: jest
.fn()
.mockReturnValue( JSON.stringify( localStorageMock ) ),
setItem: jest.fn(),
},
writable: true,
} );
};
// This is a simplified version of the Mini-Cart DOM generated by MiniCart.php.
const getMiniCartDOM = () => {
const div = document.createElement( 'div' );
div.innerHTML = `
<div class="wc-block-mini-cart">
<div class="wc-block-mini-cart__amount" data-testid="amount"></div>
<div class="wc-block-mini-cart__badge" data-testid="quantity"></div>
</div>`;
return div;
};
jest.mock( '@woocommerce/settings', () => {
return {
...jest.requireActual( '@woocommerce/settings' ),
getSettingWithCoercion: jest.fn(),
};
} );
jest.mock( '@wordpress/api-fetch' );
describe( 'Mini-Cart frontend script when "the display prices during cart and checkout" option is set to "Including Tax"', () => {
beforeAll( () => {
( getSettingWithCoercion as jest.Mock ).mockReturnValue( true );
} );
afterAll( () => {
jest.resetModules();
} );
it( 'updates the cart contents based on the localStorage values', async () => {
initializeLocalStorage();
const container = getMiniCartDOM();
document.body.appendChild( container );
updateTotals( getMiniCartTotalsFromLocalStorage() );
// Assert that we are rendering the amount.
await waitFor( () =>
expect( getByTestId( container, 'amount' ).textContent ).toBe(
'$16.00'
)
);
// Assert that we are rendering the quantity.
await waitFor( () =>
expect( getByTestId( container, 'quantity' ).textContent ).toBe(
'2'
)
);
} );
it( 'updates the cart contents based on the API response', async () => {
apiFetch.mockResolvedValue( responseMock );
const container = getMiniCartDOM();
document.body.appendChild( container );
getMiniCartTotalsFromServer().then( updateTotals );
// Assert we called the correct endpoint.
await waitFor( () =>
expect( apiFetch ).toHaveBeenCalledWith( {
path: '/wc/store/v1/cart',
} )
);
// Assert we saved the values returned to the localStorage.
await waitFor( () =>
expect( window.localStorage.setItem.mock.calls[ 0 ][ 1 ] ).toEqual(
JSON.stringify( localStorageMock )
)
);
// Assert that we are rendering the amount.
await waitFor( () =>
expect( getByTestId( container, 'amount' ).textContent ).toBe(
'$16.00'
)
);
// Assert that we are rendering the quantity.
await waitFor( () =>
expect( getByTestId( container, 'quantity' ).textContent ).toBe(
'2'
)
);
jest.restoreAllMocks();
} );
} );
describe( 'Mini-Cart frontend script when "the display prices during cart and checkout" option is set to "Excluding Tax"', () => {
beforeAll( () => {
( getSettingWithCoercion as jest.Mock ).mockReturnValue( false );
} );
it( 'updates the cart contents based on the localStorage values', async () => {
initializeLocalStorage();
const container = getMiniCartDOM();
document.body.appendChild( container );
updateTotals( getMiniCartTotalsFromLocalStorage() );
// Assert that we are rendering the amount.
await waitFor( () =>
expect( getByTestId( container, 'amount' ).textContent ).toBe(
'$14.00'
)
);
// Assert that we are rendering the quantity.
await waitFor( () =>
expect( getByTestId( container, 'quantity' ).textContent ).toBe(
'2'
)
);
} );
it( 'updates the cart contents based on the API response', async () => {
apiFetch.mockResolvedValue( responseMock );
const container = getMiniCartDOM();
document.body.appendChild( container );
getMiniCartTotalsFromServer().then( updateTotals );
// Assert we called the correct endpoint.
await waitFor( () =>
expect( apiFetch ).toHaveBeenCalledWith( {
path: '/wc/store/v1/cart',
} )
);
// Assert we saved the values returned to the localStorage.
await waitFor( () =>
expect( window.localStorage.setItem.mock.calls[ 0 ][ 1 ] ).toEqual(
JSON.stringify( localStorageMock )
)
);
// Assert that we are rendering the amount.
await waitFor( () =>
expect( getByTestId( container, 'amount' ).textContent ).toBe(
'$14.00'
)
);
// Assert that we are rendering the quantity.
await waitFor( () =>
expect( getByTestId( container, 'quantity' ).textContent ).toBe(
'2'
)
);
jest.restoreAllMocks();
} );
} );
const mockAttributes = {
miniCartIcon: 'cart',
addToCartBehaviour: 'inline',
hasHiddenPrice: false,
cartAndCheckoutRenderStyle: true,
priceColorValue: '#000000',
iconColorValue: '#ffffff',
productCountColorValue: '#ff0000',
};
describe( 'migrateAttributesToColorPanel tests', () => {
test( 'it correctly migrates attributes to color panel', () => {
const migratedAttributes =
migrateAttributesToColorPanel( mockAttributes );
expect( migratedAttributes ).toEqual( {
miniCartIcon: 'cart',
addToCartBehaviour: 'inline',
hasHiddenPrice: false,
cartAndCheckoutRenderStyle: true,
priceColor: {
color: '#000000',
},
iconColor: {
color: '#ffffff',
},
productCountColor: {
color: '#ff0000',
},
} );
} );
} );