';
+ $button =
+ ''
+ . $img[0];
$body_content = preg_replace( '/]+>/', $button, $body_content );
- // Add src to the modal image.
+ // We need both a responsive image and an enlarged image to animate
+ // the zoom seamlessly on slow internet connections; the responsive
+ // image is a copy of the one in the body, which animates immediately
+ // as the lightbox is opened, while the enlarged one is a full-sized
+ // version that will likely still be loading as the animation begins.
$m = new WP_HTML_Tag_Processor( $content );
$m->next_tag( 'figure' );
$m->add_class( 'responsive-image' );
$m->next_tag( 'img' );
+ // We want to set the 'src' attribute to an empty string in the responsive image
+ // because otherwise, as of this writing, the wp_filter_content_tags() function in
+ // WordPress will automatically add a 'srcset' attribute to the image, which will at
+ // times cause the incorrectly sized image to be loaded in the lightbox on Firefox.
+ // Because of this, we bind the 'src' attribute explicitly the current src to reliably
+ // use the exact same image as in the content when the lightbox is first opened while
+ // we wait for the larger image to load.
$m->set_attribute( 'src', '' );
- $m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.responsiveImgSrc' );
+ $m->set_attribute( 'data-wp-bind--src', 'context.core.image.imageCurrentSrc' );
+ $m->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' );
$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' );
+
+ // We set the 'src' attribute to an empty string to prevent the browser from loading the image
+ // on initial page load, then bind the attribute to a selector that returns the full-sized image src when
+ // the lightbox is opened. We could use 'loading=lazy' in combination with the 'hidden' attribute to
+ // accomplish the same behavior, but that approach breaks progressive loading of the image in Safari
+ // and Chrome (see https://github.com/WordPress/gutenberg/pull/52765#issuecomment-1674008151). Until that
+ // is resolved, manually setting the 'src' seems to be the best solution to load the large image on demand.
$q->set_attribute( 'src', '' );
$q->set_attribute( 'data-wp-bind--src', 'selectors.core.image.enlargedImgSrc' );
+ $q->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' );
$enlarged_image_content = $q->get_updated_html();
$background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) );
@@ -185,8 +225,8 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
- $initial_image_content
- $enlarged_image_content
+
$initial_image_content
+
$enlarged_image_content
HTML;
diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss
index e6816e873b776..c7eec224f6587 100644
--- a/packages/block-library/src/image/style.scss
+++ b/packages/block-library/src/image/style.scss
@@ -153,10 +153,7 @@
}
.wp-lightbox-container {
-
- .img-container {
- position: relative;
- }
+ position: relative;
button {
border: none;
@@ -183,6 +180,7 @@
overflow: hidden;
width: 100vw;
height: 100vh;
+ box-sizing: border-box;
visibility: hidden;
cursor: zoom-out;
@@ -195,25 +193,39 @@
z-index: 5000000;
}
+ .lightbox-image-container {
+ position: absolute;
+ overflow: hidden;
+ top: 50%;
+ left: 50%;
+ transform-origin: top left;
+ transform: translate(-50%, -50%);
+ width: var(--wp--lightbox-container-width);
+ height: var(--wp--lightbox-container-height);
+ z-index: 9999999999;
+ }
+
.wp-block-image {
+ position: relative;
+ transform-origin: 0 0;
+ display: flex;
width: 100%;
height: 100%;
- position: absolute;
- z-index: 3000000;
- box-sizing: border-box;
- display: flex;
justify-content: center;
align-items: center;
- flex-direction: column;
+ box-sizing: border-box;
+ z-index: 3000000;
+ margin: 0;
- figcaption {
- display: none;
+ img {
+ min-width: var(--wp--lightbox-image-width);
+ min-height: var(--wp--lightbox-image-height);
+ width: var(--wp--lightbox-image-width);
+ height: var(--wp--lightbox-image-height);
}
- img {
- max-width: 100%;
- max-height: 100%;
- width: auto;
+ figcaption {
+ display: none;
}
}
@@ -231,78 +243,61 @@
opacity: 0.9;
}
- &.fade {
- .wp-block-image {
- padding: 40px 0;
-
- @media screen and (min-width: 480px) {
- padding: 40px;
- }
-
- @media screen and (min-width: 1920px) {
- padding: 40px 80px;
- }
+ // When fading, make the image come in slightly slower
+ // or faster than the scrim to give a sense of depth.
+ &.active {
+ visibility: visible;
+ animation: both turn-on-visibility 0.25s;
+ img {
+ animation: both turn-on-visibility 0.35s;
}
-
- &.active {
- visibility: visible;
- animation: both turn-on-visibility 0.25s;
-
+ }
+ &.hideanimationenabled {
+ &:not(.active) {
+ animation: both turn-off-visibility 0.35s;
img {
- animation: both turn-on-visibility 0.3s;
- }
- }
- &.hideanimationenabled {
- &:not(.active) {
- animation: both turn-off-visibility 0.3s;
-
- img {
- animation: both turn-off-visibility 0.25s;
- }
+ animation: both turn-off-visibility 0.25s;
}
}
}
- &.zoom {
- img {
- position: absolute;
- transform-origin: top left;
- width: var(--lightbox-image-max-width);
- height: var(--lightbox-image-max-height);
- }
-
- &.active {
- opacity: 1;
- visibility: visible;
- .wp-block-image img {
- animation: lightbox-zoom-in 0.4s forwards;
-
- @media (prefers-reduced-motion) {
- animation: both turn-on-visibility 0.4s;
- }
- }
- .scrim {
- animation: turn-on-visibility 0.4s forwards;
- }
- }
- &.hideanimationenabled {
- &:not(.active) {
- .wp-block-image img {
- animation: lightbox-zoom-out 0.4s forwards;
-
- @media (prefers-reduced-motion) {
- animation: both turn-off-visibility 0.4s;
+ @media (prefers-reduced-motion: no-preference) {
+ &.zoom {
+ &.active {
+ opacity: 1;
+ visibility: visible;
+ animation: none;
+ .lightbox-image-container {
+ animation: lightbox-zoom-in 0.4s;
+ // Override fade animation for image
+ img {
+ animation: none;
}
}
.scrim {
- animation: turn-off-visibility 0.4s forwards;
+ animation: turn-on-visibility 0.4s forwards;
+ }
+ }
+ &.hideanimationenabled {
+ &:not(.active) {
+ animation: none;
+ .lightbox-image-container {
+ animation: lightbox-zoom-out 0.4s;
+ // Override fade animation for image
+ img {
+ animation: none;
+ }
+ }
+ .scrim {
+ animation: turn-off-visibility 0.4s forwards;
+ }
}
}
}
}
}
-html.has-lightbox-open {
+html.wp-has-lightbox-open {
overflow: hidden;
}
@@ -332,30 +327,23 @@ html.has-lightbox-open {
@keyframes lightbox-zoom-in {
0% {
- left: var(--lightbox-initial-left-position);
- top: var(--lightbox-initial-top-position);
- transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height));
+ transform: translate(calc(-50vw + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale));
}
100% {
- left: var(--lightbox-target-left-position);
- top: var(--lightbox-target-top-position);
- transform: scale(1, 1);
+ transform: translate(-50%, -50%) scale(1, 1);
}
}
@keyframes lightbox-zoom-out {
0% {
visibility: visible;
- left: var(--lightbox-target-left-position);
- top: var(--lightbox-target-top-position);
- transform: scale(1, 1);
+ transform: translate(-50%, -50%) scale(1, 1);
}
99% {
visibility: visible;
}
100% {
- left: var(--lightbox-initial-left-position);
- top: var(--lightbox-initial-top-position);
- transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height));
+ visibility: hidden;
+ transform: translate(calc(-50vw + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale));
}
}
diff --git a/packages/block-library/src/image/view-interactivity.js b/packages/block-library/src/image/view-interactivity.js
index e0fd68646964b..53a2d1f7d567f 100644
--- a/packages/block-library/src/image/view-interactivity.js
+++ b/packages/block-library/src/image/view-interactivity.js
@@ -17,293 +17,431 @@ const focusableSelectors = [
'[tabindex]:not([tabindex^="-"])',
];
-store( {
- actions: {
- 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;
- }
- 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 () {
- context.core.image.activateLargeImage = true;
- };
- imgDom.setAttribute(
- 'src',
- context.core.image.imageUploadedSrc
- );
+store(
+ {
+ state: {
+ core: {
+ image: {
+ windowWidth: window.innerWidth,
+ windowHeight: window.innerHeight,
},
- hideLightbox: async ( { context, event } ) => {
- context.core.image.hideAnimationEnabled = true;
- if ( context.core.image.lightboxEnabled ) {
- // If scrolling, wait a moment before closing the lightbox.
- if ( context.core.image.lightboxAnimation === 'fade' ) {
- context.core.image.scrollDelta += event.deltaY;
- if (
- event.type === 'mousewheel' &&
- Math.abs(
- window.scrollY -
- context.core.image.scrollDelta
- ) < 10
- ) {
- return;
- }
- } else if (
- context.core.image.lightboxAnimation === 'zoom'
- ) {
- // Disable scroll until the zoom animation ends.
- // Get the current page scroll position
- const scrollTop =
- window.pageYOffset ||
- document.documentElement.scrollTop;
- const scrollLeft =
- window.pageXOffset ||
- document.documentElement.scrollLeft;
- // if any scroll is attempted, set this to the previous value.
- window.onscroll = function () {
- window.scrollTo( scrollLeft, scrollTop );
- };
- // Enable scrolling after the animation finishes
- setTimeout( function () {
- window.onscroll = function () {};
- }, 400 );
+ },
+ },
+ actions: {
+ 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;
}
+ context.core.image.initialized = true;
+ context.core.image.lastFocusedElement =
+ window.document.activeElement;
+ context.core.image.scrollDelta = 0;
- document.documentElement.classList.remove(
- 'has-lightbox-open'
+ context.core.image.lightboxEnabled = true;
+ setStyles( 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(
+ 'wp-has-lightbox-open'
);
-
- context.core.image.lightboxEnabled = false;
- context.core.image.lastFocusedElement.focus();
- }
- },
- handleKeydown: ( { context, actions, event } ) => {
- if ( context.core.image.lightboxEnabled ) {
- if ( event.key === 'Tab' || event.keyCode === 9 ) {
- // If shift + tab it change the direction
+ },
+ hideLightbox: async ( { context, event } ) => {
+ context.core.image.hideAnimationEnabled = true;
+ if ( context.core.image.lightboxEnabled ) {
+ // If scrolling, wait a moment before closing the lightbox.
if (
- event.shiftKey &&
- window.document.activeElement ===
- context.core.image.firstFocusableElement
+ context.core.image.lightboxAnimation === 'fade'
) {
- event.preventDefault();
- context.core.image.lastFocusableElement.focus();
+ context.core.image.scrollDelta += event.deltaY;
+ if (
+ event.type === 'mousewheel' &&
+ Math.abs(
+ window.scrollY -
+ context.core.image.scrollDelta
+ ) < 10
+ ) {
+ return;
+ }
} else if (
- ! event.shiftKey &&
- window.document.activeElement ===
- context.core.image.lastFocusableElement
+ context.core.image.lightboxAnimation === 'zoom'
) {
- event.preventDefault();
- context.core.image.firstFocusableElement.focus();
+ // Disable scroll until the zoom animation ends.
+ // Get the current page scroll position
+ const scrollTop =
+ window.pageYOffset ||
+ document.documentElement.scrollTop;
+ const scrollLeft =
+ window.pageXOffset ||
+ document.documentElement.scrollLeft;
+ // if any scroll is attempted, set this to the previous value.
+ window.onscroll = function () {
+ window.scrollTo( scrollLeft, scrollTop );
+ };
+ // Enable scrolling after the animation finishes
+ setTimeout( function () {
+ window.onscroll = function () {};
+ }, 400 );
}
- }
- if ( event.key === 'Escape' || event.keyCode === 27 ) {
- actions.core.image.hideLightbox( {
- context,
- event,
+ document.documentElement.classList.remove(
+ 'wp-has-lightbox-open'
+ );
+
+ context.core.image.lightboxEnabled = false;
+ context.core.image.lastFocusedElement.focus( {
+ preventScroll: true,
} );
}
- }
- },
- preloadLightboxImage: ( { context } ) => {
- if ( ! context.core.image.preloadInitialized ) {
- context.core.image.preloadInitialized = true;
- const imgDom = document.createElement( 'img' );
- imgDom.setAttribute(
- 'src',
- context.core.image.imageUploadedSrc
- );
- }
+ },
+ handleKeydown: ( { context, actions, event } ) => {
+ if ( context.core.image.lightboxEnabled ) {
+ if ( event.key === 'Tab' || event.keyCode === 9 ) {
+ // If shift + tab it change the direction
+ if (
+ event.shiftKey &&
+ window.document.activeElement ===
+ context.core.image.firstFocusableElement
+ ) {
+ event.preventDefault();
+ context.core.image.lastFocusableElement.focus();
+ } else if (
+ ! event.shiftKey &&
+ window.document.activeElement ===
+ context.core.image.lastFocusableElement
+ ) {
+ event.preventDefault();
+ context.core.image.firstFocusableElement.focus();
+ }
+ }
+
+ if (
+ event.key === 'Escape' ||
+ event.keyCode === 27
+ ) {
+ actions.core.image.hideLightbox( {
+ context,
+ event,
+ } );
+ }
+ }
+ },
+ handleLoad: ( { state, context, effects, ref } ) => {
+ context.core.image.imageLoaded = true;
+ context.core.image.imageCurrentSrc = ref.currentSrc;
+ effects.core.image.setButtonStyles( {
+ state,
+ context,
+ ref,
+ } );
+ },
},
},
},
- },
- selectors: {
- core: {
- image: {
- roleAttribute: ( { context } ) => {
- return context.core.image.lightboxEnabled ? 'dialog' : '';
- },
- responsiveImgSrc: ( { context } ) => {
- return context.core.image.activateLargeImage &&
- context.core.image.hideAnimationEnabled
- ? ''
- : context.core.image.imageCurrentSrc;
- },
- enlargedImgSrc: ( { context } ) => {
- return context.core.image.initialized
- ? context.core.image.imageUploadedSrc
- : '';
+ selectors: {
+ core: {
+ image: {
+ roleAttribute: ( { context } ) => {
+ return context.core.image.lightboxEnabled
+ ? 'dialog'
+ : '';
+ },
+ lightboxObjectFit: ( { context } ) => {
+ if ( context.core.image.initialized ) {
+ return 'cover';
+ }
+ },
+ enlargedImgSrc: ( { context } ) => {
+ return context.core.image.initialized
+ ? context.core.image.imageUploadedSrc
+ : '';
+ },
},
},
},
- },
- 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 () {
+ effects: {
+ core: {
+ image: {
+ setCurrentSrc: ( { context, ref } ) => {
+ if ( ref.complete ) {
context.core.image.imageLoaded = true;
- context.core.image.imageCurrentSrc =
- this.currentSrc;
- } );
- }
- },
- initLightbox: async ( { context, ref } ) => {
- context.core.image.figureRef =
- ref.querySelector( 'figure' );
- context.core.image.imageRef = ref.querySelector( 'img' );
- if ( context.core.image.lightboxEnabled ) {
- const focusableElements =
- ref.querySelectorAll( focusableSelectors );
- context.core.image.firstFocusableElement =
- focusableElements[ 0 ];
- context.core.image.lastFocusableElement =
- focusableElements[ focusableElements.length - 1 ];
+ context.core.image.imageCurrentSrc = ref.currentSrc;
+ }
+ },
+ initLightbox: async ( { context, ref } ) => {
+ context.core.image.figureRef =
+ ref.querySelector( 'figure' );
+ context.core.image.imageRef =
+ ref.querySelector( 'img' );
+ if ( context.core.image.lightboxEnabled ) {
+ const focusableElements =
+ ref.querySelectorAll( focusableSelectors );
+ context.core.image.firstFocusableElement =
+ focusableElements[ 0 ];
+ context.core.image.lastFocusableElement =
+ focusableElements[
+ focusableElements.length - 1
+ ];
+
+ ref.querySelector( '.close-button' ).focus();
+ }
+ },
+ setButtonStyles: ( { state, context, ref } ) => {
+ const {
+ naturalWidth,
+ naturalHeight,
+ offsetWidth,
+ offsetHeight,
+ } = ref;
+
+ // If the image isn't loaded yet, we can't
+ // calculate how big the button should be.
+ if ( naturalWidth === 0 || naturalHeight === 0 ) {
+ return;
+ }
- ref.querySelector( '.close-button' ).focus();
- }
+ // Subscribe to the window dimensions so we can
+ // recalculate the styles if the window is resized.
+ if (
+ ( state.core.image.windowWidth ||
+ state.core.image.windowHeight ) &&
+ context.core.image.scaleAttr === 'contain'
+ ) {
+ // In the case of an image with object-fit: contain, the
+ // size of the img element can be larger than the image itself,
+ // so we need to calculate the size of the button to match.
+
+ // Natural ratio of the image.
+ const naturalRatio = naturalWidth / naturalHeight;
+ // Offset ratio of the image.
+ const offsetRatio = offsetWidth / offsetHeight;
+
+ if ( naturalRatio > offsetRatio ) {
+ // If it reaches the width first, keep
+ // the width and recalculate the height.
+ context.core.image.imageButtonWidth =
+ offsetWidth;
+ const buttonHeight = offsetWidth / naturalRatio;
+ context.core.image.imageButtonHeight =
+ buttonHeight;
+ context.core.image.imageButtonTop =
+ ( offsetHeight - buttonHeight ) / 2;
+ } else {
+ // If it reaches the height first, keep
+ // the height and recalculate the width.
+ context.core.image.imageButtonHeight =
+ offsetHeight;
+ const buttonWidth = offsetHeight * naturalRatio;
+ context.core.image.imageButtonWidth =
+ buttonWidth;
+ context.core.image.imageButtonLeft =
+ ( offsetWidth - buttonWidth ) / 2;
+ }
+ } else {
+ // In all other cases, we can trust that the size of
+ // the image is the right size for the button as well.
+
+ context.core.image.imageButtonWidth = offsetWidth;
+ context.core.image.imageButtonHeight = offsetHeight;
+ }
+ },
},
},
},
},
-} );
+ {
+ afterLoad: ( { state } ) => {
+ window.addEventListener(
+ 'resize',
+ debounce( () => {
+ state.core.image.windowWidth = window.innerWidth;
+ state.core.image.windowHeight = window.innerHeight;
+ } )
+ );
+ },
+ }
+);
+
+function setStyles( context, event ) {
+ // The reference img element lies adjacent
+ // to the event target button in the DOM.
+ let {
+ naturalWidth,
+ naturalHeight,
+ offsetWidth: originalWidth,
+ offsetHeight: originalHeight,
+ } = event.target.nextElementSibling;
+ let { x: screenPosX, y: screenPosY } =
+ event.target.nextElementSibling.getBoundingClientRect();
+
+ // Natural ratio of the image clicked to open the lightbox.
+ const naturalRatio = naturalWidth / naturalHeight;
+ // Original ratio of the image clicked to open the lightbox.
+ let originalRatio = originalWidth / originalHeight;
+
+ // If it has object-fit: contain, recalculate the original sizes
+ // and the screen position without the blank spaces.
+ if ( context.core.image.scaleAttr === 'contain' ) {
+ if ( naturalRatio > originalRatio ) {
+ const heightWithoutSpace = originalWidth / naturalRatio;
+ // Recalculate screen position without the top space.
+ screenPosY += ( originalHeight - heightWithoutSpace ) / 2;
+ originalHeight = heightWithoutSpace;
+ } else {
+ const widthWithoutSpace = originalHeight * naturalRatio;
+ // Recalculate screen position without the left space.
+ screenPosX += ( originalWidth - widthWithoutSpace ) / 2;
+ originalWidth = widthWithoutSpace;
+ }
+ }
+ originalRatio = originalWidth / originalHeight;
-function setZoomStyles( imgDom, context, event ) {
// Typically, we use the image's full-sized dimensions. If those
// dimensions have not been set (i.e. an external image with only one size),
// the image's dimensions in the lightbox are the same
// as those of the image in the content.
- let targetWidth =
+ let imgMaxWidth = parseFloat(
context.core.image.targetWidth !== 'none'
? context.core.image.targetWidth
- : event.target.nextElementSibling.naturalWidth;
- let targetHeight =
+ : naturalWidth
+ );
+ let imgMaxHeight = parseFloat(
context.core.image.targetHeight !== 'none'
? context.core.image.targetHeight
- : event.target.nextElementSibling.naturalHeight;
+ : naturalHeight
+ );
+
+ // Ratio of the biggest image stored in the database.
+ let imgRatio = imgMaxWidth / imgMaxHeight;
+ let containerMaxWidth = imgMaxWidth;
+ let containerMaxHeight = imgMaxHeight;
+ let containerWidth = imgMaxWidth;
+ let containerHeight = imgMaxHeight;
+ // Check if the target image has a different ratio than the original one (thumbnail).
+ // Recalculate the width and height.
+ if ( naturalRatio.toFixed( 2 ) !== imgRatio.toFixed( 2 ) ) {
+ if ( naturalRatio > imgRatio ) {
+ // If the width is reached before the height, we keep the maxWidth
+ // and recalculate the height.
+ // Unless the difference between the maxHeight and the reducedHeight
+ // is higher than the maxWidth, where we keep the reducedHeight and
+ // recalculate the width.
+ const reducedHeight = imgMaxWidth / naturalRatio;
+ if ( imgMaxHeight - reducedHeight > imgMaxWidth ) {
+ imgMaxHeight = reducedHeight;
+ imgMaxWidth = reducedHeight * naturalRatio;
+ } else {
+ imgMaxHeight = imgMaxWidth / naturalRatio;
+ }
+ } else {
+ // If the height is reached before the width, we keep the maxHeight
+ // and recalculate the width.
+ // Unless the difference between the maxWidth and the reducedWidth
+ // is higher than the maxHeight, where we keep the reducedWidth and
+ // recalculate the height.
+ const reducedWidth = imgMaxHeight * naturalRatio;
+ if ( imgMaxWidth - reducedWidth > imgMaxHeight ) {
+ imgMaxWidth = reducedWidth;
+ imgMaxHeight = reducedWidth / naturalRatio;
+ } else {
+ imgMaxWidth = imgMaxHeight * naturalRatio;
+ }
+ }
+ containerWidth = imgMaxWidth;
+ containerHeight = imgMaxHeight;
+ imgRatio = imgMaxWidth / imgMaxHeight;
- // Since the lightbox image has `position:absolute`, it
- // ignores its parent's padding, so we need to set padding here
- // to calculate dimensions and positioning.
+ // Calculate the max size of the container.
+ if ( originalRatio > imgRatio ) {
+ containerMaxWidth = imgMaxWidth;
+ containerMaxHeight = containerMaxWidth / originalRatio;
+ } else {
+ containerMaxHeight = imgMaxHeight;
+ containerMaxWidth = containerMaxHeight * originalRatio;
+ }
+ }
- // As per the design, let's constrain the height with fixed padding
- const containerOuterHeight = window.innerHeight;
- const verticalPadding = 40;
- const containerInnerHeight = containerOuterHeight - verticalPadding * 2;
+ // If the image has been pixelated on purpose, keep that size.
+ if ( originalWidth > containerWidth || originalHeight > containerHeight ) {
+ containerWidth = originalWidth;
+ containerHeight = originalHeight;
+ }
- // Let's set a variable horizontal padding based on the container width
- const containerOuterWidth = window.innerWidth;
+ // Calculate the final lightbox image size and the
+ // scale factor. MaxWidth is either the window container
+ // (accounting for padding) or the image resolution.
let horizontalPadding = 0;
- if ( containerOuterWidth > 480 ) {
- horizontalPadding = 40;
- } else if ( containerOuterWidth > 1920 ) {
+ if ( window.innerWidth > 480 ) {
horizontalPadding = 80;
+ } else if ( window.innerWidth > 1920 ) {
+ horizontalPadding = 160;
}
- const containerInnerWidth = containerOuterWidth - horizontalPadding * 2;
+ const verticalPadding = 80;
- // Check difference between the image and figure dimensions
- const widthOverflow = Math.abs(
- Math.min( containerInnerWidth - targetWidth, 0 )
+ const targetMaxWidth = Math.min(
+ window.innerWidth - horizontalPadding,
+ containerWidth
);
- const heightOverflow = Math.abs(
- Math.min( containerInnerHeight - targetHeight, 0 )
+ const targetMaxHeight = Math.min(
+ window.innerHeight - verticalPadding,
+ containerHeight
);
+ const targetContainerRatio = targetMaxWidth / targetMaxHeight;
- // If the image is larger than the container, let's resize
- // it along the greater axis relative to the container
- if ( widthOverflow > 0 || heightOverflow > 0 ) {
- const containerInnerAspectRatio =
- containerInnerWidth / containerInnerHeight;
- const imageAspectRatio = targetWidth / targetHeight;
-
- if ( imageAspectRatio > containerInnerAspectRatio ) {
- targetWidth = containerInnerWidth;
- targetHeight =
- ( targetWidth * imgDom.naturalHeight ) / imgDom.naturalWidth;
- } else {
- targetHeight = containerInnerHeight;
- targetWidth =
- ( targetHeight * imgDom.naturalWidth ) / imgDom.naturalHeight;
- }
+ if ( originalRatio > targetContainerRatio ) {
+ // If targetMaxWidth is reached before targetMaxHeight
+ containerWidth = targetMaxWidth;
+ containerHeight = containerWidth / originalRatio;
+ } else {
+ // If targetMaxHeight is reached before targetMaxWidth
+ containerHeight = targetMaxHeight;
+ containerWidth = containerHeight * originalRatio;
}
- // The reference img element lies adjacent to the event target button in the DOM
- const { x: originLeft, y: originTop } =
- event.target.nextElementSibling.getBoundingClientRect();
- const scaleWidth =
- event.target.nextElementSibling.offsetWidth / targetWidth;
- const scaleHeight =
- event.target.nextElementSibling.offsetHeight / targetHeight;
+ const containerScale = originalWidth / containerWidth;
+ const lightboxImgWidth =
+ imgMaxWidth * ( containerWidth / containerMaxWidth );
+ const lightboxImgHeight =
+ imgMaxHeight * ( containerHeight / containerMaxHeight );
- // Get values used to center the image
- let targetLeft = 0;
- if ( targetWidth >= containerInnerWidth ) {
- targetLeft = horizontalPadding;
- } else {
- targetLeft = ( containerOuterWidth - targetWidth ) / 2;
- }
- let targetTop = 0;
- if ( targetHeight >= containerInnerHeight ) {
- targetTop = verticalPadding;
- } else {
- targetTop = ( containerOuterHeight - targetHeight ) / 2;
+ // Add the CSS variables needed.
+ let styleTag = document.getElementById( 'wp-lightbox-styles' );
+ if ( ! styleTag ) {
+ styleTag = document.createElement( 'style' );
+ styleTag.id = 'wp-lightbox-styles';
+ document.head.appendChild( styleTag );
}
- const root = document.documentElement;
- root.style.setProperty( '--lightbox-scale-width', scaleWidth );
- root.style.setProperty( '--lightbox-scale-height', scaleHeight );
- root.style.setProperty( '--lightbox-image-max-width', targetWidth + 'px' );
- root.style.setProperty(
- '--lightbox-image-max-height',
- targetHeight + 'px'
- );
- root.style.setProperty(
- '--lightbox-initial-left-position',
- originLeft + 'px'
- );
- root.style.setProperty(
- '--lightbox-initial-top-position',
- originTop + 'px'
- );
- root.style.setProperty(
- '--lightbox-target-left-position',
- targetLeft + 'px'
- );
- root.style.setProperty(
- '--lightbox-target-top-position',
- targetTop + 'px'
- );
+ // As of this writing, using the calculations above will render the lightbox
+ // with a small, erroneous whitespace on the left side of the image in iOS Safari,
+ // perhaps due to an inconsistency in how browsers handle absolute positioning and CSS
+ // transformation. In any case, adding 1 pixel to the container width and height solves
+ // the problem, though this can be removed if the issue is fixed in the future.
+ styleTag.innerHTML = `
+ :root {
+ --wp--lightbox-initial-top-position: ${ screenPosY }px;
+ --wp--lightbox-initial-left-position: ${ screenPosX }px;
+ --wp--lightbox-container-width: ${ containerWidth + 1 }px;
+ --wp--lightbox-container-height: ${ containerHeight + 1 }px;
+ --wp--lightbox-image-width: ${ lightboxImgWidth }px;
+ --wp--lightbox-image-height: ${ lightboxImgHeight }px;
+ --wp--lightbox-scale: ${ containerScale };
+ }
+ `;
+}
+
+function debounce( func, wait = 50 ) {
+ let timeout;
+ return () => {
+ const later = () => {
+ timeout = null;
+ func();
+ };
+ clearTimeout( timeout );
+ timeout = setTimeout( later, wait );
+ };
}
diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js
index ce18b9599fbfb..c9d7bbe464428 100644
--- a/test/e2e/specs/editor/blocks/image.spec.js
+++ b/test/e2e/specs/editor/blocks/image.spec.js
@@ -885,40 +885,41 @@ test.describe( 'Image - interactivity', () => {
await expect( lightbox ).toBeVisible();
- const document = page.getByRole( 'document' );
- const lightboxStyleCheck = await document.evaluate( ( doc ) => {
- // We don't have access to the getPropertyValue() method
- // on the CSSStyleDeclaration returned form getComputedStyle()
- // in the Playwright outer context, so we need to evaluate it here
- // in the browser context here.
- const documentStyles = window.getComputedStyle( doc );
- const lightboxStyleVars = [
- '--lightbox-scale-width',
- '--lightbox-scale-height',
- '--lightbox-image-max-width',
- '--lightbox-image-max-height',
- '--lightbox-initial-left-position',
- '--lightbox-initial-top-position',
- ];
- const lightboxStyleErrors = [];
- lightboxStyleVars.forEach( ( styleVar ) => {
- if ( ! documentStyles.getPropertyValue( styleVar ) ) {
- lightboxStyleErrors.push( styleVar );
- }
- } );
-
- return lightboxStyleErrors.length > 0
- ? lightboxStyleErrors
- : true;
+ // Use page.evaluate to get the content of the style tag
+ const styleTagContent = await page.evaluate( () => {
+ const styleTag = document.querySelector(
+ 'style#wp-lightbox-styles'
+ );
+ return styleTag ? styleTag.textContent : '';
} );
- expect( lightboxStyleCheck ).toBe( true );
+
+ // Define the keys you want to check for
+ const keysToCheck = [
+ '--wp--lightbox-initial-top-position',
+ '--wp--lightbox-initial-left-position',
+ '--wp--lightbox-container-width',
+ '--wp--lightbox-container-height',
+ '--wp--lightbox-image-width',
+ '--wp--lightbox-image-height',
+ '--wp--lightbox-scale',
+ ];
+
+ // Check if all the keys are present in the style tag's content
+ const keysPresent = keysToCheck.every( ( key ) =>
+ styleTagContent.includes( key )
+ );
+
+ expect( keysPresent ).toBe( true );
const closeButton = lightbox.getByRole( 'button', {
name: 'Close',
} );
await closeButton.click();
- await expect( responsiveImage ).toHaveAttribute( 'src', '' );
+ await expect( responsiveImage ).toHaveAttribute(
+ 'src',
+ contentImageCurrentSrc
+ );
await expect( enlargedImage ).toHaveAttribute(
'src',
imageUploadedSrc
@@ -1013,7 +1014,10 @@ test.describe( 'Image - interactivity', () => {
} );
await closeButton.click();
- await expect( responsiveImage ).toHaveAttribute( 'src', '' );
+ await expect( responsiveImage ).toHaveAttribute(
+ 'src',
+ contentImageCurrentSrc
+ );
await expect( enlargedImage ).toHaveAttribute(
'src',
imageUploadedSrc
@@ -1362,7 +1366,7 @@ test.describe( 'Image - interactivity', () => {
await page.getByRole( 'button', { name: 'Close' } ).click();
- await expect( responsiveImage ).toHaveAttribute( 'src', '' );
+ await expect( responsiveImage ).toHaveAttribute( 'src', imgUrl );
await expect( enlargedImage ).toHaveAttribute( 'src', imgUrl );
} );
} );