rebase from live enviornment
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
} );
|
||||
} );
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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 );
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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';
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultCartButtonLabel } from './constants';
|
||||
|
||||
export default {
|
||||
cartButtonLabel: {
|
||||
type: 'string',
|
||||
default: defaultCartButtonLabel,
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const defaultCartButtonLabel = __(
|
||||
'View my cart',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultCheckoutButtonLabel } from './constants';
|
||||
|
||||
export default {
|
||||
checkoutButtonLabel: {
|
||||
type: 'string',
|
||||
default: defaultCheckoutButtonLabel,
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const defaultCheckoutButtonLabel = __(
|
||||
'Go to checkout',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
defaultCartButtonLabel,
|
||||
defaultCheckoutButtonLabel,
|
||||
} from './constants';
|
||||
|
||||
export default {
|
||||
cartButtonLabel: {
|
||||
type: 'string',
|
||||
default: defaultCartButtonLabel,
|
||||
},
|
||||
checkoutButtonLabel: {
|
||||
type: 'string',
|
||||
default: defaultCheckoutButtonLabel,
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.editor-styles-wrapper .wc-block-mini-cart__footer .block-editor-inner-blocks {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultStartShoppingButtonLabel } from './constants';
|
||||
|
||||
export default {
|
||||
startShoppingButtonLabel: {
|
||||
type: 'string',
|
||||
default: defaultStartShoppingButtonLabel,
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const defaultStartShoppingButtonLabel = __(
|
||||
'Start shopping',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultYourCartLabel } from './constants';
|
||||
|
||||
export default {
|
||||
label: {
|
||||
type: 'string',
|
||||
default: defaultYourCartLabel,
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const defaultYourCartLabel = __(
|
||||
'Your cart',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
} );
|
||||
@@ -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'
|
||||
)
|
||||
),
|
||||
} );
|
||||
@@ -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;
|
||||
} );
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
} );
|
||||
} );
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const defaultColorItem = {
|
||||
name: undefined,
|
||||
color: undefined,
|
||||
slug: undefined,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
},
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user