Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image block: Lightbox animation improvements #51721

Merged
merged 9 commits into from
Jun 26, 2023
61 changes: 53 additions & 8 deletions lib/block-supports/behaviors.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,19 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
// We want to store the src in the context so we can set it dynamically when the lightbox is opened.
$z = new WP_HTML_Tag_Processor( $content );
$z->next_tag( 'img' );

if ( isset( $block['attrs']['id'] ) ) {
$img_src = wp_get_attachment_url( $block['attrs']['id'] );
$img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
$img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
$img_width = $img_metadata['width'];
$img_height = $img_metadata['height'];
$img_uploaded_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'] );
} else {
$img_src = $z->get_attribute( 'src' );
$img_uploaded_src = $z->get_attribute( 'src' );
$img_dimensions = wp_getimagesize( $img_uploaded_src );
$img_width = $img_dimensions[0];
$img_height = $img_dimensions[1];
$img_uploaded_srcset = '';
}

$w = new WP_HTML_Tag_Processor( $content );
Expand All @@ -99,24 +108,59 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$w->set_attribute( 'data-wp-interactive', true );
$w->set_attribute(
'data-wp-context',
sprintf( '{ "core":{ "image": { "initialized": false, "imageSrc": "%s", "lightboxEnabled": false, "lightboxAnimation": "%s", "hideAnimationEnabled": false } } }', $img_src, $lightbox_animation )
sprintf(
'{ "core":
{ "image":
{ "imageLoaded": false,
"initialized": false,
"lightboxEnabled": false,
"hideAnimationEnabled": false,
"preloadInitialized": false,
"lightboxAnimation": "%s",
"imageUploadedSrc": "%s",
"imageCurrentSrc": "",
"imageSrcSet": "%s",
"targetWidth": "%s",
"targetHeight": "%s"
}
}
}',
$lightbox_animation,
$img_uploaded_src,
$img_uploaded_srcset,
$img_width,
$img_height
)
);
$w->next_tag( 'img' );
$w->set_attribute( 'data-wp-effect', 'effects.core.image.setCurrentSrc' );
$body_content = $w->get_updated_html();

// Wrap the image in the body content with a button.
$img = null;
preg_match( '/<img[^>]+>/', $content, $img );
preg_match( '/<img[^>]+>/', $body_content, $img );
$button = '<div class="img-container">
<button type="button" aria-haspopup="dialog" aria-label="' . esc_attr( $aria_label ) . '" data-wp-on--click="actions.core.image.showLightbox"></button>'
<button type="button" aria-haspopup="dialog" aria-label="' . esc_attr( $aria_label ) . '" data-wp-on--click="actions.core.image.showLightbox" data-wp-effect="effects.core.image.preloadLightboxImage"></button>'
. $img[0] .
'</div>';
$body_content = preg_replace( '/<img[^>]+>/', $button, $body_content );

// Add src to the modal image.
$m = new WP_HTML_Tag_Processor( $content );
$m->next_tag( 'figure' );
$m->add_class( 'responsive-image' );
$m->next_tag( 'img' );
$m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.imageSrc' );
$modal_content = $m->get_updated_html();
$m->set_attribute( 'src', '' );
$m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.responsiveImgSrc' );
$initial_image_content = $m->get_updated_html();

$q = new WP_HTML_Tag_Processor( $content );
$q->next_tag( 'figure' );
$q->add_class( 'enlarged-image' );
$q->next_tag( 'img' );
$q->set_attribute( 'src', '' );
$q->set_attribute( 'data-wp-bind--src', 'selectors.core.image.enlargedImgSrc' );
$enlarged_image_content = $q->get_updated_html();

$background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) );

Expand All @@ -142,7 +186,8 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
<button type="button" aria-label="$close_button_label" style="fill: $close_button_color" class="close-button" data-wp-on--click="actions.core.image.hideLightbox">
$close_button_icon
</button>
$modal_content
$initial_image_content
$enlarged_image_content
<div class="scrim" style="background-color: $background_color"></div>
</div>
HTML;
Expand Down
78 changes: 59 additions & 19 deletions packages/block-library/src/image/interactivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,43 @@ store( {
core: {
image: {
showLightbox: ( { context, event } ) => {
// We can't initialize the lightbox until the reference
// image is loaded, otherwise the UX is broken.
if ( ! context.core.image.imageLoaded ) {
return;
}
Comment on lines +25 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope time to interactive does not being affected here too much.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the user's bandwidth. But also, I think it's reasonable to prevent a user from zooming in on an image that hasn't fully loaded yet? 🤔

Another note: We can likely make the time to interactivity instant with the fade animation; it's just the zoom animation that causes the trouble.

If it's worth exploring, I could make this initialization step specific to the zoom animation.

Another note: Is the concern here that users may want to go immediately to the zoomed image without needing to wait for the rest of the page to load? @mtias has mentioned before the idea of having a separate URL for the zoomed version of the image inside of the lightbox as an overall usability improvement for WordPress sites.

Perhaps we can continue to explore this in a subsequent PR where we start to identify how this would behave, (what should the URL be, how to implement the experience of initializing the page with the lightbox already open, crossover or not with the Media page, etc.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the user's bandwidth. But also, I think it's reasonable to prevent a user from zooming in on an image that hasn't fully loaded yet? 🤔

It is indeed. But, for example, IGDB on their Lightbox, shows a loading text if the image is not fully loaded. Still, is something that we can explore in future PRs.

Is the concern here that users may want to go immediately to the zoomed image without needing to wait for the rest of the page to load?

Yes, this could happen. Usually, clicking on an image and wait for the zoom to appear is not ideal.

We can wait and see how that ticket is evolving.

context.core.image.initialized = true;
context.core.image.lastFocusedElement =
window.document.activeElement;
context.core.image.scrollDelta = 0;

context.core.image.lightboxEnabled = true;
if ( context.core.image.lightboxAnimation === 'zoom' ) {
setZoomStyles(
event.target.nextElementSibling,
context,
event
);
}
// Hide overflow only when the animation is in progress,
// otherwise the removal of the scrollbars will draw attention
// to itself and look like an error
document.documentElement.classList.add(
'has-lightbox-open'
);

// Since the img is hidden and its src not loaded until
// the lightbox is opened, let's create an img element on the fly
// so we can get the dimensions we need to calculate the styles
context.core.image.preloadInitialized = true;
const imgDom = document.createElement( 'img' );

imgDom.onload = function () {
// Enable the lightbox only after the image
// is loaded to prevent flashing of unstyled content
context.core.image.lightboxEnabled = true;
if ( context.core.image.lightboxAnimation === 'zoom' ) {
setZoomStyles( imgDom, context, event );
}

// Hide overflow only when the animation is in progress,
// otherwise the removal of the scrollbars will draw attention
// to itself and look like an error
document.documentElement.classList.add(
'has-lightbox-open'
);
context.core.image.activateLargeImage = true;
};
imgDom.setAttribute( 'src', context.core.image.imageSrc );
imgDom.setAttribute(
'src',
context.core.image.imageUploadedSrc
);
},
hideLightbox: async ( { context, event } ) => {
context.core.image.hideAnimationEnabled = true;
Expand Down Expand Up @@ -131,9 +142,14 @@ store( {
roleAttribute: ( { context } ) => {
return context.core.image.lightboxEnabled ? 'dialog' : '';
},
imageSrc: ( { context } ) => {
responsiveImgSrc: ( { context } ) => {
return context.core.image.activateLargeImage
? ''
: context.core.image.imageCurrentSrc;
},
enlargedImgSrc: ( { context } ) => {
return context.core.image.initialized
? context.core.image.imageSrc
? context.core.image.imageUploadedSrc
: '';
},
},
Expand All @@ -142,6 +158,30 @@ store( {
effects: {
core: {
image: {
setCurrentSrc: ( { context, ref } ) => {
if ( ref.complete ) {
context.core.image.imageLoaded = true;
context.core.image.imageCurrentSrc = ref.currentSrc;
} else {
ref.addEventListener( 'load', function () {
context.core.image.imageLoaded = true;
context.core.image.imageCurrentSrc =
this.currentSrc;
} );
}
},
preloadLightboxImage: ( { context, ref } ) => {
ref.addEventListener( 'mouseover', () => {
Comment on lines +173 to +174
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @artemiomorales, out of curiosity, why did you use useEffect instead of data-wp-on--mouseover?

Copy link
Contributor Author

@artemiomorales artemiomorales Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this! It's an oversight on my part. Will revise in this upcoming PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no problem. Thanks, Artemio.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luisherranz Update: I decided to break this out into a separate PR for easier review and approval.

if ( ! context.core.image.preloadInitialized ) {
context.core.image.preloadInitialized = true;
const imgDom = document.createElement( 'img' );
imgDom.setAttribute(
'src',
context.core.image.imageUploadedSrc
);
}
} );
},
initLightbox: async ( { context, ref } ) => {
context.core.image.figureRef =
ref.querySelector( 'figure' );
Expand All @@ -163,8 +203,8 @@ store( {
} );

function setZoomStyles( imgDom, context, event ) {
let targetWidth = imgDom.naturalWidth;
let targetHeight = imgDom.naturalHeight;
let targetWidth = context.core.image.targetWidth;
let targetHeight = context.core.image.targetHeight;

const verticalPadding = 40;

Expand Down
25 changes: 19 additions & 6 deletions test/e2e/specs/editor/blocks/image.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -987,13 +987,19 @@ test.describe( 'Image - interactivity', () => {

const lightbox = page.locator( '.wp-lightbox-overlay' );
await expect( lightbox ).toBeHidden();
const image = lightbox.locator( 'img' );
const responsiveImage = lightbox.locator( '.responsive-image img' );
const enlargedImage = lightbox.locator( '.enlarged-image img' );

await expect( image ).toHaveAttribute( 'src', '' );
await expect( responsiveImage ).toHaveAttribute(
'src',
new RegExp( filename )
);
await expect( enlargedImage ).toHaveAttribute( 'src', '' );

await page.getByRole( 'button', { name: 'Enlarge image' } ).click();

await expect( image ).toHaveAttribute(
await expect( responsiveImage ).toHaveAttribute( 'src', '' );
await expect( enlargedImage ).toHaveAttribute(
'src',
new RegExp( filename )
);
Expand Down Expand Up @@ -1176,12 +1182,19 @@ test.describe( 'Image - interactivity', () => {
await page.goto( `/?p=${ postId }` );

const lightbox = page.locator( '.wp-lightbox-overlay' );
const imageDom = lightbox.locator( 'img' );
await expect( imageDom ).toHaveAttribute( 'src', '' );
const responsiveImage = lightbox.locator( '.responsive-image img' );
const enlargedImage = lightbox.locator( '.enlarged-image img' );

await expect( responsiveImage ).toHaveAttribute(
'src',
new RegExp( imgUrl )
);
await expect( enlargedImage ).toHaveAttribute( 'src', '' );

await page.getByRole( 'button', { name: 'Enlarge image' } ).click();

await expect( imageDom ).toHaveAttribute( 'src', imgUrl );
await expect( responsiveImage ).toHaveAttribute( 'src', '' );
await expect( enlargedImage ).toHaveAttribute( 'src', imgUrl );
} );
} );

Expand Down