woocommerce/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/frontend.tsx

309 lines
7.9 KiB
TypeScript

/**
* External dependencies
*/
import {
store,
getContext as getContextFn,
getElement,
} from '@woocommerce/interactivity';
import { StorePart } from '@woocommerce/utils';
export interface ProductGalleryContext {
selectedImage: string;
firstMainImageId: string;
imageId: string;
visibleImagesIds: string[];
dialogVisibleImagesIds: string[];
isDialogOpen: boolean;
productId: string;
elementThatTriggeredDialogOpening: HTMLElement | null;
}
const getContext = ( ns?: string ) =>
getContextFn< ProductGalleryContext >( ns );
type Store = typeof productGallery & StorePart< ProductGallery >;
const { state, actions } = 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 closeDialog = ( context: ProductGalleryContext ) => {
context.isDialogOpen = false;
// Reset the main image.
context.selectedImage = context.firstMainImageId;
document.body.classList.remove( 'wc-block-product-gallery-modal-open' );
if ( context.elementThatTriggeredDialogOpening ) {
context.elementThatTriggeredDialogOpening?.focus();
context.elementThatTriggeredDialogOpening = null;
}
};
const productGallery = {
state: {
get isSelected() {
const { selectedImage, imageId } = getContext();
return selectedImage === imageId;
},
get pagerDotFillOpacity(): number {
return state.isSelected ? 1 : 0.2;
},
get pagerButtonPressed(): boolean {
return state.isSelected ? true : false;
},
get thumbnailTabIndex(): string {
return state.isSelected ? '0' : '-1';
},
},
actions: {
closeDialog: () => {
const context = getContext();
closeDialog( context );
},
openDialog: () => {
const context = getContext();
context.isDialogOpen = true;
document.body.classList.add(
'wc-block-product-gallery-modal-open'
);
const dialogPopUp = document.querySelector(
'dialog[aria-label="Product gallery"]'
);
if ( ! dialogPopUp ) {
return;
}
( dialogPopUp as HTMLElement ).focus();
const dialogPreviousButton = dialogPopUp.querySelectorAll(
'.wc-block-product-gallery-large-image-next-previous--button'
)[ 0 ];
if ( ! dialogPreviousButton ) {
return;
}
setTimeout( () => {
( dialogPreviousButton as HTMLButtonElement ).focus();
}, 100 );
},
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' );
},
onThumbnailKeyDown: ( event: KeyboardEvent ) => {
const context = getContext();
if (
event.code === 'Enter' ||
event.code === 'Space' ||
event.code === 'NumpadEnter'
) {
if ( event.code === 'Space' ) {
event.preventDefault();
}
context.selectedImage = context.imageId;
}
},
onSelectedLargeImageKeyDown: ( event: KeyboardEvent ) => {
if (
( state.isSelected && event.code === 'Enter' ) ||
event.code === 'Space' ||
event.code === 'NumpadEnter'
) {
if ( event.code === 'Space' ) {
event.preventDefault();
}
actions.openDialog();
const largeImageElement = getElement()?.ref as HTMLElement;
const context = getContext();
context.elementThatTriggeredDialogOpening = largeImageElement;
}
},
onViewAllImagesKeyDown: ( event: KeyboardEvent ) => {
if (
event.code === 'Enter' ||
event.code === 'Space' ||
event.code === 'NumpadEnter'
) {
if ( event.code === 'Space' ) {
event.preventDefault();
}
actions.openDialog();
const viewAllImagesElement = getElement()?.ref as HTMLElement;
const context = getContext();
context.elementThatTriggeredDialogOpening =
viewAllImagesElement;
}
},
},
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;
}
}
} );
observer.observe( variableProductCartForm, {
attributes: true,
} );
const clearVariationsLink = document.querySelector(
'.wp-block-add-to-cart-form .reset_variations'
);
const selectFirstImage = () => {
context.selectedImage = context.firstMainImageId;
};
if ( clearVariationsLink ) {
clearVariationsLink.addEventListener(
'click',
selectFirstImage
);
}
return () => {
observer.disconnect();
document.removeEventListener( 'click', selectFirstImage );
};
},
keyboardAccess: () => {
const context = getContext();
let allowNavigation = true;
const handleKeyEvents = ( event: KeyboardEvent ) => {
if ( ! allowNavigation || ! context.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' ) {
closeDialog( context );
}
// Check if left arrow key is pressed.
if ( event.code === 'ArrowLeft' ) {
selectImage( context, 'previous' );
}
// Check if right arrow key is pressed.
if ( event.code === 'ArrowRight' ) {
selectImage( context, 'next' );
}
};
document.addEventListener( 'keydown', handleKeyEvents );
return () =>
document.removeEventListener( 'keydown', handleKeyEvents );
},
dialogFocusTrap: () => {
const dialogPopUp = document.querySelector(
'dialog[aria-label="Product gallery"]'
) as HTMLElement | null;
if ( ! dialogPopUp ) {
return;
}
const handleKeyEvents = ( event: KeyboardEvent ) => {
if ( event.code === 'Tab' ) {
const focusableElementsSelectors =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
const focusableElements = dialogPopUp.querySelectorAll(
focusableElementsSelectors
);
if ( ! focusableElements.length ) {
return;
}
const firstFocusableElement =
focusableElements[ 0 ] as HTMLElement;
const lastFocusableElement = focusableElements[
focusableElements.length - 1
] as HTMLElement;
if (
! event.shiftKey &&
event.target === lastFocusableElement
) {
event.preventDefault();
firstFocusableElement.focus();
}
if (
event.shiftKey &&
event.target === firstFocusableElement
) {
event.preventDefault();
lastFocusableElement.focus();
}
}
};
dialogPopUp.addEventListener( 'keydown', handleKeyEvents );
return () =>
dialogPopUp.removeEventListener( 'keydown', handleKeyEvents );
},
},
};
store( 'woocommerce/product-gallery', productGallery );
export type ProductGallery = typeof productGallery;