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

Interactivity API and Product Button #10006

Merged
merged 83 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
1c582ed
Update Interactivity API JS files
DAreRodz Jun 19, 2023
cd0af51
Disable TS checks in the Interactivity API for now
DAreRodz Jun 19, 2023
5528a2f
Add new SSR files
DAreRodz Jun 19, 2023
e6d5a25
Replace wp_ prefixes with wc_ ones
DAreRodz Jun 19, 2023
68fb185
Replace wp- prefix with wc-
DAreRodz Jun 19, 2023
bf443b8
Replace guternberg_ prefix with woocommerce_
DAreRodz Jun 19, 2023
2e070ff
Remove file comments from Gutenberg
DAreRodz Jun 19, 2023
ada935d
Rename files with `wp` prefix
DAreRodz Jun 19, 2023
9185199
Fix code to load Interactivity API php files
DAreRodz Jun 19, 2023
fd08faa
Remove TODO comments
DAreRodz Jun 19, 2023
901151b
Replace @wordpress with @woocommerce
DAreRodz Jun 19, 2023
4f653f5
Update Webpack configuration
DAreRodz Jun 19, 2023
8920120
Fix directive prefix
DAreRodz Jun 19, 2023
9d14a7e
Remove interactivity folder from tsconfig exclude
DAreRodz Jun 19, 2023
872e827
Add client-side navigation meta tag code
DAreRodz Jun 19, 2023
ad7d14a
Remove unneeded blocks.php file
DAreRodz Jun 19, 2023
b7488bb
Fix store tag id
DAreRodz Jun 20, 2023
7ae8b81
Register Interactivity API runtime script
DAreRodz Jun 20, 2023
b27eedf
Fix Interactivity API runtime registering
DAreRodz Jun 20, 2023
f08b670
Remove all files related to directive processing in PHP
DAreRodz Jun 21, 2023
9a45fff
Merge branch 'trunk' into update/interactivity-api
DAreRodz Jun 22, 2023
e0a3e8d
Move json_encode to Store's render method
DAreRodz Jun 22, 2023
5b05ec8
Merge branch 'update/interactivity-api' of https://github.com/woocomm…
gigitux Jun 22, 2023
81dcfbb
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jun 26, 2023
a8d5017
WIP
gigitux Jun 26, 2023
5852f96
WIP
gigitux Jun 27, 2023
7f7356a
WIP
gigitux Jun 27, 2023
fa3f985
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jun 27, 2023
2dfa29c
WIP
gigitux Jun 29, 2023
4d579a0
Preserve previous context
luisherranz Jun 29, 2023
b7559bf
Ignore Minicart block on client-side navigation
luisherranz Jun 29, 2023
d1d0031
Refresh page on store updatRefresh page on store updatee
luisherranz Jun 29, 2023
02a5c74
Refactor logic
luisherranz Jun 29, 2023
b0cd3ae
Add console error when a path is missing
luisherranz Jun 29, 2023
97f7c4f
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jun 30, 2023
c1e172f
Merge branch 'trunk' into add/6949-interactivity-api-product-button
gigitux Jul 3, 2023
974c1e0
Merge branch 'add/6949-interactivity-api-product-button' of https://g…
gigitux Jul 3, 2023
5b6a169
fix PHP lint error
gigitux Jul 3, 2023
a136b65
Merge branch 'add/6949-interactivity-api-product-button' of https://g…
gigitux Jul 3, 2023
4b3201a
WIP store
gigitux Jul 4, 2023
1ff2802
use store approach
gigitux Jul 4, 2023
8119563
Merge branch 'add/6949-interactivity-api-product-button-store' of htt…
gigitux Jul 4, 2023
feb494c
Merge branch 'trunk' into add/6949-interactivity-api-product-button
gigitux Jul 4, 2023
1b6268c
update jest configuration
gigitux Jul 4, 2023
e18c169
Merge branch 'add/6949-interactivity-api-product-button' of https://g…
gigitux Jul 4, 2023
cffc0a2
restore Mini Cart changes
gigitux Jul 4, 2023
6123ec5
move cart store subscription to interactivity package
gigitux Jul 5, 2023
9fe03ee
Merge branch 'trunk' into add/6949-interactivity-api-product-button
gigitux Jul 5, 2023
db778c0
move interactivity flag
gigitux Jul 5, 2023
ee6cdb9
Merge branch 'add/6949-interactivity-api-product-button' of https://g…
gigitux Jul 5, 2023
2191c0f
format HTML
gigitux Jul 5, 2023
f83cce3
move addToCartText to the context
gigitux Jul 6, 2023
c790213
Load product-query stylesheet when rendering the Products block
Aljullu Jul 6, 2023
95d71a3
Merge branch 'fix/product-query-stylesheet' of https://github.com/woo…
gigitux Jul 6, 2023
e897a8b
update sideEffects array
gigitux Jul 6, 2023
b161ce6
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jul 6, 2023
1b17804
Merge branch 'trunk' into add/6949-interactivity-api-product-button
gigitux Jul 7, 2023
a493959
fix catch
gigitux Jul 10, 2023
830e71f
rename moreThanOneItem to isThereMoreThanOneItem
gigitux Jul 10, 2023
de4ff24
improve how scripts are enqueued
gigitux Jul 10, 2023
ef954c9
update default value for the filter woocommerce_blocks_enable_interac…
gigitux Jul 10, 2023
dc4b341
Update assets/js/atomic/blocks/product-elements/button/block.json
gigitux Jul 10, 2023
b675b45
Update assets/js/interactivity/cart/cart-store.ts
gigitux Jul 10, 2023
345d62e
fix block.json
gigitux Jul 10, 2023
5fae283
remove updateStore function
gigitux Jul 12, 2023
23063a9
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jul 12, 2023
9f0fc03
restore interactivity api changes
gigitux Jul 12, 2023
630b55b
import cart store
gigitux Jul 12, 2023
efbfe3b
show notice when there is an error
gigitux Jul 24, 2023
07c86ab
add logic to dequeue script on classic themes and block themes
gigitux Jul 25, 2023
72ff247
imrpove logic about notice
gigitux Jul 25, 2023
4cde829
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jul 28, 2023
2decda9
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Jul 28, 2023
5cdf3d5
Interactivity API: add `afterLoad` callbacks to `store()` function (#…
DAreRodz Aug 3, 2023
79bbe9c
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Aug 3, 2023
0f38d86
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Aug 3, 2023
e672d85
update deepsignal
gigitux Aug 3, 2023
a5d06e3
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Aug 9, 2023
5432309
remove added class
gigitux Aug 9, 2023
d696a34
update deepsignal
gigitux Aug 9, 2023
5648aa0
Merge branch 'trunk' into add/6949-interactivity-api-product-button
gigitux Aug 10, 2023
941e800
Interactivity API and Product Button: Add E2E tests (#10036)
gigitux Aug 10, 2023
00a8505
Merge branch 'trunk' into add/6949-interactivity-api-product-button
gigitux Aug 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/js/atomic/blocks/product-elements/button/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"background": false,
"link": true
},
"interactivity": true,
"html": false,
"typography": {
"fontSize": true,
Expand All @@ -57,6 +58,9 @@
"label": "Outline"
}
],
"viewScript": [
"wc-product-button-interactivity-frontend"
],
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}
279 changes: 279 additions & 0 deletions assets/js/atomic/blocks/product-elements/button/frontend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* External dependencies
*/
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { store as interactivityStore } from '@woocommerce/interactivity';
import { dispatch, select, subscribe } from '@wordpress/data';
import { Cart } from '@woocommerce/type-defs/cart';
import { createRoot } from '@wordpress/element';
import NoticeBanner from '@woocommerce/base-components/notice-banner';

type Context = {
woocommerce: {
isLoading: boolean;
addToCartText: string;
productId: number;
displayViewCart: boolean;
quantityToAdd: number;
temporaryNumberOfItems: number;
animationStatus: AnimationStatus;
};
};

enum AnimationStatus {
IDLE = 'IDLE',
SLIDE_OUT = 'SLIDE-OUT',
SLIDE_IN = 'SLIDE-IN',
}

type State = {
woocommerce: {
cart: Cart | undefined;
inTheCartText: string;
};
};

type Store = {
state: State;
context: Context;
selectors: any;
ref: HTMLElement;
};

const storeNoticeClass = '.wc-block-store-notices';

const createNoticeContainer = () => {
const noticeContainer = document.createElement( 'div' );
noticeContainer.classList.add( storeNoticeClass.replace( '.', '' ) );
return noticeContainer;
};

const injectNotice = ( domNode: Element, errorMessage: string ) => {
const root = createRoot( domNode );

root.render(
<NoticeBanner status="error" onRemove={ () => root.unmount() }>
{ errorMessage }
</NoticeBanner>
);

domNode?.scrollIntoView( {
behavior: 'smooth',
inline: 'nearest',
} );
};

const getProductById = ( cartState: Cart | undefined, productId: number ) => {
return cartState?.items.find( ( item ) => item.id === productId );
};

const getTextButton = ( {
addToCartText,
inTheCartText,
numberOfItems,
}: {
addToCartText: string;
inTheCartText: string;
numberOfItems: number;
} ) => {
if ( numberOfItems === 0 ) {
return addToCartText;
}
return inTheCartText.replace( '###', numberOfItems.toString() );
};

const productButtonSelectors = {
woocommerce: {
addToCartText: ( store: Store ) => {
const { context, state, selectors } = store;

// We use the temporary number of items when there's no animation, or the
// second part of the animation hasn't started.
if (
context.woocommerce.animationStatus === AnimationStatus.IDLE ||
context.woocommerce.animationStatus ===
AnimationStatus.SLIDE_OUT
) {
return getTextButton( {
addToCartText: context.woocommerce.addToCartText,
inTheCartText: state.woocommerce.inTheCartText,
numberOfItems: context.woocommerce.temporaryNumberOfItems,
} );
}

return getTextButton( {
addToCartText: context.woocommerce.addToCartText,
inTheCartText: state.woocommerce.inTheCartText,
numberOfItems:
selectors.woocommerce.numberOfItemsInTheCart( store ),
} );
},
displayViewCart: ( store: Store ) => {
const { context, selectors } = store;
if ( ! context.woocommerce.displayViewCart ) return false;
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
return context.woocommerce.temporaryNumberOfItems > 0;
}
return selectors.woocommerce.numberOfItemsInTheCart( store ) > 0;
},
hasCartLoaded: ( { state }: { state: State } ) => {
return state.woocommerce.cart !== undefined;
},
numberOfItemsInTheCart: ( { state, context }: Store ) => {
const product = getProductById(
state.woocommerce.cart,
context.woocommerce.productId
);
return product?.quantity || 0;
},
slideOutAnimation: ( { context }: Store ) =>
context.woocommerce.animationStatus === AnimationStatus.SLIDE_OUT,
slideInAnimation: ( { context }: Store ) =>
context.woocommerce.animationStatus === AnimationStatus.SLIDE_IN,
},
};

interactivityStore(
// @ts-expect-error: Store function isn't typed.
{
selectors: productButtonSelectors,
actions: {
woocommerce: {
addToCart: async ( store: Store ) => {
const { context, selectors, ref } = store;

if ( ! ref.classList.contains( 'ajax_add_to_cart' ) ) {
return;
}

context.woocommerce.isLoading = true;

// Allow 3rd parties to validate and quit early.
// https://github.com/woocommerce/woocommerce/blob/154dd236499d8a440edf3cde712511b56baa8e45/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js/#L74-L77
const event = new CustomEvent(
'should_send_ajax_request.adding_to_cart',
{ detail: [ ref ], cancelable: true }
);
const shouldSendRequest =
document.body.dispatchEvent( event );

if ( shouldSendRequest === false ) {
const ajaxNotSentEvent = new CustomEvent(
'ajax_request_not_sent.adding_to_cart',
{ detail: [ false, false, ref ] }
);
document.body.dispatchEvent( ajaxNotSentEvent );
return true;
}

try {
await dispatch( storeKey ).addItemToCart(
context.woocommerce.productId,
context.woocommerce.quantityToAdd
);

// After the cart has been updated, sync the temporary number of
// items again.
context.woocommerce.temporaryNumberOfItems =
selectors.woocommerce.numberOfItemsInTheCart(
store
);
} catch ( error ) {
const storeNoticeBlock =
document.querySelector( storeNoticeClass );

if ( ! storeNoticeBlock ) {
document
.querySelector( '.entry-content' )
?.prepend( createNoticeContainer() );
}

const domNode =
storeNoticeBlock ??
document.querySelector( storeNoticeClass );

if ( domNode ) {
injectNotice( domNode, error.message );
}

// We don't care about errors blocking execution, but will
// console.error for troubleshooting.
// eslint-disable-next-line no-console
console.error( error );
} finally {
context.woocommerce.displayViewCart = true;
context.woocommerce.isLoading = false;
}
},
handleAnimationEnd: (
store: Store & { event: AnimationEvent }
) => {
const { event, context, selectors } = store;
if ( event.animationName === 'slideOut' ) {
// When the first part of the animation (slide-out) ends, we move
// to the second part (slide-in).
context.woocommerce.animationStatus =
AnimationStatus.SLIDE_IN;
} else if ( event.animationName === 'slideIn' ) {
// When the second part of the animation ends, we update the
// temporary number of items to sync it with the cart and reset the
// animation status so it can be triggered again.
context.woocommerce.temporaryNumberOfItems =
selectors.woocommerce.numberOfItemsInTheCart(
store
);
context.woocommerce.animationStatus =
AnimationStatus.IDLE;
}
},
},
},
effects: {
woocommerce: {
startAnimation: ( store: Store ) => {
const { context, selectors } = store;
// We start the animation if the cart has loaded, the temporary number
// of items is out of sync with the number of items in the cart, the
// button is not loading (because that means the user started the
// interaction) and the animation hasn't started yet.
if (
selectors.woocommerce.hasCartLoaded( store ) &&
context.woocommerce.temporaryNumberOfItems !==
selectors.woocommerce.numberOfItemsInTheCart(
store
) &&
! context.woocommerce.isLoading &&
context.woocommerce.animationStatus ===
AnimationStatus.IDLE
) {
context.woocommerce.animationStatus =
AnimationStatus.SLIDE_OUT;
}
},
},
},
},
{
afterLoad: ( store: Store ) => {
const { state, selectors } = store;
// Subscribe to changes in Cart data.
subscribe( () => {
const cartData = select( storeKey ).getCartData();
const isResolutionFinished =
select( storeKey ).hasFinishedResolution( 'getCartData' );
if ( isResolutionFinished ) {
state.woocommerce.cart = cartData;
}
}, storeKey );

// This selector triggers a fetch of the Cart data. It is done in a
// `requestIdleCallback` to avoid potential performance issues.
requestIdleCallback( () => {
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
select( storeKey ).getCartData();
}
} );
},
}
);
67 changes: 65 additions & 2 deletions assets/js/atomic/blocks/product-elements/button/style.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,78 @@
.wp-block-button.wc-block-components-product-button {
word-break: break-word;
white-space: normal;
display: flex;
justify-content: center;
align-items: center;
gap: $gap-small;

.wp-block-button__link {
word-break: break-word;
white-space: normal;
display: inline-flex;
justify-content: center;
text-align: center;
// Set button font size and padding so it inherits from parent.
padding: 0.5em 1em;
font-size: 1em;

&.loading {
opacity: 0.25;
}

&.loading::after {
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e031";
animation: spin 2s linear infinite;
margin-left: 0.5em;
display: inline-block;
width: auto;
height: auto;
}
}

a[hidden] {
display: none;
}

@keyframes slideOut {
from {
transform: translateY(0);
}
to {
transform: translateY(-100%);
}
}

@keyframes slideIn {
from {
transform: translateY(90%);
opacity: 0;
}
to {
transform: translate(0);
opacity: 1;
}
}

.wc-block-components-product-button__button {
border-style: none;
display: inline-flex;
justify-content: center;
margin-right: auto;
margin-left: auto;
white-space: normal;
word-break: break-word;
width: 150px;
overflow: hidden;

span {

&.wc-block-slide-out {
animation: slideOut 0.1s linear 1 normal forwards;
}
&.wc-block-slide-in {
animation: slideIn 0.1s linear 1 normal;
}
}
}

.wc-block-components-product-button__button--placeholder {
Expand Down
5 changes: 4 additions & 1 deletion assets/js/interactivity/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import registerDirectives from './directives';
import { init } from './router';
export { store } from './store';
import { rawStore, afterLoads } from './store';

export { navigate } from './router';
export { store } from './store';

/**
* Initialize the Interactivity API.
*/
document.addEventListener( 'DOMContentLoaded', async () => {
registerDirectives();
await init();
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
// eslint-disable-next-line no-console
console.log( 'Interactivity API started' );
} );
Loading