Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit 4c2272e

Browse files
authored
Make Mini-Cart block work well with caching plugins (#9493)
* Make Mini-Cart block work well with caching plugins * Add tests * Add back aria-label to Mini-Cart menu * Fetch Mini-Cart data before page finishes loading * Store and retrieve Mini-Cart values from localStorage for better performance * Update styles as early as possible * Reorder code * Remove overrideTotals param from updateTotals() function * Update tests * Initialize local storage inside a function and add act to filter tests * Replace void with undefined types in several funtions * Fix 0 quantity badge appearing on page load
1 parent b82f742 commit 4c2272e

File tree

11 files changed

+392
-224
lines changed

11 files changed

+392
-224
lines changed

assets/js/blocks/mini-cart/frontend.ts

Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import lazyLoadScript from '@woocommerce/base-utils/lazy-load-script';
66
import getNavigationType from '@woocommerce/base-utils/get-navigation-type';
77
import { translateJQueryEventToNative } from '@woocommerce/base-utils/legacy-events';
88

9+
/**
10+
* Internal dependencies
11+
*/
12+
import {
13+
getMiniCartTotalsFromLocalStorage,
14+
getMiniCartTotalsFromServer,
15+
updateTotals,
16+
} from './utils/data';
17+
import setStyles from './utils/set-styles';
18+
919
interface dependencyData {
1020
src: string;
1121
version?: string;
@@ -14,19 +24,9 @@ interface dependencyData {
1424
translations?: string;
1525
}
1626

17-
function getClosestColor(
18-
element: Element | null,
19-
colorType: 'color' | 'backgroundColor'
20-
): string | null {
21-
if ( ! element ) {
22-
return null;
23-
}
24-
const color = window.getComputedStyle( element )[ colorType ];
25-
if ( color !== 'rgba(0, 0, 0, 0)' && color !== 'transparent' ) {
26-
return color;
27-
}
28-
return getClosestColor( element.parentElement, colorType );
29-
}
27+
updateTotals( getMiniCartTotalsFromLocalStorage() );
28+
getMiniCartTotalsFromServer().then( updateTotals );
29+
setStyles();
3030

3131
window.addEventListener( 'load', () => {
3232
const miniCartBlocks = document.querySelectorAll( '.wc-block-mini-cart' );
@@ -182,40 +182,4 @@ window.addEventListener( 'load', () => {
182182
);
183183
}
184184
} );
185-
186-
/**
187-
* Get the background color of the body then set it as the background color
188-
* of the Mini-Cart Contents block.
189-
*
190-
* We only set the background color, instead of the whole background. As
191-
* we only provide the option to customize the background color.
192-
*/
193-
const style = document.createElement( 'style' );
194-
const backgroundColor = getComputedStyle( document.body ).backgroundColor;
195-
// For simplicity, we only consider the background color of the first Mini-Cart button.
196-
const firstMiniCartButton = document.querySelector(
197-
'.wc-block-mini-cart__button'
198-
);
199-
const badgeTextColor = firstMiniCartButton
200-
? getClosestColor( firstMiniCartButton, 'backgroundColor' )
201-
: 'inherit';
202-
const badgeBackgroundColor = firstMiniCartButton
203-
? getClosestColor( firstMiniCartButton, 'color' )
204-
: 'inherit';
205-
206-
// We use :where here to reduce specificity so customized colors and theme
207-
// CSS take priority.
208-
style.appendChild(
209-
document.createTextNode(
210-
`:where(.wp-block-woocommerce-mini-cart-contents) {
211-
background-color: ${ backgroundColor };
212-
}
213-
:where(.wc-block-mini-cart__badge) {
214-
background-color: ${ badgeBackgroundColor };
215-
color: ${ badgeTextColor };
216-
}`
217-
)
218-
);
219-
220-
document.head.appendChild( style );
221185
} );

assets/js/blocks/mini-cart/quantity-badge/style.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
padding: 0 em($gap-smallest);
2020
position: absolute;
2121
transform: translateY(-50%);
22-
transition: all 0.15s;
2322
white-space: nowrap;
2423
z-index: 1;
2524
}

assets/js/blocks/mini-cart/style.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
}
2525

2626
.wc-block-mini-cart__amount {
27-
display: none;
27+
margin-right: 0.5em;
2828
}
2929

3030
.wc-block-mini-cart--preview {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { _n, sprintf } from '@wordpress/i18n';
5+
import {
6+
getCurrencyFromPriceResponse,
7+
formatPrice,
8+
} from '@woocommerce/price-format';
9+
import { CartResponse } from '@woocommerce/types';
10+
11+
export const updateTotals = ( totals: [ string, number ] | undefined ) => {
12+
if ( ! totals ) {
13+
return;
14+
}
15+
const [ amount, quantity ] = totals;
16+
const miniCartButtons = document.querySelectorAll(
17+
'.wc-block-mini-cart__button'
18+
);
19+
const miniCartQuantities = document.querySelectorAll(
20+
'.wc-block-mini-cart__badge'
21+
);
22+
const miniCartAmounts = document.querySelectorAll(
23+
'.wc-block-mini-cart__amount'
24+
);
25+
26+
miniCartButtons.forEach( ( miniCartButton ) => {
27+
miniCartButton.setAttribute(
28+
'aria-label',
29+
sprintf(
30+
/* translators: %s number of products in cart. */
31+
_n(
32+
'%1$d item in cart, total price of %2$s',
33+
'%1$d items in cart, total price of %2$s',
34+
quantity,
35+
'woo-gutenberg-products-block'
36+
),
37+
quantity,
38+
amount
39+
)
40+
);
41+
} );
42+
miniCartQuantities.forEach( ( miniCartQuantity ) => {
43+
if ( quantity > 0 || miniCartQuantity.textContent !== '' ) {
44+
miniCartQuantity.textContent = quantity.toString();
45+
}
46+
} );
47+
miniCartAmounts.forEach( ( miniCartAmount ) => {
48+
miniCartAmount.textContent = amount;
49+
} );
50+
51+
// Show the tax label only if there are products in the cart.
52+
if ( quantity > 0 ) {
53+
const miniCartTaxLabels = document.querySelectorAll(
54+
'.wc-block-mini-cart__tax-label'
55+
);
56+
miniCartTaxLabels.forEach( ( miniCartTaxLabel ) => {
57+
miniCartTaxLabel.removeAttribute( 'hidden' );
58+
} );
59+
}
60+
};
61+
62+
export const getMiniCartTotalsFromLocalStorage = ():
63+
| [ string, number ]
64+
| undefined => {
65+
const rawMiniCartTotals = localStorage.getItem(
66+
'wc-blocks_mini_cart_totals'
67+
);
68+
if ( ! rawMiniCartTotals ) {
69+
return undefined;
70+
}
71+
const miniCartTotals = JSON.parse( rawMiniCartTotals );
72+
const currency = getCurrencyFromPriceResponse( miniCartTotals.totals );
73+
const formattedPrice = formatPrice(
74+
miniCartTotals.totals.total_price,
75+
currency
76+
);
77+
return [ formattedPrice, miniCartTotals.itemsCount ] as [ string, number ];
78+
};
79+
80+
export const getMiniCartTotalsFromServer = async (): Promise<
81+
[ string, number ] | undefined
82+
> => {
83+
return fetch( '/wp-json/wc/store/v1/cart/' )
84+
.then( ( response ) => {
85+
// Check if the response was successful.
86+
if ( ! response.ok ) {
87+
throw new Error();
88+
}
89+
90+
return response.json();
91+
} )
92+
.then( ( data: CartResponse ) => {
93+
const currency = getCurrencyFromPriceResponse( data.totals );
94+
const formattedPrice = formatPrice(
95+
data.totals.total_price,
96+
currency
97+
);
98+
// Save server data to local storage, so we can re-fetch it faster
99+
// on the next page load.
100+
localStorage.setItem(
101+
'wc-blocks_mini_cart_totals',
102+
JSON.stringify( {
103+
totals: data.totals,
104+
itemsCount: data.items_count,
105+
} )
106+
);
107+
return [ formattedPrice, data.items_count ] as [ string, number ];
108+
} )
109+
.catch( ( error ) => {
110+
// eslint-disable-next-line no-console
111+
console.error( error );
112+
return undefined;
113+
} );
114+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
function getClosestColor(
2+
element: Element | null,
3+
colorType: 'color' | 'backgroundColor'
4+
): string | null {
5+
if ( ! element ) {
6+
return null;
7+
}
8+
const color = window.getComputedStyle( element )[ colorType ];
9+
if ( color !== 'rgba(0, 0, 0, 0)' && color !== 'transparent' ) {
10+
return color;
11+
}
12+
return getClosestColor( element.parentElement, colorType );
13+
}
14+
15+
function setStyles() {
16+
/**
17+
* Get the background color of the body then set it as the background color
18+
* of the Mini-Cart Contents block.
19+
*
20+
* We only set the background color, instead of the whole background. As
21+
* we only provide the option to customize the background color.
22+
*/
23+
const style = document.createElement( 'style' );
24+
const backgroundColor = getComputedStyle( document.body ).backgroundColor;
25+
// For simplicity, we only consider the background color of the first Mini-Cart button.
26+
const firstMiniCartButton = document.querySelector(
27+
'.wc-block-mini-cart__button'
28+
);
29+
const badgeTextColor = firstMiniCartButton
30+
? getClosestColor( firstMiniCartButton, 'backgroundColor' )
31+
: 'inherit';
32+
const badgeBackgroundColor = firstMiniCartButton
33+
? getClosestColor( firstMiniCartButton, 'color' )
34+
: 'inherit';
35+
36+
// We use :where here to reduce specificity so customized colors and theme
37+
// CSS take priority.
38+
style.appendChild(
39+
document.createTextNode(
40+
`:where(.wp-block-woocommerce-mini-cart-contents) {
41+
background-color: ${ backgroundColor };
42+
}
43+
:where(.wc-block-mini-cart__badge) {
44+
background-color: ${ badgeBackgroundColor };
45+
color: ${ badgeTextColor };
46+
}`
47+
)
48+
);
49+
50+
document.head.appendChild( style );
51+
}
52+
53+
export default setStyles;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// eslint-disable testing-library/no-dom-import
2+
/**
3+
* External dependencies
4+
*/
5+
import { getByTestId, waitFor } from '@testing-library/dom';
6+
7+
/**
8+
* Internal dependencies
9+
*/
10+
import {
11+
getMiniCartTotalsFromLocalStorage,
12+
getMiniCartTotalsFromServer,
13+
updateTotals,
14+
} from '../data';
15+
16+
// This is a simplified version of the response of the Cart API endpoint.
17+
const responseMock = {
18+
ok: true,
19+
json: async () => ( {
20+
totals: {
21+
total_price: '1600',
22+
currency_code: 'USD',
23+
currency_symbol: '$',
24+
currency_minor_unit: 2,
25+
currency_decimal_separator: '.',
26+
currency_thousand_separator: ',',
27+
currency_prefix: '$',
28+
currency_suffix: '',
29+
},
30+
items_count: 2,
31+
} ),
32+
} as Response;
33+
const localStorageMock = {
34+
totals: {
35+
total_price: '1600',
36+
currency_code: 'USD',
37+
currency_symbol: '$',
38+
currency_minor_unit: 2,
39+
currency_decimal_separator: '.',
40+
currency_thousand_separator: ',',
41+
currency_prefix: '$',
42+
currency_suffix: '',
43+
},
44+
itemsCount: 2,
45+
};
46+
47+
const initializeLocalStorage = () => {
48+
Object.defineProperty( window, 'localStorage', {
49+
value: {
50+
getItem: jest
51+
.fn()
52+
.mockReturnValue( JSON.stringify( localStorageMock ) ),
53+
setItem: jest.fn(),
54+
},
55+
writable: true,
56+
} );
57+
};
58+
59+
// This is a simplified version of the Mini-Cart DOM generated by MiniCart.php.
60+
const getMiniCartDOM = () => {
61+
const div = document.createElement( 'div' );
62+
div.innerHTML = `
63+
<div class="wc-block-mini-cart">
64+
<div class="wc-block-mini-cart__amount" data-testid="amount"></div>
65+
<div class="wc-block-mini-cart__badge" data-testid="quantity"></div>
66+
</div>`;
67+
return div;
68+
};
69+
70+
describe( 'Mini-Cart frontend script', () => {
71+
it( 'updates the cart contents based on the localStorage values', async () => {
72+
initializeLocalStorage();
73+
const container = getMiniCartDOM();
74+
document.body.appendChild( container );
75+
76+
updateTotals( getMiniCartTotalsFromLocalStorage() );
77+
78+
// Assert that we are rendering the amount.
79+
await waitFor( () =>
80+
expect( getByTestId( container, 'amount' ).textContent ).toBe(
81+
'$16.00'
82+
)
83+
);
84+
// Assert that we are rendering the quantity.
85+
await waitFor( () =>
86+
expect( getByTestId( container, 'quantity' ).textContent ).toBe(
87+
'2'
88+
)
89+
);
90+
} );
91+
92+
it( 'updates the cart contents based on the API response', async () => {
93+
jest.spyOn( window, 'fetch' ).mockResolvedValue( responseMock );
94+
const container = getMiniCartDOM();
95+
document.body.appendChild( container );
96+
97+
getMiniCartTotalsFromServer().then( updateTotals );
98+
99+
// Assert we called the correct endpoint.
100+
await waitFor( () =>
101+
expect( window.fetch ).toHaveBeenCalledWith(
102+
'/wp-json/wc/store/v1/cart/'
103+
)
104+
);
105+
106+
// Assert we saved the values returned to the localStorage.
107+
await waitFor( () =>
108+
expect( window.localStorage.setItem.mock.calls[ 0 ][ 1 ] ).toEqual(
109+
JSON.stringify( localStorageMock )
110+
)
111+
);
112+
113+
// Assert that we are rendering the amount.
114+
await waitFor( () =>
115+
expect( getByTestId( container, 'amount' ).textContent ).toBe(
116+
'$16.00'
117+
)
118+
);
119+
// Assert that we are rendering the quantity.
120+
await waitFor( () =>
121+
expect( getByTestId( container, 'quantity' ).textContent ).toBe(
122+
'2'
123+
)
124+
);
125+
jest.restoreAllMocks();
126+
} );
127+
} );

0 commit comments

Comments
 (0)