diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php
index 333f1033e3bfec..ab22133398c58b 100644
--- a/packages/block-library/src/image/index.php
+++ b/packages/block-library/src/image/index.php
@@ -20,26 +20,26 @@ function render_block_core_image( $attributes, $content, $block ) {
return '';
}
- $processor = new WP_HTML_Tag_Processor( $content );
+ $p = new WP_HTML_Tag_Processor( $content );
- if ( ! $processor->next_tag( 'img' ) || null === $processor->get_attribute( 'src' ) ) {
+ if ( ! $p->next_tag( 'img' ) || null === $p->get_attribute( 'src' ) ) {
return '';
}
if ( isset( $attributes['data-id'] ) ) {
- // Add the data-id="$id" attribute to the img element
- // to provide backwards compatibility for the Gallery Block,
- // which now wraps Image Blocks within innerBlocks.
- // The data-id attribute is added in a core/gallery `render_block_data` hook.
- $processor->set_attribute( 'data-id', $attributes['data-id'] );
+ // Adds the data-id="$id" attribute to the img element to provide backwards
+ // compatibility for the Gallery Block, which now wraps Image Blocks within
+ // innerBlocks. The data-id attribute is added in a core/gallery
+ // `render_block_data` hook.
+ $p->set_attribute( 'data-id', $attributes['data-id'] );
}
$link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none';
$lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block );
/*
- * If the lightbox is enabled and the image is not linked, add the filter
- * and the JavaScript view file.
+ * If the lightbox is enabled and the image is not linked, adds the filter and
+ * the JavaScript view file.
*/
if (
isset( $lightbox_settings ) &&
@@ -65,9 +65,9 @@ function render_block_core_image( $attributes, $content, $block ) {
* This render needs to happen in a filter with priority 15 to ensure that
* it runs after the duotone filter and that duotone styles are applied to
* the image in the lightbox. Lightbox has to work with any plugins that
- * might use filters as well. Removing this can be considered in the
- * future if the way the blocks are rendered changes, or if a
- * new kind of filter is introduced.
+ * might use filters as well. Removing this can be considered in the future
+ * if the way the blocks are rendered changes, or if a new kind of filter is
+ * introduced.
*/
add_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15, 2 );
} else {
@@ -77,7 +77,7 @@ function render_block_core_image( $attributes, $content, $block ) {
remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 );
}
- return $processor->get_updated_html();
+ return $p->get_updated_html();
}
/**
@@ -90,7 +90,7 @@ function render_block_core_image( $attributes, $content, $block ) {
* @return array Filtered block data.
*/
function block_core_image_get_lightbox_settings( $block ) {
- // Get the lightbox setting from the block attributes.
+ // Gets the lightbox setting from the block attributes.
if ( isset( $block['attrs']['lightbox'] ) ) {
$lightbox_settings = $block['attrs']['lightbox'];
}
@@ -101,9 +101,9 @@ function block_core_image_get_lightbox_settings( $block ) {
// If not present in global settings, check the top-level global settings.
//
// NOTE: If no block-level settings are found, the previous call to
- // `wp_get_global_settings` will return the whole `theme.json`
- // structure in which case we can check if the "lightbox" key is present at
- // the top-level of the global settings and use its value.
+ // `wp_get_global_settings` will return the whole `theme.json` structure in
+ // which case we can check if the "lightbox" key is present at the top-level
+ // of the global settings and use its value.
if ( isset( $lightbox_settings['lightbox'] ) ) {
$lightbox_settings = wp_get_global_settings( array( 'lightbox' ) );
}
@@ -122,103 +122,76 @@ function block_core_image_get_lightbox_settings( $block ) {
*/
function block_core_image_render_lightbox( $block_content, $block ) {
/*
- * If it's not possible that an IMG element exists then return the given
- * block content as-is. It may be that there's no actual image in the block
- * or it could be that another plugin already modified this HTML.
+ * If there's no IMG tag in the block then return the given block content
+ * as-is. There's nothing that this code can knowingly modify to add the
+ * lightbox behavior.
*/
- if ( false === stripos( $block_content, 'next_tag( 'figure' ) ) {
+ $p->set_bookmark( 'figure' );
}
-
- $processor = new WP_HTML_Tag_Processor( $block_content );
-
- $aria_label = __( 'Enlarge image' );
-
- /*
- * If there's definitely no IMG element in the block then return the given
- * block content as-is. There's nothing that this code can knowingly modify
- * to add the lightbox behavior.
- */
- if ( ! $processor->next_tag( 'img' ) ) {
+ if ( ! $p->next_tag( 'img' ) ) {
return $block_content;
}
- $alt_attribute = $processor->get_attribute( 'alt' );
+ $alt = $p->get_attribute( 'alt' );
+ $img_uploaded_src = $p->get_attribute( 'src' );
+ $img_class_names = $p->get_attribute( 'class' );
+ $img_styles = $p->get_attribute( 'style' );
+ $img_width = 'none';
+ $img_height = 'none';
+ $aria_label = __( 'Enlarge image' );
- // An empty alt attribute `alt=""` is valid for decorative images.
- 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 ) {
+ if ( $alt ) {
/* translators: %s: Image alt text. */
- $aria_label = sprintf( __( 'Enlarge image: %s' ), $alt_attribute );
+ $aria_label = sprintf( __( 'Enlarge image: %s' ), $alt );
}
- // Currently, we are only enabling the zoom animation.
- $lightbox_animation = 'zoom';
-
- // 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'] ?? 'none';
$img_height = $img_metadata['height'] ?? 'none';
- } else {
- $img_uploaded_src = $processor->get_attribute( 'src' );
- $img_width = 'none';
- $img_height = 'none';
}
- if ( isset( $block['attrs']['scale'] ) ) {
- $scale_attr = $block['attrs']['scale'];
- } else {
- $scale_attr = false;
- }
-
- $w = new WP_HTML_Tag_Processor( $block_content );
- $w->next_tag( 'figure' );
- $w->add_class( 'wp-lightbox-container' );
- $w->set_attribute( 'data-wp-interactive', '{"namespace":"core/image"}' );
-
- $w->set_attribute(
+ // Figure.
+ $p->seek( 'figure' );
+ $figure_class_names = $p->get_attribute( 'class' );
+ $figure_styles = $p->get_attribute( 'style' );
+ $p->add_class( 'wp-lightbox-container' );
+ $p->set_attribute( 'data-wp-interactive', '{"namespace":"core/image"}' );
+ $p->set_attribute(
'data-wp-context',
- sprintf(
- '{ "imageLoaded": false,
- "initialized": false,
- "lightboxEnabled": false,
- "hideAnimationEnabled": false,
- "preloadInitialized": false,
- "lightboxAnimation": "%s",
- "imageUploadedSrc": "%s",
- "imageCurrentSrc": "",
- "targetWidth": "%s",
- "targetHeight": "%s",
- "scaleAttr": "%s",
- "dialogLabel": "%s"
- }',
- $lightbox_animation,
- $img_uploaded_src,
- $img_width,
- $img_height,
- $scale_attr,
- __( 'Enlarged image' )
+ wp_json_encode(
+ array(
+ 'uploadedSrc' => $img_uploaded_src,
+ 'figureClassNames' => $figure_class_names,
+ 'figureStyles' => $figure_styles,
+ 'imgClassNames' => $img_class_names,
+ 'imgStyles' => $img_styles,
+ 'targetWidth' => $img_width,
+ 'targetHeight' => $img_height,
+ 'scaleAttr' => $block['attrs']['scale'] ?? false,
+ 'ariaLabel' => $aria_label,
+ 'alt' => $alt,
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
- $w->next_tag( 'img' );
- $w->set_attribute( 'data-wp-init', 'callbacks.initOriginImage' );
- $w->set_attribute( 'data-wp-on--load', 'actions.handleLoad' );
- $w->set_attribute( 'data-wp-watch', 'callbacks.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.showLightbox' );
- $w->set_attribute( 'data-wp-watch--setStylesOnResize', 'callbacks.setStylesOnResize' );
- $body_content = $w->get_updated_html();
- // Add a button alongside image in the body content.
+ // Image.
+ $p->next_tag( 'img' );
+ $p->set_attribute( 'data-wp-init', 'callbacks.setButtonStyles' );
+ $p->set_attribute( 'data-wp-on--load', 'callbacks.setButtonStyles' );
+ $p->set_attribute( 'data-wp-on-window--resize', 'callbacks.setButtonStyles' );
+ // Sets an event callback on the `img` because the `figure` element can also
+ // contain a caption, and we don't want to trigger the lightbox when the
+ // caption is clicked.
+ $p->set_attribute( 'data-wp-on--click', 'actions.showLightbox' );
+
+ $body_content = $p->get_updated_html();
+
+ // Adds a button alongside image in the body content.
$img = null;
preg_match( '/]+>/', $body_content, $img );
@@ -241,46 +214,17 @@ class="lightbox-trigger"
$body_content = preg_replace( '/]+>/', $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( $block_content );
- $m->next_tag( 'figure' );
- $m->add_class( 'responsive-image' );
- $m->next_tag( 'img' );
- // We want to set the 'src' attribute to an empty string in the responsive image
- // because otherwise, as of this writing, the wp_filter_content_tags() function in
- // WordPress will automatically add a 'srcset' attribute to the image, which will at
- // times cause the incorrectly sized image to be loaded in the lightbox on Firefox.
- // Because of this, we bind the 'src' attribute explicitly the current src to reliably
- // use the exact same image as in the content when the lightbox is first opened while
- // we wait for the larger image to load.
- $m->set_attribute( 'src', '' );
- $m->set_attribute( 'data-wp-bind--src', 'context.imageCurrentSrc' );
- $m->set_attribute( 'data-wp-style--object-fit', 'state.lightboxObjectFit' );
- $initial_image_content = $m->get_updated_html();
+ add_action( 'wp_footer', 'block_core_image_print_lightbox_overlay' );
- $q = new WP_HTML_Tag_Processor( $block_content );
- $q->next_tag( 'figure' );
- $q->add_class( 'enlarged-image' );
- $q->next_tag( 'img' );
+ return $body_content;
+}
- // We set the 'src' attribute to an empty string to prevent the browser from loading the image
- // on initial page load, then bind the attribute to a selector that returns the full-sized image src when
- // the lightbox is opened. We could use 'loading=lazy' in combination with the 'hidden' attribute to
- // accomplish the same behavior, but that approach breaks progressive loading of the image in Safari
- // and Chrome (see https://github.com/WordPress/gutenberg/pull/52765#issuecomment-1674008151). Until that
- // is resolved, manually setting the 'src' seems to be the best solution to load the large image on demand.
- $q->set_attribute( 'src', '' );
- $q->set_attribute( 'data-wp-bind--src', 'state.enlargedImgSrc' );
- $q->set_attribute( 'data-wp-style--object-fit', 'state.lightboxObjectFit' );
- $enlarged_image_content = $q->get_updated_html();
+function block_core_image_print_lightbox_overlay() {
+ $close_button_label = esc_attr__( 'Close' );
- // If the current theme does NOT have a `theme.json`, or the colors are not defined,
- // we need to set the background color & close button color to some default values
- // because we can't get them from the Global Styles.
+ // If the current theme does NOT have a `theme.json`, or the colors are not
+ // defined, it needs to set the background color & close button color to some
+ // default values because it can't get them from the Global Styles.
$background_color = '#fff';
$close_button_color = '#000';
if ( wp_theme_has_theme_json() ) {
@@ -293,35 +237,43 @@ class="lightbox-trigger"
}
}
- $close_button_icon = '';
- $close_button_label = esc_attr__( 'Close' );
-
- $lightbox_html = <<
-
-
$initial_image_content
-
$enlarged_image_content
-
-
+ echo <<
+
+
+
+
+
+
+
+
+
+
HTML;
-
- return str_replace( '', $lightbox_html . '', $body_content );
}
/**
diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss
index 303f43ce4ed5f1..4387d45d5699ec 100644
--- a/packages/block-library/src/image/style.scss
+++ b/packages/block-library/src/image/style.scss
@@ -301,7 +301,7 @@
animation: both turn-on-visibility 0.35s;
}
}
- &.hideanimationenabled {
+ &.show-closing-animation {
&:not(.active) {
animation: both turn-off-visibility 0.35s;
img {
@@ -327,7 +327,7 @@
animation: turn-on-visibility 0.4s forwards;
}
}
- &.hideanimationenabled {
+ &.show-closing-animation {
&:not(.active) {
animation: none;
.lightbox-image-container {
diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js
index 8ae0149726570c..c8ffdcba60b379 100644
--- a/packages/block-library/src/image/view.js
+++ b/packages/block-library/src/image/view.js
@@ -3,261 +3,346 @@
*/
import { store, getContext, getElement } from '@wordpress/interactivity';
-const focusableSelectors = [
- 'a[href]',
- 'area[href]',
- 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
- 'select:not([disabled]):not([aria-hidden])',
- 'textarea:not([disabled]):not([aria-hidden])',
- 'button:not([disabled]):not([aria-hidden])',
- 'iframe',
- 'object',
- 'embed',
- '[contenteditable]',
- '[tabindex]:not([tabindex^="-"])',
-];
-
/**
- * Stores a context-bound scroll handler.
- *
- * This callback could be defined inline inside of the store
- * object but it's created externally to avoid confusion about
- * how its logic is called. This logic is not referenced directly
- * by the directives in the markup because the scroll event we
- * need to listen to is triggered on the window; so by defining it
- * outside of the store, we signal that the behavior here is different.
- * If we find a compelling reason to move it to the store, feel free.
- *
- * @type {Function}
- */
-let scrollCallback;
-
-/**
- * Tracks whether user is touching screen; used to
- * differentiate behavior for touch and mouse input.
+ * Tracks whether user is touching screen; used to differentiate behavior for
+ * touch and mouse input.
*
* @type {boolean}
*/
let isTouching = false;
/**
- * Tracks the last time the screen was touched; used to
- * differentiate behavior for touch and mouse input.
+ * Tracks the last time the screen was touched; used to differentiate behavior
+ * for touch and mouse input.
*
* @type {number}
*/
let lastTouchTime = 0;
/**
- * Lightbox page-scroll handler: prevents scrolling.
- *
- * This handler is added to prevent scrolling behaviors that
- * trigger content shift while the lightbox is open.
+ * Stores the image reference of the currently opened lightbox.
*
- * It would be better to accomplish this through CSS alone, but
- * using overflow: hidden is currently the only way to do so, and
- * that causes the layout to shift and prevents the zoom animation
- * from working in some cases because we're unable to account for
- * the layout shift when doing the animation calculations. Instead,
- * here we use JavaScript to prevent and reset the scrolling
- * behavior. In the future, we may be able to use CSS or overflow: hidden
- * instead to not rely on JavaScript, but this seems to be the best approach
- * for now that provides the best visual experience.
+ * @type {HTMLElement}
+ */
+let imageRef;
+
+/**
+ * Stores the button reference of the currently opened lightbox.
*
- * @param {Object} ctx Context object with the `core/image` namespace.
+ * @type {HTMLElement}
*/
-function handleScroll( ctx ) {
- // We can't override the scroll behavior on mobile devices
- // because doing so breaks the pinch to zoom functionality, and we
- // want to allow users to zoom in further on the high-res image.
- if ( ! isTouching && Date.now() - lastTouchTime > 450 ) {
- // We are unable to use event.preventDefault() to prevent scrolling
- // because the scroll event can't be canceled, so we reset the position instead.
- window.scrollTo( ctx.scrollLeftReset, ctx.scrollTopReset );
- }
-}
+let buttonRef;
const { state, actions, callbacks } = store(
'core/image',
{
state: {
- windowWidth: window.innerWidth,
- windowHeight: window.innerHeight,
+ currentImage: {},
+ get overlayOpened() {
+ return state.currentImage.currentSrc;
+ },
get roleAttribute() {
- const ctx = getContext();
- return ctx.lightboxEnabled ? 'dialog' : null;
+ return state.overlayOpened ? 'dialog' : null;
},
get ariaModal() {
- const ctx = getContext();
- return ctx.lightboxEnabled ? 'true' : null;
+ return state.overlayOpened ? 'true' : null;
},
- get dialogLabel() {
- const ctx = getContext();
- return ctx.lightboxEnabled ? ctx.dialogLabel : null;
+ get enlargedSrc() {
+ return (
+ state.currentImage.uploadedSrc ||
+ 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
+ );
},
- get lightboxObjectFit() {
- const ctx = getContext();
- if ( ctx.initialized ) {
- return 'cover';
- }
- },
- get enlargedImgSrc() {
- const ctx = getContext();
- return ctx.initialized
- ? ctx.imageUploadedSrc
- : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
+ get imgStyles() {
+ return (
+ state.overlayOpened &&
+ `${ state.currentImage.imgStyles?.replace(
+ /;$/,
+ ''
+ ) }; object-fit:cover;`
+ );
},
},
actions: {
- showLightbox( event ) {
+ showLightbox() {
const ctx = getContext();
- // We can't initialize the lightbox until the reference
- // image is loaded, otherwise the UX is broken.
- if ( ! ctx.imageLoaded ) {
+
+ // Bails out if the image has not loaded yet.
+ if ( ! ctx.imageRef?.complete ) {
return;
}
- ctx.initialized = true;
- ctx.lastFocusedElement = window.document.activeElement;
- ctx.scrollDelta = 0;
- ctx.pointerType = event.pointerType;
-
- ctx.lightboxEnabled = true;
- setStyles( ctx, ctx.imageRef );
- ctx.scrollTopReset =
- window.pageYOffset || document.documentElement.scrollTop;
+ // Stores the positons of the scroll to fix it until the overlay is
+ // closed.
+ state.scrollTopReset = document.documentElement.scrollTop;
+ state.scrollLeftReset = document.documentElement.scrollLeft;
- // In most cases, this value will be 0, but this is included
- // in case a user has created a page with horizontal scrolling.
- ctx.scrollLeftReset =
- window.pageXOffset || document.documentElement.scrollLeft;
+ // Moves the information of the expaned image to the state.
+ ctx.currentSrc = ctx.imageRef.currentSrc;
+ imageRef = ctx.imageRef;
+ buttonRef = ctx.buttonRef;
+ state.currentImage = ctx;
+ state.overlayEnabled = true;
- // We define and bind the scroll callback here so
- // that we can pass the context and as an argument.
- // We may be able to change this in the future if we
- // define the scroll callback in the store instead, but
- // this approach seems to tbe clearest for now.
- scrollCallback = handleScroll.bind( null, ctx );
-
- // We need to add a scroll event listener to the window
- // here because we are unable to otherwise access it via
- // the Interactivity API directives. If we add a native way
- // to access the window, we can remove this.
- window.addEventListener( 'scroll', scrollCallback, false );
+ // Computes the styles of the overlay for the animation.
+ callbacks.setOverlayStyles();
},
hideLightbox() {
- const ctx = getContext();
- ctx.hideAnimationEnabled = true;
- if ( ctx.lightboxEnabled ) {
- // We want to wait until the close animation is completed
- // before allowing a user to scroll again. The duration of this
- // animation is defined in the styles.scss and depends on if the
- // animation is 'zoom' or 'fade', but in any case we should wait
- // a few milliseconds longer than the duration, otherwise a user
- // may scroll too soon and cause the animation to look sloppy.
+ if ( state.overlayEnabled ) {
+ // Waits until the close animation has completed before allowing a
+ // user to scroll again. The duration of this animation is defined in
+ // the `styles.scss` file, but in any case we should wait a few
+ // milliseconds longer than the duration, otherwise a user may scroll
+ // too soon and cause the animation to look sloppy.
setTimeout( function () {
- window.removeEventListener( 'scroll', scrollCallback );
- // If we don't delay before changing the focus,
- // the focus ring will appear on Firefox before
- // the image has finished animating, which looks broken.
- ctx.lightboxTriggerRef.focus( {
+ // Delays before changing the focus. Otherwise the focus ring will
+ // appear on Firefox before the image has finished animating, which
+ // looks broken.
+ buttonRef.focus( {
preventScroll: true,
} );
+
+ // Resets the current image to mark the overlay as closed.
+ state.currentImage = {};
+ imageRef = null;
+ buttonRef = null;
}, 450 );
- ctx.lightboxEnabled = false;
+ // Starts the overlay closing animation. The showClosingAnimation
+ // class is used to avoid showing it on page load.
+ state.showClosingAnimation = true;
+ state.overlayEnabled = false;
}
},
handleKeydown( event ) {
- const ctx = getContext();
- if ( ctx.lightboxEnabled ) {
- if ( event.key === 'Tab' || event.keyCode === 9 ) {
- // If shift + tab it change the direction
- if (
- event.shiftKey &&
- window.document.activeElement ===
- ctx.firstFocusableElement
- ) {
- event.preventDefault();
- ctx.lastFocusableElement.focus();
- } else if (
- ! event.shiftKey &&
- window.document.activeElement ===
- ctx.lastFocusableElement
- ) {
- event.preventDefault();
- ctx.firstFocusableElement.focus();
- }
+ if ( state.overlayEnabled ) {
+ // Focuses the close button when the user presses the tab key.
+ if ( event.key === 'Tab' ) {
+ event.preventDefault();
+ const { ref } = getElement();
+ ref.querySelector( 'button' ).focus();
}
-
- if ( event.key === 'Escape' || event.keyCode === 27 ) {
- actions.hideLightbox( event );
+ // Closes the lightbox when the user presses the escape key.
+ if ( event.key === 'Escape' ) {
+ actions.hideLightbox();
}
}
},
- // This is fired just by lazily loaded
- // images on the page, not all images.
- handleLoad() {
- const ctx = getContext();
- const { ref } = getElement();
- ctx.imageLoaded = true;
- ctx.imageCurrentSrc = ref.currentSrc;
- callbacks.setButtonStyles();
- },
- handleTouchStart() {
- isTouching = true;
- },
handleTouchMove( event ) {
- const ctx = getContext();
- // On mobile devices, we want to prevent triggering the
- // scroll event because otherwise the page jumps around as
- // we reset the scroll position. This also means that closing
- // the lightbox requires that a user perform a simple tap. This
- // may be changed in the future if we find a better alternative
- // to override or reset the scroll position during swipe actions.
- if ( ctx.lightboxEnabled ) {
+ // On mobile devices, prevents triggering the scroll event because
+ // otherwise the page jumps around when it resets the scroll position.
+ // This also means that closing the lightbox requires that a user
+ // perform a simple tap. This may be changed in the future if there is a
+ // better alternative to override or reset the scroll position during
+ // swipe actions.
+ if ( state.overlayEnabled ) {
event.preventDefault();
}
},
+ handleTouchStart() {
+ isTouching = true;
+ },
handleTouchEnd() {
- // We need to wait a few milliseconds before resetting
- // to ensure that pinch to zoom works consistently
- // on mobile devices when the lightbox is open.
+ // Waits a few milliseconds before resetting to ensure that pinch to
+ // zoom works consistently on mobile devices when the lightbox is open.
lastTouchTime = Date.now();
isTouching = false;
},
+ handleScroll() {
+ // Prevents scrolling behaviors that trigger content shift while the
+ // lightbox is open. It would be better to accomplish through CSS alone,
+ // but using overflow: hidden is currently the only way to do so and
+ // that causes a layout to shift and prevents the zoom animation from
+ // working in some cases because it's not possible to account for the
+ // layout shift when doing the animation calculations. Instead, it uses
+ // JavaScript to prevent and reset the scrolling behavior.
+ if ( state.overlayOpened ) {
+ // Avoids overriding the scroll behavior on mobile devices because
+ // doing so breaks the pinch to zoom functionality, and users should
+ // be able to zoom in further on the high-res image.
+ if ( ! isTouching && Date.now() - lastTouchTime > 450 ) {
+ // It doesn't rely on `event.preventDefault()` to prevent scrolling
+ // because the scroll event can't be canceled, so it resets the
+ // position instead.
+ window.scrollTo(
+ state.scrollLeftReset,
+ state.scrollTopReset
+ );
+ }
+ }
+ },
},
callbacks: {
- initOriginImage() {
- const ctx = getContext();
- const { ref } = getElement();
- ctx.imageRef = ref;
- if ( ref.complete ) {
- ctx.imageLoaded = true;
- ctx.imageCurrentSrc = ref.currentSrc;
+ setOverlayStyles() {
+ if ( ! imageRef ) return;
+
+ let {
+ naturalWidth,
+ naturalHeight,
+ offsetWidth: originalWidth,
+ offsetHeight: originalHeight,
+ } = imageRef;
+ let { x: screenPosX, y: screenPosY } =
+ imageRef.getBoundingClientRect();
+
+ // Natural ratio of the image clicked to open the lightbox.
+ const naturalRatio = naturalWidth / naturalHeight;
+ // Original ratio of the image clicked to open the lightbox.
+ let originalRatio = originalWidth / originalHeight;
+
+ // If it has object-fit: contain, recalculates the original sizes
+ // and the screen position without the blank spaces.
+ if ( state.currentImage.scaleAttr === 'contain' ) {
+ if ( naturalRatio > originalRatio ) {
+ const heightWithoutSpace = originalWidth / naturalRatio;
+ // Recalculates screen position without the top space.
+ screenPosY +=
+ ( originalHeight - heightWithoutSpace ) / 2;
+ originalHeight = heightWithoutSpace;
+ } else {
+ const widthWithoutSpace = originalHeight * naturalRatio;
+ // Recalculates screen position without the left space.
+ screenPosX += ( originalWidth - widthWithoutSpace ) / 2;
+ originalWidth = widthWithoutSpace;
+ }
+ }
+ originalRatio = originalWidth / originalHeight;
+
+ // Typically, it uses the image's full-sized dimensions. If those
+ // dimensions have not been set (i.e. an external image with only one
+ // size), the image's dimensions in the lightbox are the same
+ // as those of the image in the content.
+ let imgMaxWidth = parseFloat(
+ state.currentImage.targetWidth !== 'none'
+ ? state.currentImage.targetWidth
+ : naturalWidth
+ );
+ let imgMaxHeight = parseFloat(
+ state.currentImage.targetHeight !== 'none'
+ ? state.currentImage.targetHeight
+ : naturalHeight
+ );
+
+ // Ratio of the biggest image stored in the database.
+ let imgRatio = imgMaxWidth / imgMaxHeight;
+ let containerMaxWidth = imgMaxWidth;
+ let containerMaxHeight = imgMaxHeight;
+ let containerWidth = imgMaxWidth;
+ let containerHeight = imgMaxHeight;
+ // Checks if the target image has a different ratio than the original
+ // one (thumbnail). Recalculates the width and height.
+ if ( naturalRatio.toFixed( 2 ) !== imgRatio.toFixed( 2 ) ) {
+ if ( naturalRatio > imgRatio ) {
+ // If the width is reached before the height, it keeps the maxWidth
+ // and recalculates the height unless the difference between the
+ // maxHeight and the reducedHeight is higher than the maxWidth,
+ // where it keeps the reducedHeight and recalculate the width.
+ const reducedHeight = imgMaxWidth / naturalRatio;
+ if ( imgMaxHeight - reducedHeight > imgMaxWidth ) {
+ imgMaxHeight = reducedHeight;
+ imgMaxWidth = reducedHeight * naturalRatio;
+ } else {
+ imgMaxHeight = imgMaxWidth / naturalRatio;
+ }
+ } else {
+ // If the height is reached before the width, it keeps the maxHeight
+ // and recalculate the width unlesss the difference between the
+ // maxWidth and the reducedWidth is higher than the maxHeight, where
+ // it keeps the reducedWidth and recalculate the height.
+ const reducedWidth = imgMaxHeight * naturalRatio;
+ if ( imgMaxWidth - reducedWidth > imgMaxHeight ) {
+ imgMaxWidth = reducedWidth;
+ imgMaxHeight = reducedWidth / naturalRatio;
+ } else {
+ imgMaxWidth = imgMaxHeight * naturalRatio;
+ }
+ }
+ containerWidth = imgMaxWidth;
+ containerHeight = imgMaxHeight;
+ imgRatio = imgMaxWidth / imgMaxHeight;
+
+ // Calculates the max size of the container.
+ if ( originalRatio > imgRatio ) {
+ containerMaxWidth = imgMaxWidth;
+ containerMaxHeight = containerMaxWidth / originalRatio;
+ } else {
+ containerMaxHeight = imgMaxHeight;
+ containerMaxWidth = containerMaxHeight * originalRatio;
+ }
}
- },
- initTriggerButton() {
- const ctx = getContext();
- const { ref } = getElement();
- ctx.lightboxTriggerRef = ref;
- },
- initLightbox() {
- const ctx = getContext();
- const { ref } = getElement();
- if ( ctx.lightboxEnabled ) {
- const focusableElements =
- ref.querySelectorAll( focusableSelectors );
- ctx.firstFocusableElement = focusableElements[ 0 ];
- ctx.lastFocusableElement =
- focusableElements[ focusableElements.length - 1 ];
- // Move focus to the dialog when opening it.
- ref.focus();
+ // If the image has been pixelated on purpose, it keeps that size.
+ if (
+ originalWidth > containerWidth ||
+ originalHeight > containerHeight
+ ) {
+ containerWidth = originalWidth;
+ containerHeight = originalHeight;
+ }
+
+ // Calculates the final lightbox image size and the scale factor.
+ // MaxWidth is either the window container (accounting for padding) or
+ // the image resolution.
+ let horizontalPadding = 0;
+ if ( window.innerWidth > 480 ) {
+ horizontalPadding = 80;
+ } else if ( window.innerWidth > 1920 ) {
+ horizontalPadding = 160;
+ }
+ const verticalPadding = 80;
+
+ const targetMaxWidth = Math.min(
+ window.innerWidth - horizontalPadding,
+ containerWidth
+ );
+ const targetMaxHeight = Math.min(
+ window.innerHeight - verticalPadding,
+ containerHeight
+ );
+ const targetContainerRatio = targetMaxWidth / targetMaxHeight;
+
+ if ( originalRatio > targetContainerRatio ) {
+ // If targetMaxWidth is reached before targetMaxHeight.
+ containerWidth = targetMaxWidth;
+ containerHeight = containerWidth / originalRatio;
+ } else {
+ // If targetMaxHeight is reached before targetMaxWidth.
+ containerHeight = targetMaxHeight;
+ containerWidth = containerHeight * originalRatio;
}
+
+ const containerScale = originalWidth / containerWidth;
+ const lightboxImgWidth =
+ imgMaxWidth * ( containerWidth / containerMaxWidth );
+ const lightboxImgHeight =
+ imgMaxHeight * ( containerHeight / containerMaxHeight );
+
+ // As of this writing, using the calculations above will render the
+ // lightbox with a small, erroneous whitespace on the left side of the
+ // image in iOS Safari, perhaps due to an inconsistency in how browsers
+ // handle absolute positioning and CSS transformation. In any case,
+ // adding 1 pixel to the container width and height solves the problem,
+ // though this can be removed if the issue is fixed in the future.
+ state.overlayStyles = `
+ :root {
+ --wp--lightbox-initial-top-position: ${ screenPosY }px;
+ --wp--lightbox-initial-left-position: ${ screenPosX }px;
+ --wp--lightbox-container-width: ${ containerWidth + 1 }px;
+ --wp--lightbox-container-height: ${ containerHeight + 1 }px;
+ --wp--lightbox-image-width: ${ lightboxImgWidth }px;
+ --wp--lightbox-image-height: ${ lightboxImgHeight }px;
+ --wp--lightbox-scale: ${ containerScale };
+ --wp--lightbox-scrollbar-width: ${
+ window.innerWidth - document.documentElement.clientWidth
+ }px;
+ }
+ `;
},
setButtonStyles() {
+ const ctx = getContext();
const { ref } = getElement();
+ ctx.imageRef = ref;
+
const {
naturalWidth,
naturalHeight,
@@ -265,8 +350,8 @@ const { state, actions, callbacks } = store(
offsetHeight,
} = ref;
- // If the image isn't loaded yet, we can't
- // calculate where the button should be.
+ // If the image isn't loaded yet, it can't calculate where the button
+ // should be.
if ( naturalWidth === 0 || naturalHeight === 0 ) {
return;
}
@@ -274,10 +359,9 @@ const { state, actions, callbacks } = store(
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
+ // It needs special handling for the height because a caption will cause
+ // the figure to be taller than the image, which means it needs 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' );
@@ -300,11 +384,9 @@ const { state, actions, callbacks } = store(
const buttonOffsetTop = figureHeight - offsetHeight;
const buttonOffsetRight = figureWidth - offsetWidth;
- const ctx = getContext();
-
- // In the case of an image with object-fit: contain, the
- // size of the element can be larger than the image itself,
- // so we need to calculate where to place the button.
+ // In the case of an image with object-fit: contain, the size of the
+ // element can be larger than the image itself, so it needs to
+ // calculate where to place the button.
if ( ctx.scaleAttr === 'contain' ) {
// Natural ratio of the image.
const naturalRatio = naturalWidth / naturalHeight;
@@ -312,8 +394,8 @@ const { state, actions, callbacks } = store(
const offsetRatio = offsetWidth / offsetHeight;
if ( naturalRatio >= offsetRatio ) {
- // If it reaches the width first, keep
- // the width and compute the height.
+ // If it reaches the width first, it keeps the width and compute the
+ // height.
const referenceHeight = offsetWidth / naturalRatio;
ctx.imageButtonTop =
( offsetHeight - referenceHeight ) / 2 +
@@ -321,8 +403,8 @@ const { state, actions, callbacks } = store(
16;
ctx.imageButtonRight = buttonOffsetRight + 16;
} else {
- // If it reaches the height first, keep
- // the height and compute the width.
+ // If it reaches the height first, it keeps the height and compute
+ // the width.
const referenceWidth = offsetHeight * naturalRatio;
ctx.imageButtonTop = buttonOffsetTop + 16;
ctx.imageButtonRight =
@@ -335,217 +417,19 @@ const { state, actions, callbacks } = store(
ctx.imageButtonRight = buttonOffsetRight + 16;
}
},
- setStylesOnResize() {
+ setOverlayFocus() {
+ if ( state.overlayEnabled ) {
+ // Moves the focus to the dialog when it opens.
+ const { ref } = getElement();
+ ref.focus();
+ }
+ },
+ initTriggerButton() {
const ctx = getContext();
const { ref } = getElement();
- if (
- ctx.lightboxEnabled &&
- ( state.windowWidth || state.windowHeight )
- ) {
- setStyles( ctx, ref );
- }
+ ctx.buttonRef = ref;
},
},
},
{ lock: true }
);
-
-window.addEventListener(
- 'resize',
- debounce( () => {
- state.windowWidth = window.innerWidth;
- state.windowHeight = window.innerHeight;
- } )
-);
-
-/**
- * Computes styles for the lightbox and adds them to the document.
- *
- * @function
- * @param {Object} ctx - Context for the `core/image` namespace.
- * @param {Object} ref - The element reference.
- */
-function setStyles( ctx, ref ) {
- // The reference img element lies adjacent
- // to the event target button in the DOM.
- let {
- naturalWidth,
- naturalHeight,
- offsetWidth: originalWidth,
- offsetHeight: originalHeight,
- } = ref;
- let { x: screenPosX, y: screenPosY } = ref.getBoundingClientRect();
-
- // Natural ratio of the image clicked to open the lightbox.
- const naturalRatio = naturalWidth / naturalHeight;
- // Original ratio of the image clicked to open the lightbox.
- let originalRatio = originalWidth / originalHeight;
-
- // If it has object-fit: contain, recalculate the original sizes
- // and the screen position without the blank spaces.
- if ( ctx.scaleAttr === 'contain' ) {
- if ( naturalRatio > originalRatio ) {
- const heightWithoutSpace = originalWidth / naturalRatio;
- // Recalculate screen position without the top space.
- screenPosY += ( originalHeight - heightWithoutSpace ) / 2;
- originalHeight = heightWithoutSpace;
- } else {
- const widthWithoutSpace = originalHeight * naturalRatio;
- // Recalculate screen position without the left space.
- screenPosX += ( originalWidth - widthWithoutSpace ) / 2;
- originalWidth = widthWithoutSpace;
- }
- }
- originalRatio = originalWidth / originalHeight;
-
- // Typically, we use the image's full-sized dimensions. If those
- // dimensions have not been set (i.e. an external image with only one size),
- // the image's dimensions in the lightbox are the same
- // as those of the image in the content.
- let imgMaxWidth = parseFloat(
- ctx.targetWidth !== 'none' ? ctx.targetWidth : naturalWidth
- );
- let imgMaxHeight = parseFloat(
- ctx.targetHeight !== 'none' ? ctx.targetHeight : naturalHeight
- );
-
- // Ratio of the biggest image stored in the database.
- let imgRatio = imgMaxWidth / imgMaxHeight;
- let containerMaxWidth = imgMaxWidth;
- let containerMaxHeight = imgMaxHeight;
- let containerWidth = imgMaxWidth;
- let containerHeight = imgMaxHeight;
- // Check if the target image has a different ratio than the original one (thumbnail).
- // Recalculate the width and height.
- if ( naturalRatio.toFixed( 2 ) !== imgRatio.toFixed( 2 ) ) {
- if ( naturalRatio > imgRatio ) {
- // If the width is reached before the height, we keep the maxWidth
- // and recalculate the height.
- // Unless the difference between the maxHeight and the reducedHeight
- // is higher than the maxWidth, where we keep the reducedHeight and
- // recalculate the width.
- const reducedHeight = imgMaxWidth / naturalRatio;
- if ( imgMaxHeight - reducedHeight > imgMaxWidth ) {
- imgMaxHeight = reducedHeight;
- imgMaxWidth = reducedHeight * naturalRatio;
- } else {
- imgMaxHeight = imgMaxWidth / naturalRatio;
- }
- } else {
- // If the height is reached before the width, we keep the maxHeight
- // and recalculate the width.
- // Unless the difference between the maxWidth and the reducedWidth
- // is higher than the maxHeight, where we keep the reducedWidth and
- // recalculate the height.
- const reducedWidth = imgMaxHeight * naturalRatio;
- if ( imgMaxWidth - reducedWidth > imgMaxHeight ) {
- imgMaxWidth = reducedWidth;
- imgMaxHeight = reducedWidth / naturalRatio;
- } else {
- imgMaxWidth = imgMaxHeight * naturalRatio;
- }
- }
- containerWidth = imgMaxWidth;
- containerHeight = imgMaxHeight;
- imgRatio = imgMaxWidth / imgMaxHeight;
-
- // Calculate the max size of the container.
- if ( originalRatio > imgRatio ) {
- containerMaxWidth = imgMaxWidth;
- containerMaxHeight = containerMaxWidth / originalRatio;
- } else {
- containerMaxHeight = imgMaxHeight;
- containerMaxWidth = containerMaxHeight * originalRatio;
- }
- }
-
- // If the image has been pixelated on purpose, keep that size.
- if ( originalWidth > containerWidth || originalHeight > containerHeight ) {
- containerWidth = originalWidth;
- containerHeight = originalHeight;
- }
-
- // Calculate the final lightbox image size and the
- // scale factor. MaxWidth is either the window container
- // (accounting for padding) or the image resolution.
- let horizontalPadding = 0;
- if ( window.innerWidth > 480 ) {
- horizontalPadding = 80;
- } else if ( window.innerWidth > 1920 ) {
- horizontalPadding = 160;
- }
- const verticalPadding = 80;
-
- const targetMaxWidth = Math.min(
- window.innerWidth - horizontalPadding,
- containerWidth
- );
- const targetMaxHeight = Math.min(
- window.innerHeight - verticalPadding,
- containerHeight
- );
- const targetContainerRatio = targetMaxWidth / targetMaxHeight;
-
- if ( originalRatio > targetContainerRatio ) {
- // If targetMaxWidth is reached before targetMaxHeight
- containerWidth = targetMaxWidth;
- containerHeight = containerWidth / originalRatio;
- } else {
- // If targetMaxHeight is reached before targetMaxWidth
- containerHeight = targetMaxHeight;
- containerWidth = containerHeight * originalRatio;
- }
-
- const containerScale = originalWidth / containerWidth;
- const lightboxImgWidth =
- imgMaxWidth * ( containerWidth / containerMaxWidth );
- const lightboxImgHeight =
- imgMaxHeight * ( containerHeight / containerMaxHeight );
-
- // Add the CSS variables needed.
- let styleTag = document.getElementById( 'wp-lightbox-styles' );
- if ( ! styleTag ) {
- styleTag = document.createElement( 'style' );
- styleTag.id = 'wp-lightbox-styles';
- document.head.appendChild( styleTag );
- }
-
- // As of this writing, using the calculations above will render the lightbox
- // with a small, erroneous whitespace on the left side of the image in iOS Safari,
- // perhaps due to an inconsistency in how browsers handle absolute positioning and CSS
- // transformation. In any case, adding 1 pixel to the container width and height solves
- // the problem, though this can be removed if the issue is fixed in the future.
- styleTag.innerHTML = `
- :root {
- --wp--lightbox-initial-top-position: ${ screenPosY }px;
- --wp--lightbox-initial-left-position: ${ screenPosX }px;
- --wp--lightbox-container-width: ${ containerWidth + 1 }px;
- --wp--lightbox-container-height: ${ containerHeight + 1 }px;
- --wp--lightbox-image-width: ${ lightboxImgWidth }px;
- --wp--lightbox-image-height: ${ lightboxImgHeight }px;
- --wp--lightbox-scale: ${ containerScale };
- --wp--lightbox-scrollbar-width: ${
- window.innerWidth - document.documentElement.clientWidth
- }px;
- }
- `;
-}
-
-/**
- * Debounces a function call.
- *
- * @function
- * @param {Function} func - A function to be called
- * @param {number} wait - The time to wait before calling the function
- */
-function debounce( func, wait = 50 ) {
- let timeout;
- return () => {
- const later = () => {
- timeout = null;
- func();
- };
- clearTimeout( timeout );
- timeout = setTimeout( later, wait );
- };
-}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json
deleted file mode 100644
index 44ae61f90d10b8..00000000000000
--- a/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "$schema": "https://schemas.wp.org/trunk/block.json",
- "apiVersion": 2,
- "name": "test/directive-body",
- "title": "E2E Interactivity tests - directive body",
- "category": "text",
- "icon": "heart",
- "description": "",
- "supports": {
- "interactivity": true
- },
- "textdomain": "e2e-interactivity",
- "viewScript": "directive-body-view",
- "render": "file:./render.php"
-}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-body/render.php
deleted file mode 100644
index 31d5deb57eb116..00000000000000
--- a/packages/e2e-tests/plugins/interactive-blocks/directive-body/render.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-body/view.js
deleted file mode 100644
index 40f3f9e3be5d11..00000000000000
--- a/packages/e2e-tests/plugins/interactive-blocks/directive-body/view.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { store, getContext } from '@wordpress/interactivity';
-
-store( 'directive-body', {
- actions: {
- toggleText: () => {
- const context = getContext();
- context.text = context.text === 'text-1' ? 'text-2' : 'text-1';
- },
- },
-} );
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index 1a566d30fa1c6e..6baa1d2662c034 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+### Breaking changes
+
+- Remove the style prop (`key`) and class name arguments the `data-wp-style` and `data-wp-class` directives. ([#58835](https://github.com/WordPress/gutenberg/pull/58835)).
+- Remove the `data-wp-body` directive. ([#58835](https://github.com/WordPress/gutenberg/pull/58835))
+
### Enhancements
- Break up init with yielding to main to prevent long task from hydration. ([#58227](https://github.com/WordPress/gutenberg/pull/58227))
diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js
index bf722dd3d5118a..9184fb1d6d8035 100644
--- a/packages/interactivity/src/directives.js
+++ b/packages/interactivity/src/directives.js
@@ -10,7 +10,6 @@ import { deepSignal, peek } from 'deepsignal';
/**
* Internal dependencies
*/
-import { createPortal } from './portals';
import { useWatch, useInit } from './utils';
import { directive, getScope, getEvaluate } from './hooks';
import { kebabToCamelCase } from './utils/kebab-to-camelcase';
@@ -126,11 +125,6 @@ export default () => {
{ priority: 5 }
);
- // data-wp-body
- directive( 'body', ( { props: { children } } ) => {
- return createPortal( children, document.body );
- } );
-
// data-wp-watch--[name]
directive( 'watch', ( { directives: { watch }, evaluate } ) => {
watch.forEach( ( entry ) => {
@@ -165,15 +159,15 @@ export default () => {
// data-wp-class--[classname]
directive(
'class',
- ( { directives: { class: className }, element, evaluate } ) => {
- className
+ ( { directives: { class: classNames }, element, evaluate } ) => {
+ classNames
.filter( ( { suffix } ) => suffix !== 'default' )
.forEach( ( entry ) => {
- const name = entry.suffix;
- const result = evaluate( entry, { className: name } );
+ const className = entry.suffix;
+ const result = evaluate( entry );
const currentClass = element.props.class || '';
const classFinder = new RegExp(
- `(^|\\s)${ name }(\\s|$)`,
+ `(^|\\s)${ className }(\\s|$)`,
'g'
);
if ( ! result )
@@ -182,8 +176,8 @@ export default () => {
.trim();
else if ( ! classFinder.test( currentClass ) )
element.props.class = currentClass
- ? `${ currentClass } ${ name }`
- : name;
+ ? `${ currentClass } ${ className }`
+ : className;
useInit( () => {
/*
@@ -192,29 +186,29 @@ export default () => {
* need deps because it only needs to do it the first time.
*/
if ( ! result ) {
- element.ref.current.classList.remove( name );
+ element.ref.current.classList.remove( className );
} else {
- element.ref.current.classList.add( name );
+ element.ref.current.classList.add( className );
}
} );
} );
}
);
- // data-wp-style--[style-key]
+ // data-wp-style--[style-prop]
directive( 'style', ( { directives: { style }, element, evaluate } ) => {
style
.filter( ( { suffix } ) => suffix !== 'default' )
.forEach( ( entry ) => {
- const key = entry.suffix;
- const result = evaluate( entry, { key } );
+ const styleProp = entry.suffix;
+ const result = evaluate( entry );
element.props.style = element.props.style || {};
if ( typeof element.props.style === 'string' )
element.props.style = cssStringToObject(
element.props.style
);
- if ( ! result ) delete element.props.style[ key ];
- else element.props.style[ key ] = result;
+ if ( ! result ) delete element.props.style[ styleProp ];
+ else element.props.style[ styleProp ] = result;
useInit( () => {
/*
@@ -223,9 +217,9 @@ export default () => {
* because it only needs to do it the first time.
*/
if ( ! result ) {
- element.ref.current.style.removeProperty( key );
+ element.ref.current.style.removeProperty( styleProp );
} else {
- element.ref.current.style[ key ] = result;
+ element.ref.current.style[ styleProp ] = result;
}
} );
} );
@@ -252,7 +246,11 @@ export default () => {
* property excluding the following special cases. We follow Preact's
* logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129
*/
- if (
+ if ( attribute === 'style' ) {
+ if ( typeof result === 'string' )
+ el.style.cssText = result;
+ return;
+ } else if (
attribute !== 'width' &&
attribute !== 'height' &&
attribute !== 'href' &&
diff --git a/packages/interactivity/src/portals.js b/packages/interactivity/src/portals.js
deleted file mode 100644
index ccb293d6c20e80..00000000000000
--- a/packages/interactivity/src/portals.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement, render } from 'preact';
-
-/**
- * @param {import('../../src/index').RenderableProps<{ context: any }>} props
- */
-function ContextProvider( props ) {
- this.getChildContext = () => props.context;
- return props.children;
-}
-
-/**
- * Portal component
- *
- * @this {import('./internal').Component}
- * @param {object | null | undefined} props
- *
- * TODO: use createRoot() instead of fake root
- */
-function Portal( props ) {
- const _this = this;
- const container = props._container;
-
- _this.componentWillUnmount = function () {
- render( null, _this._temp );
- _this._temp = null;
- _this._container = null;
- };
-
- // When we change container we should clear our old container and
- // indicate a new mount.
- if ( _this._container && _this._container !== container ) {
- _this.componentWillUnmount();
- }
-
- // When props.vnode is undefined/false/null we are dealing with some kind of
- // conditional vnode. This should not trigger a render.
- if ( props._vnode ) {
- if ( ! _this._temp ) {
- _this._container = container;
-
- // Create a fake DOM parent node that manages a subset of `container`'s children:
- _this._temp = {
- nodeType: 1,
- parentNode: container,
- childNodes: [],
- appendChild( child ) {
- this.childNodes.push( child );
- _this._container.appendChild( child );
- },
- insertBefore( child ) {
- this.childNodes.push( child );
- _this._container.appendChild( child );
- },
- removeChild( child ) {
- this.childNodes.splice(
- // eslint-disable-next-line no-bitwise
- this.childNodes.indexOf( child ) >>> 1,
- 1
- );
- _this._container.removeChild( child );
- },
- };
- }
-
- // Render our wrapping element into temp.
- render(
- createElement(
- ContextProvider,
- { context: _this.context },
- props._vnode
- ),
- _this._temp
- );
- }
- // When we come from a conditional render, on a mounted
- // portal we should clear the DOM.
- else if ( _this._temp ) {
- _this.componentWillUnmount();
- }
-}
-
-/**
- * Create a `Portal` to continue rendering the vnode tree at a different DOM node
- *
- * @param {import('./internal').VNode} vnode The vnode to render
- * @param {import('./internal').PreactElement} container The DOM node to continue rendering in to.
- */
-export function createPortal( vnode, container ) {
- const el = createElement( Portal, {
- _vnode: vnode,
- _container: container,
- } );
- el.containerInfo = container;
- return el;
-}
diff --git a/test/e2e/specs/interactivity/directive-body.spec.ts b/test/e2e/specs/interactivity/directive-body.spec.ts
deleted file mode 100644
index be11cfc556b591..00000000000000
--- a/test/e2e/specs/interactivity/directive-body.spec.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Internal dependencies
- */
-import { test, expect } from './fixtures';
-
-test.describe( 'data-wp-body', () => {
- test.beforeAll( async ( { interactivityUtils: utils } ) => {
- await utils.activatePlugins();
- await utils.addPostWithBlock( 'test/directive-body' );
- } );
-
- test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
- await page.goto( utils.getLink( 'test/directive-body' ) );
- } );
-
- test.afterAll( async ( { interactivityUtils: utils } ) => {
- await utils.deactivatePlugins();
- await utils.deleteAllPosts();
- } );
-
- test( "should move the element to the document's body", async ( {
- page,
- } ) => {
- const container = page.getByTestId( 'container' );
- const parentTag = page
- .getByTestId( 'element with data-wp-body' )
- .locator( 'xpath=..' );
-
- await expect( container ).toBeEmpty();
- await expect( parentTag ).toHaveJSProperty( 'tagName', 'BODY' );
- } );
-
- test( 'should make context accessible for inner elements', async ( {
- page,
- } ) => {
- const text = page
- .getByTestId( 'element with data-wp-body' )
- .getByTestId( 'text' );
- const toggle = page.getByTestId( 'toggle text' );
-
- await expect( text ).toHaveText( 'text-1' );
- await toggle.click();
- await expect( text ).toHaveText( 'text-2' );
- await toggle.click();
- await expect( text ).toHaveText( 'text-1' );
- } );
-} );