Skip to content

Commit

Permalink
Image block: Add animation toggle to lightbox behavior (WordPress#51357)
Browse files Browse the repository at this point in the history
* Remove reset button in behaviors, use dropdown option instead

* Use default option reading from the theme, prevent autoupdate

* Add zoom animation UI and styles

* 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 <figure> element and the <image>
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 <img>
element created on the fly.

* Remove extraneous help text

* 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.

* 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.

* Add prefers reduced motion accessibility styles

* Modify fade styles to prevent image flashing

* Simplify code for lightbox UI

* Fix PHP error and linter syntax

* 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.

* Fix bug wherein newly placed images were not setting lightbox animation

* 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.

* Update e2e tests

* Update e2e tests, fix selector showing when it should not

---------

Co-authored-by: Carlos Bravo <[email protected]>
  • Loading branch information
2 people authored and sethrubenstein committed Jul 13, 2023
1 parent 0bc1a65 commit 36dea55
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 171 deletions.
38 changes: 25 additions & 13 deletions lib/block-supports/behaviors.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -75,11 +75,28 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
}
$content = $processor->get_updated_html();

$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'] ) ) {
$img_src = wp_get_attachment_url( $block['attrs']['id'] );
} else {
$img_src = $z->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 } } }' );
$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.
Expand All @@ -91,15 +108,9 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
'</div>';
$body_content = preg_replace( '/<img[^>]+>/', $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' );
if ( isset( $block['attrs']['id'] ) ) {
$img_src = wp_get_attachment_url( $block['attrs']['id'] );
} 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' );
$modal_content = $m->get_updated_html();

Expand All @@ -111,11 +122,12 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$close_button_label = esc_attr__( 'Close', 'gutenberg' );

$lightbox_html = <<<HTML
<div data-wp-body="" class="wp-lightbox-overlay"
<div data-wp-body="" class="wp-lightbox-overlay $lightbox_animation"
data-wp-bind--role="selectors.core.image.roleAttribute"
aria-label="$dialog_label"
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"
data-wp-bind--aria-modal="context.core.image.lightboxEnabled"
data-wp-effect="effects.core.image.initLightbox"
Expand Down
5 changes: 4 additions & 1 deletion lib/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"behaviors": {
"blocks": {
"core/image": {
"lightbox": false
"lightbox": {
"enabled": false,
"animation": ""
}
}
}
},
Expand Down
121 changes: 82 additions & 39 deletions packages/block-editor/src/hooks/behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,15 +14,11 @@ 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,
onChange,
onChangeBehavior,
onChangeAnimation,
disabled = false,
} ) {
const { settings, themeBehaviors } = useSelect(
Expand All @@ -43,36 +35,56 @@ function BehaviorsControl( {
[ blockName ]
);

const noBehaviorsOption = {
value: '',
label: __( 'No behaviors' ),
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 = [ noBehaviorsOption, ...behaviorsOptions ];
const options = [
...Object.values( defaultBehaviors ),
...behaviorsOptions,
];

// If every behavior is disabled, do not show the behaviors inspector control.
if ( behaviorsOptions.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.enabled ) {
return 'lightbox';
}
return '';
};

return (
<InspectorControls group="advanced">
Expand All @@ -81,24 +93,37 @@ function BehaviorsControl( {
<SelectControl
label={ __( 'Behaviors' ) }
// At the moment we are only supporting one behavior (Lightbox)
value={ behaviors?.lightbox ? 'lightbox' : '' }
value={ value() }
options={ options }
onChange={ onChange }
onChange={ onChangeBehavior }
hideCancelButton={ true }
help={ helpText }
size="__unstable-large"
disabled={ disabled }
/>
{ behaviors?.lightbox.enabled && (
<SelectControl
label={ __( 'Animation' ) }
// At the moment we are only supporting one behavior (Lightbox)
value={
behaviors?.lightbox.animation
? behaviors?.lightbox.animation
: ''
}
options={ [
{
value: 'zoom',
label: __( 'Zoom' ),
},
{ value: 'fade', label: 'Fade' },
] }
onChange={ onChangeAnimation }
hideCancelButton={ false }
size="__unstable-large"
disabled={ disabled }
/>
) }
</div>
<HStack justify="flex-end">
<Button
isSmall
disabled={ disabled }
onClick={ () => onChange( undefined ) }
>
{ __( 'Reset' ) }
</Button>
</HStack>
</InspectorControls>
);
}
Expand Down Expand Up @@ -129,8 +154,8 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => {
<BehaviorsControl
blockName={ props.name }
blockBehaviors={ props.attributes.behaviors }
onChange={ ( nextValue ) => {
if ( nextValue === undefined ) {
onChangeBehavior={ ( nextValue ) => {
if ( nextValue === 'default' ) {
props.setAttributes( {
behaviors: undefined,
} );
Expand All @@ -139,11 +164,29 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => {
// change the default value (true) so we save it in the attributes.
props.setAttributes( {
behaviors: {
lightbox: nextValue === 'lightbox',
lightbox: {
enabled: nextValue === 'lightbox',
animation:
nextValue === 'lightbox'
? 'zoom'
: '',
},
},
} );
}
} }
onChangeAnimation={ ( nextValue ) => {
props.setAttributes( {
behaviors: {
lightbox: {
enabled:
props.attributes.behaviors.lightbox
.enabled,
animation: nextValue,
},
},
} );
} }
disabled={ blockHasLink }
/>
</>
Expand Down
Loading

0 comments on commit 36dea55

Please sign in to comment.