Skip to content

Commit

Permalink
Image: Reimplement lightbox trigger as a minimal button in corner of …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
artemiomorales authored Oct 17, 2023
1 parent 46603dc commit 017d1ad
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 59 deletions.
22 changes: 16 additions & 6 deletions packages/block-library/src/image/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( '/<img[^>]+>/', $body_content, $img );

Expand All @@ -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"
></button>';
style="background: #000"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 5H5V9" stroke="#FFFFFF" stroke-width="1.5"/>
<path d="M15 19L19 19L19 15" stroke="#FFFFFF" stroke-width="1.5"/>
<path d="M15 5H19V9" stroke="#FFFFFF" stroke-width="1.5"/>
<path d="M9 19L5 19L5 15" stroke="#FFFFFF" stroke-width="1.5"/>
</svg>
</button>';

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

Expand Down
31 changes: 27 additions & 4 deletions packages/block-library/src/image/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -157,25 +157,48 @@
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;
outline: 5px auto -webkit-focus-ring-color;
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;
}
}
Expand Down
125 changes: 76 additions & 49 deletions packages/block-library/src/image/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down Expand Up @@ -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
Expand All @@ -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 } ) => {
Expand Down Expand Up @@ -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,
} );
Expand Down Expand Up @@ -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 );
Expand All @@ -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,
Expand All @@ -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 <img> 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 } ) => {
Expand Down

0 comments on commit 017d1ad

Please sign in to comment.