Make Mini Cart block react to removed_from_cart events (https://github.com/woocommerce/woocommerce-blocks/pull/4947)

* Make Mini Cart block react to removed_from_cart events

* Move listening to add to cart and remove from cart events to the useStoreCart hook

* Add tests
This commit is contained in:
Albert Juhé Lluveras 2021-10-25 18:05:01 +02:00 committed by GitHub
parent dc1f979ae1
commit 4253a5fe10
9 changed files with 214 additions and 68 deletions

View File

@ -0,0 +1,87 @@
/**
* External dependencies
*/
import { useEffect } from '@wordpress/element';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { dispatch } from '@wordpress/data';
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
interface StoreCartListenersType {
count: number;
remove: () => void;
}
declare global {
interface Window {
wcBlocksStoreCartListeners: StoreCartListenersType;
}
}
const refreshData = ( e ): void => {
const eventDetail = e.detail;
if ( ! eventDetail || ! eventDetail.preserveCartData ) {
dispatch( storeKey ).invalidateResolutionForStore();
}
};
const setUp = (): void => {
if ( ! window.wcBlocksStoreCartListeners ) {
window.wcBlocksStoreCartListeners = {
count: 0,
remove: () => void null,
};
}
};
const addListeners = (): void => {
setUp();
if ( ! window.wcBlocksStoreCartListeners.count ) {
const removeJQueryAddedToCartEvent = translateJQueryEventToNative(
'added_to_cart',
`wc-blocks_added_to_cart`
) as () => () => void;
const removeJQueryRemovedFromCartEvent = translateJQueryEventToNative(
'removed_from_cart',
`wc-blocks_removed_from_cart`
) as () => () => void;
document.body.addEventListener(
`wc-blocks_added_to_cart`,
refreshData
);
document.body.addEventListener(
`wc-blocks_removed_from_cart`,
refreshData
);
window.wcBlocksStoreCartListeners.count = 0;
window.wcBlocksStoreCartListeners.remove = () => {
removeJQueryAddedToCartEvent();
removeJQueryRemovedFromCartEvent();
document.body.removeEventListener(
`wc-blocks_added_to_cart`,
refreshData
);
document.body.removeEventListener(
`wc-blocks_removed_from_cart`,
refreshData
);
};
}
window.wcBlocksStoreCartListeners.count++;
};
const removeListeners = (): void => {
if ( window.wcBlocksStoreCartListeners.count === 1 ) {
window.wcBlocksStoreCartListeners.remove();
}
window.wcBlocksStoreCartListeners.count--;
};
export const useStoreCartEventListeners = (): void => {
useEffect( () => {
addListeners();
return removeListeners;
}, [] );
};

View File

@ -37,6 +37,7 @@ import {
* Internal dependencies
*/
import { useEditorContext } from '../../providers/editor-context';
import { useStoreCartEventListeners } from './use-store-cart-event-listeners';
declare module '@wordpress/html-entities' {
// eslint-disable-next-line @typescript-eslint/no-shadow
@ -137,6 +138,11 @@ export const useStoreCart = (
const previewCart = previewData?.previewCart;
const { shouldSelect } = options;
const currentResults = useRef();
// This will keep track of jQuery and DOM events triggered by other blocks
// or components and will invalidate the store resolution accordingly.
useStoreCartEventListeners();
const results: StoreCart = useSelect(
( select, { dispatch } ) => {
if ( ! shouldSelect ) {

View File

@ -2,8 +2,6 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { dispatch } from '@wordpress/data';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { useEffect } from '@wordpress/element';
import LoadingMask from '@woocommerce/base-components/loading-mask';
@ -48,44 +46,23 @@ const Cart = ( { children, attributes } ) => {
const ScrollOnError = ( { scrollToTop } ) => {
useEffect( () => {
const invalidateCartData = ( e ) => {
const eventDetail = e.detail;
if ( ! eventDetail || ! eventDetail.preserveCartData ) {
dispatch( storeKey ).invalidateResolutionForStore();
}
scrollToTop();
};
// Make it so we can read jQuery events triggered by WC Core elements.
const removeJQueryAddedToCartEvent = translateJQueryEventToNative(
'added_to_cart',
'wc-blocks_added_to_cart'
);
const removeJQueryRemovedFromCartEvent = translateJQueryEventToNative(
'removed_from_cart',
'wc-blocks_removed_from_cart'
);
document.body.addEventListener(
'wc-blocks_added_to_cart',
invalidateCartData
);
document.body.addEventListener(
'wc-blocks_removed_from_cart',
invalidateCartData
scrollToTop
);
return () => {
removeJQueryAddedToCartEvent();
removeJQueryRemovedFromCartEvent();
document.body.removeEventListener(
'wc-blocks_added_to_cart',
invalidateCartData
);
document.body.removeEventListener(
'wc-blocks_removed_from_cart',
invalidateCartData
scrollToTop
);
};
}, [ scrollToTop ] );

View File

@ -4,11 +4,9 @@
import classNames from 'classnames';
import { __, _n, sprintf } from '@wordpress/i18n';
import { useState, useEffect, useRef } from '@wordpress/element';
import { dispatch } from '@wordpress/data';
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import Drawer from '@woocommerce/base-components/drawer';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import {
formatPrice,
getCurrencyFromPriceResponse,
@ -22,11 +20,11 @@ import CartLineItemsTable from '../cart/cart-line-items-table';
import './style.scss';
interface MiniCartBlockProps {
isPlaceholderOpen?: boolean;
isInitiallyOpen?: boolean;
}
const MiniCartBlock = ( {
isPlaceholderOpen = false,
isInitiallyOpen = false,
}: MiniCartBlockProps ): JSX.Element => {
const {
cartItems,
@ -34,20 +32,16 @@ const MiniCartBlock = ( {
cartIsLoading,
cartTotals,
} = useStoreCart();
const [ isOpen, setIsOpen ] = useState< boolean >( isPlaceholderOpen );
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 >(
isPlaceholderOpen
isInitiallyOpen
);
useEffect( () => {
const openMiniCartAndRefreshData = ( e ) => {
const eventDetail = e.detail;
if ( ! eventDetail || ! eventDetail.preserveCartData ) {
dispatch( storeKey ).invalidateResolutionForStore();
}
const openMiniCart = () => {
setSkipSlideIn( false );
setIsOpen( true );
};
@ -60,7 +54,7 @@ const MiniCartBlock = ( {
document.body.addEventListener(
'wc-blocks_added_to_cart',
openMiniCartAndRefreshData
openMiniCart
);
return () => {
@ -68,7 +62,7 @@ const MiniCartBlock = ( {
document.body.removeEventListener(
'wc-blocks_added_to_cart',
openMiniCartAndRefreshData
openMiniCart
);
};
}, [] );

View File

@ -31,14 +31,14 @@ const renderMiniCartFrontend = () => {
Block: withMiniCartConditionalHydration( MiniCartBlock ),
getProps: ( el: HTMLElement ) => ( {
isDataOutdated: el.dataset.isDataOutdated,
isPlaceholderOpen: el.dataset.isPlaceholderOpen === 'true',
isInitiallyOpen: el.dataset.isInitiallyOpen === 'true',
} ),
} );
// Refocus previously focused button if drawer is not open.
if (
focusedMiniCartBlock instanceof HTMLElement &&
! focusedMiniCartBlock.dataset.isPlaceholderOpen
! focusedMiniCartBlock.dataset.isInitiallyOpen
) {
const innerButton = focusedMiniCartBlock.querySelector(
'.wc-block-mini-cart__button'

View File

@ -46,6 +46,10 @@ window.onload = () => {
'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.
@ -90,40 +94,62 @@ window.onload = () => {
return;
}
const showContents = () => {
const loadContents = () => {
if ( ! wasLoadScriptsCalled ) {
loadScripts();
}
document.body.removeEventListener(
'wc-blocks_added_to_cart',
// eslint-disable-next-line @typescript-eslint/no-use-before-define
showContentsAndUpdate
openDrawerWithRefresh
);
miniCartBlock.dataset.isPlaceholderOpen = 'true';
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'
);
removeJQueryAddedToCartEvent();
loadContents();
};
const showContentsAndUpdate = () => {
const openDrawerWithRefresh = () => {
miniCartBlock.dataset.isDataOutdated = 'true';
showContents();
openDrawer();
};
const loadContentsWithRefresh = () => {
miniCartBlock.dataset.isDataOutdated = 'true';
miniCartBlock.dataset.isInitiallyOpen = 'false';
loadContents();
};
miniCartButton.addEventListener( 'mouseover', loadScripts );
miniCartButton.addEventListener( 'focus', loadScripts );
miniCartButton.addEventListener( 'click', showContents );
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',
showContentsAndUpdate
openDrawerWithRefresh
);
document.body.addEventListener(
'wc-blocks_removed_from_cart',
loadContentsWithRefresh
);
}
} );

View File

@ -1,7 +1,14 @@
/**
* External dependencies
*/
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import {
act,
render,
screen,
fireEvent,
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';
@ -19,14 +26,30 @@ const MiniCartBlock = ( props ) => (
<Block { ...props } />
</SlotFillProvider>
);
const mockEmptyCart = () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/cart/ ) ) {
return Promise.resolve(
JSON.stringify( defaultCartState.cartData )
);
}
return Promise.resolve( '' );
} );
};
const mockFullCart = () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/cart/ ) ) {
return Promise.resolve( JSON.stringify( previewCart ) );
}
return Promise.resolve( '' );
} );
};
describe( 'Testing cart', () => {
beforeEach( async () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/cart/ ) ) {
return Promise.resolve( JSON.stringify( previewCart ) );
}
return Promise.resolve( '' );
} );
mockFullCart();
// need to clear the store resolution state between tests.
await dispatch( storeKey ).invalidateResolutionForStore();
await dispatch( storeKey ).receiveCart( defaultCartState.cartData );
@ -48,14 +71,7 @@ describe( 'Testing cart', () => {
} );
it( 'renders empty cart if there are no items in the cart', async () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/cart/ ) ) {
return Promise.resolve(
JSON.stringify( defaultCartState.cartData )
);
}
return Promise.resolve( '' );
} );
mockEmptyCart();
render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
@ -64,4 +80,43 @@ describe( 'Testing cart', () => {
expect( screen.getByText( /Cart is empty/i ) ).toBeInTheDocument();
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.queryByText( /3 items/i )
);
await waitFor( () =>
expect( screen.getByText( /0 items/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 removedFromCartEvent = new Event( 'wc-blocks_added_to_cart' );
act( () => {
document.body.dispatchEvent( removedFromCartEvent );
} );
await waitForElementToBeRemoved( () =>
screen.queryByText( /0 items/i )
);
await waitFor( () =>
expect( screen.getAllByText( /3 items/i ).length > 0 )
);
} );
} );

View File

@ -10,9 +10,10 @@ interface MiniCartBlockInterface {
// Signals whether the cart data is outdated. That happens when
// opening the mini cart after adding a product to the cart.
isDataOutdated?: boolean;
// Signals that the HTML placeholder drawer has been opened. Needed
// to know whether we have to skip the slide in animation.
isPlaceholderOpen?: boolean;
// Signals whether it should be open when the React component is loaded. For
// example, when adding a product to the cart, the Mini Cart should open
// when loaded, but when removing a product from the cart, it shouldn't.
isInitiallyOpen?: boolean;
}
// Custom HOC to conditionally hydrate API data depending on the isDataOutdated

View File

@ -18,7 +18,7 @@ _Example usage in WC Blocks:_ Mini Cart block listens to this event to append it
This event is the equivalent to the jQuery event `added_to_cart` triggered by WooCommerce core. It indicates that the process of adding a product to the cart has finished with success.
_Example usage in WC Blocks:_ Cart and Mini Cart blocks listen to this event to know if they need to update their contents.
_Example usage in WC Blocks:_ Cart and Mini Cart blocks (via the `useStoreCart()` hook) listen to this event to know if they need to update their contents.
#### `detail` parameters:
@ -30,5 +30,5 @@ _Example usage in WC Blocks:_ Cart and Mini Cart blocks listen to this event to
This event is the equivalent to the jQuery event `removed_from_cart` triggered by WooCommerce core. It indicates that a product has been removed from the cart.
_Example usage in WC Blocks:_ Cart and Mini Cart blocks listen to this event to know if they need to update their contents.
_Example usage in WC Blocks:_ Cart and Mini Cart blocks (via the `useStoreCart()` hook) listen to this event to know if they need to update their contents.