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

Commit a8ce473

Browse files
gigituxalbarin
andauthored
Product Gallery: Add animation when large image changes (#11113)
* Add slide animation * Remove placeholder and pagination (#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 <[email protected]>
1 parent ea812d3 commit a8ce473

File tree

6 files changed

+139
-55
lines changed

6 files changed

+139
-55
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface Context {
1616
};
1717
}
1818

19-
interface Selectors {
19+
export interface ProductGallerySelectors {
2020
woocommerce: {
2121
isSelected: ( store: unknown ) => boolean;
2222
pagerDotFillOpacity: ( store: SelectorsStore ) => number;
@@ -36,7 +36,7 @@ interface Actions {
3636
interface Store {
3737
state: State;
3838
context: Context;
39-
selectors: Selectors;
39+
selectors: ProductGallerySelectors;
4040
actions: Actions;
4141
ref?: HTMLElement;
4242
}

assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/frontend.tsx

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,56 @@
33
*/
44
import { store as interactivityStore } from '@woocommerce/interactivity';
55

6+
/**
7+
* Internal dependencies
8+
*/
9+
import { ProductGallerySelectors } from '../../frontend';
10+
611
type Context = {
712
woocommerce: {
8-
styles: {
9-
// eslint-disable-next-line @typescript-eslint/naming-convention
10-
'transform-origin': string;
11-
transform: string;
12-
transition: string;
13-
};
13+
styles:
14+
| {
15+
// eslint-disable-next-line @typescript-eslint/naming-convention
16+
'transform-origin': string;
17+
transform: string;
18+
transition: string;
19+
}
20+
| undefined;
1421
isDialogOpen: boolean;
1522
};
1623
};
1724

1825
type Store = {
1926
context: Context;
20-
selectors: typeof productButtonSelectors;
27+
selectors: typeof productGalleryLargeImageSelectors &
28+
ProductGallerySelectors;
2129
ref: HTMLElement;
2230
};
2331

24-
const productButtonSelectors = {
32+
const productGalleryLargeImageSelectors = {
2533
woocommerce: {
26-
styles: ( { context }: Store ) => {
27-
const { styles } = context.woocommerce;
34+
productGalleryLargeImage: {
35+
styles: ( { context }: Store ) => {
36+
const { styles } = context.woocommerce;
2837

29-
return Object.entries( styles ).reduce( ( acc, [ key, value ] ) => {
30-
const style = `${ key }:${ value };`;
31-
return acc.length > 0 ? `${ acc } ${ style }` : style;
32-
}, '' );
38+
return Object.entries( styles ?? [] ).reduce(
39+
( acc, [ key, value ] ) => {
40+
const style = `${ key }:${ value };`;
41+
return acc.length > 0 ? `${ acc } ${ style }` : style;
42+
},
43+
''
44+
);
45+
},
3346
},
3447
},
3548
};
3649

50+
let isDialogStatusChanged = false;
51+
3752
interactivityStore(
3853
// @ts-expect-error: Store function isn't typed.
3954
{
40-
selectors: productButtonSelectors,
55+
selectors: productGalleryLargeImageSelectors,
4156
actions: {
4257
woocommerce: {
4358
handleMouseMove: ( {
@@ -78,5 +93,58 @@ interactivityStore(
7893
},
7994
},
8095
},
96+
effects: {
97+
woocommerce: {
98+
scrollInto: ( store: Store ) => {
99+
if ( ! store.selectors.woocommerce.isSelected( store ) ) {
100+
return;
101+
}
102+
103+
// Scroll to the selected image with a smooth animation.
104+
if (
105+
store.context.woocommerce.isDialogOpen ===
106+
isDialogStatusChanged
107+
) {
108+
store.ref.scrollIntoView( {
109+
behavior: 'smooth',
110+
block: 'nearest',
111+
inline: 'center',
112+
} );
113+
}
114+
115+
// Scroll to the selected image when the dialog is being opened without an animation.
116+
if (
117+
store.context.woocommerce.isDialogOpen &&
118+
store.context.woocommerce.isDialogOpen !==
119+
isDialogStatusChanged &&
120+
store.ref.closest( 'dialog' )
121+
) {
122+
store.ref.scrollIntoView( {
123+
behavior: 'instant',
124+
block: 'nearest',
125+
inline: 'center',
126+
} );
127+
128+
isDialogStatusChanged =
129+
store.context.woocommerce.isDialogOpen;
130+
}
131+
132+
// Scroll to the selected image when the dialog is being closed without an animation.
133+
if (
134+
! store.context.woocommerce.isDialogOpen &&
135+
store.context.woocommerce.isDialogOpen !==
136+
isDialogStatusChanged
137+
) {
138+
store.ref.scrollIntoView( {
139+
behavior: 'instant',
140+
block: 'nearest',
141+
inline: 'center',
142+
} );
143+
isDialogStatusChanged =
144+
store.context.woocommerce.isDialogOpen;
145+
}
146+
},
147+
},
148+
},
81149
}
82150
);

assets/js/blocks/product-gallery/style.scss

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,43 @@ $outside-image-max-width: calc(100% - (2 * $outside-image-offset));
5757
overflow: hidden;
5858

5959
// Restrict the width of the Large Image when the Next/Previous buttons are outside the image.
60-
#{$gallery-next-previous-outside-image} & .wc-block-woocommerce-product-gallery-large-image__container {
60+
#{$gallery-next-previous-outside-image} & .wc-block-product-gallery-large-image__image-element {
6161
overflow: hidden;
62-
max-width: $outside-image-max-width;
6362
margin: 0 auto;
63+
max-width: $outside-image-max-width;
64+
}
65+
66+
.wc-block-product-gallery-large-image__wrapper {
67+
flex-shrink: 0;
68+
max-width: 100%;
69+
overflow: hidden;
70+
width: 100%;
6471
}
6572

66-
#{$gallery-next-previous-inside-image} & .wc-block-woocommerce-product-gallery-large-image__container {
67-
overflow: unset;
68-
max-width: unset;
69-
margin: unset;
73+
.wc-block-product-gallery-large-image__container {
74+
display: flex;
75+
overflow-x: hidden;
76+
scroll-snap-type: x mandatory;
77+
width: fit-content;
78+
height: fit-content;
79+
scroll-behavior: auto;
80+
align-items: center;
7081
}
7182

83+
#{$gallery-next-previous-inside-image} & .wc-block-product-gallery-large-image__image-element {
84+
width: fit-content;
85+
overflow: hidden;
86+
margin: 0 auto;
87+
}
88+
89+
7290
img {
7391
display: block;
7492
position: relative;
7593
margin: 0 auto;
7694
z-index: 1;
7795
transition: all 0.1s linear;
96+
width: auto;
7897

7998
// Keep the order in this way. The hoverZoom class should override the full-screen-on-click class when both are applied.
8099
&.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));
84103
&.wc-block-woocommerce-product-gallery-large-image__image--hoverZoom {
85104
cursor: zoom-in;
86105
}
87-
88-
&[hidden] {
89-
display: none;
90-
}
91106
}
92107

93108
.wc-block-product-gallery-large-image__inner-blocks {

src/BlockTypes/ProductGalleryLargeImage.php

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,10 @@ protected function render( $attributes, $content, $block ) {
8787

8888
return strtr(
8989
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
90-
{visible_main_image}
91-
{main_images}
92-
<div class="wc-block-woocommerce-product-gallery-large-image__content">
93-
{content}
90+
<div class="wc-block-product-gallery-large-image__container">
91+
{main_images}
9492
</div>
93+
{content}
9594
</div>',
9695
array(
9796
'{visible_main_image}' => $visible_main_image,
@@ -118,9 +117,9 @@ function( $carry, $key ) use ( $directives ) {
118117
*/
119118
private function get_main_images_html( $context, $product_id ) {
120119
$attributes = array(
121-
'data-wc-bind--hidden' => '!selectors.woocommerce.isSelected',
122-
'hidden' => true,
123-
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
120+
'data-wc-bind--style' => 'selectors.woocommerce.productGalleryLargeImage.styles',
121+
'data-wc-effect' => 'effects.woocommerce.scrollInto',
122+
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
124123

125124
);
126125

@@ -130,23 +129,25 @@ private function get_main_images_html( $context, $product_id ) {
130129

131130
if ( $context['hoverZoom'] ) {
132131
$attributes['class'] .= ' wc-block-woocommerce-product-gallery-large-image__image--hoverZoom';
133-
$attributes['data-wc-bind--style'] = 'selectors.woocommerce.styles';
132+
$attributes['data-wc-bind--style'] = 'selectors.woocommerce.productGalleryLargeImage.styles';
134133
}
135134

136135
$main_images = ProductGalleryUtils::get_product_gallery_images(
137136
$product_id,
138137
'full',
139138
$attributes,
140-
'wc-block-woocommerce-product-gallery-large-image__container'
139+
'wc-block-product-gallery-large-image__image-element'
141140
);
142141

143-
$visible_main_image = array_shift( $main_images );
144-
$visible_main_image_processor = new \WP_HTML_Tag_Processor( $visible_main_image );
145-
$visible_main_image_processor->next_tag();
146-
$visible_main_image_processor->remove_attribute( 'hidden' );
147-
$visible_main_image = $visible_main_image_processor->get_updated_html();
142+
$main_image_with_wrapper = array_map(
143+
function( $main_image_element ) {
144+
return "<div class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</div>';
145+
},
146+
$main_images
147+
);
148148

149-
return array( $visible_main_image, $main_images );
149+
$visible_main_image = array_shift( $main_images );
150+
return array( $visible_main_image, $main_image_with_wrapper );
150151

151152
}
152153

tests/e2e/tests/product-gallery/inner-blocks/product-gallery-large-image/product-gallery-large-image.block_theme.side_effects.spec.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ test.describe( `${ blockData.name }`, () => {
9999
} );
100100

101101
// img[style] is the selector because the style attribute is Interactivity API.
102-
const imgElement = blockFrontend.locator(
103-
'img[style]:not([hidden])'
104-
);
102+
const imgElement = blockFrontend.locator( 'img' ).first();
105103
const style = await imgElement.evaluate( ( el ) => el.style );
106104

107105
await expect( style.transform ).toBe( 'scale(1)' );
@@ -137,14 +135,18 @@ test.describe( `${ blockData.name }`, () => {
137135
page: 'frontend',
138136
} );
139137

140-
// img[style] is the selector because the style attribute is added by Interactivity API. In this case, the style attribute should not be added.
141-
const imgElement = blockFrontend.locator(
142-
'img[style]:not([hidden])'
138+
const imgElement = blockFrontend.locator( 'img' ).first();
139+
const style = await imgElement.evaluate( ( el ) => el.style );
140+
141+
await expect( style.transform ).toBe( '' );
142+
143+
await imgElement.hover();
144+
145+
const styleOnHover = await imgElement.evaluate(
146+
( el ) => el.style
143147
);
144-
await expect( imgElement ).toBeHidden();
145-
await expect(
146-
blockFrontend.locator( 'img:not([hidden])' )
147-
).toBeVisible();
148+
149+
await expect( styleOnHover.transform ).toBe( '' );
148150
} );
149151
} );
150152
} );

tests/e2e/tests/product-gallery/product-gallery.block_theme.side_effects.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ const test = base.extend< { pageObject: ProductGalleryPage } >( {
3434
export const getVisibleLargeImageId = async (
3535
mainImageBlockLocator: Locator
3636
) => {
37-
const mainImage = mainImageBlockLocator.locator(
38-
'img:not([hidden])'
39-
) as Locator;
37+
const mainImage = mainImageBlockLocator.locator( 'img' ).first() as Locator;
4038

4139
const mainImageContext = ( await mainImage.getAttribute(
4240
'data-wc-context'

0 commit comments

Comments
 (0)