Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Image block lightbox missing alt attribute and improve accessibility #55010

Merged
merged 15 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions lib/block-supports/behaviors.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,35 +84,35 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {

$aria_label = __( 'Enlarge image', 'gutenberg' );

$processor->next_tag( 'img' );
$alt_attribute = $processor->get_attribute( 'alt' );

if ( null !== $alt_attribute ) {
// An empty alt attribute `alt=""` is valid for decorative images.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excellent comment, and excellent fix above. one small note: null indicates that the alt attribute is missing, but an empty value would be '' or if it's only present as a name like src="…" alt loading=eager then the value would be true

if ( is_string( $alt_attribute ) ) {

this might be more appropriate, as it ensures that that we're dealing with an alt=something situation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a way, the check is a bit redundant in the first place. As far as I know, WordPress enforces at least an empty alt="" attribute everywhere. I'd agree to check for string anyways, just in case.

if ( is_string( $alt_attribute ) ) {
$alt_attribute = trim( $alt_attribute );
}

// It only makes sense to append the alt text to the button aria-label when the alt text is non-empty.
if ( $alt_attribute ) {
/* translators: %s: Image alt text. */
$aria_label = sprintf( __( 'Enlarge image: %s', 'gutenberg' ), $alt_attribute );
}
$content = $processor->get_updated_html();

// If we don't set a default, it won't work if Lightbox is set to enabled by default.
$lightbox_animation = 'zoom';
if ( isset( $lightbox_settings['animation'] ) && '' !== $lightbox_settings['animation'] ) {
$lightbox_animation = $lightbox_settings['animation'];
}

// We want to store the src in the context so we can set it dynamically when the lightbox is opened.
$z = new WP_HTML_Tag_Processor( $content );
$z->next_tag( 'img' );

// Note: We want to store the `src` in the context so we
// can set it dynamically when the lightbox is opened.
if ( isset( $block['attrs']['id'] ) ) {
$img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
$img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
$img_width = $img_metadata['width'];
$img_height = $img_metadata['height'];
} else {
$img_uploaded_src = $z->get_attribute( 'src' );
$img_uploaded_src = $processor->get_attribute( 'src' );
$img_width = 'none';
$img_height = 'none';
}
Expand All @@ -123,7 +123,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$scale_attr = false;
}

$w = new WP_HTML_Tag_Processor( $content );
$w = new WP_HTML_Tag_Processor( $block_content );
$w->next_tag( 'figure' );
$w->add_class( 'wp-lightbox-container' );
$w->set_attribute( 'data-wp-interactive', true );
Expand Down Expand Up @@ -163,27 +163,28 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
// Wrap the image in the body content with a button.
$img = null;
preg_match( '/<img[^>]+>/', $body_content, $img );
$button =
'<button
type="button"
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--top="context.core.image.imageButtonTop"
>
</button>'
. $img[0];

$button =
$img[0]
. '<button
type="button"
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--top="context.core.image.imageButtonTop"
></button>';

$body_content = preg_replace( '/<img[^>]+>/', $button, $body_content );

// 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 = new WP_HTML_Tag_Processor( $block_content );
$m->next_tag( 'figure' );
$m->add_class( 'responsive-image' );
$m->next_tag( 'img' );
Expand All @@ -199,7 +200,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$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 = new WP_HTML_Tag_Processor( $block_content );
$q->next_tag( 'figure' );
$q->add_class( 'enlarged-image' );
$q->next_tag( 'img' );
Expand All @@ -219,7 +220,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {

$close_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>';
$close_button_color = esc_attr( wp_get_global_styles( array( 'color', 'text' ) ) );
$dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image', 'gutenberg' );
$dialog_label = esc_attr__( 'Enlarged image', 'gutenberg' );
$close_button_label = esc_attr__( 'Close', 'gutenberg' );

$lightbox_html = <<<HTML
Expand Down
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,4 @@ function () {
require __DIR__ . '/block-supports/duotone.php';
require __DIR__ . '/block-supports/shadow.php';
require __DIR__ . '/block-supports/background.php';
require __DIR__ . '/block-supports/behaviors.php';
5 changes: 5 additions & 0 deletions packages/block-library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Bug Fix

- Fix Image block lightbox missing alt attribute and improve accessibility. ([#54608](https://github.com/WordPress/gutenberg/pull/55010))


## 8.20.0 (2023-10-05)

## 8.19.0 (2023-09-20)
Expand Down
94 changes: 50 additions & 44 deletions packages/block-library/src/image/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* Renders the `core/image` block on the server,
* adding a data-id attribute to the element if core/gallery has added on pre-render.
*
* @param array $attributes The block attributes.
* @param string $content The block content.
* @param WP_Block $block The block object.
* @return string Returns the block content with the data-id attribute added.
* @param array $attributes The block attributes.
* @param string $content The block content.
* @param WP_Block $block The block object.
*
* @return string The block content with the data-id attribute added.
*/
function render_block_core_image( $attributes, $content, $block ) {

Expand Down Expand Up @@ -76,12 +77,13 @@ function render_block_core_image( $attributes, $content, $block ) {
}

/**
* Add the lightboxEnabled flag to the block data.
* Adds the lightboxEnabled flag to the block data.
*
* This is used to determine whether the lightbox should be rendered or not.
*
* @param array $block Block data.
* @return array Filtered block data.
* @param array $block Block data.
*
* @return array Filtered block data.
*/
function block_core_image_get_lightbox_settings( $block ) {
// Get the lightbox setting from the block attributes.
Expand Down Expand Up @@ -113,43 +115,44 @@ function block_core_image_get_lightbox_settings( $block ) {
}

/**
* Add the directives and layout needed for the lightbox behavior.
* Adds the directives and layout needed for the lightbox behavior.
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
* @return string Filtered block content.
*/
function block_core_image_render_lightbox( $block_content, $block ) {
$processor = new WP_HTML_Tag_Processor( $block_content );

$aria_label = __( 'Enlarge image' );

$processor->next_tag( 'img' );
$alt_attribute = $processor->get_attribute( 'alt' );

if ( null !== $alt_attribute ) {
// An empty alt attribute `alt=""` is valid for decorative images.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above comment about is_string()

if ( is_string( $alt_attribute ) ) {
$alt_attribute = trim( $alt_attribute );
}

// It only makes sense to append the alt text to the button aria-label when the alt text is non-empty.
if ( $alt_attribute ) {
/* translators: %s: Image alt text. */
$aria_label = sprintf( __( 'Enlarge image: %s' ), $alt_attribute );
}
$content = $processor->get_updated_html();

// Currently, we are only enabling the zoom animation.
$lightbox_animation = 'zoom';

// We want to store the src in the context so we can set it dynamically when the lightbox is opened.
$z = new WP_HTML_Tag_Processor( $content );
$z->next_tag( 'img' );

// Note: We want to store the `src` in the context so we
// can set it dynamically when the lightbox is opened.
if ( isset( $block['attrs']['id'] ) ) {
$img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
$img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
$img_width = $img_metadata['width'];
$img_height = $img_metadata['height'];
} else {
$img_uploaded_src = $z->get_attribute( 'src' );
$img_uploaded_src = $processor->get_attribute( 'src' );
$img_width = 'none';
$img_height = 'none';
}
Expand All @@ -160,7 +163,7 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$scale_attr = false;
}

$w = new WP_HTML_Tag_Processor( $content );
$w = new WP_HTML_Tag_Processor( $block_content );
$w->next_tag( 'figure' );
$w->add_class( 'wp-lightbox-container' );
$w->set_attribute( 'data-wp-interactive', true );
Expand All @@ -180,15 +183,17 @@ function block_core_image_render_lightbox( $block_content, $block ) {
"imageCurrentSrc": "",
"targetWidth": "%s",
"targetHeight": "%s",
"scaleAttr": "%s"
"scaleAttr": "%s",
"dialogLabel": "%s"
}
}
}',
$lightbox_animation,
$img_uploaded_src,
$img_width,
$img_height,
$scale_attr
$scale_attr,
__( 'Enlarged image' )
)
);
$w->next_tag( 'img' );
Expand All @@ -200,27 +205,28 @@ function block_core_image_render_lightbox( $block_content, $block ) {
// Wrap the image in the body content with a button.
$img = null;
preg_match( '/<img[^>]+>/', $body_content, $img );
$button =
'<button
type="button"
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--top="context.core.image.imageButtonTop"
>
</button>'
. $img[0];

$button =
$img[0]
. '<button
type="button"
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--top="context.core.image.imageButtonTop"
></button>';

$body_content = preg_replace( '/<img[^>]+>/', $button, $body_content );

// 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 = new WP_HTML_Tag_Processor( $block_content );
$m->next_tag( 'figure' );
$m->add_class( 'responsive-image' );
$m->next_tag( 'img' );
Expand All @@ -236,7 +242,7 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$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 = new WP_HTML_Tag_Processor( $block_content );
$q->next_tag( 'figure' );
$q->add_class( 'enlarged-image' );
$q->next_tag( 'img' );
Expand Down Expand Up @@ -268,20 +274,16 @@ function block_core_image_render_lightbox( $block_content, $block ) {
}

$close_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>';
$dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image' );
$close_button_label = esc_attr__( 'Close' );

$lightbox_html = <<<HTML
<div data-wp-body="" class="wp-lightbox-overlay $lightbox_animation"
data-wp-bind--role="selectors.core.image.roleAttribute"
aria-label="$dialog_label"
data-wp-bind--aria-label="selectors.core.image.dialogLabel"
data-wp-class--initialized="context.core.image.initialized"
data-wp-class--active="context.core.image.lightboxEnabled"
data-wp-class--hideAnimationEnabled="context.core.image.hideAnimationEnabled"
data-wp-bind--aria-hidden="!context.core.image.lightboxEnabled"
aria-hidden="true"
data-wp-bind--aria-modal="context.core.image.lightboxEnabled"
aria-modal="false"
data-wp-bind--aria-modal="selectors.core.image.ariaModal"
data-wp-effect="effects.core.image.initLightbox"
data-wp-on--keydown="actions.core.image.handleKeydown"
data-wp-on--touchstart="actions.core.image.handleTouchStart"
Expand All @@ -294,19 +296,21 @@ function block_core_image_render_lightbox( $block_content, $block ) {
</button>
<div class="lightbox-image-container">$initial_image_content</div>
<div class="lightbox-image-container">$enlarged_image_content</div>
<div class="scrim" style="background-color: $background_color"></div>
<div class="scrim" style="background-color: $background_color" aria-hidden="true"></div>
</div>
HTML;

return str_replace( '</figure>', $lightbox_html . '</figure>', $body_content );
}

/**
* Ensure that the view script has the `wp-interactivity` dependency.
* Ensures that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
*
* @global WP_Scripts $wp_scripts
*
* @return void
*/
function block_core_image_ensure_interactivity_dependency() {
global $wp_scripts;
Expand All @@ -322,6 +326,8 @@ function block_core_image_ensure_interactivity_dependency() {

/**
* Registers the `core/image` block on server.
*
* @return void
*/
function register_block_core_image() {
register_block_type_from_metadata(
Expand Down
11 changes: 9 additions & 2 deletions packages/block-library/src/image/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@

.wp-lightbox-container {
position: relative;
display: flex;
flex-direction: column;

button {
border: none;
Expand Down Expand Up @@ -193,11 +195,16 @@

.close-button {
position: absolute;
top: calc(env(safe-area-inset-top) + 20px);
right: calc(env(safe-area-inset-right) + 20px);
top: calc(env(safe-area-inset-top) + 16px); // equivalent to $grid-unit-20
right: calc(env(safe-area-inset-right) + 16px); // equivalent to $grid-unit-20
padding: 0;
cursor: pointer;
z-index: 5000000;
min-width: 40px; // equivalent to $button-size-next-default-40px
min-height: 40px; // equivalent to $button-size-next-default-40px
display: flex;
align-items: center;
justify-content: center;

&:hover,
&:focus,
Expand Down
Loading
Loading