From fd5125061b58d1d272512c94f612b31a5e7e0992 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Wed, 19 Feb 2025 16:55:40 +0100 Subject: [PATCH] Fix frontend implementation --- packages/block-library/src/gallery/index.php | 93 +++----- packages/block-library/src/image/index.php | 52 +++-- packages/block-library/src/image/view.js | 213 ++++++++----------- 3 files changed, 150 insertions(+), 208 deletions(-) diff --git a/packages/block-library/src/gallery/index.php b/packages/block-library/src/gallery/index.php index 46d7431c2a392..0a014b2da1f07 100644 --- a/packages/block-library/src/gallery/index.php +++ b/packages/block-library/src/gallery/index.php @@ -135,12 +135,10 @@ function block_core_gallery_render( $attributes, $content, $block ) { wp_style_engine_get_stylesheet_from_css_rules( $gallery_styles, - array( - 'context' => 'block-supports', - ) + array( 'context' => 'block-supports' ) ); - // Get all image IDs from the state that match this gallery's ID. + // Gets all image IDs from the state that match this gallery's ID. $state = wp_interactivity_state( 'core/image' ); $gallery_id = $block->context['galleryId'] ?? null; $image_ids = array(); @@ -156,14 +154,34 @@ function block_core_gallery_render( $attributes, $content, $block ) { $processed_content->set_attribute( 'data-wp-context', wp_json_encode( - array( - 'images' => $image_ids, - ), + array( 'galleryId' => $gallery_id ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); - add_filter( 'render_block_core/gallery', 'block_core_gallery_render_lightbox' ); + // Populates the aria label for each image in the gallery. + if ( ! empty( $image_ids ) ) { + if ( 1 <= count( $image_ids ) ) { + for ( $i = 0; $i < count( $image_ids ); $i++ ) { + $image_id = $image_ids[ $i ]; + $alt = $state['metadata'][ $image_id ]['alt']; + wp_interactivity_state( + 'core/image', + array( + 'metadata' => array( + $image_id => array( + 'customAriaLabel' => empty( $alt ) + /* translators: %1$s: current image index, %2$s: total number of images */ + ? sprintf( __( 'Enlarged image %1$s of %2$s' ), $i + 1, count( $image_ids ) ) + /* translators: %1$s: current image index, %2$s: total number of images, %3$s: Image alt text */ + : sprintf( __( 'Enlarged image %1$s of %2$s: %3$s' ), $i + 1, count( $image_ids ), $alt ), + ), + ), + ) + ); + } + } + } // The WP_HTML_Tag_Processor class calls get_updated_html() internally // when the instance is treated as a string, but here we explicitly @@ -211,65 +229,6 @@ static function () use ( $image_blocks, &$i ) { return $content; } -/** - * Handles state updates needed for the lightbox behavior in a Gallery Block. - * - * Now that the Gallery Block contains inner Image Blocks, - * we add translations for the screen reader text before rendering the gallery - * so that the Image Block can pick it up in its render_callback. - * - * @since 6.7.0 - * - * @param string $block_content Rendered block content. - * - * @return string Filtered block content. - */ -function block_core_gallery_render_lightbox( $block_content ) { - $state = wp_interactivity_state( 'core/gallery' ); - $gallery_id = $state['galleryId']; - $images = $state['images'][ $gallery_id ] ?? array(); - $translations = array(); - - if ( ! empty( $images ) ) { - if ( 1 === count( $images ) ) { - $image_id = $images[0]; - $translations[ $image_id ] = __( 'Enlarged image', 'gutenberg' ); - } else { - for ( $i = 0; $i < count( $images ); $i++ ) { - $image_id = $images[ $i ]; - /* translators: %1$s: current image index, %2$s: total number of images */ - $translations[ $image_id ] = sprintf( __( 'Enlarged image %1$s of %2$s', 'gutenberg' ), $i + 1, count( $images ) ); - } - } - - $image_state = wp_interactivity_state( 'core/image' ); - - foreach ( $translations as $image_id => $translation ) { - $alt = $image_state['metadata'][ $image_id ]['alt']; - wp_interactivity_state( - 'core/image', - array( - 'metadata' => array( - $image_id => array( - 'screenReaderText' => empty( $alt ) ? $translation : "$translation: $alt", - ), - ), - ) - ); - } - } - - // reset galleryId - wp_interactivity_state( - 'core/gallery', - array( - 'galleryId' => null, - ) - ); - - return $block_content; -} - /** * Registers the `core/gallery` block on server. * diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 690eeaa809495..45cca7aba44df 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -143,6 +143,7 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan * lightbox behavior. */ $p = new WP_HTML_Tag_Processor( $block_content ); + if ( $p->next_tag( 'figure' ) ) { $p->set_bookmark( 'figure' ); } @@ -150,14 +151,22 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan return $block_content; } - $alt = $p->get_attribute( 'alt' ); - $img_uploaded_src = $p->get_attribute( 'src' ); - $img_class_names = $p->get_attribute( 'class' ); - $img_styles = $p->get_attribute( 'style' ); - $img_width = 'none'; - $img_height = 'none'; - $aria_label = __( 'Enlarge' ); - $dialog_aria_label = __( 'Enlarged image' ); + $alt = $p->get_attribute( 'alt' ); + $img_uploaded_src = $p->get_attribute( 'src' ); + $img_class_names = $p->get_attribute( 'class' ); + $img_styles = $p->get_attribute( 'style' ); + $img_width = 'none'; + $img_height = 'none'; + + wp_interactivity_config( + 'core/image', + array( 'defaultAriaLabel' => __( 'Enlarged image' ) ) + ); + + if ( $alt ) { + /* translators: %s: Image alt text. */ + $custom_aria_label = sprintf( __( 'Enlarged image: %s' ), $alt ); + } if ( isset( $block['attrs']['id'] ) ) { $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] ); @@ -186,10 +195,9 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan 'targetWidth' => $img_width, 'targetHeight' => $img_height, 'scaleAttr' => $block['attrs']['scale'] ?? false, - 'ariaLabel' => $dialog_aria_label, 'alt' => $alt, - 'screenReaderText' => empty( $alt ) ? $screen_reader_text : "$screen_reader_text: $alt", 'galleryId' => $block_instance->context['galleryId'] ?? null, + 'customAriaLabel' => $custom_aria_label ?? null, ), ), ) @@ -200,9 +208,7 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan $p->set_attribute( 'data-wp-context', wp_json_encode( - array( - 'imageId' => $unique_image_id, - ), + array( 'imageId' => $unique_image_id ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); @@ -231,11 +237,11 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan class="lightbox-trigger" type="button" aria-haspopup="dialog" - aria-label="' . esc_attr( $aria_label ) . '" + aria-label="' . esc_attr( __( 'Enlarge' ) ) . '" data-wp-init="callbacks.initTriggerButton" data-wp-on-async--click="actions.showLightbox" - data-wp-style--right="state.imageButtonRight" - data-wp-style--top="state.imageButtonTop" + data-wp-style--right="state.thisImage.buttonRight" + data-wp-style--top="state.thisImage.buttonTop" > @@ -280,10 +286,12 @@ class="wp-lightbox-overlay zoom" data-wp-interactive="core/image" data-wp-context='{}' data-wp-bind--role="state.roleAttribute" + data-wp-bind--aria-label="state.ariaLabel" data-wp-bind--aria-modal="state.ariaModal" data-wp-class--active="state.overlayEnabled" data-wp-class--show-closing-animation="state.showClosingAnimation" - data-wp-watch="callbacks.setOverlayFocus" + data-wp-watch--focus="callbacks.setOverlayFocus" + data-wp-watch--inert="callbacks.setInertElements" data-wp-on--keydown="actions.handleKeydown" data-wp-on-async--touchstart="actions.handleTouchStart" data-wp-on--touchmove="actions.handleTouchMove" @@ -304,16 +312,16 @@ class="wp-lightbox-overlay zoom" - + HTML; diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 5039e6dbb22c0..baff5e42a08f2 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -1,8 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; -// import { __, sprintf } from '@wordpress/i18n'; +import { + store, + getContext, + getElement, + getConfig, +} from '@wordpress/interactivity'; /** * Tracks whether user is touching screen; used to differentiate behavior for @@ -20,39 +24,47 @@ let isTouching = false; */ let lastTouchTime = 0; -/** - * Holds all elements that are made inert when the lightbox is open; used to - * remove inert attribute of only those elements explicitly made inert. - * - * @type {Array} - */ -let inertElements = []; - const { state, actions, callbacks } = store( 'core/image', { state: { - images: [], - currentImageIndex: -1, - get currentImageId() { - return state.currentImageIndex > -1 && state.images.length > 0 - ? state.images[ state.currentImageIndex ] - : null; + selectedImageId: null, + selectedGalleryId: null, + get galleryImages() { + return state.selectedGalleryId + ? Object.entries( state.metadata ) + .filter( + ( [ , value ] ) => + value.galleryId === state.selectedGalleryId + ) + .map( ( [ key ] ) => key ) + : [ state.selectedImageId ]; + }, + get selectedImageIndex() { + return state.galleryImages.findIndex( + ( id ) => id === state.selectedImageId + ); + }, + get selectedImage() { + return state.metadata[ state.selectedImageId ]; }, - get currentImage() { - return state.metadata[ state.currentImageId ]; + get thisImage() { + const { imageId } = getContext(); + return state.metadata[ imageId ]; }, get hasNavigation() { - return state.images.length > 1; + return state.galleryImages.length > 1; }, get hasNextImage() { - return state.currentImageIndex + 1 < state.images.length; + return ( + state.selectedImageIndex + 1 < state.galleryImages.length + ); }, get hasPreviousImage() { - return state.currentImageIndex - 1 >= 0; + return state.selectedImageIndex - 1 >= 0; }, get overlayOpened() { - return state.currentImageId !== null; + return state.selectedImageId !== null; }, get roleAttribute() { return state.overlayOpened ? 'dialog' : null; @@ -60,16 +72,22 @@ const { state, actions, callbacks } = store( get ariaModal() { return state.overlayOpened ? 'true' : null; }, + get ariaLabel() { + return ( + state.selectedImage.customAriaLabel || + getConfig().defaultAriaLabel + ); + }, get enlargedSrc() { return ( - state.currentImage.uploadedSrc || + state.selectedImage.uploadedSrc || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' ); }, get figureStyles() { return ( state.overlayOpened && - `${ state.currentImage.figureStyles?.replace( + `${ state.selectedImage.figureStyles?.replace( /margin[^;]*;?/g, '' ) };` @@ -78,31 +96,24 @@ const { state, actions, callbacks } = store( get imgStyles() { return ( state.overlayOpened && - `${ state.currentImage.imgStyles?.replace( + `${ state.selectedImage.imgStyles?.replace( /;$/, '' ) }; object-fit:cover;` ); }, - get imageButtonRight() { - const { imageId } = getContext(); - return state.metadata[ imageId ].imageButtonRight; - }, - get imageButtonTop() { - const { imageId } = getContext(); - return state.metadata[ imageId ].imageButtonTop; - }, get isContentHidden() { const ctx = getContext(); return ( - state.overlayEnabled && state.currentImageId === ctx.imageId + state.overlayEnabled && + state.selectedImageId === ctx.imageId ); }, get isContentVisible() { const ctx = getContext(); return ( ! state.overlayEnabled && - state.currentImageId === ctx.imageId + state.selectedImageId === ctx.imageId ); }, }, @@ -120,32 +131,14 @@ const { state, actions, callbacks } = store( state.scrollTopReset = document.documentElement.scrollTop; state.scrollLeftReset = document.documentElement.scrollLeft; - const { state: galleryState } = store( 'core/gallery' ); - const { lightbox, galleryId } = - getContext( 'core/gallery' ) || {}; - state.images = lightbox - ? galleryState.images[ galleryId ] || [] - : [ imageId ]; - - // Sets the current image index to the one that was clicked. - callbacks.setCurrentImageIndex( imageId ); - - // Sets the current expanded image in the state and enables the overlay. + // Sets the selected image and gallery and enables the overlay. + state.selectedImageId = imageId; + const { galleryId } = getContext( 'core/gallery' ) || {}; + state.selectedGalleryId = galleryId || null; state.overlayEnabled = true; // Computes the styles of the overlay for the animation. callbacks.setOverlayStyles(); - - // make all children of the document inert exempt .wp-lightbox-overlay - inertElements = []; - document - .querySelectorAll( 'body > :not(.wp-lightbox-overlay)' ) - .forEach( ( el ) => { - if ( ! el.hasAttribute( 'inert' ) ) { - el.setAttribute( 'inert', '' ); - inertElements.push( el ); - } - } ); }, hideLightbox() { if ( state.overlayEnabled ) { @@ -163,54 +156,38 @@ const { state, actions, callbacks } = store( // Delays before changing the focus. Otherwise the focus ring will // appear on Firefox before the image has finished animating, which // looks broken. - state.currentImage.buttonRef.focus( { + state.selectedImage.buttonRef.focus( { preventScroll: true, } ); - // Resets the current image index to mark the overlay as closed. - state.currentImageIndex = -1; - state.images = []; + // Resets the selected image and gallery ids. + state.selectedImageId = null; + state.selectedGalleryId = null; }, 450 ); - - // remove inert attribute from all children of the document - inertElements.forEach( ( el ) => { - el.removeAttribute( 'inert' ); - } ); - inertElements = []; } }, showPreviousImage( e ) { - if ( ! state.hasNavigation ) { - return; + if ( state.hasPreviousImage ) { + e.stopPropagation(); + state.selectedImageId = + state.galleryImages[ state.selectedImageIndex - 1 ]; + callbacks.setOverlayStyles(); } - - e.stopPropagation(); - if ( state.currentImageIndex - 1 < 0 ) { - return; - } - state.currentImageIndex = state.currentImageIndex - 1; - callbacks.setOverlayStyles(); }, showNextImage( e ) { - if ( ! state.hasNavigation ) { - return; - } + if ( state.hasNextImage ) { + e.stopPropagation(); - e.stopPropagation(); - if ( state.currentImageIndex + 1 >= state.images.length ) { - return; + state.selectedImageId = + state.galleryImages[ state.selectedImageIndex + 1 ]; + callbacks.setOverlayStyles(); } - state.currentImageIndex = state.currentImageIndex + 1; - callbacks.setOverlayStyles(); }, handleKeydown( event ) { if ( state.overlayEnabled ) { - // Closes the lightbox when the user presses the escape key. if ( event.key === 'Escape' ) { actions.hideLightbox(); - } - - if ( event.key === 'ArrowLeft' ) { + } else if ( event.key === 'ArrowLeft' ) { actions.showPreviousImage( event ); } else if ( event.key === 'ArrowRight' ) { actions.showNextImage( event ); @@ -262,12 +239,6 @@ const { state, actions, callbacks } = store( }, }, callbacks: { - setCurrentImageIndex( imageId ) { - const currentIndex = state.images.findIndex( - ( id ) => id === imageId - ); - state.currentImageIndex = currentIndex; - }, setOverlayStyles() { if ( ! state.overlayEnabled ) { return; @@ -278,9 +249,9 @@ const { state, actions, callbacks } = store( naturalHeight, offsetWidth: originalWidth, offsetHeight: originalHeight, - } = state.currentImage.imageRef; + } = state.selectedImage.imageRef; let { x: screenPosX, y: screenPosY } = - state.currentImage.imageRef.getBoundingClientRect(); + state.selectedImage.imageRef.getBoundingClientRect(); // Natural ratio of the image clicked to open the lightbox. const naturalRatio = naturalWidth / naturalHeight; @@ -289,7 +260,7 @@ const { state, actions, callbacks } = store( // If it has object-fit: contain, recalculates the original sizes // and the screen position without the blank spaces. - if ( state.currentImage.scaleAttr === 'contain' ) { + if ( state.selectedImage.scaleAttr === 'contain' ) { if ( naturalRatio > originalRatio ) { const heightWithoutSpace = originalWidth / naturalRatio; // Recalculates screen position without the top space. @@ -310,15 +281,15 @@ const { state, actions, callbacks } = store( // size), the image's dimensions in the lightbox are the same // as those of the image in the content. let imgMaxWidth = parseFloat( - state.currentImage.targetWidth && - state.currentImage.targetWidth !== 'none' - ? state.currentImage.targetWidth + state.selectedImage.targetWidth && + state.selectedImage.targetWidth !== 'none' + ? state.selectedImage.targetWidth : naturalWidth ); let imgMaxHeight = parseFloat( - state.currentImage.targetHeight && - state.currentImage.targetHeight !== 'none' - ? state.currentImage.targetHeight + state.selectedImage.targetHeight && + state.selectedImage.targetHeight !== 'none' + ? state.selectedImage.targetHeight : naturalHeight ); @@ -436,14 +407,6 @@ const { state, actions, callbacks } = store( }px; `; }, - setScreenReaderText() { - const { ref } = getElement(); - if ( ! state.overlayEnabled ) { - ref.textContent = ''; - } else { - ref.textContent = state.currentImage.screenReaderText; - } - }, setButtonStyles() { const { imageId } = getContext(); const { ref } = getElement(); @@ -492,8 +455,8 @@ const { state, actions, callbacks } = store( const buttonOffsetTop = figureHeight - offsetHeight; const buttonOffsetRight = figureWidth - offsetWidth; - let imageButtonTop = buttonOffsetTop + 16; - let imageButtonRight = buttonOffsetRight + 16; + let buttonTop = buttonOffsetTop + 16; + let buttonRight = buttonOffsetRight + 16; // In the case of an image with object-fit: contain, the size of the // element can be larger than the image itself, so it needs to @@ -508,25 +471,25 @@ const { state, actions, callbacks } = store( // If it reaches the width first, it keeps the width and compute the // height. const referenceHeight = offsetWidth / naturalRatio; - imageButtonTop = + buttonTop = ( offsetHeight - referenceHeight ) / 2 + buttonOffsetTop + 16; - imageButtonRight = buttonOffsetRight + 16; + buttonRight = buttonOffsetRight + 16; } else { // If it reaches the height first, it keeps the height and compute // the width. const referenceWidth = offsetHeight * naturalRatio; - imageButtonTop = buttonOffsetTop + 16; - imageButtonRight = + buttonTop = buttonOffsetTop + 16; + buttonRight = ( offsetWidth - referenceWidth ) / 2 + buttonOffsetRight + 16; } } - state.metadata[ imageId ].imageButtonTop = imageButtonTop; - state.metadata[ imageId ].imageButtonRight = imageButtonRight; + state.metadata[ imageId ].buttonTop = buttonTop; + state.metadata[ imageId ].buttonRight = buttonRight; }, setOverlayFocus() { if ( state.overlayEnabled ) { @@ -535,6 +498,18 @@ const { state, actions, callbacks } = store( ref.focus(); } }, + setInertElements() { + // Makes all children of the document inert exempt .wp-lightbox-overlay. + document + .querySelectorAll( 'body > :not(.wp-lightbox-overlay)' ) + .forEach( ( el ) => { + if ( state.overlayEnabled ) { + el.setAttribute( 'inert', '' ); + } else { + el.removeAttribute( 'inert' ); + } + } ); + }, initTriggerButton() { const { imageId } = getContext(); const { ref } = getElement();