From 017d1ad368b0a0c4dcd9d148fa196919ebe43246 Mon Sep 17 00:00:00 2001 From: Artemio Morales Date: Tue, 17 Oct 2023 06:33:31 -0500 Subject: [PATCH] Image: Reimplement lightbox trigger as a minimal button in corner of image (#55212) * Reimplement lightbox trigger as a minimal button in corner of image * Remove obsolete directives * Update directives to fire logic properly via image or button click * Ensure lightbox button only appears when hovering over image, not whole figure * Ensure close button does not receive focus when opening lightbox via mouse * Ensure button only receives focus when lightbox is closed via keyboard * Add comments * Prevent unnecessary focus being shown on mobile * Add dynamic positioning for button when image uses 'contain' setting * WORK IN PROGRESS - Begin accounting for various edge cases We need to account for the fact that an image can have custom dimensions, aspect ratio, cover or contain, captions, thumbnail dimensions, and potentially other scenarios. This commit begins to address those issues but notably fails in cases where one uses a horizontal image, at full scale, with custom aspect ratio, using 'contain'. It seems to work in all other cases that I've checked but needs more thorough testing and the code can probably be cleaner, and may contain some unnecessary items. * Simplify and improve button placement logic * Simplify logic to show button on hover * Fix styles * Simplify calls to showLightbox * Fix style inconsistency between browsers * Change button position slightly * Reduce button offset * Add style override for better consistency across themes * Fix logic so lightbox animates as intended; remove extraneous code * Update comment --- packages/block-library/src/image/index.php | 22 +++- packages/block-library/src/image/style.scss | 31 ++++- packages/block-library/src/image/view.js | 125 ++++++++++++-------- 3 files changed, 119 insertions(+), 59 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 64b7457dd863d3..dd2e72aebe5ef2 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -218,13 +218,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 ); @@ -235,11 +239,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 } ) => {