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:
parent
dc1f979ae1
commit
4253a5fe10
|
@ -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;
|
||||
}, [] );
|
||||
};
|
|
@ -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 ) {
|
||||
|
|
|
@ -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 ] );
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}, [] );
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
|
|
@ -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 )
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue