first commit

This commit is contained in:
2024-11-10 21:08:49 +01:00
commit 0d932ce5ee
14455 changed files with 2567501 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
/**
* External dependencies
*/
import { renderParentBlock } from '@woocommerce/atomic-utils';
import Drawer from '@woocommerce/base-components/drawer';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
import { getRegisteredBlockComponents } from '@woocommerce/blocks-registry';
import {
formatPrice,
getCurrencyFromPriceResponse,
} from '@woocommerce/price-format';
import { getSettingWithCoercion } from '@woocommerce/settings';
import {
CartResponseTotals,
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 QuantityBadge from './quantity-badge';
import { MiniCartContentsBlock } from './mini-cart-contents/block';
import './style.scss';
import { blockName } from './mini-cart-contents/attributes';
interface Props {
isInitiallyOpen?: boolean;
colorClassNames?: string;
style?: Record< string, Record< string, string > >;
contents: string;
addToCartBehaviour: string;
}
const MiniCartBlock = ( {
isInitiallyOpen = false,
colorClassNames,
style,
contents = '',
addToCartBehaviour = 'none',
}: Props ): JSX.Element => {
const {
cartItemsCount: cartItemsCountFromApi,
cartIsLoading,
cartTotals: cartTotalsFromApi,
} = useStoreCart();
const isFirstLoadingCompleted = useRef( cartIsLoading );
useEffect( () => {
if ( isFirstLoadingCompleted.current && ! cartIsLoading ) {
isFirstLoadingCompleted.current = false;
}
}, [ cartIsLoading, isFirstLoadingCompleted ] );
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 ) {
if ( isOpen ) {
Object.assign( body.style, { overflow: 'hidden' } );
} else {
Object.assign( body.style, { overflow: '' } );
}
}
}, [ 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,
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 preFetchedCartTotals =
getSettingWithCoercion< CartResponseTotals | null >(
'cartTotals',
null,
isCartResponseTotals
);
const preFetchedCartItemsCount = getSettingWithCoercion< number >(
'cartItemsCount',
0,
isNumber
);
const taxLabel = getSettingWithCoercion( 'taxLabel', '', isString );
const cartTotals =
! isFirstLoadingCompleted.current || preFetchedCartTotals === null
? cartTotalsFromApi
: preFetchedCartTotals;
const cartItemsCount = ! isFirstLoadingCompleted.current
? cartItemsCountFromApi
: preFetchedCartItemsCount;
const subTotal = showIncludingTax
? parseInt( cartTotals.total_items, 10 ) +
parseInt( cartTotals.total_items_tax, 10 )
: parseInt( cartTotals.total_items, 10 );
const ariaLabel = sprintf(
/* translators: %1$d is the number of products in the cart. %2$s is the cart total */
_n(
'%1$d item in cart, total price of %2$s',
'%1$d items in cart, total price of %2$s',
cartItemsCount,
'woo-gutenberg-products-block'
),
cartItemsCount,
formatPrice( subTotal, getCurrencyFromPriceResponse( cartTotals ) )
);
const colorStyle = {
backgroundColor: style?.color?.background,
color: style?.color?.text,
};
return (
<>
<button
className={ `wc-block-mini-cart__button ${ colorClassNames }` }
style={ colorStyle }
onClick={ () => {
if ( ! isOpen ) {
setIsOpen( true );
setSkipSlideIn( false );
}
} }
aria-label={ ariaLabel }
>
<span className="wc-block-mini-cart__amount">
{ formatPrice(
subTotal,
getCurrencyFromPriceResponse( cartTotals )
) }
</span>
{ taxLabel !== '' && subTotal !== 0 && (
<small className="wc-block-mini-cart__tax-label">
{ taxLabel }
</small>
) }
<QuantityBadge count={ cartItemsCount } />
</button>
<Drawer
className={ classnames(
'wc-block-mini-cart__drawer',
'is-mobile',
{
'is-loading': cartIsLoading,
}
) }
title=""
isOpen={ isOpen }
onClose={ () => {
setIsOpen( false );
} }
slideIn={ ! skipSlideIn }
>
<div
className="wc-block-mini-cart__template-part"
ref={ contentsRef }
dangerouslySetInnerHTML={ { __html: contents } }
></div>
</Drawer>
</>
);
};
export default MiniCartBlock;

View File

@@ -0,0 +1,66 @@
/**
* 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 !== null ) {
colorClassNames = button.classList
.toString()
.replace( 'wc-block-mini-cart__button', '' );
}
return {
isDataOutdated: el.dataset.isDataOutdated,
isInitiallyOpen: el.dataset.isInitiallyOpen === 'true',
colorClassNames,
style: el.dataset.style ? JSON.parse( el.dataset.style ) : {},
addToCartBehaviour: el.dataset.addToCartBehaviour || 'none',
contents:
el.querySelector( '.wc-block-mini-cart__template-part' )
?.innerHTML ?? '',
};
},
} );
// Refocus previously focused button if drawer is not open.
if (
focusedMiniCartBlock instanceof HTMLElement &&
! focusedMiniCartBlock.dataset.isInitiallyOpen
) {
const innerButton = focusedMiniCartBlock.querySelector(
'.wc-block-mini-cart__button'
);
if ( innerButton instanceof HTMLElement ) {
innerButton.focus();
}
}
};
renderMiniCartFrontend();

View File

@@ -0,0 +1,116 @@
/**
* External dependencies
*/
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import type { ReactElement } from 'react';
import { formatPrice } from '@woocommerce/price-format';
import { CartCheckoutCompatibilityNotice } from '@woocommerce/editor-components/compatibility-notices';
import { PanelBody, ExternalLink, SelectControl } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
import { __ } from '@wordpress/i18n';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
*/
import QuantityBadge from './quantity-badge';
interface Attributes {
addToCartBehaviour: string;
}
interface Props {
attributes: Attributes;
setAttributes: ( attributes: Record< string, unknown > ) => void;
}
const Edit = ( { attributes, setAttributes }: Props ): ReactElement => {
const { addToCartBehaviour } = attributes;
const blockProps = useBlockProps( {
className: `wc-block-mini-cart`,
} );
const templatePartEditUri = getSetting(
'templatePartEditUri',
''
) as string;
const productCount = 0;
const productTotal = 0;
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody
title={ __(
'Mini Cart Settings',
'woo-gutenberg-products-block'
) }
>
<SelectControl
label={ __(
'Add-to-Cart behaviour',
'woo-gutenberg-products-block'
) }
value={ addToCartBehaviour }
onChange={ ( value ) => {
setAttributes( { addToCartBehaviour: value } );
} }
help={ __(
'Select what happens when a customer adds a product to the cart.',
'woo-gutenberg-products-block'
) }
options={ [
{
value: 'none',
label: __(
'Do nothing',
'woo-gutenberg-products-block'
),
},
{
value: 'open_drawer',
label: __(
'Open cart drawer',
'woo-gutenberg-products-block'
),
},
] }
/>
</PanelBody>
{ templatePartEditUri && (
<PanelBody
title={ __(
'Template settings',
'woo-gutenberg-products-block'
) }
>
<p>
{ __(
'Edit the appearance of your empty and filled mini cart contents.',
'woo-gutenberg-products-block'
) }
</p>
<ExternalLink href={ templatePartEditUri }>
{ __(
'Edit Mini Cart template part',
'woo-gutenberg-products-block'
) }
</ExternalLink>
</PanelBody>
) }
</InspectorControls>
<Noninteractive>
<button className="wc-block-mini-cart__button">
<span className="wc-block-mini-cart__amount">
{ formatPrice( productTotal ) }
</span>
<QuantityBadge count={ productCount } />
</button>
</Noninteractive>
<CartCheckoutCompatibilityNotice blockName="mini-cart" />
</div>
);
};
export default Edit;

View File

@@ -0,0 +1,155 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import preloadScript from '@woocommerce/base-utils/preload-script';
import lazyLoadScript from '@woocommerce/base-utils/lazy-load-script';
import { translateJQueryEventToNative } from '@woocommerce/base-utils/legacy-events';
interface dependencyData {
src: string;
version?: string;
after?: string;
before?: string;
translations?: string;
}
window.addEventListener( 'load', () => {
const miniCartBlocks = document.querySelectorAll( '.wc-block-mini-cart' );
let wasLoadScriptsCalled = false;
if ( miniCartBlocks.length === 0 ) {
return;
}
const dependencies = getSetting(
'mini_cart_block_frontend_dependencies',
{}
) as Record< string, dependencyData >;
// Preload scripts
for ( const dependencyHandle in dependencies ) {
const dependency = dependencies[ dependencyHandle ];
preloadScript( {
handle: dependencyHandle,
...dependency,
} );
}
// Make it so we can read jQuery events triggered by WC Core elements.
const removeJQueryAddingToCartEvent = translateJQueryEventToNative(
'adding_to_cart',
'wc-blocks_adding_to_cart'
);
const removeJQueryAddedToCartEvent = translateJQueryEventToNative(
'added_to_cart',
'wc-blocks_added_to_cart'
);
const 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 );
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
openDrawerWithRefresh
);
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 = () => {
miniCartBlock.dataset.isDataOutdated = 'true';
openDrawer();
};
const loadContentsWithRefresh = () => {
miniCartBlock.dataset.isDataOutdated = 'true';
miniCartBlock.dataset.isInitiallyOpen = 'false';
loadContents();
};
miniCartButton.addEventListener( 'mouseover', loadScripts );
miniCartButton.addEventListener( 'focus', loadScripts );
miniCartButton.addEventListener( 'click', openDrawer );
// 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',
openDrawerWithRefresh
);
document.body.addEventListener(
'wc-blocks_removed_from_cart',
loadContentsWithRefresh
);
}
} );
} );

View File

@@ -0,0 +1,68 @@
/**
* 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 from './edit';
const settings: BlockConfiguration = {
apiVersion: 2,
title: __( 'Mini Cart', '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: {
html: false,
multiple: false,
color: true,
typography: {
fontSize: true,
...( isFeaturePluginBuild() && {
__experimentalFontFamily: true,
} ),
},
},
example: {
attributes: {
isPreview: true,
},
},
attributes: {
isPreview: {
type: 'boolean',
default: false,
save: false,
},
addToCartBehaviour: {
type: 'string',
default: 'none',
},
},
edit,
save() {
return null;
},
};
registerBlockType( 'woocommerce/mini-cart', settings );

View File

@@ -0,0 +1 @@
export const blockName = 'woocommerce/mini-cart-contents';

View File

@@ -0,0 +1,18 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import './inner-blocks/register-components';
type MiniCartContentsBlockProps = {
children: JSX.Element | JSX.Element[];
};
export const MiniCartContentsBlock = ( {
children,
}: MiniCartContentsBlockProps ): JSX.Element => {
return <>{ children }</>;
};

View File

@@ -0,0 +1,99 @@
/* eslint-disable jsdoc/check-alignment */
/**
* External dependencies
*/
import type { ReactElement } from 'react';
import {
useBlockProps,
InnerBlocks,
BlockControls,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { filledCart, removeCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { EditorProvider } from '@woocommerce/base-context';
import type { TemplateArray } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { useViewSwitcher, useForcedLayout } from '../../cart-checkout-shared';
import { MiniCartInnerBlocksStyle } from './inner-blocks-style';
import './editor.scss';
// Array of allowed block names.
const ALLOWED_BLOCKS = [
'woocommerce/filled-mini-cart-contents-block',
'woocommerce/empty-mini-cart-contents-block',
];
const views = [
{
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 } />,
},
];
interface Props {
clientId: string;
}
const Edit = ( { clientId }: Props ): ReactElement => {
const blockProps = useBlockProps( {
/**
* This is a workaround for the Site Editor to calculate the
* correct height of the Mini Cart template part on the first load.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5825
*/
style: {
minHeight: '100vh',
},
} );
const defaultTemplate = [
[ 'woocommerce/filled-mini-cart-contents-block', {}, [] ],
[ 'woocommerce/empty-mini-cart-contents-block', {}, [] ],
] as TemplateArray;
const { currentView, component: ViewSwitcherComponent } = useViewSwitcher(
clientId,
views
);
useForcedLayout( {
clientId,
registeredBlocks: ALLOWED_BLOCKS,
defaultTemplate,
} );
return (
<div { ...blockProps }>
<EditorProvider currentView={ currentView }>
<BlockControls>{ ViewSwitcherComponent }</BlockControls>
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={ defaultTemplate }
templateLock={ false }
/>
</EditorProvider>
<MiniCartInnerBlocksStyle style={ blockProps.style } />
</div>
);
};
export default Edit;
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@@ -0,0 +1,81 @@
.editor-styles-wrapper .wp-block-woocommerce-mini-cart-contents {
max-width: 480px;
/* We need to override the margin top here to simulate the layout of
the mini cart contents on the front end. */
margin: 0 auto !important;
.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);
margin: $gap-largest $gap 0;
}
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;
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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';
/**
* Internal dependencies
*/
import edit, { Save as save } from './edit';
import { blockName } 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,
},
attributes: {
isPreview: {
type: 'boolean',
default: false,
save: false,
},
lock: {
type: 'object',
default: {
remove: true,
move: true,
},
},
},
example: {
attributes: {
isPreview: true,
},
},
edit,
save,
};
registerBlockType( blockName, settings );

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
{
"name": "woocommerce/empty-mini-cart-contents-block",
"version": "1.0.0",
"title": "Empty Mini Cart Contents",
"description": "Contains 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": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { removeCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ removeCart }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,27 @@
{
"name": "woocommerce/filled-mini-cart-contents-block",
"version": "1.0.0",
"title": "Filled Mini Cart Contents",
"description": "Contains blocks that are displayed when the mini cart has products.",
"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": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@@ -0,0 +1,5 @@
const Block = ( { children }: { children: JSX.Element } ): JSX.Element => {
return <>{ children }</>;
};
export default Block;

View File

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

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
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 }>{ children }</div>;
};
export default FilledMiniCartContentsBlock;

View File

@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { filledCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ filledCart }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

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

View File

@@ -0,0 +1,27 @@
{
"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": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { TotalsItem } from '@woocommerce/blocks-checkout';
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 { CART_URL, CHECKOUT_URL } from '@woocommerce/block-settings';
import Button from '@woocommerce/base-components/button';
import { PaymentMethodDataProvider } from '@woocommerce/base-context';
import classNames from 'classnames';
const PaymentMethodIconsElement = (): JSX.Element => {
const { paymentMethods } = usePaymentMethods();
return (
<PaymentMethodIcons
icons={ getIconsFromPaymentMethods( paymentMethods ) }
/>
);
};
interface Props {
className?: string;
}
const Block = ( { className }: 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 );
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">
{ CART_URL && (
<Button
className="wc-block-mini-cart__footer-cart"
href={ CART_URL }
variant="outlined"
>
{ __( 'View my cart', 'woo-gutenberg-products-block' ) }
</Button>
) }
{ CHECKOUT_URL && (
<Button
className="wc-block-mini-cart__footer-checkout"
href={ CHECKOUT_URL }
>
{ __(
'Go to checkout',
'woo-gutenberg-products-block'
) }
</Button>
) }
</div>
<PaymentMethodDataProvider>
<PaymentMethodIconsElement />
</PaymentMethodDataProvider>
</div>
);
};
export default Block;

View File

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

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { Icon, payment } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ payment }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,27 @@
{
"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": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

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

View File

@@ -0,0 +1,22 @@
/**
* 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' ) }>
{ children }
</div>
);
};
export default Block;

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { Icon, grid } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ grid }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,27 @@
{
"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": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { Icon, list } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: (
<Icon icon={ list } className="wc-block-editor-components-block-icon" />
),
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,26 @@
{
"name": "woocommerce/mini-cart-shopping-button-block",
"version": "1.0.0",
"title": "Mini Cart Shopping Button",
"description": "Block that displays the shopping button for the Mini Cart block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": true
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": false,
"move": false
}
}
},
"parent": [ "woocommerce/empty-mini-cart-contents-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { SHOP_URL } from '@woocommerce/block-settings';
import classNames from 'classnames';
/**
* Internal dependencies
*/
type MiniCartShoppingButtonBlockProps = {
className: string;
};
const Block = ( {
className,
}: MiniCartShoppingButtonBlockProps ): JSX.Element | null => {
if ( ! SHOP_URL ) {
return null;
}
return (
<div
className={ classNames(
className,
'wc-block-mini-cart__shopping-button'
) }
>
<a href={ SHOP_URL }>
{ __( 'Start shopping', 'woo-gutenberg-products-block' ) }
</a>
</div>
);
};
export default Block;

View File

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

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { Icon, button } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ button }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,27 @@
{
"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
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/filled-mini-cart-contents-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

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

View File

@@ -0,0 +1,23 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import Block from './block';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<Block />
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() }></div>;
};

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { Icon, heading } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerBlockType( metadata, {
icon: {
src: (
<Icon
icon={ heading }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );

View File

@@ -0,0 +1,90 @@
/**
* 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 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';
// 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,
component: lazy(
() =>
import(
/* webpackChunkName: "mini-cart-contents-block/title" */ './mini-cart-title-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'
)
),
} );

View File

@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import { miniCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './style.scss';
interface Props {
count: number;
colorClassNames?: string;
style?: Record< string, string | undefined >;
}
const QuantityBadge = ( {
count,
colorClassNames,
style,
}: Props ): JSX.Element => {
return (
<span className="wc-block-mini-cart__quantity-badge">
<Icon
className="wc-block-mini-cart__icon"
size={ 20 }
icon={ miniCart }
/>
<span
className={ `wc-block-mini-cart__badge ${ colorClassNames }` }
style={ style }
>
{ count }
</span>
</span>
);
};
export default QuantityBadge;

View File

@@ -0,0 +1,34 @@
.wc-block-mini-cart__quantity-badge {
align-items: center;
display: flex;
}
.wc-block-mini-cart__badge {
align-items: center;
background: transparent;
border: 0.15em solid;
border-radius: 1em;
box-sizing: border-box;
color: inherit;
display: flex;
font-size: 0.875em;
font-weight: 600;
height: math.div(em(20px), 0.875);
justify-content: center;
margin-left: math.div(em(-10px), 0.875);
min-width: math.div(em(20px), 0.875);
padding: 0 em($gap-smallest);
transform: translateY(-50%);
white-space: nowrap;
z-index: 1;
}
.wc-block-mini-cart__icon {
display: block;
height: em(24px);
width: em(24px);
html[dir="rtl"] & {
transform: scaleX(-1);
}
}

View File

@@ -0,0 +1,186 @@
.wc-block-mini-cart {
display: inline-block;
}
.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: 400;
padding: em($gap-small) em($gap-smaller);
&:hover:not([disabled]) {
opacity: 0.6;
}
}
.wc-block-mini-cart__amount {
display: none;
}
.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-weight: 600;
margin-right: $gap-smaller;
}
}
.modal-open .wc-block-mini-cart__button {
pointer-events: none;
}
// Reset font size so it doesn't depend on drawer's ancestors.
.wc-block-mini-cart__drawer {
font-size: 1rem;
.components-modal__content {
padding: 0;
position: relative;
}
.components-modal__header {
position: absolute;
top: $gap-largest;
right: $gap;
button {
color: inherit;
z-index: 9999;
}
svg {
fill: currentColor;
}
}
}
.wp-block-woocommerce-mini-cart-contents {
background: #fff;
box-sizing: border-box;
height: 100vh;
padding: 0;
justify-content: center;
}
.wp-block-woocommerce-empty-mini-cart-contents-block,
.wp-block-woocommerce-filled-mini-cart-contents-block {
height: 100%;
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 {
@include font-size(larger);
margin: $gap-largest $gap 0;
}
.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;
margin-right: -$gap;
padding-right: $gap;
.wc-block-cart-items__row {
padding-top: $gap-smaller;
padding-bottom: $gap-smaller;
&:last-child::after {
content: none;
}
}
}
}
.wc-block-mini-cart__footer {
border-top: 1px solid $gray-300;
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;
}
}
}
.wc-block-mini-cart__footer-actions {
display: flex;
gap: $gap;
.wc-block-mini-cart__footer-cart.wc-block-components-button {
box-shadow: inset 0 0 0 1px currentColor;
color: currentColor;
display: none;
flex-grow: 1;
font-weight: 600;
@media only screen and (min-width: 480px) {
display: inline-flex;
}
}
.wc-block-mini-cart__footer-checkout {
flex-grow: 1;
font-weight: 600;
}
}
.wc-block-components-payment-method-icons {
margin-top: $gap;
}
}
.wc-block-mini-cart__shopping-button {
display: flex;
justify-content: center;
a {
border: 2px solid;
color: currentColor;
font-weight: 600;
padding: $gap-small $gap-large;
text-decoration: none;
&:hover,
&:focus {
background-color: $gray-900;
border-color: $gray-900;
color: $white;
}
}
}

View File

@@ -0,0 +1,127 @@
/**
* External dependencies
*/
import {
act,
render,
screen,
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/default-states';
const MiniCartBlock = ( props ) => (
<SlotFillProvider>
<Block
contents='<div class="wc-block-mini-cart-contents"></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( '' );
} );
};
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( 'opens Mini Cart drawer when clicking on button', async () => {
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
userEvent.click( screen.getByLabelText( /items/i ) );
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
} );
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()
);
} );
} );