Product Gallery: Add animation when large image changes (https://github.com/woocommerce/woocommerce-blocks/pull/11113)
* Add slide animation * Remove placeholder and pagination (https://github.com/woocommerce/woocommerce-blocks/pull/11145) * Add titles to patterns and set the aligment to Wide * Replace product query patterns with product collection ones * Remove pagination and no results query from product query patterns * Add aspect ratio to the product image attributes * Add portrait aspect ratio to product X column and product gallery patterns * improve animation * improve naming * fix regression * fix css * improve code style * remove check on tag image * align image * fix crash when zoom is disabled * fix E2E tests * improve CSS --------- Co-authored-by: Alba Rincón <albarin@users.noreply.github.com>
This commit is contained in:
parent
9ba4f34d31
commit
face8d2b57
|
@ -16,7 +16,7 @@ interface Context {
|
|||
};
|
||||
}
|
||||
|
||||
interface Selectors {
|
||||
export interface ProductGallerySelectors {
|
||||
woocommerce: {
|
||||
isSelected: ( store: unknown ) => boolean;
|
||||
pagerDotFillOpacity: ( store: SelectorsStore ) => number;
|
||||
|
@ -36,7 +36,7 @@ interface Actions {
|
|||
interface Store {
|
||||
state: State;
|
||||
context: Context;
|
||||
selectors: Selectors;
|
||||
selectors: ProductGallerySelectors;
|
||||
actions: Actions;
|
||||
ref?: HTMLElement;
|
||||
}
|
||||
|
|
|
@ -3,41 +3,56 @@
|
|||
*/
|
||||
import { store as interactivityStore } from '@woocommerce/interactivity';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductGallerySelectors } from '../../frontend';
|
||||
|
||||
type Context = {
|
||||
woocommerce: {
|
||||
styles: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'transform-origin': string;
|
||||
transform: string;
|
||||
transition: string;
|
||||
};
|
||||
styles:
|
||||
| {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'transform-origin': string;
|
||||
transform: string;
|
||||
transition: string;
|
||||
}
|
||||
| undefined;
|
||||
isDialogOpen: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type Store = {
|
||||
context: Context;
|
||||
selectors: typeof productButtonSelectors;
|
||||
selectors: typeof productGalleryLargeImageSelectors &
|
||||
ProductGallerySelectors;
|
||||
ref: HTMLElement;
|
||||
};
|
||||
|
||||
const productButtonSelectors = {
|
||||
const productGalleryLargeImageSelectors = {
|
||||
woocommerce: {
|
||||
styles: ( { context }: Store ) => {
|
||||
const { styles } = context.woocommerce;
|
||||
productGalleryLargeImage: {
|
||||
styles: ( { context }: Store ) => {
|
||||
const { styles } = context.woocommerce;
|
||||
|
||||
return Object.entries( styles ).reduce( ( acc, [ key, value ] ) => {
|
||||
const style = `${ key }:${ value };`;
|
||||
return acc.length > 0 ? `${ acc } ${ style }` : style;
|
||||
}, '' );
|
||||
return Object.entries( styles ?? [] ).reduce(
|
||||
( acc, [ key, value ] ) => {
|
||||
const style = `${ key }:${ value };`;
|
||||
return acc.length > 0 ? `${ acc } ${ style }` : style;
|
||||
},
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isDialogStatusChanged = false;
|
||||
|
||||
interactivityStore(
|
||||
// @ts-expect-error: Store function isn't typed.
|
||||
{
|
||||
selectors: productButtonSelectors,
|
||||
selectors: productGalleryLargeImageSelectors,
|
||||
actions: {
|
||||
woocommerce: {
|
||||
handleMouseMove: ( {
|
||||
|
@ -78,5 +93,58 @@ interactivityStore(
|
|||
},
|
||||
},
|
||||
},
|
||||
effects: {
|
||||
woocommerce: {
|
||||
scrollInto: ( store: Store ) => {
|
||||
if ( ! store.selectors.woocommerce.isSelected( store ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to the selected image with a smooth animation.
|
||||
if (
|
||||
store.context.woocommerce.isDialogOpen ===
|
||||
isDialogStatusChanged
|
||||
) {
|
||||
store.ref.scrollIntoView( {
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'center',
|
||||
} );
|
||||
}
|
||||
|
||||
// Scroll to the selected image when the dialog is being opened without an animation.
|
||||
if (
|
||||
store.context.woocommerce.isDialogOpen &&
|
||||
store.context.woocommerce.isDialogOpen !==
|
||||
isDialogStatusChanged &&
|
||||
store.ref.closest( 'dialog' )
|
||||
) {
|
||||
store.ref.scrollIntoView( {
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
inline: 'center',
|
||||
} );
|
||||
|
||||
isDialogStatusChanged =
|
||||
store.context.woocommerce.isDialogOpen;
|
||||
}
|
||||
|
||||
// Scroll to the selected image when the dialog is being closed without an animation.
|
||||
if (
|
||||
! store.context.woocommerce.isDialogOpen &&
|
||||
store.context.woocommerce.isDialogOpen !==
|
||||
isDialogStatusChanged
|
||||
) {
|
||||
store.ref.scrollIntoView( {
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
inline: 'center',
|
||||
} );
|
||||
isDialogStatusChanged =
|
||||
store.context.woocommerce.isDialogOpen;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -57,17 +57,35 @@ $outside-image-max-width: calc(100% - (2 * $outside-image-offset));
|
|||
overflow: hidden;
|
||||
|
||||
// Restrict the width of the Large Image when the Next/Previous buttons are outside the image.
|
||||
#{$gallery-next-previous-outside-image} & .wc-block-woocommerce-product-gallery-large-image__container {
|
||||
#{$gallery-next-previous-outside-image} & .wc-block-product-gallery-large-image__image-element {
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
max-width: $outside-image-max-width;
|
||||
}
|
||||
|
||||
.wc-block-product-gallery-large-image__wrapper {
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-product-gallery-large-image__container {
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
scroll-behavior: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#{$gallery-next-previous-inside-image} & .wc-block-product-gallery-large-image__image-element {
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#{$gallery-next-previous-inside-image} & .wc-block-woocommerce-product-gallery-large-image__container {
|
||||
overflow: unset;
|
||||
max-width: unset;
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
|
@ -75,6 +93,7 @@ $outside-image-max-width: calc(100% - (2 * $outside-image-offset));
|
|||
margin: 0 auto;
|
||||
z-index: 1;
|
||||
transition: all 0.1s linear;
|
||||
width: auto;
|
||||
|
||||
// Keep the order in this way. The hoverZoom class should override the full-screen-on-click class when both are applied.
|
||||
&.wc-block-woocommerce-product-gallery-large-image__image--full-screen-on-click {
|
||||
|
@ -84,10 +103,6 @@ $outside-image-max-width: calc(100% - (2 * $outside-image-offset));
|
|||
&.wc-block-woocommerce-product-gallery-large-image__image--hoverZoom {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-product-gallery-large-image__inner-blocks {
|
||||
|
|
|
@ -87,11 +87,10 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
|||
|
||||
return strtr(
|
||||
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
|
||||
{visible_main_image}
|
||||
{main_images}
|
||||
<div class="wc-block-woocommerce-product-gallery-large-image__content">
|
||||
{content}
|
||||
<div class="wc-block-product-gallery-large-image__container">
|
||||
{main_images}
|
||||
</div>
|
||||
{content}
|
||||
</div>',
|
||||
array(
|
||||
'{visible_main_image}' => $visible_main_image,
|
||||
|
@ -118,9 +117,9 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
|||
*/
|
||||
private function get_main_images_html( $context, $product_id ) {
|
||||
$attributes = array(
|
||||
'data-wc-bind--hidden' => '!selectors.woocommerce.isSelected',
|
||||
'hidden' => true,
|
||||
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
|
||||
'data-wc-bind--style' => 'selectors.woocommerce.productGalleryLargeImage.styles',
|
||||
'data-wc-effect' => 'effects.woocommerce.scrollInto',
|
||||
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
|
||||
|
||||
);
|
||||
|
||||
|
@ -130,23 +129,25 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
|||
|
||||
if ( $context['hoverZoom'] ) {
|
||||
$attributes['class'] .= ' wc-block-woocommerce-product-gallery-large-image__image--hoverZoom';
|
||||
$attributes['data-wc-bind--style'] = 'selectors.woocommerce.styles';
|
||||
$attributes['data-wc-bind--style'] = 'selectors.woocommerce.productGalleryLargeImage.styles';
|
||||
}
|
||||
|
||||
$main_images = ProductGalleryUtils::get_product_gallery_images(
|
||||
$product_id,
|
||||
'full',
|
||||
$attributes,
|
||||
'wc-block-woocommerce-product-gallery-large-image__container'
|
||||
'wc-block-product-gallery-large-image__image-element'
|
||||
);
|
||||
|
||||
$visible_main_image = array_shift( $main_images );
|
||||
$visible_main_image_processor = new \WP_HTML_Tag_Processor( $visible_main_image );
|
||||
$visible_main_image_processor->next_tag();
|
||||
$visible_main_image_processor->remove_attribute( 'hidden' );
|
||||
$visible_main_image = $visible_main_image_processor->get_updated_html();
|
||||
$main_image_with_wrapper = array_map(
|
||||
function( $main_image_element ) {
|
||||
return "<div class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</div>';
|
||||
},
|
||||
$main_images
|
||||
);
|
||||
|
||||
return array( $visible_main_image, $main_images );
|
||||
$visible_main_image = array_shift( $main_images );
|
||||
return array( $visible_main_image, $main_image_with_wrapper );
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -99,9 +99,7 @@ test.describe( `${ blockData.name }`, () => {
|
|||
} );
|
||||
|
||||
// img[style] is the selector because the style attribute is Interactivity API.
|
||||
const imgElement = blockFrontend.locator(
|
||||
'img[style]:not([hidden])'
|
||||
);
|
||||
const imgElement = blockFrontend.locator( 'img' ).first();
|
||||
const style = await imgElement.evaluate( ( el ) => el.style );
|
||||
|
||||
await expect( style.transform ).toBe( 'scale(1)' );
|
||||
|
@ -137,14 +135,18 @@ test.describe( `${ blockData.name }`, () => {
|
|||
page: 'frontend',
|
||||
} );
|
||||
|
||||
// img[style] is the selector because the style attribute is added by Interactivity API. In this case, the style attribute should not be added.
|
||||
const imgElement = blockFrontend.locator(
|
||||
'img[style]:not([hidden])'
|
||||
const imgElement = blockFrontend.locator( 'img' ).first();
|
||||
const style = await imgElement.evaluate( ( el ) => el.style );
|
||||
|
||||
await expect( style.transform ).toBe( '' );
|
||||
|
||||
await imgElement.hover();
|
||||
|
||||
const styleOnHover = await imgElement.evaluate(
|
||||
( el ) => el.style
|
||||
);
|
||||
await expect( imgElement ).toBeHidden();
|
||||
await expect(
|
||||
blockFrontend.locator( 'img:not([hidden])' )
|
||||
).toBeVisible();
|
||||
|
||||
await expect( styleOnHover.transform ).toBe( '' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -34,9 +34,7 @@ const test = base.extend< { pageObject: ProductGalleryPage } >( {
|
|||
export const getVisibleLargeImageId = async (
|
||||
mainImageBlockLocator: Locator
|
||||
) => {
|
||||
const mainImage = mainImageBlockLocator.locator(
|
||||
'img:not([hidden])'
|
||||
) as Locator;
|
||||
const mainImage = mainImageBlockLocator.locator( 'img' ).first() as Locator;
|
||||
|
||||
const mainImageContext = ( await mainImage.getAttribute(
|
||||
'data-wc-context'
|
||||
|
|
Loading…
Reference in New Issue