From 1541be6fdb17f330574f0e65d1ff11c9cbfd422b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 19 Nov 2021 12:47:48 +0100 Subject: [PATCH] Mini Cart as template part (#5025) * Fix wrong event prefix in doc comment * Make className prop in CartLineItemsTableProps optional * Mini Cart as template part * Remove BlockTemplatePartsController and instead use BlockTemplatesController * Remove old code * Clean up frontend rendering * Update tests * Improve if clause * Fix wrong tests title * Fix wrong variable name * Make sure Mini Cart contents block is unmounted whem mini cart closes or unmounts * Remove unnecessary waitFor * Fix PaymentMethodDataProvider wrong children type * TypeScript fixes * Make comment shorter * Remove test code * Fix contant unmounts of Mini Cart contents block * Fix wrong template_type passed * Set Template part area to 'uncategorized' * Set Template part area to the correct value * Move template dir check outside loop --- .../cart/use-store-cart-event-listeners.ts | 2 +- .../payment-method-data-context.tsx | 2 +- assets/js/base/utils/render-frontend.js | 48 ++++-- .../cart/cart-line-items-table/index.tsx | 2 +- .../mini-cart-contents/block.tsx | 104 ++++++++++++ .../cart-checkout/mini-cart-contents/edit.tsx | 17 ++ .../mini-cart-contents/index.tsx | 62 ++++++++ .../blocks/cart-checkout/mini-cart/block.tsx | 148 ++++++------------ .../mini-cart/component-frontend.tsx | 3 + .../cart-checkout/mini-cart/test/block.js | 23 ++- bin/webpack-entries.js | 4 + src/BlockTemplatesController.php | 81 +++++++--- src/BlockTypes/MiniCart.php | 85 ++-------- src/BlockTypes/MiniCartContents.php | 68 ++++++++ src/BlockTypesController.php | 1 + src/Utils/BlockTemplateUtils.php | 13 ++ templates/block-template-parts/mini-cart.html | 1 + 17 files changed, 456 insertions(+), 208 deletions(-) create mode 100644 assets/js/blocks/cart-checkout/mini-cart-contents/block.tsx create mode 100644 assets/js/blocks/cart-checkout/mini-cart-contents/edit.tsx create mode 100644 assets/js/blocks/cart-checkout/mini-cart-contents/index.tsx create mode 100644 src/BlockTypes/MiniCartContents.php create mode 100644 templates/block-template-parts/mini-cart.html diff --git a/assets/js/base/context/hooks/cart/use-store-cart-event-listeners.ts b/assets/js/base/context/hooks/cart/use-store-cart-event-listeners.ts index 51896c843f5..a092913fad7 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart-event-listeners.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart-event-listeners.ts @@ -36,7 +36,7 @@ const setUp = (): void => { const addListeners = (): void => { setUp(); - if ( ! window.wcBlocksStoreCartListeners.count ) { + if ( window.wcBlocksStoreCartListeners.count === 0 ) { const removeJQueryAddedToCartEvent = translateJQueryEventToNative( 'added_to_cart', `wc-blocks_added_to_cart` diff --git a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx index 919185a2a66..c8c6da01254 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx +++ b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx @@ -60,7 +60,7 @@ export const usePaymentMethodDataContext = (): PaymentMethodDataContextType => { export const PaymentMethodDataProvider = ( { children, }: { - children: React.ReactChildren; + children: React.ReactNode; } ): JSX.Element => { const { isProcessing: checkoutIsProcessing, diff --git a/assets/js/base/utils/render-frontend.js b/assets/js/base/utils/render-frontend.js index 1e75c68262f..7063352357e 100644 --- a/assets/js/base/utils/render-frontend.js +++ b/assets/js/base/utils/render-frontend.js @@ -53,17 +53,47 @@ const renderBlockInContainers = ( { }; el.classList.remove( 'is-loading' ); - render( - - }> - - - , - el - ); + renderBlock( { + Block, + container: el, + props, + attributes, + errorBoundaryProps, + } ); } ); }; +/** + * Renders a block component in a single `container` node. + * + * @param {Object} props Render props. + * @param {Function} props.Block React component to use as a + * replacement. + * @param {Node} props.container Container to replace with + * the Block component. + * @param {Object} [props.attributes] Attributes object for the + * block. + * @param {Object} [props.props] Props object for the block. + * @param {Object} [props.errorBoundaryProps] Props object for the error + * boundary. + */ +export const renderBlock = ( { + Block, + container, + attributes = {}, + props = {}, + errorBoundaryProps = {}, +} ) => { + render( + + }> + + + , + container + ); +}; + /** * Renders the block frontend in the elements matched by the selector which are * outside the wrapper elements. @@ -141,7 +171,7 @@ const renderBlockInsideWrapper = ( { * Renders the block frontend on page load. If the block is contained inside a * wrapper element that should be excluded from initial load, it adds the * appropriate event listeners to render the block when the - * `blocks_render_blocks_frontend` event is triggered. + * `wc-blocks_render_blocks_frontend` event is triggered. * * @param {Object} props Render props. * @param {Function} props.Block React component to use as a diff --git a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/index.tsx b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/index.tsx index 986d36c3748..e9a22366255 100644 --- a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/index.tsx +++ b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/index.tsx @@ -19,7 +19,7 @@ const placeholderRows = [ ...Array( 3 ) ].map( ( _x, i ) => ( interface CartLineItemsTableProps { lineItems: CartResponseItem[]; isLoading: boolean; - className: string; + className?: string; } const setRefs = ( lineItems: CartResponseItem[] ) => { diff --git a/assets/js/blocks/cart-checkout/mini-cart-contents/block.tsx b/assets/js/blocks/cart-checkout/mini-cart-contents/block.tsx new file mode 100644 index 00000000000..76b161733c7 --- /dev/null +++ b/assets/js/blocks/cart-checkout/mini-cart-contents/block.tsx @@ -0,0 +1,104 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef } from '@wordpress/element'; +import { + usePaymentMethods, + useStoreCart, +} from '@woocommerce/base-context/hooks'; +import { TotalsItem } from '@woocommerce/blocks-checkout'; +import { CART_URL, CHECKOUT_URL } from '@woocommerce/block-settings'; +import Button from '@woocommerce/base-components/button'; +import { PaymentMethodDataProvider } from '@woocommerce/base-context'; +import { getIconsFromPaymentMethods } from '@woocommerce/base-utils'; +import PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons'; +import { getCurrencyFromPriceResponse } from '@woocommerce/price-format'; +import { getSetting } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import CartLineItemsTable from '../cart/cart-line-items-table'; + +const PaymentMethodIconsElement = (): JSX.Element => { + const { paymentMethods } = usePaymentMethods(); + return ( + + ); +}; + +const MiniCartContentsBlock = (): JSX.Element => { + const { cartItems, cartIsLoading, cartTotals } = useStoreCart(); + const emptyCartRef = useRef< HTMLDivElement | null >( null ); + + const subTotal = getSetting( 'displayCartPricesIncludingTax', false ) + ? parseInt( cartTotals.total_items, 10 ) + + parseInt( cartTotals.total_items_tax, 10 ) + : parseInt( cartTotals.total_items, 10 ); + + useEffect( () => { + // If the cart has been completely emptied, move focus to empty cart + // element. + if ( ! cartIsLoading && cartItems.length === 0 ) { + if ( emptyCartRef.current instanceof HTMLElement ) { + emptyCartRef.current.focus(); + } + } + }, [ cartIsLoading, cartItems.length, emptyCartRef ] ); + + return ! cartIsLoading && cartItems.length === 0 ? ( +
+ { __( 'Cart is empty', 'woo-gutenberg-products-block' ) } +
+ ) : ( + <> +
+ +
+
+ +
+ + +
+ + + +
+ + ); +}; + +export default MiniCartContentsBlock; diff --git a/assets/js/blocks/cart-checkout/mini-cart-contents/edit.tsx b/assets/js/blocks/cart-checkout/mini-cart-contents/edit.tsx new file mode 100644 index 00000000000..68ee465cc79 --- /dev/null +++ b/assets/js/blocks/cart-checkout/mini-cart-contents/edit.tsx @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import type { ReactElement } from 'react'; +import { useBlockProps } from '@wordpress/block-editor'; + +const Edit = (): ReactElement => { + const blockProps = useBlockProps(); + + return ( +
+

Editing the mini cart contents

+
+ ); +}; + +export default Edit; diff --git a/assets/js/blocks/cart-checkout/mini-cart-contents/index.tsx b/assets/js/blocks/cart-checkout/mini-cart-contents/index.tsx new file mode 100644 index 00000000000..b35ec0a3b5b --- /dev/null +++ b/assets/js/blocks/cart-checkout/mini-cart-contents/index.tsx @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Icon, cart } from '@woocommerce/icons'; +import { registerExperimentalBlockType } from '@woocommerce/block-settings'; + +/** + * Internal dependencies + */ +import edit from './edit'; + +const settings = { + apiVersion: 2, + title: __( 'Mini Cart Contents', 'woo-gutenberg-products-block' ), + icon: { + src: , + foreground: '#7f54b3', + }, + 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, + }, + attributes: { + lock: { + type: 'object', + default: { + remove: true, + move: true, + }, + }, + }, + example: { + attributes: { + isPreview: true, + }, + }, + attributes: { + isPreview: { + type: 'boolean', + default: false, + save: false, + }, + }, + + edit, + + save() { + return null; + }, +}; + +registerExperimentalBlockType( 'woocommerce/mini-cart-contents', settings ); diff --git a/assets/js/blocks/cart-checkout/mini-cart/block.tsx b/assets/js/blocks/cart-checkout/mini-cart/block.tsx index 31086d5eba8..d181ee3c363 100644 --- a/assets/js/blocks/cart-checkout/mini-cart/block.tsx +++ b/assets/js/blocks/cart-checkout/mini-cart/block.tsx @@ -3,69 +3,89 @@ */ import classnames from 'classnames'; import { __, _n, sprintf } from '@wordpress/i18n'; -import { useState, useEffect, useRef } from '@wordpress/element'; import { + RawHTML, + useState, + useEffect, + useRef, + unmountComponentAtNode, +} from '@wordpress/element'; +import { + renderBlock, translateJQueryEventToNative, - getIconsFromPaymentMethods, } from '@woocommerce/base-utils'; -import { - useStoreCart, - usePaymentMethods, -} from '@woocommerce/base-context/hooks'; +import { useStoreCart } from '@woocommerce/base-context/hooks'; import Drawer from '@woocommerce/base-components/drawer'; import { formatPrice, getCurrencyFromPriceResponse, } from '@woocommerce/price-format'; import { getSetting } from '@woocommerce/settings'; -import { TotalsItem } from '@woocommerce/blocks-checkout'; -import PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons'; -import { CART_URL, CHECKOUT_URL } from '@woocommerce/block-settings'; -import Button from '@woocommerce/base-components/button'; -import { PaymentMethodDataProvider } from '@woocommerce/base-context'; /** * Internal dependencies */ -import CartLineItemsTable from '../cart/cart-line-items-table'; import QuantityBadge from './quantity-badge'; +import MiniCartContentsBlock from '../mini-cart-contents/block'; import './style.scss'; -const PaymentMethodIconsElement = (): JSX.Element => { - const { paymentMethods } = usePaymentMethods(); - return ( - - ); -}; - interface Props { isInitiallyOpen?: boolean; transparentButton: boolean; colorClassNames?: string; style?: Record< string, Record< string, string > >; + contents: string; } const MiniCartBlock = ( { isInitiallyOpen = false, colorClassNames, style, + contents = '', }: Props ): JSX.Element => { - const { - cartItems, - cartItemsCount, - cartIsLoading, - cartTotals, - } = useStoreCart(); + const { cartItemsCount, cartIsLoading, cartTotals } = useStoreCart(); const [ isOpen, setIsOpen ] = useState< boolean >( isInitiallyOpen ); - const emptyCartRef = useRef< HTMLDivElement | null >( null ); // We already rendered the HTML drawer placeholder, so we want to skip the // slide in animation. const [ skipSlideIn, setSkipSlideIn ] = useState< boolean >( isInitiallyOpen ); + const contentsRef = useRef() as React.MutableRefObject< HTMLDivElement >; + + useEffect( () => { + if ( contentsRef.current instanceof Element ) { + const container = contentsRef.current.querySelector( + '.wc-block-mini-cart-contents' + ); + if ( ! container ) { + return; + } + if ( isOpen ) { + renderBlock( { + Block: MiniCartContentsBlock, + container, + } ); + } else { + unmountComponentAtNode( container ); + } + } + }, [ isOpen ] ); + + useEffect( () => { + return () => { + const contentsNode = contentsRef.current as unknown; + if ( contentsNode instanceof Element ) { + const container = contentsNode.querySelector( + '.wc-block-mini-cart-contents' + ); + if ( container ) { + unmountComponentAtNode( container ); + } + } + }; + }, [] ); + useEffect( () => { const openMiniCart = () => { setSkipSlideIn( false ); @@ -93,16 +113,6 @@ const MiniCartBlock = ( { }; }, [] ); - useEffect( () => { - // If the cart has been completely emptied, move focus to empty cart - // element. - if ( isOpen && ! cartIsLoading && cartItems.length === 0 ) { - if ( emptyCartRef.current instanceof HTMLElement ) { - emptyCartRef.current.focus(); - } - } - }, [ isOpen, cartIsLoading, cartItems.length, emptyCartRef ] ); - const subTotal = getSetting( 'displayCartPricesIncludingTax', false ) ? parseInt( cartTotals.total_items, 10 ) + parseInt( cartTotals.total_items_tax, 10 ) @@ -125,64 +135,6 @@ const MiniCartBlock = ( { color: style?.color?.text, }; - const contents = - ! cartIsLoading && cartItems.length === 0 ? ( -
- { __( 'Cart is empty', 'woo-gutenberg-products-block' ) } -
- ) : ( - <> -
- -
-
- -
- - -
- - - -
- - ); - return ( <> ' - . $this->get_cart_contents_markup( $cart_contents ) . - ' + +
' + . $template_part_contents . + '
+ '; } - - /** - * Render the markup of the Cart contents. - * - * @param array $cart_contents Array of contents in the cart. - * - * @return string The HTML markup. - */ - protected function get_cart_contents_markup( $cart_contents ) { - // Force mobile styles. - return ' - - - - - - - - ' . implode( array_map( array( $this, 'get_cart_item_markup' ), $cart_contents ) ) . ' -
'; - } - - /** - * Render the skeleton of a Cart item. - * - * @return string The skeleton HTML markup. - */ - protected function get_cart_item_markup() { - return ' - - - - -
-
- -
-
- - - -
- -
- - -
-
-
- - '; - } - - /** - * Get the supports array for this block type. - * - * @see $this->register_block_type() - * @return string; - */ - protected function get_block_type_supports() { - return array_merge( - parent::get_block_type_supports(), - array( - 'html' => false, - 'multiple' => false, - 'color' => true, - '__experimentalSelector' => '.wc-block-mini-cart__button, .wc-block-mini-cart__badge', - ) - ); - } } diff --git a/src/BlockTypes/MiniCartContents.php b/src/BlockTypes/MiniCartContents.php new file mode 100644 index 00000000000..c5eeec0c0cb --- /dev/null +++ b/src/BlockTypes/MiniCartContents.php @@ -0,0 +1,68 @@ + 'wc-' . $this->block_name . '-block', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), + 'dependencies' => [ 'wc-blocks' ], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Get the frontend script handle for this block type. + * + * @param string $key Data to get, or default to everything. + * + * @return null + */ + protected function get_block_type_script( $key = null ) { + // The frontend script is a dependency of the Mini Cart block so it's + // already lazy-loaded. + return null; + } + + /** + * Render the markup for the Mini Cart contents block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + if ( is_admin() || WC()->is_rest_api_request() ) { + // In the editor we will display the placeholder, so no need to + // print the markup. + return ''; + } + + return '
'; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index d36c9d4cb4c..b742aa2d94e 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -185,6 +185,7 @@ protected function get_block_types() { if ( Package::feature()->is_experimental_build() ) { $block_types[] = 'SingleProduct'; $block_types[] = 'MiniCart'; + $block_types[] = 'MiniCartContents'; } /** diff --git a/src/Utils/BlockTemplateUtils.php b/src/Utils/BlockTemplateUtils.php index 23c090a3b3f..8cc24edcbff 100644 --- a/src/Utils/BlockTemplateUtils.php +++ b/src/Utils/BlockTemplateUtils.php @@ -109,6 +109,12 @@ public static function gutenberg_build_template_result_from_post( $post ) { $template->has_theme_file = $has_theme_file; $template->is_custom = true; $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown. + if ( 'wp_template_part' === $post->post_type ) { + $type_terms = get_the_terms( $post, 'wp_template_part_area' ); + if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) { + $template->area = $type_terms[0]->name; + } + } return $template; } @@ -137,6 +143,13 @@ public static function gutenberg_build_template_result_from_file( $template_file $template->has_theme_file = true; $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are. $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown. + if ( 'wp_template_part' === $template_type ) { + if ( 'mini-cart' === $template_file->slug ) { + $template->area = 'mini-cart'; + } else { + $template->area = 'uncategorized'; + } + } return $template; } diff --git a/templates/block-template-parts/mini-cart.html b/templates/block-template-parts/mini-cart.html new file mode 100644 index 00000000000..6ccda3e7236 --- /dev/null +++ b/templates/block-template-parts/mini-cart.html @@ -0,0 +1 @@ +