From 6f9e6e54a9ac1e19d71b834395b5888946e0e08f Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Tue, 13 Jun 2023 16:35:09 +0200 Subject: [PATCH 01/16] Remove reset button in behaviors, use dropdown option instead --- packages/block-editor/src/hooks/behaviors.js | 52 ++++++++----------- .../specs/editor/various/behaviors.spec.js | 18 +++---- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 6676b51442579b..03b79359062c3c 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -2,11 +2,7 @@ * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { - SelectControl, - Button, - __experimentalHStack as HStack, -} from '@wordpress/components'; +import { SelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; @@ -48,6 +44,11 @@ function BehaviorsControl( { label: __( 'No behaviors' ), }; + const defaultBehaviorsOption = { + value: 'default', + label: __( 'Default' ), + }; + const behaviorsOptions = Object.entries( settings ) .filter( ( [ behaviorName, behaviorValue ] ) => @@ -65,7 +66,11 @@ function BehaviorsControl( { // If every behavior is disabled, do not show the behaviors inspector control. if ( behaviorsOptions.length === 0 ) return null; - const options = [ noBehaviorsOption, ...behaviorsOptions ]; + const options = [ + defaultBehaviorsOption, + noBehaviorsOption, + ...behaviorsOptions, + ]; // Block behaviors take precedence over theme behaviors. const behaviors = merge( themeBehaviors, blockBehaviors || {} ); @@ -77,28 +82,17 @@ function BehaviorsControl( { return ( { /* This div is needed to prevent a margin bottom between the dropdown and the button. */ } -
- -
- - - +
); } @@ -130,7 +124,7 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => { blockName={ props.name } blockBehaviors={ props.attributes.behaviors } onChange={ ( nextValue ) => { - if ( nextValue === undefined ) { + if ( nextValue === 'default' ) { props.setAttributes( { behaviors: undefined, } ); diff --git a/test/e2e/specs/editor/various/behaviors.spec.js b/test/e2e/specs/editor/various/behaviors.spec.js index dc03dd166b001e..51fd5abd4a35d8 100644 --- a/test/e2e/specs/editor/various/behaviors.spec.js +++ b/test/e2e/specs/editor/various/behaviors.spec.js @@ -83,7 +83,7 @@ test.describe( 'Testing behaviors functionality', () => { await expect( select ).toHaveValue( '' ); // By default, you should be able to select the Lightbox behavior. - await expect( select.getByRole( 'option' ) ).toHaveCount( 2 ); + await expect( select.getByRole( 'option' ) ).toHaveCount( 3 ); } ); test( 'Behaviors UI can be disabled in the `theme.json`', async ( { @@ -162,8 +162,8 @@ test.describe( 'Testing behaviors functionality', () => { // attributes takes precedence over the theme's value. await expect( select ).toHaveValue( 'lightbox' ); - // There should be 2 options available: `No behaviors` and `Lightbox`. - await expect( select.getByRole( 'option' ) ).toHaveCount( 2 ); + // There should be 3 options available: `No behaviors` and `Lightbox`. + await expect( select.getByRole( 'option' ) ).toHaveCount( 3 ); // We can change the value of the behaviors dropdown to `No behaviors`. await select.selectOption( { label: 'No behaviors' } ); @@ -210,7 +210,7 @@ test.describe( 'Testing behaviors functionality', () => { await expect( select ).toHaveValue( 'lightbox' ); // There should be 2 options available: `No behaviors` and `Lightbox`. - await expect( select.getByRole( 'option' ) ).toHaveCount( 2 ); + await expect( select.getByRole( 'option' ) ).toHaveCount( 3 ); // We can change the value of the behaviors dropdown to `No behaviors`. await select.selectOption( { label: 'No behaviors' } ); @@ -254,7 +254,7 @@ test.describe( 'Testing behaviors functionality', () => { await expect( select ).toBeDisabled(); } ); - test( 'Lightbox behavior control has a Reset button that removes the markup', async ( { + test( 'Lightbox behavior control has a default option that removes the markup', async ( { admin, editor, requestUtils, @@ -293,13 +293,11 @@ test.describe( 'Testing behaviors functionality', () => { .last() .click(); - const resetButton = editorSettings.getByRole( 'button', { - name: 'Reset', + const select = editorSettings.getByRole( 'combobox', { + name: 'Behavior', } ); - expect( resetButton ).toBeDefined(); - - await resetButton.last().click(); + await select.selectOption( { label: 'Default' } ); expect( await editor.getEditedPostContent() ) .toBe( `
1024x768_e2e_test_image_size.jpeg
From 4f1001443c04ea0e95a791ad11a5e3917f16f7af Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Wed, 14 Jun 2023 15:20:04 +0200 Subject: [PATCH 02/16] Use default option reading from the theme, prevent autoupdate --- packages/block-editor/src/hooks/behaviors.js | 57 +++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 03b79359062c3c..cb790861833d7e 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -14,11 +14,6 @@ import { useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '../store'; import { InspectorControls } from '../components'; -/** - * External dependencies - */ -import merge from 'deepmerge'; - function BehaviorsControl( { blockName, blockBehaviors, @@ -39,53 +34,63 @@ function BehaviorsControl( { [ blockName ] ); - const noBehaviorsOption = { - value: '', - label: __( 'No behaviors' ), - }; - - const defaultBehaviorsOption = { - value: 'default', - label: __( 'Default' ), + const defaultBehaviors = { + default: { + value: 'default', + label: __( 'Default' ), + }, + noBehaviors: { + value: '', + label: __( 'No behaviors' ), + }, }; const behaviorsOptions = Object.entries( settings ) .filter( ( [ behaviorName, behaviorValue ] ) => - hasBlockSupport( blockName, 'behaviors.' + behaviorName ) && + hasBlockSupport( blockName, `behaviors.${ behaviorName }` ) && behaviorValue ) // Filter out behaviors that are disabled. .map( ( [ behaviorName ] ) => ( { value: behaviorName, - label: - // Capitalize the first letter of the behavior name. - behaviorName[ 0 ].toUpperCase() + - behaviorName.slice( 1 ).toLowerCase(), + // Capitalize the first letter of the behavior name. + label: `${ behaviorName.charAt( 0 ).toUpperCase() }${ behaviorName + .slice( 1 ) + .toLowerCase() }`, } ) ); - // If every behavior is disabled, do not show the behaviors inspector control. - if ( behaviorsOptions.length === 0 ) return null; - const options = [ - defaultBehaviorsOption, - noBehaviorsOption, + ...Object.values( defaultBehaviors ), ...behaviorsOptions, ]; + // If every behavior is disabled, do not show the behaviors inspector control. + if ( options.length === 0 ) { + return null; + } // Block behaviors take precedence over theme behaviors. - const behaviors = merge( themeBehaviors, blockBehaviors || {} ); + const behaviors = { ...themeBehaviors, ...( blockBehaviors || {} ) }; const helpText = disabled ? __( 'The lightbox behavior is disabled for linked images.' ) : __( 'Add behaviors.' ); + const value = () => { + if ( blockBehaviors === undefined ) { + return 'default'; + } + if ( behaviors?.lightbox ) { + return 'lightbox'; + } + return ''; + }; + return ( - { /* This div is needed to prevent a margin bottom between the dropdown and the button. */ } Date: Thu, 8 Jun 2023 16:53:40 -0500 Subject: [PATCH 03/16] Add zoom animation UI and styles --- lib/block-supports/behaviors.php | 21 +++-- lib/theme.json | 5 +- packages/block-editor/src/hooks/behaviors.js | 63 +++++++++++--- .../block-library/src/image/interactivity.js | 68 +++++++++++++-- packages/block-library/src/image/style.scss | 82 +++++++++++++++++-- 5 files changed, 201 insertions(+), 38 deletions(-) diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php index 55a3419e466fbb..62d484e3beeb85 100644 --- a/lib/block-supports/behaviors.php +++ b/lib/block-supports/behaviors.php @@ -48,18 +48,18 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { $link_destination = isset( $block['attrs']['linkDestination'] ) ? $block['attrs']['linkDestination'] : 'none'; // Get the lightbox setting from the block attributes. if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) { - $lightbox = $block['attrs']['behaviors']['lightbox']; + $lightbox_settings = $block['attrs']['behaviors']['lightbox']; // If the lightbox setting is not set in the block attributes, get it from the theme.json file. } else { $theme_data = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->get_data(); if ( isset( $theme_data['behaviors']['blocks'][ $block['blockName'] ]['lightbox'] ) ) { - $lightbox = $theme_data['behaviors']['blocks'][ $block['blockName'] ]['lightbox']; + $lightbox_settings = $theme_data['behaviors']['blocks'][ $block['blockName'] ]['lightbox']; } else { - $lightbox = false; + $lightbox_settings = null; } } - if ( ! $lightbox || 'none' !== $link_destination || empty( $experiments['gutenberg-interactivity-api-core-blocks'] ) ) { + if ( ! $lightbox_settings || 'none' !== $link_destination || empty( $experiments['gutenberg-interactivity-api-core-blocks'] ) ) { return $block_content; } @@ -75,11 +75,13 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { } $content = $processor->get_updated_html(); + $lightbox_animation = $lightbox_settings['animation']; + $w = new WP_HTML_Tag_Processor( $content ); $w->next_tag( 'figure' ); $w->add_class( 'wp-lightbox-container' ); $w->set_attribute( 'data-wp-interactive', true ); - $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "lightboxEnabled": false } } }' ); + $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "lightboxEnabled": false, "lightboxAnimation": "' . $lightbox_animation . '" } } }' ); $body_content = $w->get_updated_html(); // Wrap the image in the body content with a button. @@ -99,8 +101,11 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { } else { $img_src = $m->get_attribute( 'src' ); } - $m->set_attribute( 'data-wp-context', '{ "core": { "image": { "imageSrc": "' . $img_src . '"} } }' ); - $m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.imageSrc' ); + + // Need to figure out how to smoothly transition image animation when using larger image + // + // $m->set_attribute( 'data-wp-context', '{ "core": { "image": { "imageSrc": "' . $img_src . '"} } }' ); + // $m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.imageSrc' ); $modal_content = $m->get_updated_html(); $background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) ); @@ -111,7 +116,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { $close_button_label = esc_attr__( 'Close', 'gutenberg' ); $lightbox_html = << - + { /* This div is needed to prevent a margin bottom between the dropdown and the button. */ } +
+ + { behaviors?.lightbox.enabled && ( + + ) } +
); } @@ -136,9 +162,22 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => { } else { // If the user selects something, it means that they want to // change the default value (true) so we save it in the attributes. + const enabled = + nextValue === 'lightbox' || + nextValue === 'zoom' || + nextValue === 'fade' + ? true + : false; + const animation = + nextValue === 'zoom' || nextValue === 'fade' + ? nextValue + : 'zoom'; props.setAttributes( { behaviors: { - lightbox: nextValue === 'lightbox', + lightbox: { + enabled, + animation, + }, }, } ); } diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js index 552bdf13a66ca2..5606927985bda2 100644 --- a/packages/block-library/src/image/interactivity.js +++ b/packages/block-library/src/image/interactivity.js @@ -21,7 +21,7 @@ store( { actions: { core: { image: { - showLightbox: ( { context } ) => { + showLightbox: ( { context, event } ) => { context.core.image.initialized = true; context.core.image.lightboxEnabled = true; context.core.image.lastFocusedElement = @@ -30,19 +30,69 @@ store( { document.documentElement.classList.add( 'has-lightbox-open' ); + + if ( context.core.image.lightboxAnimation === 'zoom' ) { + const { x: leftPosition, y: topPosition } = + event.target.nextElementSibling.getBoundingClientRect(); + const scaleWidth = + event.target.nextElementSibling.offsetWidth / + event.target.nextElementSibling.naturalWidth; + const scaleHeight = + event.target.nextElementSibling.offsetHeight / + event.target.nextElementSibling.naturalHeight; + const root = document.documentElement; + root.style.setProperty( + '--lightbox-left-position', + leftPosition + 'px' + ); + root.style.setProperty( + '--lightbox-top-position', + topPosition + 'px' + ); + root.style.setProperty( + '--lightbox-scale-width', + scaleWidth + ); + root.style.setProperty( + '--lightbox-scale-height', + scaleHeight + ); + } }, hideLightbox: async ( { context, event } ) => { if ( context.core.image.lightboxEnabled ) { // If scrolling, wait a moment before closing the lightbox. - if ( - event.type === 'mousewheel' && - Math.abs( - window.scrollY - - context.core.image.scrollPosition - ) < 5 + if ( context.core.image.lightboxAnimation === 'fade' ) { + if ( + event.type === 'mousewheel' && + Math.abs( + window.scrollY - + context.core.image.scrollPosition + ) < 5 + ) { + return; + } + } else if ( + context.core.image.lightboxAnimation === 'zoom' ) { - return; + // Disable scroll until the zoom animation ends. + // Get the current page scroll position + const scrollTop = + window.pageYOffset || + document.documentElement.scrollTop; + const scrollLeft = + window.pageXOffset || + document.documentElement.scrollLeft; + // if any scroll is attempted, set this to the previous value. + window.onscroll = function () { + window.scrollTo( scrollLeft, scrollTop ); + }; + // Enable scrolling after the animation finishes + setTimeout( function () { + window.onscroll = function () {}; + }, 400 ); } + document.documentElement.classList.remove( 'has-lightbox-open' ); @@ -102,6 +152,8 @@ store( { image: { initLightbox: async ( { context, ref } ) => { if ( context.core.image.lightboxEnabled ) { + context.core.image.lightboxImage = + ref.querySelector( 'img' ); const focusableElements = ref.querySelectorAll( focusableSelectors ); context.core.image.firstFocusableElement = diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 6de2bdd6898596..1fb4804d0cb809 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -231,13 +231,7 @@ opacity: 0.9; } - &.initialized { - animation: both turn-off-visibility 300ms; - - img { - animation: both turn-off-visibility 250ms; - } - + &.fade { &.active { visibility: visible; animation: both turn-on-visibility 250ms; @@ -246,6 +240,43 @@ animation: both turn-on-visibility 300ms; } } + &.initialized { + &:not(.active) { + animation: both turn-off-visibility 300ms; + + img { + animation: both turn-off-visibility 250ms; + } + } + } + } + + &.zoom { + img { + position: absolute; + transform-origin: top left; + } + + &.active { + opacity: 1; + visibility: visible; + .wp-block-image img { + animation: lightbox-zoom-in .4s forwards; + } + .scrim { + animation: turn-on-visibility .4s forwards; + } + } + &.initialized { + &:not(.active) { + .wp-block-image img { + animation: lightbox-zoom-out .4s forwards; + } + .scrim { + animation: turn-off-visibility .4s forwards; + } + } + } } } @@ -273,6 +304,39 @@ } } -html.has-lightbox-open { - overflow: hidden; +// This line causes the zoom animation to jump +// +// html.has-lightbox-open { +// overflow: hidden; +// } + +@keyframes lightbox-zoom-in { + 0% { + left: var(--lightbox-left-position); + top: var(--lightbox-top-position); + transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height)); + } + 100% { + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(1, 1); + } +} + +@keyframes lightbox-zoom-out { + 0% { + visibility: visible; + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(1, 1); + } + 99% { + visibility: visible; + } + 100% { + + left: var(--lightbox-left-position); + top: var(--lightbox-top-position); + transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height)); + } } From 7b19ee7228e92baf083a8d7a1e271d9ffe399111 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Fri, 9 Jun 2023 14:15:45 -0500 Subject: [PATCH 04/16] Add logic to use original image and scale its dimensions Because the image in the lightbox has absolute positioning and does not respect the padding of its parent container, we need to get references to both the parent
element and the element to set the right scale and smoothly animate the zoom. To accomplish that, since we don't have access to the img src or its natural width and height until it actually appears in the DOM, I needed to hoist the imgSrc up to the parent context to allow for retrieval of the target dimensions from an element created on the fly. --- lib/block-supports/behaviors.php | 21 ++- .../block-library/src/image/interactivity.js | 130 ++++++++++++++---- packages/block-library/src/image/style.scss | 2 + 3 files changed, 113 insertions(+), 40 deletions(-) diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php index 62d484e3beeb85..310d52052714fb 100644 --- a/lib/block-supports/behaviors.php +++ b/lib/block-supports/behaviors.php @@ -77,11 +77,19 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { $lightbox_animation = $lightbox_settings['animation']; + $z = new WP_HTML_Tag_Processor( $content ); + $z->next_tag('img'); + if ( isset( $block['attrs']['id'] ) ) { + $img_src = wp_get_attachment_url( $block['attrs']['id'] ); + } else { + $img_src = $m->get_attribute( 'src' ); + } + $w = new WP_HTML_Tag_Processor( $content ); $w->next_tag( 'figure' ); $w->add_class( 'wp-lightbox-container' ); $w->set_attribute( 'data-wp-interactive', true ); - $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "lightboxEnabled": false, "lightboxAnimation": "' . $lightbox_animation . '" } } }' ); + $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "imageSrc": "' . $img_src . '", "lightboxEnabled": false, "lightboxAnimation": "' . $lightbox_animation . '" } } }' ); $body_content = $w->get_updated_html(); // Wrap the image in the body content with a button. @@ -96,16 +104,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { // Add directive to expand modal image if appropriate. $m = new WP_HTML_Tag_Processor( $content ); $m->next_tag( 'img' ); - if ( isset( $block['attrs']['id'] ) ) { - $img_src = wp_get_attachment_url( $block['attrs']['id'] ); - } else { - $img_src = $m->get_attribute( 'src' ); - } - - // Need to figure out how to smoothly transition image animation when using larger image - // - // $m->set_attribute( 'data-wp-context', '{ "core": { "image": { "imageSrc": "' . $img_src . '"} } }' ); - // $m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.imageSrc' ); + $m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.imageSrc' ); $modal_content = $m->get_updated_html(); $background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) ); diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js index 5606927985bda2..a07e0228b41495 100644 --- a/packages/block-library/src/image/interactivity.js +++ b/packages/block-library/src/image/interactivity.js @@ -31,33 +31,104 @@ store( { 'has-lightbox-open' ); - if ( context.core.image.lightboxAnimation === 'zoom' ) { - const { x: leftPosition, y: topPosition } = - event.target.nextElementSibling.getBoundingClientRect(); - const scaleWidth = - event.target.nextElementSibling.offsetWidth / - event.target.nextElementSibling.naturalWidth; - const scaleHeight = - event.target.nextElementSibling.offsetHeight / - event.target.nextElementSibling.naturalHeight; - const root = document.documentElement; - root.style.setProperty( - '--lightbox-left-position', - leftPosition + 'px' - ); - root.style.setProperty( - '--lightbox-top-position', - topPosition + 'px' - ); - root.style.setProperty( - '--lightbox-scale-width', - scaleWidth - ); - root.style.setProperty( - '--lightbox-scale-height', - scaleHeight - ); - } + const imgDom = document.createElement( 'img' ); + imgDom.setAttribute( 'src', context.core.image.imageSrc ); + imgDom.onload = function () { + if ( context.core.image.lightboxAnimation === 'zoom' ) { + let targetWidth = imgDom.naturalWidth; + let targetHeight = imgDom.naturalHeight; + + const figureStyle = window.getComputedStyle( + context.core.image.figureRef + ); + + const topPadding = parseInt( + figureStyle.getPropertyValue( 'padding-top' ) + ); + const bottomPadding = parseInt( + figureStyle.getPropertyValue( 'padding-bottom' ) + ); + const leftPadding = parseInt( + figureStyle.getPropertyValue( 'padding-left' ) + ); + const rightPadding = parseInt( + figureStyle.getPropertyValue( 'padding-right' ) + ); + + const figureWidth = + context.core.image.figureRef.clientWidth - + leftPadding - + rightPadding; + const figureHeight = + context.core.image.figureRef.clientHeight - + topPadding - + bottomPadding; + + // Check difference between the image and figure dimensions + const widthOverflow = Math.abs( + Math.min( figureWidth - targetWidth, 0 ) + ); + const heightOverflow = Math.abs( + Math.min( figureHeight - targetHeight, 0 ) + ); + + // If image is larger than the figure, resize along its largest axis + if ( widthOverflow > 0 || heightOverflow > 0 ) { + if ( widthOverflow > heightOverflow ) { + targetWidth = figureWidth; + targetHeight = + context.core.image.imageRef + .naturalHeight * + ( targetWidth / + context.core.image.imageRef + .naturalWidth ); + } else { + targetHeight = figureHeight; + targetWidth = + context.core.image.imageRef + .naturalWidth * + ( targetHeight / + context.core.image.imageRef + .naturalHeight ); + } + } + + const { x: leftPosition, y: topPosition } = + event.target.nextElementSibling.getBoundingClientRect(); + const scaleWidth = + event.target.nextElementSibling.offsetWidth / + targetWidth; + const scaleHeight = + event.target.nextElementSibling.offsetHeight / + targetHeight; + const root = document.documentElement; + + root.style.setProperty( + '--lightbox-image-max-width', + targetWidth + 'px' + ); + root.style.setProperty( + '--lightbox-image-max-height', + targetHeight + 'px' + ); + root.style.setProperty( + '--lightbox-left-position', + leftPosition + 'px' + ); + root.style.setProperty( + '--lightbox-top-position', + topPosition + 'px' + ); + root.style.setProperty( + '--lightbox-scale-width', + scaleWidth + ); + root.style.setProperty( + '--lightbox-scale-height', + scaleHeight + ); + } + }; }, hideLightbox: async ( { context, event } ) => { if ( context.core.image.lightboxEnabled ) { @@ -151,9 +222,10 @@ store( { core: { image: { initLightbox: async ( { context, ref } ) => { + context.core.image.figureRef = + ref.querySelector( 'figure' ); + context.core.image.imageRef = ref.querySelector( 'img' ); if ( context.core.image.lightboxEnabled ) { - context.core.image.lightboxImage = - ref.querySelector( 'img' ); const focusableElements = ref.querySelectorAll( focusableSelectors ); context.core.image.firstFocusableElement = diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 1fb4804d0cb809..f4ebfeb17f7648 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -255,6 +255,8 @@ img { position: absolute; transform-origin: top left; + width: var(--lightbox-image-max-width); + height: var(--lightbox-image-max-height); } &.active { From fb810fba819bef56f43aec3cd8e6c52cffbee967 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Fri, 9 Jun 2023 14:49:22 -0500 Subject: [PATCH 05/16] Remove extraneous help text --- packages/block-editor/src/hooks/behaviors.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 667df92826cb9b..746f0f2800d3dc 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -73,7 +73,7 @@ function BehaviorsControl( { const helpText = disabled ? __( 'The lightbox behavior is disabled for linked images.' ) - : __( 'Add behaviors.' ); + : ''; const value = () => { if ( blockBehaviors === undefined ) { @@ -102,7 +102,7 @@ function BehaviorsControl( { /> { behaviors?.lightbox.enabled && ( From 4e3c571d15a8e015a8aa65da306b1b74906a8bcb Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 07:28:32 -0500 Subject: [PATCH 06/16] Manually center lightbox image to improve animation performance The previous method of centering the image was peforming poorly on mobile. By doing more manual calculation, the animation now performs much better. --- lib/block-supports/behaviors.php | 3 +- .../block-library/src/image/interactivity.js | 68 ++++++++++++------- packages/block-library/src/image/style.scss | 30 ++++---- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php index 310d52052714fb..7900845330826b 100644 --- a/lib/block-supports/behaviors.php +++ b/lib/block-supports/behaviors.php @@ -89,7 +89,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { $w->next_tag( 'figure' ); $w->add_class( 'wp-lightbox-container' ); $w->set_attribute( 'data-wp-interactive', true ); - $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "imageSrc": "' . $img_src . '", "lightboxEnabled": false, "lightboxAnimation": "' . $lightbox_animation . '" } } }' ); + $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "imageSrc": "' . $img_src . '", "lightboxEnabled": false, "lightboxAnimation": "' . $lightbox_animation . '", "animateOutEnabled": false } } }' ); $body_content = $w->get_updated_html(); // Wrap the image in the body content with a button. @@ -120,6 +120,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { aria-label="$dialog_label" data-wp-class--initialized="context.core.image.initialized" data-wp-class--active="context.core.image.lightboxEnabled" + data-wp-class--animateOutEnabled="context.core.image.animateOutEnabled" data-wp-bind--aria-hidden="!context.core.image.lightboxEnabled" data-wp-bind--aria-modal="context.core.image.lightboxEnabled" data-wp-effect="effects.core.image.initLightbox" diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js index a07e0228b41495..fb1eaefee6b015 100644 --- a/packages/block-library/src/image/interactivity.js +++ b/packages/block-library/src/image/interactivity.js @@ -23,7 +23,6 @@ store( { image: { showLightbox: ( { context, event } ) => { context.core.image.initialized = true; - context.core.image.lightboxEnabled = true; context.core.image.lastFocusedElement = window.document.activeElement; context.core.image.scrollPosition = window.scrollY; @@ -32,8 +31,8 @@ store( { ); const imgDom = document.createElement( 'img' ); - imgDom.setAttribute( 'src', context.core.image.imageSrc ); imgDom.onload = function () { + context.core.image.lightboxEnabled = true; if ( context.core.image.lightboxAnimation === 'zoom' ) { let targetWidth = imgDom.naturalWidth; let targetHeight = imgDom.naturalHeight; @@ -48,17 +47,15 @@ store( { const bottomPadding = parseInt( figureStyle.getPropertyValue( 'padding-bottom' ) ); - const leftPadding = parseInt( - figureStyle.getPropertyValue( 'padding-left' ) - ); - const rightPadding = parseInt( - figureStyle.getPropertyValue( 'padding-right' ) - ); - const figureWidth = - context.core.image.figureRef.clientWidth - - leftPadding - - rightPadding; + context.core.image.figureRef.clientWidth; + let horizontalPadding = 0; + if ( figureWidth > 480 ) { + horizontalPadding = 40; + } else if ( figureWidth > 1920 ) { + horizontalPadding = 80; + } + const figureHeight = context.core.image.figureRef.clientHeight - topPadding - @@ -75,21 +72,16 @@ store( { // If image is larger than the figure, resize along its largest axis if ( widthOverflow > 0 || heightOverflow > 0 ) { if ( widthOverflow > heightOverflow ) { - targetWidth = figureWidth; + targetWidth = + figureWidth - horizontalPadding * 2; targetHeight = - context.core.image.imageRef - .naturalHeight * - ( targetWidth / - context.core.image.imageRef - .naturalWidth ); + imgDom.naturalHeight * + ( targetWidth / imgDom.naturalWidth ); } else { targetHeight = figureHeight; targetWidth = - context.core.image.imageRef - .naturalWidth * - ( targetHeight / - context.core.image.imageRef - .naturalHeight ); + imgDom.naturalWidth * + ( targetHeight / imgDom.naturalHeight ); } } @@ -98,9 +90,25 @@ store( { const scaleWidth = event.target.nextElementSibling.offsetWidth / targetWidth; + const scaleHeight = event.target.nextElementSibling.offsetHeight / targetHeight; + let targetLeft = 0; + if ( targetWidth >= figureWidth ) { + targetLeft = horizontalPadding; + } else { + targetLeft = ( figureWidth - targetWidth ) / 2; + } + + let targetTop = 0; + if ( targetHeight >= figureHeight ) { + targetTop = topPadding; + } else { + targetTop = + ( figureHeight - targetHeight ) / 2 + + topPadding; + } const root = document.documentElement; root.style.setProperty( @@ -112,13 +120,21 @@ store( { targetHeight + 'px' ); root.style.setProperty( - '--lightbox-left-position', + '--lightbox-initial-left-position', leftPosition + 'px' ); root.style.setProperty( - '--lightbox-top-position', + '--lightbox-initial-top-position', topPosition + 'px' ); + root.style.setProperty( + '--lightbox-target-left-position', + targetLeft + 'px' + ); + root.style.setProperty( + '--lightbox-target-top-position', + targetTop + 'px' + ); root.style.setProperty( '--lightbox-scale-width', scaleWidth @@ -129,8 +145,10 @@ store( { ); } }; + imgDom.setAttribute( 'src', context.core.image.imageSrc ); }, hideLightbox: async ( { context, event } ) => { + context.core.image.animateOutEnabled = true; if ( context.core.image.lightboxEnabled ) { // If scrolling, wait a moment before closing the lightbox. if ( context.core.image.lightboxAnimation === 'fade' ) { diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index f4ebfeb17f7648..71e61b58bf3a59 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -263,19 +263,19 @@ opacity: 1; visibility: visible; .wp-block-image img { - animation: lightbox-zoom-in .4s forwards; + animation: lightbox-zoom-in 0.4s forwards; } .scrim { - animation: turn-on-visibility .4s forwards; + animation: turn-on-visibility 0.4s forwards; } } - &.initialized { + &.animateoutenabled { &:not(.active) { .wp-block-image img { - animation: lightbox-zoom-out .4s forwards; + animation: lightbox-zoom-out 0.4s forwards; } .scrim { - animation: turn-off-visibility .4s forwards; + animation: turn-off-visibility 0.4s forwards; } } } @@ -314,31 +314,31 @@ @keyframes lightbox-zoom-in { 0% { - left: var(--lightbox-left-position); - top: var(--lightbox-top-position); + left: var(--lightbox-initial-left-position); + top: var(--lightbox-initial-top-position); transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height)); } 100% { - left: 50%; - top: 50%; - transform: translate(-50%, -50%) scale(1, 1); + left: var(--lightbox-target-left-position); + top: var(--lightbox-target-top-position); + transform: scale(1, 1); } } @keyframes lightbox-zoom-out { 0% { visibility: visible; - left: 50%; - top: 50%; - transform: translate(-50%, -50%) scale(1, 1); + left: var(--lightbox-target-left-position); + top: var(--lightbox-target-top-position); + transform: scale(1, 1); } 99% { visibility: visible; } 100% { - left: var(--lightbox-left-position); - top: var(--lightbox-top-position); + left: var(--lightbox-initial-left-position); + top: var(--lightbox-initial-top-position); transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height)); } } From a49c47cfbaa36588a3e947762e9452009e412c58 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 07:36:48 -0500 Subject: [PATCH 07/16] Move and reenable class declaration for overflow The 'has-lightbox-open' class was previously causing the content to shift before the image animation occurred and looked like a mistake. I've now moved the declaration so that the class is added during the animation so it draws less attention. --- packages/block-library/src/image/interactivity.js | 6 +++--- packages/block-library/src/image/style.scss | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js index fb1eaefee6b015..1ba34bc0d38a9e 100644 --- a/packages/block-library/src/image/interactivity.js +++ b/packages/block-library/src/image/interactivity.js @@ -26,9 +26,6 @@ store( { context.core.image.lastFocusedElement = window.document.activeElement; context.core.image.scrollPosition = window.scrollY; - document.documentElement.classList.add( - 'has-lightbox-open' - ); const imgDom = document.createElement( 'img' ); imgDom.onload = function () { @@ -144,6 +141,9 @@ store( { scaleHeight ); } + document.documentElement.classList.add( + 'has-lightbox-open' + ); }; imgDom.setAttribute( 'src', context.core.image.imageSrc ); }, diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 71e61b58bf3a59..bd3f66d31af7f4 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -308,9 +308,9 @@ // This line causes the zoom animation to jump // -// html.has-lightbox-open { -// overflow: hidden; -// } +html.has-lightbox-open { + overflow: hidden; +} @keyframes lightbox-zoom-in { 0% { From 198f7230f6d1730faad95166255dd235caf67644 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 07:55:56 -0500 Subject: [PATCH 08/16] Add prefers reduced motion accessibility styles --- packages/block-library/src/image/style.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index bd3f66d31af7f4..1007af701c9c91 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -264,6 +264,10 @@ visibility: visible; .wp-block-image img { animation: lightbox-zoom-in 0.4s forwards; + + @media (prefers-reduced-motion) { + animation: both turn-on-visibility 0.3s; + } } .scrim { animation: turn-on-visibility 0.4s forwards; @@ -273,6 +277,10 @@ &:not(.active) { .wp-block-image img { animation: lightbox-zoom-out 0.4s forwards; + + @media (prefers-reduced-motion) { + animation: both turn-on-visibility 0.3s; + } } .scrim { animation: turn-off-visibility 0.4s forwards; From 9b471d3c94b2da0092ae2282e8e8f95e3eb9f432 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 07:57:15 -0500 Subject: [PATCH 09/16] Modify fade styles to prevent image flashing --- packages/block-library/src/image/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 1007af701c9c91..96e5669b637161 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -240,7 +240,7 @@ animation: both turn-on-visibility 300ms; } } - &.initialized { + &.animateoutenabled { &:not(.active) { animation: both turn-off-visibility 300ms; From 2325293d020a50f1833024b426ecf4464e155206 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 11:41:13 -0500 Subject: [PATCH 10/16] Simplify code for lightbox UI --- packages/block-editor/src/hooks/behaviors.js | 36 +++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 746f0f2800d3dc..8d3c086c1a8dcd 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -17,7 +17,8 @@ import { InspectorControls } from '../components'; function BehaviorsControl( { blockName, blockBehaviors, - onChange, + onChangeBehavior, + onChangeAnimation, disabled = false, } ) { const { settings, themeBehaviors } = useSelect( @@ -79,7 +80,7 @@ function BehaviorsControl( { if ( blockBehaviors === undefined ) { return 'default'; } - if ( behaviors?.lightbox ) { + if ( behaviors?.lightbox.enabled ) { return 'lightbox'; } return ''; @@ -94,7 +95,7 @@ function BehaviorsControl( { // At the moment we are only supporting one behavior (Lightbox) value={ value() } options={ options } - onChange={ onChange } + onChange={ onChangeBehavior } hideCancelButton={ true } help={ helpText } size="__unstable-large" @@ -116,7 +117,7 @@ function BehaviorsControl( { }, { value: 'fade', label: 'Fade' }, ] } - onChange={ onChange } + onChange={ onChangeAnimation } hideCancelButton={ false } size="__unstable-large" disabled={ disabled } @@ -153,7 +154,7 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => { { + onChangeBehavior={ ( nextValue ) => { if ( nextValue === 'default' ) { props.setAttributes( { behaviors: undefined, @@ -161,26 +162,27 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => { } else { // If the user selects something, it means that they want to // change the default value (true) so we save it in the attributes. - const enabled = - nextValue === 'lightbox' || - nextValue === 'zoom' || - nextValue === 'fade' - ? true - : false; - const animation = - nextValue === 'zoom' || nextValue === 'fade' - ? nextValue - : 'zoom'; props.setAttributes( { behaviors: { lightbox: { - enabled, - animation, + enabled: nextValue === 'lightbox', }, }, } ); } } } + onChangeAnimation={ ( nextValue ) => { + props.setAttributes( { + behaviors: { + lightbox: { + enabled: + props.attributes.behaviors.lightbox + .enabled, + animation: nextValue, + }, + }, + } ); + } } disabled={ blockHasLink } /> From 335fcbc65d7f3fc1022d4b458e338fbe96fbf67d Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 11:43:29 -0500 Subject: [PATCH 11/16] Fix PHP error and linter syntax --- lib/block-supports/behaviors.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php index 7900845330826b..682c6d2791abde 100644 --- a/lib/block-supports/behaviors.php +++ b/lib/block-supports/behaviors.php @@ -78,11 +78,11 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { $lightbox_animation = $lightbox_settings['animation']; $z = new WP_HTML_Tag_Processor( $content ); - $z->next_tag('img'); + $z->next_tag( 'img' ); if ( isset( $block['attrs']['id'] ) ) { $img_src = wp_get_attachment_url( $block['attrs']['id'] ); } else { - $img_src = $m->get_attribute( 'src' ); + $img_src = $z->get_attribute( 'src' ); } $w = new WP_HTML_Tag_Processor( $content ); From 9c62703c488e55eef238e25c1303d4daca0f6b41 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 14:10:47 -0500 Subject: [PATCH 12/16] Clean up code; fix bug, add comments Mostly moved code around, renamed variables for clarity, and add comments. Fixed a bug wherein the lightbox wouldn't close on scroll when using a fade animation. --- lib/block-supports/behaviors.php | 15 +- .../block-library/src/image/interactivity.js | 227 +++++++++--------- packages/block-library/src/image/style.scss | 27 +-- 3 files changed, 137 insertions(+), 132 deletions(-) diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php index 682c6d2791abde..27e5faeb8e8a42 100644 --- a/lib/block-supports/behaviors.php +++ b/lib/block-supports/behaviors.php @@ -75,8 +75,12 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { } $content = $processor->get_updated_html(); - $lightbox_animation = $lightbox_settings['animation']; + $lightbox_animation = ''; + if ( isset( $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' ); if ( isset( $block['attrs']['id'] ) ) { @@ -89,7 +93,10 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { $w->next_tag( 'figure' ); $w->add_class( 'wp-lightbox-container' ); $w->set_attribute( 'data-wp-interactive', true ); - $w->set_attribute( 'data-wp-context', '{ "core": { "image": { "initialized": false, "imageSrc": "' . $img_src . '", "lightboxEnabled": false, "lightboxAnimation": "' . $lightbox_animation . '", "animateOutEnabled": false } } }' ); + $w->set_attribute( + 'data-wp-context', + sprintf( '{ "core":{ "image": { "initialized": false, "imageSrc": "%s", "lightboxEnabled": false, "lightboxAnimation": "%s", "hideAnimationEnabled": false } } }', $img_src, $lightbox_animation ) + ); $body_content = $w->get_updated_html(); // Wrap the image in the body content with a button. @@ -101,7 +108,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { ''; $body_content = preg_replace( '/]+>/', $button, $body_content ); - // Add directive to expand modal image if appropriate. + // Add src to the modal image. $m = new WP_HTML_Tag_Processor( $content ); $m->next_tag( 'img' ); $m->set_attribute( 'data-wp-bind--src', 'selectors.core.image.imageSrc' ); @@ -120,7 +127,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { aria-label="$dialog_label" data-wp-class--initialized="context.core.image.initialized" data-wp-class--active="context.core.image.lightboxEnabled" - data-wp-class--animateOutEnabled="context.core.image.animateOutEnabled" + data-wp-class--hideAnimationEnabled="context.core.image.hideAnimationEnabled" data-wp-bind--aria-hidden="!context.core.image.lightboxEnabled" data-wp-bind--aria-modal="context.core.image.lightboxEnabled" data-wp-effect="effects.core.image.initLightbox" diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js index 1ba34bc0d38a9e..2092e66e4f2878 100644 --- a/packages/block-library/src/image/interactivity.js +++ b/packages/block-library/src/image/interactivity.js @@ -25,122 +25,24 @@ store( { context.core.image.initialized = true; context.core.image.lastFocusedElement = window.document.activeElement; - context.core.image.scrollPosition = window.scrollY; + context.core.image.scrollDelta = 0; + // Since the img is hidden and its src not loaded until + // the lightbox is opened, let's create an img element on the fly + // so we can get the dimensions we need to calculate the styles const imgDom = document.createElement( 'img' ); + imgDom.onload = function () { + // Enable the lightbox only after the image + // is loaded to prevent flashing of unstyled content context.core.image.lightboxEnabled = true; if ( context.core.image.lightboxAnimation === 'zoom' ) { - let targetWidth = imgDom.naturalWidth; - let targetHeight = imgDom.naturalHeight; - - const figureStyle = window.getComputedStyle( - context.core.image.figureRef - ); - - const topPadding = parseInt( - figureStyle.getPropertyValue( 'padding-top' ) - ); - const bottomPadding = parseInt( - figureStyle.getPropertyValue( 'padding-bottom' ) - ); - const figureWidth = - context.core.image.figureRef.clientWidth; - let horizontalPadding = 0; - if ( figureWidth > 480 ) { - horizontalPadding = 40; - } else if ( figureWidth > 1920 ) { - horizontalPadding = 80; - } - - const figureHeight = - context.core.image.figureRef.clientHeight - - topPadding - - bottomPadding; - - // Check difference between the image and figure dimensions - const widthOverflow = Math.abs( - Math.min( figureWidth - targetWidth, 0 ) - ); - const heightOverflow = Math.abs( - Math.min( figureHeight - targetHeight, 0 ) - ); - - // If image is larger than the figure, resize along its largest axis - if ( widthOverflow > 0 || heightOverflow > 0 ) { - if ( widthOverflow > heightOverflow ) { - targetWidth = - figureWidth - horizontalPadding * 2; - targetHeight = - imgDom.naturalHeight * - ( targetWidth / imgDom.naturalWidth ); - } else { - targetHeight = figureHeight; - targetWidth = - imgDom.naturalWidth * - ( targetHeight / imgDom.naturalHeight ); - } - } - - const { x: leftPosition, y: topPosition } = - event.target.nextElementSibling.getBoundingClientRect(); - const scaleWidth = - event.target.nextElementSibling.offsetWidth / - targetWidth; - - const scaleHeight = - event.target.nextElementSibling.offsetHeight / - targetHeight; - let targetLeft = 0; - if ( targetWidth >= figureWidth ) { - targetLeft = horizontalPadding; - } else { - targetLeft = ( figureWidth - targetWidth ) / 2; - } - - let targetTop = 0; - if ( targetHeight >= figureHeight ) { - targetTop = topPadding; - } else { - targetTop = - ( figureHeight - targetHeight ) / 2 + - topPadding; - } - const root = document.documentElement; - - root.style.setProperty( - '--lightbox-image-max-width', - targetWidth + 'px' - ); - root.style.setProperty( - '--lightbox-image-max-height', - targetHeight + 'px' - ); - root.style.setProperty( - '--lightbox-initial-left-position', - leftPosition + 'px' - ); - root.style.setProperty( - '--lightbox-initial-top-position', - topPosition + 'px' - ); - root.style.setProperty( - '--lightbox-target-left-position', - targetLeft + 'px' - ); - root.style.setProperty( - '--lightbox-target-top-position', - targetTop + 'px' - ); - root.style.setProperty( - '--lightbox-scale-width', - scaleWidth - ); - root.style.setProperty( - '--lightbox-scale-height', - scaleHeight - ); + setZoomStyles( imgDom, context, event ); } + + // Hide overflow only when the animation is in progress, + // otherwise the removal of the scrollbars will draw attention + // to itself and look like an error document.documentElement.classList.add( 'has-lightbox-open' ); @@ -148,16 +50,17 @@ store( { imgDom.setAttribute( 'src', context.core.image.imageSrc ); }, hideLightbox: async ( { context, event } ) => { - context.core.image.animateOutEnabled = true; + context.core.image.hideAnimationEnabled = true; if ( context.core.image.lightboxEnabled ) { // If scrolling, wait a moment before closing the lightbox. if ( context.core.image.lightboxAnimation === 'fade' ) { + context.core.image.scrollDelta += event.deltaY; if ( event.type === 'mousewheel' && Math.abs( window.scrollY - - context.core.image.scrollPosition - ) < 5 + context.core.image.scrollDelta + ) < 10 ) { return; } @@ -258,3 +161,101 @@ store( { }, }, } ); + +function setZoomStyles( imgDom, context, event ) { + let targetWidth = imgDom.naturalWidth; + let targetHeight = imgDom.naturalHeight; + + const figureStyle = window.getComputedStyle( context.core.image.figureRef ); + const topPadding = parseInt( + figureStyle.getPropertyValue( 'padding-top' ) + ); + const bottomPadding = parseInt( + figureStyle.getPropertyValue( 'padding-bottom' ) + ); + + // As per the design, let's allow the image to stretch + // to the full width of its containing figure, but for the height, + // constrain it to the padding settings + const containerWidth = context.core.image.figureRef.clientWidth; + const containerHeight = + context.core.image.figureRef.clientHeight - topPadding - bottomPadding; + + // Check difference between the image and figure dimensions + const widthOverflow = Math.abs( + Math.min( containerWidth - targetWidth, 0 ) + ); + const heightOverflow = Math.abs( + Math.min( containerHeight - targetHeight, 0 ) + ); + + // The lightbox image has `positione:absolute` and + // ignores its parent's padding, so let's set the padding here, + // to be used when calculating the image width and positioning + let horizontalPadding = 0; + if ( containerWidth > 480 ) { + horizontalPadding = 40; + } else if ( containerWidth > 1920 ) { + horizontalPadding = 80; + } + + // If image is larger than its container, resize along its largest axis + if ( widthOverflow > 0 || heightOverflow > 0 ) { + if ( widthOverflow > heightOverflow ) { + targetWidth = containerWidth - horizontalPadding * 2; + targetHeight = + imgDom.naturalHeight * ( targetWidth / imgDom.naturalWidth ); + } else { + targetHeight = containerHeight; + targetWidth = + imgDom.naturalWidth * ( targetHeight / imgDom.naturalHeight ); + } + } + + // The reference img element lies adjacent to the event target button in the DOM + const { x: originLeft, y: originTop } = + event.target.nextElementSibling.getBoundingClientRect(); + const scaleWidth = + event.target.nextElementSibling.offsetWidth / targetWidth; + const scaleHeight = + event.target.nextElementSibling.offsetHeight / targetHeight; + + // Get values used to center the image + let targetLeft = 0; + if ( targetWidth >= containerWidth ) { + targetLeft = horizontalPadding; + } else { + targetLeft = ( containerWidth - targetWidth ) / 2; + } + let targetTop = 0; + if ( targetHeight >= containerHeight ) { + targetTop = topPadding; + } else { + targetTop = ( containerHeight - targetHeight ) / 2 + topPadding; + } + + const root = document.documentElement; + root.style.setProperty( '--lightbox-scale-width', scaleWidth ); + root.style.setProperty( '--lightbox-scale-height', scaleHeight ); + root.style.setProperty( '--lightbox-image-max-width', targetWidth + 'px' ); + root.style.setProperty( + '--lightbox-image-max-height', + targetHeight + 'px' + ); + root.style.setProperty( + '--lightbox-initial-left-position', + originLeft + 'px' + ); + root.style.setProperty( + '--lightbox-initial-top-position', + originTop + 'px' + ); + root.style.setProperty( + '--lightbox-target-left-position', + targetLeft + 'px' + ); + root.style.setProperty( + '--lightbox-target-top-position', + targetTop + 'px' + ); +} diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 96e5669b637161..24420840295bf6 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -234,18 +234,18 @@ &.fade { &.active { visibility: visible; - animation: both turn-on-visibility 250ms; + animation: both turn-on-visibility 0.25s; img { - animation: both turn-on-visibility 300ms; + animation: both turn-on-visibility 0.3s; } } - &.animateoutenabled { + &.hideanimationenabled { &:not(.active) { - animation: both turn-off-visibility 300ms; + animation: both turn-off-visibility 0.3s; img { - animation: both turn-off-visibility 250ms; + animation: both turn-off-visibility 0.25s; } } } @@ -266,20 +266,20 @@ animation: lightbox-zoom-in 0.4s forwards; @media (prefers-reduced-motion) { - animation: both turn-on-visibility 0.3s; + animation: both turn-on-visibility 0.4s; } } .scrim { animation: turn-on-visibility 0.4s forwards; } } - &.animateoutenabled { + &.hideanimationenabled { &:not(.active) { .wp-block-image img { animation: lightbox-zoom-out 0.4s forwards; @media (prefers-reduced-motion) { - animation: both turn-on-visibility 0.3s; + animation: both turn-off-visibility 0.4s; } } .scrim { @@ -290,6 +290,10 @@ } } +html.has-lightbox-open { + overflow: hidden; +} + @keyframes turn-on-visibility { 0% { opacity: 0; @@ -314,12 +318,6 @@ } } -// This line causes the zoom animation to jump -// -html.has-lightbox-open { - overflow: hidden; -} - @keyframes lightbox-zoom-in { 0% { left: var(--lightbox-initial-left-position); @@ -344,7 +342,6 @@ html.has-lightbox-open { visibility: visible; } 100% { - left: var(--lightbox-initial-left-position); top: var(--lightbox-initial-top-position); transform: scale(var(--lightbox-scale-width), var(--lightbox-scale-height)); From 3c442662d04f2615faf82a9ef46744723a2b6caa Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 14:54:43 -0500 Subject: [PATCH 13/16] Fix bug wherein newly placed images were not setting lightbox animation --- packages/block-editor/src/hooks/behaviors.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 8d3c086c1a8dcd..d4c88e38a87d13 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -166,6 +166,10 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => { behaviors: { lightbox: { enabled: nextValue === 'lightbox', + animation: + nextValue === 'lightbox' + ? 'zoom' + : '', }, }, } ); From f557f74d068d9874e8b0a48f46f39c30df2af7e4 Mon Sep 17 00:00:00 2001 From: Ricardo Artemio Morales Date: Wed, 14 Jun 2023 15:24:11 -0500 Subject: [PATCH 14/16] Fix bug wherein vertical images were stretched on mobile Removed stylesheet padding declarations for the lightbox and cleaned up logic to ensure correct dimensions get set for vertical images on mobile devices. --- .../block-library/src/image/interactivity.js | 43 +++++++++---------- packages/block-library/src/image/style.scss | 1 - 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js index 2092e66e4f2878..2ef370496a894e 100644 --- a/packages/block-library/src/image/interactivity.js +++ b/packages/block-library/src/image/interactivity.js @@ -166,28 +166,12 @@ function setZoomStyles( imgDom, context, event ) { let targetWidth = imgDom.naturalWidth; let targetHeight = imgDom.naturalHeight; - const figureStyle = window.getComputedStyle( context.core.image.figureRef ); - const topPadding = parseInt( - figureStyle.getPropertyValue( 'padding-top' ) - ); - const bottomPadding = parseInt( - figureStyle.getPropertyValue( 'padding-bottom' ) - ); + const verticalPadding = 40; // As per the design, let's allow the image to stretch // to the full width of its containing figure, but for the height, - // constrain it to the padding settings + // constrain it with a fixed padding const containerWidth = context.core.image.figureRef.clientWidth; - const containerHeight = - context.core.image.figureRef.clientHeight - topPadding - bottomPadding; - - // Check difference between the image and figure dimensions - const widthOverflow = Math.abs( - Math.min( containerWidth - targetWidth, 0 ) - ); - const heightOverflow = Math.abs( - Math.min( containerHeight - targetHeight, 0 ) - ); // The lightbox image has `positione:absolute` and // ignores its parent's padding, so let's set the padding here, @@ -199,9 +183,24 @@ function setZoomStyles( imgDom, context, event ) { horizontalPadding = 80; } - // If image is larger than its container, resize along its largest axis + const containerHeight = + context.core.image.figureRef.clientHeight - verticalPadding * 2; + + // Check difference between the image and figure dimensions + const widthOverflow = Math.abs( + Math.min( containerWidth - targetWidth, 0 ) + ); + const heightOverflow = Math.abs( + Math.min( containerHeight - targetHeight, 0 ) + ); + + // If image is larger than its container any dimension, resize along its largest axis. + // For vertically oriented devices, always maximize the width. if ( widthOverflow > 0 || heightOverflow > 0 ) { - if ( widthOverflow > heightOverflow ) { + if ( + widthOverflow >= heightOverflow || + containerHeight >= containerWidth + ) { targetWidth = containerWidth - horizontalPadding * 2; targetHeight = imgDom.naturalHeight * ( targetWidth / imgDom.naturalWidth ); @@ -229,9 +228,9 @@ function setZoomStyles( imgDom, context, event ) { } let targetTop = 0; if ( targetHeight >= containerHeight ) { - targetTop = topPadding; + targetTop = verticalPadding; } else { - targetTop = ( containerHeight - targetHeight ) / 2 + topPadding; + targetTop = ( containerHeight - targetHeight ) / 2 + verticalPadding; } const root = document.documentElement; diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 24420840295bf6..563a91ad340b31 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -204,7 +204,6 @@ justify-content: center; align-items: center; flex-direction: column; - padding: 30px; figcaption { display: none; From c9358f8b2ec1f1ca8e1ed7047f398a20af07d0af Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 15 Jun 2023 13:17:01 +0200 Subject: [PATCH 15/16] Update e2e tests --- test/e2e/specs/editor/blocks/image.spec.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 2516a6d9c5ceaa..ad07a9b3914fd7 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -811,7 +811,12 @@ test.describe( 'Image - interactivity', () => { let blocks = await editor.getBlocks(); expect( blocks[ 0 ].attributes ).toMatchObject( { - behaviors: { lightbox: true }, + behaviors: { + lightbox: { + animation: 'zoom', + enabled: true, + }, + }, linkDestination: 'none', } ); expect( blocks[ 0 ].attributes.url ).toContain( filename ); @@ -819,7 +824,12 @@ test.describe( 'Image - interactivity', () => { await page.getByLabel( 'Behaviors' ).selectOption( '' ); blocks = await editor.getBlocks(); expect( blocks[ 0 ].attributes ).toMatchObject( { - behaviors: { lightbox: false }, + behaviors: { + lightbox: { + animation: '', + enabled: false, + }, + }, linkDestination: 'none', } ); expect( blocks[ 0 ].attributes.url ).toContain( filename ); From 7fc4425fe1f4f84aec733ff2c4be2a31ca537df2 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 15 Jun 2023 14:57:21 +0200 Subject: [PATCH 16/16] Update e2e tests, fix selector showing when it should not --- packages/block-editor/src/hooks/behaviors.js | 2 +- .../specs/editor/various/behaviors.spec.js | 88 ++----------------- .../behaviors-enabled/theme.json | 5 +- .../behaviors-ui-disabled/theme.json | 4 +- 4 files changed, 12 insertions(+), 87 deletions(-) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index d4c88e38a87d13..cfb2d1ffda2acb 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -66,7 +66,7 @@ function BehaviorsControl( { ]; // If every behavior is disabled, do not show the behaviors inspector control. - if ( options.length === 0 ) { + if ( behaviorsOptions.length === 0 ) { return null; } // Block behaviors take precedence over theme behaviors. diff --git a/test/e2e/specs/editor/various/behaviors.spec.js b/test/e2e/specs/editor/various/behaviors.spec.js index 51fd5abd4a35d8..9fe1fedf175c3c 100644 --- a/test/e2e/specs/editor/various/behaviors.spec.js +++ b/test/e2e/specs/editor/various/behaviors.spec.js @@ -49,43 +49,6 @@ test.describe( 'Testing behaviors functionality', () => { await page.waitForLoadState(); } ); - test( '`No Behaviors` should be the default as defined in the core theme.json', async ( { - admin, - editor, - requestUtils, - page, - behaviorUtils, - } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - await admin.createNewPost(); - const media = await behaviorUtils.createMedia(); - await editor.insertBlock( { - name: 'core/image', - attributes: { - alt: filename, - id: media.id, - url: media.source_url, - }, - } ); - - await editor.openDocumentSettingsSidebar(); - const editorSettings = page.getByRole( 'region', { - name: 'Editor settings', - } ); - await editorSettings - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - const select = editorSettings.getByRole( 'combobox', { - name: 'Behavior', - } ); - - // By default, no behaviors should be selected. - await expect( select ).toHaveValue( '' ); - - // By default, you should be able to select the Lightbox behavior. - await expect( select.getByRole( 'option' ) ).toHaveCount( 3 ); - } ); - test( 'Behaviors UI can be disabled in the `theme.json`', async ( { admin, editor, @@ -143,7 +106,12 @@ test.describe( 'Testing behaviors functionality', () => { id: media.id, url: media.source_url, // Explicitly set the value for behaviors to true. - behaviors: { lightbox: true }, + behaviors: { + lightbox: { + enabled: true, + animation: 'zoom', + }, + }, }, } ); @@ -173,50 +141,6 @@ test.describe( 'Testing behaviors functionality', () => { // lightbox even though the theme.json has it set to false. } ); - test( 'You can set the default value for the behaviors in the theme.json', async ( { - admin, - editor, - requestUtils, - page, - behaviorUtils, - } ) => { - // In this theme, the default value for settings.behaviors.blocks.core/image.lightbox is `true`. - await requestUtils.activateTheme( 'behaviors-enabled' ); - await admin.createNewPost(); - const media = await behaviorUtils.createMedia(); - - await editor.insertBlock( { - name: 'core/image', - attributes: { - alt: filename, - id: media.id, - url: media.source_url, - }, - } ); - - await editor.openDocumentSettingsSidebar(); - const editorSettings = page.getByRole( 'region', { - name: 'Editor settings', - } ); - await editorSettings - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - const select = editorSettings.getByRole( 'combobox', { - name: 'Behavior', - } ); - - // The behaviors dropdown should be present and the value should be set to - // `lightbox`. - await expect( select ).toHaveValue( 'lightbox' ); - - // There should be 2 options available: `No behaviors` and `Lightbox`. - await expect( select.getByRole( 'option' ) ).toHaveCount( 3 ); - - // We can change the value of the behaviors dropdown to `No behaviors`. - await select.selectOption( { label: 'No behaviors' } ); - await expect( select ).toHaveValue( '' ); - } ); - test( 'Lightbox behavior is disabled if the Image has a link', async ( { admin, editor, diff --git a/test/gutenberg-test-themes/behaviors-enabled/theme.json b/test/gutenberg-test-themes/behaviors-enabled/theme.json index f49129622d9f6d..8e7ce39023fd35 100644 --- a/test/gutenberg-test-themes/behaviors-enabled/theme.json +++ b/test/gutenberg-test-themes/behaviors-enabled/theme.json @@ -3,7 +3,10 @@ "behaviors": { "blocks": { "core/image": { - "lightbox": true + "lightbox": { + "enabled": true, + "animation": "zoom" + } } } } diff --git a/test/gutenberg-test-themes/behaviors-ui-disabled/theme.json b/test/gutenberg-test-themes/behaviors-ui-disabled/theme.json index a9f920f6dd0abc..cc4b0882fd22c3 100644 --- a/test/gutenberg-test-themes/behaviors-ui-disabled/theme.json +++ b/test/gutenberg-test-themes/behaviors-ui-disabled/theme.json @@ -3,9 +3,7 @@ "settings": { "blocks": { "core/image": { - "behaviors": { - "lightbox": false - } + "behaviors": false } } }