diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php
index e1f71964622c0c..c1b0b53ae13d0d 100644
--- a/packages/block-library/src/image/index.php
+++ b/packages/block-library/src/image/index.php
@@ -197,13 +197,17 @@ function block_core_image_render_lightbox( $block_content, $block ) {
)
);
$w->next_tag( 'img' );
- $w->set_attribute( 'data-wp-init', 'effects.core.image.setCurrentSrc' );
+ $w->set_attribute( 'data-wp-init', 'effects.core.image.initOriginImage' );
$w->set_attribute( 'data-wp-on--load', 'actions.core.image.handleLoad' );
$w->set_attribute( 'data-wp-effect', 'effects.core.image.setButtonStyles' );
+ // We need to set an event callback on the `img` specifically
+ // because the `figure` element can also contain a caption, and
+ // we don't want to trigger the lightbox when the caption is clicked.
+ $w->set_attribute( 'data-wp-on--click', 'actions.core.image.showLightbox' );
$w->set_attribute( 'data-wp-effect--setStylesOnResize', 'effects.core.image.setStylesOnResize' );
$body_content = $w->get_updated_html();
- // Wrap the image in the body content with a button.
+ // Add a button alongside image in the body content.
$img = null;
preg_match( '/]+>/', $body_content, $img );
@@ -214,11 +218,17 @@ function block_core_image_render_lightbox( $block_content, $block ) {
aria-haspopup="dialog"
aria-label="' . esc_attr( $aria_label ) . '"
data-wp-on--click="actions.core.image.showLightbox"
- data-wp-style--width="context.core.image.imageButtonWidth"
- data-wp-style--height="context.core.image.imageButtonHeight"
- data-wp-style--left="context.core.image.imageButtonLeft"
+ data-wp-style--right="context.core.image.imageButtonRight"
data-wp-style--top="context.core.image.imageButtonTop"
- >';
+ style="background: #000"
+ >
+
+ ';
$body_content = preg_replace( '/]+>/', $button, $body_content );
diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss
index 2ef602982e57b5..0fde1262fdec2d 100644
--- a/packages/block-library/src/image/style.scss
+++ b/packages/block-library/src/image/style.scss
@@ -157,14 +157,28 @@
display: flex;
flex-direction: column;
+ img {
+ cursor: zoom-in;
+ }
+
+ img:hover + button {
+ opacity: 1;
+ }
+
button {
+ opacity: 0;
border: none;
- background: none;
+ background: #000;
cursor: zoom-in;
- width: 100%;
- height: 100%;
+ width: 24px;
+ height: 24px;
position: absolute;
z-index: 100;
+ top: 10px;
+ right: 10px;
+ text-align: center;
+ padding: 0;
+ border-radius: 10%;
&:focus-visible {
outline: 5px auto #212121;
@@ -172,10 +186,19 @@
outline-offset: 5px;
}
+ &:hover {
+ cursor: pointer;
+ opacity: 1;
+ }
+
+ &:focus {
+ opacity: 1;
+ }
+
&:hover,
&:focus,
&:not(:hover):not(:active):not(.has-background) {
- background: none;
+ background: #000;
border: none;
}
}
diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js
index 3f2242ad737f02..30d1259637e3d9 100644
--- a/packages/block-library/src/image/view.js
+++ b/packages/block-library/src/image/view.js
@@ -103,12 +103,10 @@ store(
context.core.image.lastFocusedElement =
window.document.activeElement;
context.core.image.scrollDelta = 0;
+ context.core.image.pointerType = event.pointerType;
context.core.image.lightboxEnabled = true;
- setStyles(
- context,
- event.target.previousElementSibling
- );
+ setStyles( context, context.core.image.imageRef );
context.core.image.scrollTopReset =
window.pageYOffset ||
@@ -137,7 +135,7 @@ store(
false
);
},
- hideLightbox: async ( { context } ) => {
+ hideLightbox: async ( { context, event } ) => {
context.core.image.hideAnimationEnabled = true;
if ( context.core.image.lightboxEnabled ) {
// We want to wait until the close animation is completed
@@ -154,9 +152,16 @@ store(
}, 450 );
context.core.image.lightboxEnabled = false;
- context.core.image.lastFocusedElement.focus( {
- preventScroll: true,
- } );
+
+ // We want to avoid drawing attention to the button
+ // after the lightbox closes for mouse and touch users.
+ // Note that the `event.pointerType` property returns
+ // as an empty string if a keyboard fired the event.
+ if ( event.pointerType === '' ) {
+ context.core.image.lastFocusedElement.focus( {
+ preventScroll: true,
+ } );
+ }
}
},
handleKeydown: ( { context, actions, event } ) => {
@@ -191,11 +196,12 @@ store(
}
}
},
- handleLoad: ( { state, context, effects, ref } ) => {
+ // This is fired just by lazily loaded
+ // images on the page, not all images.
+ handleLoad: ( { context, effects, ref } ) => {
context.core.image.imageLoaded = true;
context.core.image.imageCurrentSrc = ref.currentSrc;
effects.core.image.setButtonStyles( {
- state,
context,
ref,
} );
@@ -258,17 +264,14 @@ store(
effects: {
core: {
image: {
- setCurrentSrc: ( { context, ref } ) => {
+ initOriginImage: ( { context, ref } ) => {
+ context.core.image.imageRef = ref;
if ( ref.complete ) {
context.core.image.imageLoaded = true;
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 );
@@ -279,10 +282,17 @@ store(
focusableElements.length - 1
];
- ref.querySelector( '.close-button' ).focus();
+ // We want to avoid drawing unnecessary attention to the close
+ // button for mouse and touch users. Note that even if opening
+ // the lightbox via keyboard, the event fired is of type
+ // `pointerEvent`, so we need to rely on the `event.pointerType`
+ // property, which returns an empty string for keyboard events.
+ if ( context.core.image.pointerType === '' ) {
+ ref.querySelector( '.close-button' ).focus();
+ }
}
},
- setButtonStyles: ( { state, context, ref } ) => {
+ setButtonStyles: ( { context, ref } ) => {
const {
naturalWidth,
naturalHeight,
@@ -291,54 +301,71 @@ store(
} = ref;
// If the image isn't loaded yet, we can't
- // calculate how big the button should be.
+ // calculate where the button should be.
if ( naturalWidth === 0 || naturalHeight === 0 ) {
return;
}
- // 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.
+ const figure = ref.parentElement;
+ const figureWidth = ref.parentElement.clientWidth;
+
+ // We need special handling for the height because
+ // a caption will cause the figure to be taller than
+ // the image, which means we need to account for that
+ // when calculating the placement of the button in the
+ // top right corner of the image.
+ let figureHeight = ref.parentElement.clientHeight;
+ const caption = figure.querySelector( 'figcaption' );
+ if ( caption ) {
+ const captionComputedStyle =
+ window.getComputedStyle( caption );
+ figureHeight =
+ figureHeight -
+ caption.offsetHeight -
+ parseFloat( captionComputedStyle.marginTop ) -
+ parseFloat( captionComputedStyle.marginBottom );
+ }
+ const buttonOffsetTop = figureHeight - offsetHeight;
+ const buttonOffsetRight = figureWidth - offsetWidth;
+
+ // In the case of an image with object-fit: contain, the
+ // size of the element can be larger than the image itself,
+ // so we need to calculate where to place the button.
+ if ( context.core.image.scaleAttr === 'contain' ) {
// Natural ratio of the image.
const naturalRatio = naturalWidth / naturalHeight;
// Offset ratio of the image.
const offsetRatio = offsetWidth / offsetHeight;
- if ( naturalRatio > offsetRatio ) {
+ 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;
+ // the width and compute the height.
+ const referenceHeight =
+ offsetWidth / naturalRatio;
context.core.image.imageButtonTop =
- ( offsetHeight - buttonHeight ) / 2;
+ ( offsetHeight - referenceHeight ) / 2 +
+ buttonOffsetTop +
+ 10;
+ context.core.image.imageButtonRight =
+ buttonOffsetRight + 10;
} 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;
+ // the height and compute the width.
+ const referenceWidth =
+ offsetHeight * naturalRatio;
+ context.core.image.imageButtonTop =
+ buttonOffsetTop + 10;
+ context.core.image.imageButtonRight =
+ ( offsetWidth - referenceWidth ) / 2 +
+ buttonOffsetRight +
+ 10;
}
} 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;
+ context.core.image.imageButtonTop =
+ buttonOffsetTop + 10;
+ context.core.image.imageButtonRight =
+ buttonOffsetRight + 10;
}
},
setStylesOnResize: ( { state, context, ref } ) => {