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

Commit

Permalink
Migration of Product Collection and Product Button blocks to the new …
Browse files Browse the repository at this point in the history
…`store()` API (#11558)

* Refactor Product Button with new store() API

* Use `wc_initial_state` in Product Button

* Fix namespace

* Remove unnecessary state

* Test namespaces in directive paths

* Add test context with namespace

* Simplify woo-test context

* Move addToCart and animations to a file

* Do not pass `rawStore` to `afterLoad` callbacks

* Move callbacks and actions back to the main file

Because the animation was broken.

* Remove selectors in favor of state

* Use default ns in `getContext` for state and actions

* Remove `afterLoad` callback

* Remove unnecessary ns

* Fix getContext in add-to-cart

* Replace namespace and delete unnecessary store

* Pass context types only once

* Use an alternative for requestIdleCallback

* Add previous react code for notices

* Add namespace to Product Collection block

* Replace getTextButton with getButtonText

* Add block name to the ProductCollection namespace

* fix style HTML code

* Remove circular deps error on the Interactivity API

* Product Gallery block: Migrate to new Interactivity API store (#11721)

* Migrate Product Gallery block to new Interactivity API store

* Fix some references

* Add missing data-wc-interactive

* Fix an additional namespace

* Remove unnecessary click handler

* Dialog working

* Refactor action names

* Reindex PHP array

There was some missing indexes, which turned the array into an object in JS.

* Remove unused event handlers

* Move next/previous logic to external function

* Move StorePart util to the types folder

* Rename namespace to `woocommerce/product-gallery`

* Undo product collection namespace renaming

* Remove unnecessary namespace

* Don't hide the large image on page load

* Minor refactorings

* Fix eslint error

* Fix php cs errors with spacing and double arrows alignment

* Disable no-use-before-define rule for eslint

* Disable @typescript-eslint/ban-types rule for eslint

* Fix parsed context error in e2e tests

* Fix context parser for Thumbnail image

* Move store to the top of the frontend file

* Add interactivity api utils to the @woocommerce/utils alias

* Replace deprecated event attribute

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>
Co-authored-by: David Arenas <david.arenas@automattic.com>
Co-authored-by: roykho <roykho77@gmail.com>

---------

Co-authored-by: David Arenas <david.arenas@automattic.com>
Co-authored-by: Luigi Teschio <gigitux@gmail.com>
Co-authored-by: Alexandre Lara <allexandrelara@gmail.com>
Co-authored-by: roykho <roykho77@gmail.com>
  • Loading branch information
5 people committed Nov 16, 2023
1 parent 8fc2917 commit 9ea4bfe
Show file tree
Hide file tree
Showing 15 changed files with 522 additions and 691 deletions.
411 changes: 176 additions & 235 deletions assets/js/atomic/blocks/product-elements/button/frontend.tsx

Large diffs are not rendered by default.

332 changes: 131 additions & 201 deletions assets/js/blocks/product-gallery/frontend.tsx
Original file line number Diff line number Diff line change
@@ -1,221 +1,151 @@
/**
* External dependencies
*/
import { store as interactivityApiStore } from '@woocommerce/interactivity';

interface State {
[ key: string ]: unknown;
}

export interface ProductGalleryInteractivityApiContext {
woocommerce: {
selectedImage: string;
imageId: string;
visibleImagesIds: string[];
dialogVisibleImagesIds: string[];
isDialogOpen: boolean;
productId: string;
};
}

export interface ProductGallerySelectors {
woocommerce: {
isSelected: ( store: unknown ) => boolean;
pagerDotFillOpacity: ( store: SelectorsStore ) => number;
selectedImageIndex: ( store: SelectorsStore ) => number;
isDialogOpen: ( store: unknown ) => boolean;
};
}

interface Actions {
woocommerce: {
thumbnails: {
handleClick: (
context: ProductGalleryInteractivityApiContext
) => void;
};
handlePreviousImageButtonClick: {
( store: Store ): void;
};
handleNextImageButtonClick: {
( store: Store ): void;
};
};
}

interface Store {
state: State;
context: ProductGalleryInteractivityApiContext;
selectors: ProductGallerySelectors;
actions: Actions;
ref?: HTMLElement;
import { store, getContext as getContextFn } from '@woocommerce/interactivity';
import { StorePart } from '@woocommerce/utils';

export interface ProductGalleryContext {
selectedImage: string;
imageId: string;
visibleImagesIds: string[];
dialogVisibleImagesIds: string[];
isDialogOpen: boolean;
productId: string;
}

interface Event {
keyCode: number;
}

type SelectorsStore = Pick< Store, 'context' | 'selectors' | 'ref' >;
const getContext = ( ns?: string ) =>
getContextFn< ProductGalleryContext >( ns );

type Store = typeof productGallery & StorePart< ProductGallery >;
const { state } = store< Store >( 'woocommerce/product-gallery' );

const selectImage = (
context: ProductGalleryContext,
select: 'next' | 'previous'
) => {
const imagesIds =
context[
context.isDialogOpen ? 'dialogVisibleImagesIds' : 'visibleImagesIds'
];
const selectedImageIdIndex = imagesIds.indexOf( context.selectedImage );
const nextImageIndex =
select === 'next'
? Math.min( selectedImageIdIndex + 1, imagesIds.length - 1 )
: Math.max( selectedImageIdIndex - 1, 0 );
context.selectedImage = imagesIds[ nextImageIndex ];
};

const productGallery = {
state: {
get isSelected() {
const { selectedImage, imageId } = getContext();
return selectedImage === imageId;
},
get pagerDotFillOpacity(): number {
return state.isSelected ? 1 : 0.2;
},
},
actions: {
closeDialog: () => {
const context = getContext();
context.isDialogOpen = false;
},
openDialog: () => {
const context = getContext();
context.isDialogOpen = true;
},
selectImage: () => {
const context = getContext();
context.selectedImage = context.imageId;
},
selectNextImage: ( event: MouseEvent ) => {
event.stopPropagation();
const context = getContext();
selectImage( context, 'next' );
},
selectPreviousImage: ( event: MouseEvent ) => {
event.stopPropagation();
const context = getContext();
selectImage( context, 'previous' );
},
},
callbacks: {
watchForChangesOnAddToCartForm: () => {
const context = getContext();
const variableProductCartForm = document.querySelector(
`form[data-product_id="${ context.productId }"]`
);

if ( ! variableProductCartForm ) {
return;
}

// TODO: Replace with an interactive block that calls `actions.selectImage`.
const observer = new MutationObserver( function ( mutations ) {
for ( const mutation of mutations ) {
const mutationTarget = mutation.target as HTMLElement;
const currentImageAttribute =
mutationTarget.getAttribute( 'current-image' );
if (
mutation.type === 'attributes' &&
currentImageAttribute &&
context.visibleImagesIds.includes(
currentImageAttribute
)
) {
context.selectedImage = currentImageAttribute;
}
}
} );

enum Keys {
ESC = 27,
LEFT_ARROW = 37,
RIGHT_ARROW = 39,
}
observer.observe( variableProductCartForm, {
attributes: true,
} );

interactivityApiStore( {
state: {},
effects: {
woocommerce: {
watchForChangesOnAddToCartForm: ( store: Store ) => {
const variableProductCartForm = document.querySelector(
`form[data-product_id="${ store.context.woocommerce.productId }"]`
);
return () => {
observer.disconnect();
};
},
keyboardAccess: () => {
const context = getContext();
let allowNavigation = true;

if ( ! variableProductCartForm ) {
const handleKeyEvents = ( event: KeyboardEvent ) => {
if ( ! allowNavigation || ! context.isDialogOpen ) {
return;
}

const observer = new MutationObserver( function ( mutations ) {
for ( const mutation of mutations ) {
const mutationTarget = mutation.target as HTMLElement;
const currentImageAttribute =
mutationTarget.getAttribute( 'current-image' );
if (
mutation.type === 'attributes' &&
currentImageAttribute &&
store.context.woocommerce.visibleImagesIds.includes(
currentImageAttribute
)
) {
store.context.woocommerce.selectedImage =
currentImageAttribute;
}
}
} );
// Disable navigation for a brief period to prevent spamming.
allowNavigation = false;

observer.observe( variableProductCartForm, {
attributes: true,
requestAnimationFrame( () => {
allowNavigation = true;
} );

return () => {
observer.disconnect();
};
},
keyboardAccess: ( store: Store ) => {
const { context, actions } = store;
let allowNavigation = true;

const handleKeyEvents = ( event: Event ) => {
if (
! allowNavigation ||
! context.woocommerce?.isDialogOpen
) {
return;
}

// Disable navigation for a brief period to prevent spamming.
allowNavigation = false;

requestAnimationFrame( () => {
allowNavigation = true;
} );
// Check if the esc key is pressed.
if ( event.code === 'Escape' ) {
context.isDialogOpen = false;
}

// Check if the esc key is pressed.
if ( event.keyCode === Keys.ESC ) {
context.woocommerce.isDialogOpen = false;
}
// Check if left arrow key is pressed.
if ( event.code === 'ArrowLeft' ) {
selectImage( context, 'previous' );
}

// Check if left arrow key is pressed.
if ( event.keyCode === Keys.LEFT_ARROW ) {
actions.woocommerce.handlePreviousImageButtonClick(
store
);
}
// Check if right arrow key is pressed.
if ( event.code === 'ArrowRight' ) {
selectImage( context, 'next' );
}
};

// Check if right arrow key is pressed.
if ( event.keyCode === Keys.RIGHT_ARROW ) {
actions.woocommerce.handleNextImageButtonClick( store );
}
};
document.addEventListener( 'keydown', handleKeyEvents );

document.addEventListener( 'keydown', handleKeyEvents );
},
},
},
selectors: {
woocommerce: {
isSelected: ( { context }: Store ) => {
return (
context?.woocommerce.selectedImage ===
context?.woocommerce.imageId
);
},
pagerDotFillOpacity( store: SelectorsStore ) {
const { context } = store;

return context?.woocommerce.selectedImage ===
context?.woocommerce.imageId
? 1
: 0.2;
},
isDialogOpen: ( { context }: Store ) => {
return context.woocommerce.isDialogOpen;
},
},
},
actions: {
woocommerce: {
thumbnails: {
handleClick: ( { context }: Store ) => {
context.woocommerce.selectedImage =
context.woocommerce.imageId;
},
},
dialog: {
handleCloseButtonClick: ( { context }: Store ) => {
context.woocommerce.isDialogOpen = false;
},
},
handleSelectImage: ( { context }: Store ) => {
context.woocommerce.selectedImage = context.woocommerce.imageId;
},
handleNextImageButtonClick: ( store: Store ) => {
const { context } = store;
const imagesIds =
context.woocommerce[
context.woocommerce.isDialogOpen
? 'dialogVisibleImagesIds'
: 'visibleImagesIds'
];
const selectedImageIdIndex = imagesIds.indexOf(
context.woocommerce.selectedImage
);
const nextImageIndex = Math.min(
selectedImageIdIndex + 1,
imagesIds.length - 1
);

context.woocommerce.selectedImage = imagesIds[ nextImageIndex ];
},
handlePreviousImageButtonClick: ( store: Store ) => {
const { context } = store;
const imagesIds =
context.woocommerce[
context.woocommerce.isDialogOpen
? 'dialogVisibleImagesIds'
: 'visibleImagesIds'
];
const selectedImageIdIndex = imagesIds.indexOf(
context.woocommerce.selectedImage
);
const previousImageIndex = Math.max(
selectedImageIdIndex - 1,
0
);
context.woocommerce.selectedImage =
imagesIds[ previousImageIndex ];
},
return () =>
document.removeEventListener( 'keydown', handleKeyEvents );
},
},
} );
};

store( 'woocommerce/product-gallery', productGallery );

export type ProductGallery = typeof productGallery;
Loading

0 comments on commit 9ea4bfe

Please sign in to comment.