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:
Luigi Teschio 2023-10-20 09:42:39 +02:00 committed by GitHub
parent 9ba4f34d31
commit face8d2b57
6 changed files with 139 additions and 55 deletions

View File

@ -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;
}

View File

@ -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;
}
},
},
},
}
);

View File

@ -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 {

View File

@ -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 );
}

View File

@ -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( '' );
} );
} );
} );

View File

@ -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'