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 = << - - - - - + 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' ); - } ); -} );