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 } ) => {