From af08ecc933b78dcd7050998e052dbf3827523a3a Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 30 Oct 2024 13:51:20 -0500 Subject: [PATCH 01/64] Midway commit. Scaling out is broken --- .../src/components/iframe/content.scss | 79 +++--- .../src/components/iframe/index.js | 254 ++++++------------ 2 files changed, 123 insertions(+), 210 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index 5e390800719949..f8427a25f7dab9 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -3,68 +3,65 @@ } .block-editor-iframe__html { + $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); + $scroll-top-next: var(--wp-block-editor-iframe-zoom-out-scroll-top-next, 0); + $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); + transform-origin: top center; // We don't want to animate the transform of the translateX because it is used // to "center" the canvas. Leaving it on causes the canvas to slide around in // odd ways. - @include editor-canvas-resize-animation( transform 0s, scale 0s, padding 0s, translate 0s); + @include editor-canvas-resize-animation(transform 0s, scale 0s, padding 0s, translate 0s); &.zoom-out-animation { - $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); - $scroll-top-next: var(--wp-block-editor-iframe-zoom-out-scroll-top-next, 0); - position: fixed; left: 0; right: 0; top: calc(-1 * #{$scroll-top}); bottom: 0; translate: 0 calc(#{$scroll-top} - #{$scroll-top-next}); - // Force preserving a scrollbar gutter as scrollbar-gutter isn't supported in all browsers yet, - // and removing the scrollbar causes the content to shift. - overflow-y: scroll; - - // We only want to animate the scaling when entering zoom out. When sidebars + scale: $scale; + // we only want to animate the scaling when entering zoom out. When sidebars // are toggled, the resizing of the iframe handles scaling the canvas as well, // and the doubled animations cause very odd animations. @include editor-canvas-resize-animation( transform 0s, top 0s, bottom 0s, right 0s, left 0s ); } -} - -.block-editor-iframe__html.is-zoomed-out { - $scale: var(--wp-block-editor-iframe-zoom-out-scale); - $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); - $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); - $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); - $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); - $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); - // Apply an X translation to center the scaled content within the available space. - transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); - scale: #{$scale}; - background-color: $gray-300; - // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, - // so we need to adjust the height of the content to match the scale by using negative margins. - $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); - $total-frame-height: calc(2 * #{$frame-size} / #{$scale}); - $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); - margin-bottom: calc(-1 * #{$total-height}); - // Add the top/bottom frame size. We use scaling to account for the left/right, as - // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling - // of the content. - padding-top: calc(#{$frame-size} / #{$scale}); - padding-bottom: calc(#{$frame-size} / #{$scale}); + &.is-zoomed-out { + $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); + $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); + $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); + $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); + $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); + // Apply an X translation to center the scaled content within the available space. + transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); + scale: $scale; + background-color: $gray-300; - body { - min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); + // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, + // so we need to adjust the height of the content to match the scale by using negative margins. + $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); + $total-frame-height: calc(2 * #{$frame-size} / #{$scale}); + $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); + margin-bottom: calc(-1 * #{$total-height}); + // Add the top/bottom frame size. We use scaling to account for the left/right, as + // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling + // of the content. + padding-top: calc(#{$frame-size} / #{$scale}); + padding-bottom: calc(#{$frame-size} / #{$scale}); - > .is-root-container:not(.wp-block-post-content) { - flex: 1; - display: flex; - flex-direction: column; - height: 100%; + body { + min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); - > main { + > .is-root-container:not(.wp-block-post-content) { flex: 1; + display: flex; + flex-direction: column; + height: 100%; + + > main { + flex: 1; + } } } } diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index f8b7c25084e38d..65f790ef52e4b4 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -20,7 +20,6 @@ import { useMergeRefs, useRefEffect, useDisabled, - useReducedMotion, } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -130,9 +129,10 @@ function Iframe( { const [ before, writingFlowRef, after ] = useWritingFlow(); const [ contentResizeListener, { height: contentHeight } ] = useResizeObserver(); - const [ containerResizeListener, { width: containerWidth } ] = - useResizeObserver(); - const prefersReducedMotion = useReducedMotion(); + const [ + containerResizeListener, + { width: containerWidth, height: containerHeight }, + ] = useResizeObserver(); const setRef = useRefEffect( ( node ) => { node._load = () => { @@ -271,18 +271,19 @@ function Iframe( { containerWidth ); - const frameSizeValue = parseInt( frameSize ); - const maxWidth = 750; const scaleValue = scale === 'auto-scaled' - ? ( Math.min( containerWidth, maxWidth ) - frameSizeValue * 2 ) / + ? ( Math.min( containerWidth, maxWidth ) - + parseInt( frameSize ) * 2 ) / scaleContainerWidth : scale; - const prevScaleRef = useRef( scaleValue ); + + const frameSizeValue = parseInt( frameSize ); const prevFrameSizeRef = useRef( frameSizeValue ); - const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + + const prevContainerHeightRef = useRef( containerHeight ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ @@ -336,173 +337,90 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); + const zoomOutAnimationTimeoutRef = useRef( null ); + const isAnimatingZoomOut = useRef( null ); + // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect + // that controls settings the CSS variables, but then we would need to do more work to ensure we're + // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large + // number of dependencies. useEffect( () => { - if ( - ! iframeDocument || - // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here - // instead of the dependency array to appease the linter. - ( scaleValue === 1 ) === ( prevScaleRef.current === 1 ) - ) { + // If we're animating, don't re-update things. + if ( ! iframeDocument || isAnimatingZoomOut.current ) { return; } - // Unscaled height of the current iframe container. - const clientHeight = iframeDocument.documentElement.clientHeight; - - // Scaled height of the current iframe content. - const scrollHeight = iframeDocument.documentElement.scrollHeight; - - // Previous scale value. - const prevScale = prevScaleRef.current; - - // Unscaled size of the previous padding around the iframe content. - const prevFrameSize = prevFrameSizeRef.current; - - // Unscaled height of the previous iframe container. - const prevClientHeight = prevClientHeightRef.current ?? clientHeight; - - // We can't trust the set value from contentHeight, as it was measured - // before the zoom out mode was changed. After zoom out mode is changed, - // appenders may appear or disappear, so we need to get the height from - // the iframe at this point when we're about to animate the zoom out. - // The iframe scrollTop, scrollHeight, and clientHeight will all be - // accurate. The client height also does change when the zoom out mode - // is toggled, as the bottom bar about selecting the template is - // added/removed when toggling zoom out mode. - const scrollTop = iframeDocument.documentElement.scrollTop; - - // Step 0: Start with the current scrollTop. - let scrollTopNext = scrollTop; - - // Step 1: Undo the effects of the previous scale and frame around the - // midpoint of the visible area. - scrollTopNext = - ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / - prevScale - - prevClientHeight / 2; - - // Step 2: Apply the new scale and frame around the midpoint of the - // visible area. - scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * scaleValue + - frameSizeValue - - clientHeight / 2; - - // Step 3: Handle an edge case so that you scroll to the top of the - // iframe if the top of the iframe content is visible in the container. - // The same edge case for the bottom is skipped because changing content - // makes calculating it impossible. - scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; - - // This is the scrollTop value if you are scrolled to the bottom of the - // iframe. We can't just let the browser handle it because we need to - // animate the scaling. - const maxScrollTop = - scrollHeight * ( scaleValue / prevScale ) + - frameSizeValue * 2 - - clientHeight; - - // Step 4: Clamp the scrollTopNext between the minimum and maximum - // possible scrollTop positions. Round the value to avoid subpixel - // truncation by the browser which sometimes causes a 1px error. - scrollTopNext = Math.round( - Math.min( - Math.max( 0, scrollTopNext ), - Math.max( 0, maxScrollTop ) - ) - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); + const handleZoomOutAnimation = () => { + clearTimeout( zoomOutAnimationTimeoutRef.current ); + isAnimatingZoomOut.current = true; - iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + const scrollTop = iframeDocument.documentElement.scrollTop; - function onZoomOutTransitionEnd() { - // Remove the position fixed for the animation. - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' + // Convert previous values to the zoomed in scale. + // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. + const scrollTopOriginal = Math.round( + ( scrollTop + + prevContainerHeightRef.current / 2 - + prevFrameSizeRef.current ) / + prevScaleRef.current - + prevContainerHeightRef.current / 2 ); - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSizeValue; - prevScaleRef.current = scaleValue; - - // Set the final scroll position that was just animated to. - // Disable reason: Eslint isn't smart enough to know that this is a - // DOM element. https://github.com/facebook/react/issues/31483 - // eslint-disable-next-line react-compiler/react-compiler - iframeDocument.documentElement.scrollTop = scrollTopNext; - } - - let raf; - if ( prefersReducedMotion ) { - // Hack: Wait for the window values to recalculate. - raf = iframeDocument.defaultView.requestAnimationFrame( - onZoomOutTransitionEnd + // // Convert the zoomed in value to the new scale. + // // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. + const scrollTopNext = Math.round( + ( scrollTopOriginal + containerHeight / 2 ) * scaleValue + + frameSizeValue - + containerHeight / 2 ); - } else { - iframeDocument.documentElement.addEventListener( - 'transitionend', - onZoomOutTransitionEnd, - { once: true } - ); - } - return () => { - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top' + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next' + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` ); - iframeDocument.documentElement.classList.remove( + + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - if ( prefersReducedMotion ) { - iframeDocument.defaultView.cancelAnimationFrame( raf ); - } else { - iframeDocument.documentElement.removeEventListener( - 'transitionend', - onZoomOutTransitionEnd + + zoomOutAnimationTimeoutRef.current = setTimeout( () => { + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' ); - } + + prevContainerHeightRef.current = containerHeight; + prevFrameSizeRef.current = frameSizeValue; + prevScaleRef.current = scaleValue; + isAnimatingZoomOut.current = false; + + iframeDocument.documentElement.scrollTop = scrollTopNext; + }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss }; - }, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] ); - // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect - // that controls settings the CSS variables, but then we would need to do more work to ensure we're - // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large - // number of dependencies. - useEffect( () => { - if ( ! iframeDocument ) { - return; - } + handleZoomOutAnimation(); if ( isZoomedOut ) { iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); } else { - // HACK: Since we can't remove this in the cleanup, we need to do it here. iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); } - return () => { - // HACK: Skipping cleanup because it causes issues with the zoom out - // animation. More refactoring is needed to fix this properly. - // iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); - }; - }, [ iframeDocument, isZoomedOut ] ); + return () => {}; + }, [ + iframeDocument, + isZoomedOut, + scaleValue, + frameSizeValue, + containerHeight, + ] ); // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { - if ( ! iframeDocument ) { + if ( ! iframeDocument || ! isZoomedOut ) { return; } @@ -542,26 +460,24 @@ function Iframe( { ); return () => { - // HACK: Skipping cleanup because it causes issues with the zoom out - // animation. More refactoring is needed to fix this properly. - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-frame-size' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-content-height' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-inner-height' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-container-width' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale-container-width' - // ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scale' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-frame-size' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-content-height' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-inner-height' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-container-width' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scale-container-width' + ); }; }, [ scaleValue, From c83c99f85266d181dd5a05fd8d955711d4ed5330 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 30 Oct 2024 13:57:18 -0500 Subject: [PATCH 02/64] Fix zoom in animation --- packages/block-editor/src/components/iframe/content.scss | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index f8427a25f7dab9..2c2350b6994ad5 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -6,12 +6,12 @@ $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); $scroll-top-next: var(--wp-block-editor-iframe-zoom-out-scroll-top-next, 0); $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); - + scale: $scale; transform-origin: top center; // We don't want to animate the transform of the translateX because it is used // to "center" the canvas. Leaving it on causes the canvas to slide around in // odd ways. - @include editor-canvas-resize-animation(transform 0s, scale 0s, padding 0s, translate 0s); + @include editor-canvas-resize-animation( transform 0s, padding 0s, translate 0s); &.zoom-out-animation { position: fixed; @@ -20,7 +20,6 @@ top: calc(-1 * #{$scroll-top}); bottom: 0; translate: 0 calc(#{$scroll-top} - #{$scroll-top-next}); - scale: $scale; // we only want to animate the scaling when entering zoom out. When sidebars // are toggled, the resizing of the iframe handles scaling the canvas as well, // and the doubled animations cause very odd animations. @@ -35,9 +34,7 @@ $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); // Apply an X translation to center the scaled content within the available space. transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); - scale: $scale; background-color: $gray-300; - // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, // so we need to adjust the height of the content to match the scale by using negative margins. $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); From ec589026f3737d0b56e4fd2c8425f5791ef36891 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 30 Oct 2024 14:37:46 -0500 Subject: [PATCH 03/64] Fix scaling by preserving CSS properites --- .../src/components/iframe/content.scss | 2 +- .../src/components/iframe/index.js | 23 +------------------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index 2c2350b6994ad5..c1abf8fa1d39e3 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -11,7 +11,7 @@ // We don't want to animate the transform of the translateX because it is used // to "center" the canvas. Leaving it on causes the canvas to slide around in // odd ways. - @include editor-canvas-resize-animation( transform 0s, padding 0s, translate 0s); + @include editor-canvas-resize-animation( transform 0s, scale 0s, padding 0s, translate 0s); &.zoom-out-animation { position: fixed; diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 65f790ef52e4b4..083c3f539f447a 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -420,7 +420,7 @@ function Iframe( { // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { - if ( ! iframeDocument || ! isZoomedOut ) { + if ( ! iframeDocument ) { return; } @@ -458,27 +458,6 @@ function Iframe( { '--wp-block-editor-iframe-zoom-out-scale-container-width', `${ scaleContainerWidth }px` ); - - return () => { - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scale' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-frame-size' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-content-height' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-inner-height' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-container-width' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scale-container-width' - ); - }; }, [ scaleValue, frameSize, From 6582cce0f4456115f2df7ffebdac1afb160ac88a Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 30 Oct 2024 14:45:47 -0500 Subject: [PATCH 04/64] Prevent reflow from removing and re adding scrollbar in animation --- packages/block-editor/src/components/iframe/content.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index c1abf8fa1d39e3..ff5bd3a4052f45 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -20,6 +20,9 @@ top: calc(-1 * #{$scroll-top}); bottom: 0; translate: 0 calc(#{$scroll-top} - #{$scroll-top-next}); + // Force preserving a scrollbar gutter as scrollbar-gutter isn't supported in all browsers yet, + // and removing the scrollbar causes the content to shift. + overflow-y: scroll; // we only want to animate the scaling when entering zoom out. When sidebars // are toggled, the resizing of the iframe handles scaling the canvas as well, // and the doubled animations cause very odd animations. From ce9497e54cc7b4dfe2d755f053b1dd60466b888e Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 31 Oct 2024 09:27:50 -0500 Subject: [PATCH 05/64] Only rerun zoom out use effects if zoom out has changed. --- .../src/components/iframe/index.js | 110 +++++++++--------- 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 083c3f539f447a..4fabd86af88046 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -13,6 +13,7 @@ import { useMemo, useEffect, useRef, + useCallback, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { @@ -20,6 +21,7 @@ import { useMergeRefs, useRefEffect, useDisabled, + usePrevious, } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -259,6 +261,7 @@ function Iframe( { }, [] ); const isZoomedOut = scale !== 1; + const prevIsZoomedOut = usePrevious( isZoomedOut ); useEffect( () => { if ( ! isZoomedOut ) { @@ -338,69 +341,66 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); const zoomOutAnimationTimeoutRef = useRef( null ); - const isAnimatingZoomOut = useRef( null ); - // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect - // that controls settings the CSS variables, but then we would need to do more work to ensure we're - // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large - // number of dependencies. - useEffect( () => { - // If we're animating, don't re-update things. - if ( ! iframeDocument || isAnimatingZoomOut.current ) { - return; - } - const handleZoomOutAnimation = () => { - clearTimeout( zoomOutAnimationTimeoutRef.current ); - isAnimatingZoomOut.current = true; + const handleZoomOutAnimation = useCallback( () => { + clearTimeout( zoomOutAnimationTimeoutRef.current ); - const scrollTop = iframeDocument.documentElement.scrollTop; + const scrollTop = iframeDocument.documentElement.scrollTop; - // Convert previous values to the zoomed in scale. - // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. - const scrollTopOriginal = Math.round( - ( scrollTop + - prevContainerHeightRef.current / 2 - - prevFrameSizeRef.current ) / - prevScaleRef.current - - prevContainerHeightRef.current / 2 - ); + // Convert previous values to the zoomed in scale. + // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. + const scrollTopOriginal = Math.round( + ( scrollTop + + prevContainerHeightRef.current / 2 - + prevFrameSizeRef.current ) / + prevScaleRef.current - + prevContainerHeightRef.current / 2 + ); - // // Convert the zoomed in value to the new scale. - // // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. - const scrollTopNext = Math.round( - ( scrollTopOriginal + containerHeight / 2 ) * scaleValue + - frameSizeValue - - containerHeight / 2 - ); + // // Convert the zoomed in value to the new scale. + // // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. + const scrollTopNext = Math.round( + ( scrollTopOriginal + containerHeight / 2 ) * scaleValue + + frameSizeValue - + containerHeight / 2 + ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` + ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` + ); + + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - iframeDocument.documentElement.classList.add( + zoomOutAnimationTimeoutRef.current = setTimeout( () => { + iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); - zoomOutAnimationTimeoutRef.current = setTimeout( () => { - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); + prevContainerHeightRef.current = containerHeight; + prevFrameSizeRef.current = frameSizeValue; + prevScaleRef.current = scaleValue; - prevContainerHeightRef.current = containerHeight; - prevFrameSizeRef.current = frameSizeValue; - prevScaleRef.current = scaleValue; - isAnimatingZoomOut.current = false; + iframeDocument.documentElement.scrollTop = scrollTopNext; + }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss + }, [ scaleValue, frameSizeValue, containerHeight, iframeDocument ] ); - iframeDocument.documentElement.scrollTop = scrollTopNext; - }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss - }; + // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect + // that controls settings the CSS variables, but then we would need to do more work to ensure we're + // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large + // number of dependencies. + useEffect( () => { + // If we're animating, don't re-update things. + if ( ! iframeDocument || prevIsZoomedOut === isZoomedOut ) { + return; + } + // If zoom out mode is toggled, handle the animation handleZoomOutAnimation(); if ( isZoomedOut ) { @@ -410,17 +410,11 @@ function Iframe( { } return () => {}; - }, [ - iframeDocument, - isZoomedOut, - scaleValue, - frameSizeValue, - containerHeight, - ] ); + }, [ iframeDocument, isZoomedOut, handleZoomOutAnimation ] ); // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { - if ( ! iframeDocument ) { + if ( ! iframeDocument || prevIsZoomedOut === isZoomedOut ) { return; } From 26d9e3a35bfbbf6cb25042cf059bd2f08b9e3ee9 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 31 Oct 2024 09:35:18 -0500 Subject: [PATCH 06/64] Allow all CSS vars to update when scale changes --- packages/block-editor/src/components/iframe/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 4fabd86af88046..93e66fa74a69ec 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -414,7 +414,7 @@ function Iframe( { // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { - if ( ! iframeDocument || prevIsZoomedOut === isZoomedOut ) { + if ( ! iframeDocument ) { return; } From 3947ab063d3ac48120a6209000c224b334ff9e2a Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 31 Oct 2024 11:58:41 -0500 Subject: [PATCH 07/64] Midway commit. Math is wrong for addressing top/bottom exceptions --- .../src/components/iframe/index.js | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 93e66fa74a69ec..6af73dd9846d8b 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -357,14 +357,29 @@ function Iframe( { prevContainerHeightRef.current / 2 ); - // // Convert the zoomed in value to the new scale. - // // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. - const scrollTopNext = Math.round( + // Convert the zoomed in value to the new scale. + // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. + let scrollTopNext = Math.round( ( scrollTopOriginal + containerHeight / 2 ) * scaleValue + frameSizeValue - containerHeight / 2 ); + const edgeThreshold = prevContainerHeightRef.current / 2; + const maxScrollPosition = + contentHeight - prevContainerHeightRef.current - frameSizeValue * 2; + + const scaleToTop = scrollTopOriginal - edgeThreshold <= 0; + const scaleToBottom = + scrollTopOriginal - maxScrollPosition - edgeThreshold <= 0; + + if ( scaleToTop ) { + scrollTopNext = 0; + } else if ( scaleToBottom ) { + // Not sure on this + scrollTopNext = maxScrollPosition * scaleValue; + } + iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', `${ scrollTop }px` @@ -388,7 +403,13 @@ function Iframe( { iframeDocument.documentElement.scrollTop = scrollTopNext; }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss - }, [ scaleValue, frameSizeValue, containerHeight, iframeDocument ] ); + }, [ + scaleValue, + frameSizeValue, + containerHeight, + iframeDocument, + contentHeight, + ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect // that controls settings the CSS variables, but then we would need to do more work to ensure we're From cb47e8282376d4981353d055e56e7492991e0c74 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Thu, 31 Oct 2024 14:37:33 -0500 Subject: [PATCH 08/64] Remove usePrevious usage --- packages/block-editor/src/components/iframe/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 6af73dd9846d8b..3b8326d2b9915d 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -21,7 +21,6 @@ import { useMergeRefs, useRefEffect, useDisabled, - usePrevious, } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -261,7 +260,7 @@ function Iframe( { }, [] ); const isZoomedOut = scale !== 1; - const prevIsZoomedOut = usePrevious( isZoomedOut ); + const prevIsZoomedOutRef = useRef( isZoomedOut ); useEffect( () => { if ( ! isZoomedOut ) { @@ -416,6 +415,9 @@ function Iframe( { // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large // number of dependencies. useEffect( () => { + const prevIsZoomedOut = prevIsZoomedOutRef.current; + prevIsZoomedOutRef.current = isZoomedOut; + // If we're animating, don't re-update things. if ( ! iframeDocument || prevIsZoomedOut === isZoomedOut ) { return; From bc8cca6a0fbf816ff48cbfc65b468855db4271bd Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 5 Nov 2024 15:14:07 -0600 Subject: [PATCH 09/64] Add prefers-reduced-motion to setTimeout delay --- packages/block-editor/src/components/iframe/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 3b8326d2b9915d..ba3f3e8657d9b4 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -391,6 +391,14 @@ function Iframe( { iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + // TODO: See if there's a way to wait for CSS transition to finish. + // 400ms should match the animation speed used in components/iframe/content.scss + // Ignore the delay when reduce motion is enabled. + const reduceMotion = iframeDocument.defaultView.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; + const delay = reduceMotion ? 0 : 400; + zoomOutAnimationTimeoutRef.current = setTimeout( () => { iframeDocument.documentElement.classList.remove( 'zoom-out-animation' @@ -401,7 +409,7 @@ function Iframe( { prevScaleRef.current = scaleValue; iframeDocument.documentElement.scrollTop = scrollTopNext; - }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss + }, delay ); }, [ scaleValue, frameSizeValue, From 578c923150dda8f240040b092b7042fb41fc4d7f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 5 Nov 2024 16:31:22 -0600 Subject: [PATCH 10/64] WIP Working zoom without frame size --- .../src/components/iframe/index.js | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index ba3f3e8657d9b4..ddc12867d86d34 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -285,7 +285,8 @@ function Iframe( { const frameSizeValue = parseInt( frameSize ); const prevFrameSizeRef = useRef( frameSizeValue ); - const prevContainerHeightRef = useRef( containerHeight ); + const prevClientHeightRef = useRef( containerHeight ); + const prevScrollHeightRef = useRef( contentHeight ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ @@ -344,40 +345,48 @@ function Iframe( { const handleZoomOutAnimation = useCallback( () => { clearTimeout( zoomOutAnimationTimeoutRef.current ); + // We can't trust the set value from contentHeight, as it was measured before the zoom out mode was changed. + // After zoom out mode is changed, appenders may appear or disappear, so we need to get the height from the iframe + // at this point when we're about to animate the zoom out. The iframe scrollTop, scrollHeight, and clientHeight will all + // be accurate. The client height also does change when the zoom out mode is toggled, as the bottom bar about selecting + // the template is added/removed when toggling zoom out mode. const scrollTop = iframeDocument.documentElement.scrollTop; + // This is the unscaled height of the iframe content. + const clientHeight = iframeDocument.documentElement.clientHeight; + + // This is the scaled height of the iframe content. + const scrollHeight = iframeDocument.documentElement.scrollHeight; + + const prevClientHeight = prevClientHeightRef.current; + const prevScrollHeight = prevScrollHeightRef.current; + const prevScale = prevScaleRef.current; + const prevFrameSize = prevFrameSizeRef.current; + // Convert previous values to the zoomed in scale. // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. const scrollTopOriginal = Math.round( - ( scrollTop + - prevContainerHeightRef.current / 2 - - prevFrameSizeRef.current ) / - prevScaleRef.current - - prevContainerHeightRef.current / 2 + ( scrollTop + prevClientHeight / 2 - prevFrameSize ) / prevScale - + prevClientHeight / 2 ); // Convert the zoomed in value to the new scale. // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. let scrollTopNext = Math.round( - ( scrollTopOriginal + containerHeight / 2 ) * scaleValue + + ( scrollTopOriginal + clientHeight / 2 ) * scaleValue + frameSizeValue - - containerHeight / 2 + clientHeight / 2 ); - const edgeThreshold = prevContainerHeightRef.current / 2; - const maxScrollPosition = - contentHeight - prevContainerHeightRef.current - frameSizeValue * 2; + const scaleRatio = scaleValue / prevScale; + const maxScrollTop = scrollHeight * scaleRatio - clientHeight; - const scaleToTop = scrollTopOriginal - edgeThreshold <= 0; - const scaleToBottom = - scrollTopOriginal - maxScrollPosition - edgeThreshold <= 0; + // prettier-ignore + scrollTopNext = scrollTopNext - clientHeight / 2 <= 0 ? 0 : scrollTopNext; + // prettier-ignore + scrollTopNext = scrollTopNext + clientHeight / 2 >= maxScrollTop ? maxScrollTop : scrollTopNext; - if ( scaleToTop ) { - scrollTopNext = 0; - } else if ( scaleToBottom ) { - // Not sure on this - scrollTopNext = maxScrollPosition * scaleValue; - } + scrollTopNext = Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', @@ -404,19 +413,13 @@ function Iframe( { 'zoom-out-animation' ); - prevContainerHeightRef.current = containerHeight; + prevClientHeightRef.current = clientHeight; + prevScrollHeightRef.current = scrollHeight; prevFrameSizeRef.current = frameSizeValue; prevScaleRef.current = scaleValue; - iframeDocument.documentElement.scrollTop = scrollTopNext; }, delay ); - }, [ - scaleValue, - frameSizeValue, - containerHeight, - iframeDocument, - contentHeight, - ] ); + }, [ scaleValue, frameSizeValue, iframeDocument ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect // that controls settings the CSS variables, but then we would need to do more work to ensure we're From d2c16a67f24513db9472ec1297dab9ff6726a13c Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 6 Nov 2024 09:13:01 -0600 Subject: [PATCH 11/64] Account for changes to client height when determining edge threshold --- packages/block-editor/src/components/iframe/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index ddc12867d86d34..36bde93cab3b1d 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -359,7 +359,7 @@ function Iframe( { const scrollHeight = iframeDocument.documentElement.scrollHeight; const prevClientHeight = prevClientHeightRef.current; - const prevScrollHeight = prevScrollHeightRef.current; + // const prevScrollHeight = prevScrollHeightRef.current; const prevScale = prevScaleRef.current; const prevFrameSize = prevFrameSizeRef.current; @@ -381,10 +381,14 @@ function Iframe( { const scaleRatio = scaleValue / prevScale; const maxScrollTop = scrollHeight * scaleRatio - clientHeight; + // Account for differences in client height changes between zoom in and zoom out modes. + const edgeThreshold = + clientHeight / 2 + ( prevClientHeight - clientHeight ); + // prettier-ignore - scrollTopNext = scrollTopNext - clientHeight / 2 <= 0 ? 0 : scrollTopNext; + scrollTopNext = scrollTopNext - edgeThreshold <= 0 ? 0 : scrollTopNext; // prettier-ignore - scrollTopNext = scrollTopNext + clientHeight / 2 >= maxScrollTop ? maxScrollTop : scrollTopNext; + scrollTopNext = scrollTopNext + edgeThreshold >= maxScrollTop ? maxScrollTop : scrollTopNext; scrollTopNext = Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ); From 0faf418b28f6be346234626b0933c80e0d7e3eac Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 6 Nov 2024 11:19:37 -0600 Subject: [PATCH 12/64] Zoom to center unless it will reveal top or bottom --- .../src/components/iframe/index.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 36bde93cab3b1d..00b4ebb4299ba4 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -286,7 +286,6 @@ function Iframe( { const prevFrameSizeRef = useRef( frameSizeValue ); const prevClientHeightRef = useRef( containerHeight ); - const prevScrollHeightRef = useRef( contentHeight ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ @@ -359,7 +358,6 @@ function Iframe( { const scrollHeight = iframeDocument.documentElement.scrollHeight; const prevClientHeight = prevClientHeightRef.current; - // const prevScrollHeight = prevScrollHeightRef.current; const prevScale = prevScaleRef.current; const prevFrameSize = prevFrameSizeRef.current; @@ -379,18 +377,14 @@ function Iframe( { ); const scaleRatio = scaleValue / prevScale; - const maxScrollTop = scrollHeight * scaleRatio - clientHeight; + const maxScrollTop = + scrollHeight * scaleRatio - clientHeight + frameSizeValue * 2; - // Account for differences in client height changes between zoom in and zoom out modes. - const edgeThreshold = - clientHeight / 2 + ( prevClientHeight - clientHeight ); - - // prettier-ignore - scrollTopNext = scrollTopNext - edgeThreshold <= 0 ? 0 : scrollTopNext; - // prettier-ignore - scrollTopNext = scrollTopNext + edgeThreshold >= maxScrollTop ? maxScrollTop : scrollTopNext; - - scrollTopNext = Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ); + // scrollTopNext will zoom to the center point unless it would scroll past the top or bottom. + // In that case, it will clamp to the top or bottom. + scrollTopNext = Math.round( + Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ) + ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', @@ -418,7 +412,6 @@ function Iframe( { ); prevClientHeightRef.current = clientHeight; - prevScrollHeightRef.current = scrollHeight; prevFrameSizeRef.current = frameSizeValue; prevScaleRef.current = scaleValue; iframeDocument.documentElement.scrollTop = scrollTopNext; From ab8038e972fb188887a12a5865b7aafd63f2b45d Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 6 Nov 2024 11:28:37 -0600 Subject: [PATCH 13/64] Account for a top threshold when zooming in and out --- packages/block-editor/src/components/iframe/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 00b4ebb4299ba4..089f401a2be595 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -376,6 +376,9 @@ function Iframe( { clientHeight / 2 ); + // If we are near the top of the canvas, set the next scroll top to 0. + scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; + const scaleRatio = scaleValue / prevScale; const maxScrollTop = scrollHeight * scaleRatio - clientHeight + frameSizeValue * 2; From ba32f9913881d9225b9ca68f632905e7b123056c Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 6 Nov 2024 14:59:07 -0600 Subject: [PATCH 14/64] Clean up math and add comments --- .../src/components/iframe/index.js | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 089f401a2be595..a717c286d3246e 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -344,47 +344,65 @@ function Iframe( { const handleZoomOutAnimation = useCallback( () => { clearTimeout( zoomOutAnimationTimeoutRef.current ); - // We can't trust the set value from contentHeight, as it was measured before the zoom out mode was changed. - // After zoom out mode is changed, appenders may appear or disappear, so we need to get the height from the iframe - // at this point when we're about to animate the zoom out. The iframe scrollTop, scrollHeight, and clientHeight will all - // be accurate. The client height also does change when the zoom out mode is toggled, as the bottom bar about selecting - // the template is added/removed when toggling zoom out mode. - const scrollTop = iframeDocument.documentElement.scrollTop; - - // This is the unscaled height of the iframe content. - const clientHeight = iframeDocument.documentElement.clientHeight; - - // This is the scaled height of the iframe content. - const scrollHeight = iframeDocument.documentElement.scrollHeight; + // Previous scale value. + const prevScale = prevScaleRef.current; + // Unscaled height of the previous iframe container. const prevClientHeight = prevClientHeightRef.current; - const prevScale = prevScaleRef.current; + + // Unscaled size of the previous padding around the iframe content. const prevFrameSize = prevFrameSizeRef.current; - // Convert previous values to the zoomed in scale. - // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. - const scrollTopOriginal = Math.round( - ( scrollTop + prevClientHeight / 2 - prevFrameSize ) / prevScale - - prevClientHeight / 2 - ); + // Unscaled height of the current iframe container. + const clientHeight = iframeDocument.documentElement.clientHeight; - // Convert the zoomed in value to the new scale. - // Use Math.round to avoid subpixel scrolling which would effectively result in a Math.floor. - let scrollTopNext = Math.round( - ( scrollTopOriginal + clientHeight / 2 ) * scaleValue + - frameSizeValue - - clientHeight / 2 - ); + // Scaled height of the current iframe content. + const scrollHeight = iframeDocument.documentElement.scrollHeight; + + // We can't trust the set value from contentHeight, as it was measured + // before the zoom out mode was changed. After zoom out mode is changed, + // appenders may appear or disappear, so we need to get the height from + // the iframe at this point when we're about to animate the zoom out. + // The iframe scrollTop, scrollHeight, and clientHeight will all be + // accurate. The client height also does change when the zoom out mode + // is toggled, as the bottom bar about selecting the template is + // added/removed when toggling zoom out mode. + const scrollTop = iframeDocument.documentElement.scrollTop; - // If we are near the top of the canvas, set the next scroll top to 0. + // Step 0: Start with the current scrollTop. + let scrollTopNext = scrollTop; + + // Step 1: Undo the effects of the previous scale and frame around the + // midpoint of the visible area. + scrollTopNext = + ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / + prevScale - + prevClientHeight / 2; + + // Step 2: Apply the new scale and frame around the midpoint of the + // visible area. + scrollTopNext = + ( scrollTopNext + clientHeight / 2 ) * scaleValue + + frameSizeValue - + clientHeight / 2; + + // Step 3: Handle an edge case so that you scroll to the top of the + // iframe if the top of the iframe content is visible in the container. + // The same edge case for the bottom is skipped because changing content + // makes calculating it impossible. scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; - const scaleRatio = scaleValue / prevScale; + // This is the scrollTop value if you are scrolled to the bottom of the + // iframe. We can't just let the browser handle it because we need to + // animate the scaling. const maxScrollTop = - scrollHeight * scaleRatio - clientHeight + frameSizeValue * 2; + scrollHeight * ( scaleValue / prevScale ) + + frameSizeValue * 2 - + clientHeight; - // scrollTopNext will zoom to the center point unless it would scroll past the top or bottom. - // In that case, it will clamp to the top or bottom. + // Step 4: Clamp the scrollTopNext between the minimum and maximum + // possible scrollTop positions. Round the value to avoid subpixel + // truncation by the browser which sometimes causes a 1px error. scrollTopNext = Math.round( Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ) ); From 0b640ee1a1343093791e035ef57127d6e0df76b1 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 6 Nov 2024 15:32:11 -0600 Subject: [PATCH 15/64] Add event listener instead of timeout --- .../src/components/iframe/index.js | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index a717c286d3246e..60ac23ab407eda 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -407,6 +407,16 @@ function Iframe( { Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ) ); + const reduceMotion = iframeDocument.defaultView.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; + + if ( reduceMotion ) { + // TODO: This seems to be broken somehow. + iframeDocument.documentElement.scrollTop = scrollTopNext; + return; + } + iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', `${ scrollTop }px` @@ -419,24 +429,24 @@ function Iframe( { iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - // TODO: See if there's a way to wait for CSS transition to finish. - // 400ms should match the animation speed used in components/iframe/content.scss - // Ignore the delay when reduce motion is enabled. - const reduceMotion = iframeDocument.defaultView.matchMedia( - '(prefers-reduced-motion: reduce)' - ).matches; - const delay = reduceMotion ? 0 : 400; + iframeDocument.documentElement.addEventListener( + 'transitionend', + () => { + // Remove the position fixed for the animation. + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); - zoomOutAnimationTimeoutRef.current = setTimeout( () => { - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = frameSizeValue; + prevScaleRef.current = scaleValue; - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSizeValue; - prevScaleRef.current = scaleValue; - iframeDocument.documentElement.scrollTop = scrollTopNext; - }, delay ); + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = scrollTopNext; + }, + { once: true } + ); }, [ scaleValue, frameSizeValue, iframeDocument ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect From 6a3f7a04e7938f083142027d4834eeef76051e49 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 6 Nov 2024 15:50:11 -0600 Subject: [PATCH 16/64] Fix reduced motion --- .../src/components/iframe/index.js | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 60ac23ab407eda..b70e7ec6f97efe 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -32,6 +32,7 @@ import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; import { getCompatibilityStyles } from './get-compatibility-styles'; import { store as blockEditorStore } from '../../store'; +import { handle } from '@wordpress/icons'; function bubbleEvent( event, Constructor, frame ) { const init = {}; @@ -407,46 +408,49 @@ function Iframe( { Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ) ); - const reduceMotion = iframeDocument.defaultView.matchMedia( - '(prefers-reduced-motion: reduce)' - ).matches; + function handleZoomOutEnd() { + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = frameSizeValue; + prevScaleRef.current = scaleValue; - if ( reduceMotion ) { - // TODO: This seems to be broken somehow. + // Set the final scroll position that was just animated to. iframeDocument.documentElement.scrollTop = scrollTopNext; - return; } - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); + const reduceMotion = iframeDocument.defaultView.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; - iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + if ( reduceMotion ) { + handleZoomOutEnd(); + } else { + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` + ); - iframeDocument.documentElement.addEventListener( - 'transitionend', - () => { - // Remove the position fixed for the animation. - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` + ); - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSizeValue; - prevScaleRef.current = scaleValue; + iframeDocument.documentElement.classList.add( + 'zoom-out-animation' + ); - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; - }, - { once: true } - ); + iframeDocument.documentElement.addEventListener( + 'transitionend', + () => { + // Remove the position fixed for the animation. + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + handleZoomOutEnd(); + }, + { once: true } + ); + } }, [ scaleValue, frameSizeValue, iframeDocument ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect From 55df6febc5e84548cda755a2106889ce283cabab Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 6 Nov 2024 15:55:39 -0600 Subject: [PATCH 17/64] Remove timeout ref --- packages/block-editor/src/components/iframe/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index b70e7ec6f97efe..20acaa54d3a09b 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -340,11 +340,7 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); - const zoomOutAnimationTimeoutRef = useRef( null ); - const handleZoomOutAnimation = useCallback( () => { - clearTimeout( zoomOutAnimationTimeoutRef.current ); - // Previous scale value. const prevScale = prevScaleRef.current; From 7ba67b29b16d37fec20aa085fc7e3f0bab85cb72 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 6 Nov 2024 16:16:43 -0600 Subject: [PATCH 18/64] Refactor callback as separate effect --- .../src/components/iframe/index.js | 85 +++++++++++-------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 20acaa54d3a09b..8e8bb30809a6b1 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -13,7 +13,6 @@ import { useMemo, useEffect, useRef, - useCallback, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { @@ -32,7 +31,6 @@ import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; import { getCompatibilityStyles } from './get-compatibility-styles'; import { store as blockEditorStore } from '../../store'; -import { handle } from '@wordpress/icons'; function bubbleEvent( event, Constructor, frame ) { const init = {}; @@ -261,7 +259,6 @@ function Iframe( { }, [] ); const isZoomedOut = scale !== 1; - const prevIsZoomedOutRef = useRef( isZoomedOut ); useEffect( () => { if ( ! isZoomedOut ) { @@ -340,7 +337,15 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); - const handleZoomOutAnimation = useCallback( () => { + useEffect( () => { + if ( + ! iframeDocument || + // TODO: What should this condition be? + ( scaleValue === 1 ) === ( prevScaleRef.current === 1 ) + ) { + return; + } + // Previous scale value. const prevScale = prevScaleRef.current; @@ -404,7 +409,23 @@ function Iframe( { Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ) ); - function handleZoomOutEnd() { + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` + ); + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` + ); + + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + + function onZoomOutTransitionEnd() { + // Remove the position fixed for the animation. + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); // Update previous values. prevClientHeightRef.current = clientHeight; prevFrameSizeRef.current = frameSizeValue; @@ -419,60 +440,52 @@ function Iframe( { ).matches; if ( reduceMotion ) { - handleZoomOutEnd(); + onZoomOutTransitionEnd(); } else { - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` + iframeDocument.documentElement.addEventListener( + 'transitionend', + onZoomOutTransitionEnd, + { once: true } ); + } - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` + return () => { + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top' ); - - iframeDocument.documentElement.classList.add( + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next' + ); + iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); - - iframeDocument.documentElement.addEventListener( + iframeDocument.documentElement.removeEventListener( 'transitionend', - () => { - // Remove the position fixed for the animation. - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - handleZoomOutEnd(); - }, - { once: true } + onZoomOutTransitionEnd ); - } - }, [ scaleValue, frameSizeValue, iframeDocument ] ); + }; + }, [ iframeDocument, scaleValue, frameSizeValue ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect // that controls settings the CSS variables, but then we would need to do more work to ensure we're // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large // number of dependencies. useEffect( () => { - const prevIsZoomedOut = prevIsZoomedOutRef.current; - prevIsZoomedOutRef.current = isZoomedOut; - - // If we're animating, don't re-update things. - if ( ! iframeDocument || prevIsZoomedOut === isZoomedOut ) { + if ( ! iframeDocument ) { return; } - // If zoom out mode is toggled, handle the animation - handleZoomOutAnimation(); - if ( isZoomedOut ) { iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); } else { iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); } - return () => {}; - }, [ iframeDocument, isZoomedOut, handleZoomOutAnimation ] ); + return () => { + // TODO: Removing this causes issues with the zoom out animation. + // iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + }; + }, [ iframeDocument, isZoomedOut ] ); // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { From 2dc137f52f45003bb09c1cec4e87a10a98e2cdda Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 6 Nov 2024 16:28:50 -0600 Subject: [PATCH 19/64] Try to add back useEffect cleanups --- .../src/components/iframe/index.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 8e8bb30809a6b1..7ac7ea30b6e4ed 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -527,6 +527,28 @@ function Iframe( { '--wp-block-editor-iframe-zoom-out-scale-container-width', `${ scaleContainerWidth }px` ); + + return () => { + // TODO: Removing this causes issues with the zoom out animation. + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-scale' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-frame-size' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-content-height' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-inner-height' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-container-width' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-scale-container-width' + // ); + }; }, [ scaleValue, frameSize, From 3eabcf5e8d91e8c317ab9e829c59dd84d638d979 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Thu, 7 Nov 2024 10:22:03 -0600 Subject: [PATCH 20/64] Initialize prevClientHeight in the useEffect --- .../src/components/iframe/index.js | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 7ac7ea30b6e4ed..223adfda6e988f 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -129,10 +129,8 @@ function Iframe( { const [ before, writingFlowRef, after ] = useWritingFlow(); const [ contentResizeListener, { height: contentHeight } ] = useResizeObserver(); - const [ - containerResizeListener, - { width: containerWidth, height: containerHeight }, - ] = useResizeObserver(); + const [ containerResizeListener, { width: containerWidth } ] = + useResizeObserver(); const setRef = useRefEffect( ( node ) => { node._load = () => { @@ -283,7 +281,8 @@ function Iframe( { const frameSizeValue = parseInt( frameSize ); const prevFrameSizeRef = useRef( frameSizeValue ); - const prevClientHeightRef = useRef( containerHeight ); + // Initialized in the useEffect. + const prevClientHeightRef = useRef(); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ @@ -346,20 +345,20 @@ function Iframe( { return; } + // Unscaled height of the current iframe container. + const clientHeight = iframeDocument.documentElement.clientHeight; + + // Scaled height of the current iframe content. + const scrollHeight = iframeDocument.documentElement.scrollHeight; + // Previous scale value. const prevScale = prevScaleRef.current; - // Unscaled height of the previous iframe container. - const prevClientHeight = prevClientHeightRef.current; - // Unscaled size of the previous padding around the iframe content. const prevFrameSize = prevFrameSizeRef.current; - // Unscaled height of the current iframe container. - const clientHeight = iframeDocument.documentElement.clientHeight; - - // Scaled height of the current iframe content. - const scrollHeight = iframeDocument.documentElement.scrollHeight; + // Unscaled height of the previous iframe container. + const prevClientHeight = prevClientHeightRef.current ?? clientHeight; // We can't trust the set value from contentHeight, as it was measured // before the zoom out mode was changed. After zoom out mode is changed, From 8b8c20607c4f26eb6db52b247b0cee402c8b184f Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 7 Nov 2024 10:52:03 -0600 Subject: [PATCH 21/64] use useReducedMotion --- packages/block-editor/src/components/iframe/index.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 223adfda6e988f..175d2fa458c702 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -20,6 +20,7 @@ import { useMergeRefs, useRefEffect, useDisabled, + useReducedMotion, } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -131,6 +132,7 @@ function Iframe( { useResizeObserver(); const [ containerResizeListener, { width: containerWidth } ] = useResizeObserver(); + const prefersReducedMotion = useReducedMotion(); const setRef = useRefEffect( ( node ) => { node._load = () => { @@ -434,11 +436,7 @@ function Iframe( { iframeDocument.documentElement.scrollTop = scrollTopNext; } - const reduceMotion = iframeDocument.defaultView.matchMedia( - '(prefers-reduced-motion: reduce)' - ).matches; - - if ( reduceMotion ) { + if ( prefersReducedMotion ) { onZoomOutTransitionEnd(); } else { iframeDocument.documentElement.addEventListener( @@ -463,7 +461,7 @@ function Iframe( { onZoomOutTransitionEnd ); }; - }, [ iframeDocument, scaleValue, frameSizeValue ] ); + }, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect // that controls settings the CSS variables, but then we would need to do more work to ensure we're From 23bd453c870201926a11a465483651d113bc76e3 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 7 Nov 2024 15:32:43 -0600 Subject: [PATCH 22/64] Add test for zoom in/out location --- test/e2e/specs/site-editor/zoom-out.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js index e698a94b7cf0dc..0a89ec28865036 100644 --- a/test/e2e/specs/site-editor/zoom-out.spec.js +++ b/test/e2e/specs/site-editor/zoom-out.spec.js @@ -109,8 +109,6 @@ test.describe( 'Zoom Out', () => { page, editor, } ) => { - // Add some patterns into the page. - await editor.setContent( EDITOR_ZOOM_OUT_CONTENT ); // Find the scroll container element await page.evaluate( () => { const { activeElement } = From 8cbe2bea428c5b16dea1e72479ecb1989eebff5d Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Fri, 8 Nov 2024 11:13:08 -0600 Subject: [PATCH 23/64] Hack to fix reduced motion --- packages/block-editor/src/components/iframe/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 175d2fa458c702..5523f3f5ffcf2e 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -437,7 +437,10 @@ function Iframe( { } if ( prefersReducedMotion ) { - onZoomOutTransitionEnd(); + // Hack: Wait for the window values to recalculate. + iframeDocument.defaultView.requestAnimationFrame( + onZoomOutTransitionEnd + ); } else { iframeDocument.documentElement.addEventListener( 'transitionend', From 4c9543e4670da31cffef109047b68f36b5c28c35 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Fri, 8 Nov 2024 15:24:22 -0600 Subject: [PATCH 24/64] Clean up the frameSizeValue and scaleValue calculations --- packages/block-editor/src/components/iframe/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 5523f3f5ffcf2e..ab42953b07662f 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -271,20 +271,18 @@ function Iframe( { containerWidth ); + const frameSizeValue = parseInt( frameSize ); + const maxWidth = 750; const scaleValue = scale === 'auto-scaled' - ? ( Math.min( containerWidth, maxWidth ) - - parseInt( frameSize ) * 2 ) / + ? ( Math.min( containerWidth, maxWidth ) - frameSizeValue * 2 ) / scaleContainerWidth : scale; - const prevScaleRef = useRef( scaleValue ); - const frameSizeValue = parseInt( frameSize ); + const prevScaleRef = useRef( scaleValue ); const prevFrameSizeRef = useRef( frameSizeValue ); - - // Initialized in the useEffect. - const prevClientHeightRef = useRef(); + const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ From 4e87729a59cd77453a161df9ff5c8e8593645a87 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Fri, 8 Nov 2024 15:24:43 -0600 Subject: [PATCH 25/64] Replace TODO comments with HACK comments --- packages/block-editor/src/components/iframe/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index ab42953b07662f..0a75c81822fd9c 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -339,7 +339,8 @@ function Iframe( { useEffect( () => { if ( ! iframeDocument || - // TODO: What should this condition be? + // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here + // instead of the dependency array to appease the linter. ( scaleValue === 1 ) === ( prevScaleRef.current === 1 ) ) { return; @@ -480,7 +481,8 @@ function Iframe( { } return () => { - // TODO: Removing this causes issues with the zoom out animation. + // HACK: Skipping cleanup because it causes issues with the zoom out + // animation. More refactoring is needed to fix this properly. // iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); }; }, [ iframeDocument, isZoomedOut ] ); @@ -527,7 +529,8 @@ function Iframe( { ); return () => { - // TODO: Removing this causes issues with the zoom out animation. + // HACK: Skipping cleanup because it causes issues with the zoom out + // animation. More refactoring is needed to fix this properly. // iframeDocument.documentElement.style.removeProperty( // '--wp-block-editor-iframe-zoom-out-scale' // ); From 17f50d5b2a8f89826cd54d85a729d401991a8fbc Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Fri, 8 Nov 2024 15:32:32 -0600 Subject: [PATCH 26/64] Add cleanup for raf --- .../block-editor/src/components/iframe/index.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 0a75c81822fd9c..f9a1c2350b2e8d 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -426,6 +426,7 @@ function Iframe( { iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); + // Update previous values. prevClientHeightRef.current = clientHeight; prevFrameSizeRef.current = frameSizeValue; @@ -435,9 +436,10 @@ function Iframe( { iframeDocument.documentElement.scrollTop = scrollTopNext; } + let raf; if ( prefersReducedMotion ) { // Hack: Wait for the window values to recalculate. - iframeDocument.defaultView.requestAnimationFrame( + raf = iframeDocument.defaultView.requestAnimationFrame( onZoomOutTransitionEnd ); } else { @@ -458,10 +460,14 @@ function Iframe( { iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); - iframeDocument.documentElement.removeEventListener( - 'transitionend', - onZoomOutTransitionEnd - ); + if ( prefersReducedMotion ) { + iframeDocument.defaultView.cancelAnimationFrame( raf ); + } else { + iframeDocument.documentElement.removeEventListener( + 'transitionend', + onZoomOutTransitionEnd + ); + } }; }, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] ); From b049f4bd21b5440ff85b9fd769102ca2bdcba004 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Fri, 8 Nov 2024 15:43:47 -0600 Subject: [PATCH 27/64] Simplify CSS diff for 6.7 review --- .../src/components/iframe/content.scss | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index ff5bd3a4052f45..5e390800719949 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -3,10 +3,6 @@ } .block-editor-iframe__html { - $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); - $scroll-top-next: var(--wp-block-editor-iframe-zoom-out-scroll-top-next, 0); - $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); - scale: $scale; transform-origin: top center; // We don't want to animate the transform of the translateX because it is used // to "center" the canvas. Leaving it on causes the canvas to slide around in @@ -14,6 +10,9 @@ @include editor-canvas-resize-animation( transform 0s, scale 0s, padding 0s, translate 0s); &.zoom-out-animation { + $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); + $scroll-top-next: var(--wp-block-editor-iframe-zoom-out-scroll-top-next, 0); + position: fixed; left: 0; right: 0; @@ -23,45 +22,49 @@ // Force preserving a scrollbar gutter as scrollbar-gutter isn't supported in all browsers yet, // and removing the scrollbar causes the content to shift. overflow-y: scroll; - // we only want to animate the scaling when entering zoom out. When sidebars + + // We only want to animate the scaling when entering zoom out. When sidebars // are toggled, the resizing of the iframe handles scaling the canvas as well, // and the doubled animations cause very odd animations. @include editor-canvas-resize-animation( transform 0s, top 0s, bottom 0s, right 0s, left 0s ); } +} - &.is-zoomed-out { - $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); - $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); - $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); - $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); - $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); - // Apply an X translation to center the scaled content within the available space. - transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); - background-color: $gray-300; - // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, - // so we need to adjust the height of the content to match the scale by using negative margins. - $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); - $total-frame-height: calc(2 * #{$frame-size} / #{$scale}); - $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); - margin-bottom: calc(-1 * #{$total-height}); - // Add the top/bottom frame size. We use scaling to account for the left/right, as - // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling - // of the content. - padding-top: calc(#{$frame-size} / #{$scale}); - padding-bottom: calc(#{$frame-size} / #{$scale}); +.block-editor-iframe__html.is-zoomed-out { + $scale: var(--wp-block-editor-iframe-zoom-out-scale); + $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); + $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); + $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); + $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); + $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); + // Apply an X translation to center the scaled content within the available space. + transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); + scale: #{$scale}; + background-color: $gray-300; - body { - min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); + // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, + // so we need to adjust the height of the content to match the scale by using negative margins. + $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); + $total-frame-height: calc(2 * #{$frame-size} / #{$scale}); + $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); + margin-bottom: calc(-1 * #{$total-height}); + // Add the top/bottom frame size. We use scaling to account for the left/right, as + // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling + // of the content. + padding-top: calc(#{$frame-size} / #{$scale}); + padding-bottom: calc(#{$frame-size} / #{$scale}); - > .is-root-container:not(.wp-block-post-content) { - flex: 1; - display: flex; - flex-direction: column; - height: 100%; + body { + min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); + + > .is-root-container:not(.wp-block-post-content) { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; - > main { - flex: 1; - } + > main { + flex: 1; } } } From 61a0de93c168ce19c13fc6237fe021c81b742b32 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Fri, 8 Nov 2024 15:49:43 -0600 Subject: [PATCH 28/64] Add one more HACK comment --- packages/block-editor/src/components/iframe/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index f9a1c2350b2e8d..a8425f4c435936 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -483,6 +483,7 @@ function Iframe( { if ( isZoomedOut ) { iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); } else { + // HACK: Since we can't remove this in the cleanup, we need to do it here. iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); } From ac52f2dad1e2018ab9d6077297459d9580ae2dda Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 11 Nov 2024 12:25:34 -0600 Subject: [PATCH 29/64] Do not allow scrollTopNext to be smaller than 0 --- packages/block-editor/src/components/iframe/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index a8425f4c435936..bf4fa55df8efc0 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -406,7 +406,10 @@ function Iframe( { // possible scrollTop positions. Round the value to avoid subpixel // truncation by the browser which sometimes causes a 1px error. scrollTopNext = Math.round( - Math.min( Math.max( 0, scrollTopNext ), maxScrollTop ) + Math.min( + Math.max( 0, scrollTopNext ), + Math.max( 0, maxScrollTop ) + ) ); iframeDocument.documentElement.style.setProperty( From 6b23a3046066c40750e47eb6b8e6a399a76160ce Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 19 Nov 2024 12:23:08 -0600 Subject: [PATCH 30/64] Fix zoom-out.spec.js --- test/e2e/specs/site-editor/zoom-out.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js index 0a89ec28865036..e698a94b7cf0dc 100644 --- a/test/e2e/specs/site-editor/zoom-out.spec.js +++ b/test/e2e/specs/site-editor/zoom-out.spec.js @@ -109,6 +109,8 @@ test.describe( 'Zoom Out', () => { page, editor, } ) => { + // Add some patterns into the page. + await editor.setContent( EDITOR_ZOOM_OUT_CONTENT ); // Find the scroll container element await page.evaluate( () => { const { activeElement } = From fd74dc41f74e29733cd40763f659ade4df1e16b2 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 10:27:09 -0600 Subject: [PATCH 31/64] Move 6.7 iframe scaling work to trunk --- packages/block-editor/src/components/iframe/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index bf4fa55df8efc0..fcad128eaf8a56 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -504,7 +504,7 @@ function Iframe( { } // Note: When we initialize the zoom out when the canvas is smaller (sidebars open), - // initialContainerWidthRef will be smaller than the full page, and reflow will happen + // initialContainerWidth will be smaller than the full page, and reflow will happen // when the canvas area becomes larger due to sidebars closing. This is a known but // minor divergence for now. From bfaf7e4d1bf603f93c8f50962c08c1fa466a6c1c Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 10:39:13 -0600 Subject: [PATCH 32/64] Move calculations to useScaleCanvas hook --- .../src/components/iframe/index.js | 253 +--------------- .../src/components/iframe/use-scale-canvas.js | 274 ++++++++++++++++++ 2 files changed, 286 insertions(+), 241 deletions(-) create mode 100644 packages/block-editor/src/components/iframe/use-scale-canvas.js diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index fcad128eaf8a56..7bd2de9312f013 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -20,7 +20,6 @@ import { useMergeRefs, useRefEffect, useDisabled, - useReducedMotion, } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -31,6 +30,7 @@ import { useSelect } from '@wordpress/data'; import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; import { getCompatibilityStyles } from './get-compatibility-styles'; +import { useScaleCanvas } from './use-scale-canvas'; import { store as blockEditorStore } from '../../store'; function bubbleEvent( event, Constructor, frame ) { @@ -132,7 +132,6 @@ function Iframe( { useResizeObserver(); const [ containerResizeListener, { width: containerWidth } ] = useResizeObserver(); - const prefersReducedMotion = useReducedMotion(); const setRef = useRefEffect( ( node ) => { node._load = () => { @@ -280,9 +279,17 @@ function Iframe( { scaleContainerWidth : scale; - const prevScaleRef = useRef( scaleValue ); - const prevFrameSizeRef = useRef( frameSizeValue ); - const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + useScaleCanvas( { + scaleValue, + frameSizeValue, + iframeDocument, + iframeWindowInnerHeight, + contentHeight, + containerWidth, + windowInnerWidth, + isZoomedOut, + scaleContainerWidth, + } ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ @@ -336,242 +343,6 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); - useEffect( () => { - if ( - ! iframeDocument || - // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here - // instead of the dependency array to appease the linter. - ( scaleValue === 1 ) === ( prevScaleRef.current === 1 ) - ) { - return; - } - - // Unscaled height of the current iframe container. - const clientHeight = iframeDocument.documentElement.clientHeight; - - // Scaled height of the current iframe content. - const scrollHeight = iframeDocument.documentElement.scrollHeight; - - // Previous scale value. - const prevScale = prevScaleRef.current; - - // Unscaled size of the previous padding around the iframe content. - const prevFrameSize = prevFrameSizeRef.current; - - // Unscaled height of the previous iframe container. - const prevClientHeight = prevClientHeightRef.current ?? clientHeight; - - // We can't trust the set value from contentHeight, as it was measured - // before the zoom out mode was changed. After zoom out mode is changed, - // appenders may appear or disappear, so we need to get the height from - // the iframe at this point when we're about to animate the zoom out. - // The iframe scrollTop, scrollHeight, and clientHeight will all be - // accurate. The client height also does change when the zoom out mode - // is toggled, as the bottom bar about selecting the template is - // added/removed when toggling zoom out mode. - const scrollTop = iframeDocument.documentElement.scrollTop; - - // Step 0: Start with the current scrollTop. - let scrollTopNext = scrollTop; - - // Step 1: Undo the effects of the previous scale and frame around the - // midpoint of the visible area. - scrollTopNext = - ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / - prevScale - - prevClientHeight / 2; - - // Step 2: Apply the new scale and frame around the midpoint of the - // visible area. - scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * scaleValue + - frameSizeValue - - clientHeight / 2; - - // Step 3: Handle an edge case so that you scroll to the top of the - // iframe if the top of the iframe content is visible in the container. - // The same edge case for the bottom is skipped because changing content - // makes calculating it impossible. - scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; - - // This is the scrollTop value if you are scrolled to the bottom of the - // iframe. We can't just let the browser handle it because we need to - // animate the scaling. - const maxScrollTop = - scrollHeight * ( scaleValue / prevScale ) + - frameSizeValue * 2 - - clientHeight; - - // Step 4: Clamp the scrollTopNext between the minimum and maximum - // possible scrollTop positions. Round the value to avoid subpixel - // truncation by the browser which sometimes causes a 1px error. - scrollTopNext = Math.round( - Math.min( - Math.max( 0, scrollTopNext ), - Math.max( 0, maxScrollTop ) - ) - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); - - iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - - function onZoomOutTransitionEnd() { - // Remove the position fixed for the animation. - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSizeValue; - prevScaleRef.current = scaleValue; - - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; - } - - let raf; - if ( prefersReducedMotion ) { - // Hack: Wait for the window values to recalculate. - raf = iframeDocument.defaultView.requestAnimationFrame( - onZoomOutTransitionEnd - ); - } else { - iframeDocument.documentElement.addEventListener( - 'transitionend', - onZoomOutTransitionEnd, - { once: true } - ); - } - - return () => { - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next' - ); - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - if ( prefersReducedMotion ) { - iframeDocument.defaultView.cancelAnimationFrame( raf ); - } else { - iframeDocument.documentElement.removeEventListener( - 'transitionend', - onZoomOutTransitionEnd - ); - } - }; - }, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] ); - - // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect - // that controls settings the CSS variables, but then we would need to do more work to ensure we're - // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large - // number of dependencies. - useEffect( () => { - if ( ! iframeDocument ) { - return; - } - - if ( isZoomedOut ) { - iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); - } else { - // HACK: Since we can't remove this in the cleanup, we need to do it here. - iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); - } - - return () => { - // HACK: Skipping cleanup because it causes issues with the zoom out - // animation. More refactoring is needed to fix this properly. - // iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); - }; - }, [ iframeDocument, isZoomedOut ] ); - - // Calculate the scaling and CSS variables for the zoom out canvas - useEffect( () => { - if ( ! iframeDocument ) { - return; - } - - // Note: When we initialize the zoom out when the canvas is smaller (sidebars open), - // initialContainerWidth will be smaller than the full page, and reflow will happen - // when the canvas area becomes larger due to sidebars closing. This is a known but - // minor divergence for now. - - // This scaling calculation has to happen within the JS because CSS calc() can - // only divide and multiply by a unitless value. I.e. calc( 100px / 2 ) is valid - // but calc( 100px / 2px ) is not. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - scaleValue - ); - - // frameSize has to be a px value for the scaling and frame size to be computed correctly. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - typeof frameSize === 'number' ? `${ frameSize }px` : frameSize - ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-content-height', - `${ contentHeight }px` - ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-inner-height', - `${ iframeWindowInnerHeight }px` - ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-container-width', - `${ containerWidth }px` - ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale-container-width', - `${ scaleContainerWidth }px` - ); - - return () => { - // HACK: Skipping cleanup because it causes issues with the zoom out - // animation. More refactoring is needed to fix this properly. - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-frame-size' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-content-height' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-inner-height' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-container-width' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale-container-width' - // ); - }; - }, [ - scaleValue, - frameSize, - iframeDocument, - iframeWindowInnerHeight, - contentHeight, - containerWidth, - windowInnerWidth, - isZoomedOut, - scaleContainerWidth, - ] ); - // Make sure to not render the before and after focusable div elements in view // mode. They're only needed to capture focus in edit mode. const shouldRenderFocusCaptureElements = tabIndex >= 0 && ! isPreviewMode; diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js new file mode 100644 index 00000000000000..963507e0f079e3 --- /dev/null +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -0,0 +1,274 @@ +/** + * WordPress dependencies + */ +import { useEffect, useRef } from '@wordpress/element'; +import { useReducedMotion } from '@wordpress/compose'; + +/** + * Handles scaling the canvas for the zoom out mode and animating between + * the states. + * + * @param {Object} root0 + * @param {number} root0.contentHeight The height of the content in the iframe. + * @param {number} root0.containerWidth The width of the container. + * @param {number} root0.frameSizeValue The size of the frame around the content. + * @param {Document} root0.iframeDocument The document of the iframe. + * @param {number} root0.iframeWindowInnerHeight The height of the inner window + * @param {number} root0.windowInnerWidth The height of the inner window + * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. + * @param {number} root0.scaleValue The scale of the canvas. + * @param {number} root0.scaleContainerWidth The width of the container at the scaled size. + */ +export function useScaleCanvas( { + scaleValue, + frameSizeValue, + iframeDocument, + iframeWindowInnerHeight, + contentHeight, + containerWidth, + windowInnerWidth, + isZoomedOut, + scaleContainerWidth, +} ) { + const prefersReducedMotion = useReducedMotion(); + + const prevScaleRef = useRef( scaleValue ); + const prevFrameSizeRef = useRef( frameSizeValue ); + const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + + useEffect( () => { + if ( + ! iframeDocument || + // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here + // instead of the dependency array to appease the linter. + ( scaleValue === 1 ) === ( prevScaleRef.current === 1 ) + ) { + return; + } + + // Unscaled height of the current iframe container. + const clientHeight = iframeDocument.documentElement.clientHeight; + + // Scaled height of the current iframe content. + const scrollHeight = iframeDocument.documentElement.scrollHeight; + + // Previous scale value. + const prevScale = prevScaleRef.current; + + // Unscaled size of the previous padding around the iframe content. + const prevFrameSize = prevFrameSizeRef.current; + + // Unscaled height of the previous iframe container. + const prevClientHeight = prevClientHeightRef.current ?? clientHeight; + + // We can't trust the set value from contentHeight, as it was measured + // before the zoom out mode was changed. After zoom out mode is changed, + // appenders may appear or disappear, so we need to get the height from + // the iframe at this point when we're about to animate the zoom out. + // The iframe scrollTop, scrollHeight, and clientHeight will all be + // accurate. The client height also does change when the zoom out mode + // is toggled, as the bottom bar about selecting the template is + // added/removed when toggling zoom out mode. + const scrollTop = iframeDocument.documentElement.scrollTop; + + // Step 0: Start with the current scrollTop. + let scrollTopNext = scrollTop; + + // Step 1: Undo the effects of the previous scale and frame around the + // midpoint of the visible area. + scrollTopNext = + ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / + prevScale - + prevClientHeight / 2; + + // Step 2: Apply the new scale and frame around the midpoint of the + // visible area. + scrollTopNext = + ( scrollTopNext + clientHeight / 2 ) * scaleValue + + frameSizeValue - + clientHeight / 2; + + // Step 3: Handle an edge case so that you scroll to the top of the + // iframe if the top of the iframe content is visible in the container. + // The same edge case for the bottom is skipped because changing content + // makes calculating it impossible. + scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; + + // This is the scrollTop value if you are scrolled to the bottom of the + // iframe. We can't just let the browser handle it because we need to + // animate the scaling. + const maxScrollTop = + scrollHeight * ( scaleValue / prevScale ) + + frameSizeValue * 2 - + clientHeight; + + // Step 4: Clamp the scrollTopNext between the minimum and maximum + // possible scrollTop positions. Round the value to avoid subpixel + // truncation by the browser which sometimes causes a 1px error. + scrollTopNext = Math.round( + Math.min( + Math.max( 0, scrollTopNext ), + Math.max( 0, maxScrollTop ) + ) + ); + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` + ); + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` + ); + + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + + function onZoomOutTransitionEnd() { + // Remove the position fixed for the animation. + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = frameSizeValue; + prevScaleRef.current = scaleValue; + + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = scrollTopNext; + } + + let raf; + if ( prefersReducedMotion ) { + // Hack: Wait for the window values to recalculate. + raf = iframeDocument.defaultView.requestAnimationFrame( + onZoomOutTransitionEnd + ); + } else { + iframeDocument.documentElement.addEventListener( + 'transitionend', + onZoomOutTransitionEnd, + { once: true } + ); + } + + return () => { + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next' + ); + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + if ( prefersReducedMotion ) { + iframeDocument.defaultView.cancelAnimationFrame( raf ); + } else { + iframeDocument.documentElement.removeEventListener( + 'transitionend', + onZoomOutTransitionEnd + ); + } + }; + }, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] ); + + // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect + // that controls settings the CSS variables, but then we would need to do more work to ensure we're + // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large + // number of dependencies. + useEffect( () => { + if ( ! iframeDocument ) { + return; + } + + if ( isZoomedOut ) { + iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); + } else { + // HACK: Since we can't remove this in the cleanup, we need to do it here. + iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + } + + return () => { + // HACK: Skipping cleanup because it causes issues with the zoom out + // animation. More refactoring is needed to fix this properly. + // iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + }; + }, [ iframeDocument, isZoomedOut ] ); + + // Calculate the scaling and CSS variables for the zoom out canvas + useEffect( () => { + if ( ! iframeDocument ) { + return; + } + + // Note: When we initialize the zoom out when the canvas is smaller (sidebars open), + // initialContainerWidth will be smaller than the full page, and reflow will happen + // when the canvas area becomes larger due to sidebars closing. This is a known but + // minor divergence for now. + + // This scaling calculation has to happen within the JS because CSS calc() can + // only divide and multiply by a unitless value. I.e. calc( 100px / 2 ) is valid + // but calc( 100px / 2px ) is not. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + scaleValue + ); + + // frameSize has to be a px value for the scaling and frame size to be computed correctly. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ frameSizeValue }px` + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-content-height', + `${ contentHeight }px` + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-inner-height', + `${ iframeWindowInnerHeight }px` + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-container-width', + `${ containerWidth }px` + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale-container-width', + `${ scaleContainerWidth }px` + ); + + return () => { + // HACK: Skipping cleanup because it causes issues with the zoom out + // animation. More refactoring is needed to fix this properly. + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-scale' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-frame-size' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-content-height' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-inner-height' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-container-width' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-scale-container-width' + // ); + }; + }, [ + scaleValue, + frameSizeValue, + iframeDocument, + iframeWindowInnerHeight, + contentHeight, + containerWidth, + windowInnerWidth, + isZoomedOut, + scaleContainerWidth, + ] ); +} From cc28155545dc84e48c91382d49a4c0d3ec9fd8ee Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 10:42:08 -0600 Subject: [PATCH 33/64] Rename scaleValue to scale --- .../src/components/iframe/index.js | 12 +++++------ .../src/components/iframe/use-scale-canvas.js | 20 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 7bd2de9312f013..5bb04ff0471fb7 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -273,14 +273,14 @@ function Iframe( { const frameSizeValue = parseInt( frameSize ); const maxWidth = 750; - const scaleValue = - scale === 'auto-scaled' - ? ( Math.min( containerWidth, maxWidth ) - frameSizeValue * 2 ) / - scaleContainerWidth - : scale; useScaleCanvas( { - scaleValue, + scale: + scale === 'auto-scaled' + ? ( Math.min( containerWidth, maxWidth ) - + frameSizeValue * 2 ) / + scaleContainerWidth + : scale, frameSizeValue, iframeDocument, iframeWindowInnerHeight, diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 963507e0f079e3..fbc6cfbcc66997 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -16,11 +16,11 @@ import { useReducedMotion } from '@wordpress/compose'; * @param {number} root0.iframeWindowInnerHeight The height of the inner window * @param {number} root0.windowInnerWidth The height of the inner window * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. - * @param {number} root0.scaleValue The scale of the canvas. + * @param {number} root0.scale The scale of the canvas. * @param {number} root0.scaleContainerWidth The width of the container at the scaled size. */ export function useScaleCanvas( { - scaleValue, + scale, frameSizeValue, iframeDocument, iframeWindowInnerHeight, @@ -32,7 +32,7 @@ export function useScaleCanvas( { } ) { const prefersReducedMotion = useReducedMotion(); - const prevScaleRef = useRef( scaleValue ); + const prevScaleRef = useRef( scale ); const prevFrameSizeRef = useRef( frameSizeValue ); const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); @@ -41,7 +41,7 @@ export function useScaleCanvas( { ! iframeDocument || // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here // instead of the dependency array to appease the linter. - ( scaleValue === 1 ) === ( prevScaleRef.current === 1 ) + ( scale === 1 ) === ( prevScaleRef.current === 1 ) ) { return; } @@ -84,7 +84,7 @@ export function useScaleCanvas( { // Step 2: Apply the new scale and frame around the midpoint of the // visible area. scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * scaleValue + + ( scrollTopNext + clientHeight / 2 ) * scale + frameSizeValue - clientHeight / 2; @@ -98,7 +98,7 @@ export function useScaleCanvas( { // iframe. We can't just let the browser handle it because we need to // animate the scaling. const maxScrollTop = - scrollHeight * ( scaleValue / prevScale ) + + scrollHeight * ( scale / prevScale ) + frameSizeValue * 2 - clientHeight; @@ -133,7 +133,7 @@ export function useScaleCanvas( { // Update previous values. prevClientHeightRef.current = clientHeight; prevFrameSizeRef.current = frameSizeValue; - prevScaleRef.current = scaleValue; + prevScaleRef.current = scale; // Set the final scroll position that was just animated to. iframeDocument.documentElement.scrollTop = scrollTopNext; @@ -172,7 +172,7 @@ export function useScaleCanvas( { ); } }; - }, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] ); + }, [ iframeDocument, scale, frameSizeValue, prefersReducedMotion ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect // that controls settings the CSS variables, but then we would need to do more work to ensure we're @@ -213,7 +213,7 @@ export function useScaleCanvas( { // but calc( 100px / 2px ) is not. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', - scaleValue + scale ); // frameSize has to be a px value for the scaling and frame size to be computed correctly. @@ -261,7 +261,7 @@ export function useScaleCanvas( { // ); }; }, [ - scaleValue, + scale, frameSizeValue, iframeDocument, iframeWindowInnerHeight, From d407ee6ca7172e00dda4314cadf21a1b656fe733 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 10:44:57 -0600 Subject: [PATCH 34/64] Rename frameSizeValue to frameSize --- .../src/components/iframe/index.js | 6 ++---- .../src/components/iframe/use-scale-canvas.js | 20 +++++++++---------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 5bb04ff0471fb7..1787b94dc71522 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -270,18 +270,16 @@ function Iframe( { containerWidth ); - const frameSizeValue = parseInt( frameSize ); - const maxWidth = 750; useScaleCanvas( { scale: scale === 'auto-scaled' ? ( Math.min( containerWidth, maxWidth ) - - frameSizeValue * 2 ) / + parseInt( frameSize ) * 2 ) / scaleContainerWidth : scale, - frameSizeValue, + frameSize: parseInt( frameSize ), iframeDocument, iframeWindowInnerHeight, contentHeight, diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index fbc6cfbcc66997..63a71107f91fd7 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -11,7 +11,7 @@ import { useReducedMotion } from '@wordpress/compose'; * @param {Object} root0 * @param {number} root0.contentHeight The height of the content in the iframe. * @param {number} root0.containerWidth The width of the container. - * @param {number} root0.frameSizeValue The size of the frame around the content. + * @param {number} root0.frameSize The size of the frame around the content. * @param {Document} root0.iframeDocument The document of the iframe. * @param {number} root0.iframeWindowInnerHeight The height of the inner window * @param {number} root0.windowInnerWidth The height of the inner window @@ -21,7 +21,7 @@ import { useReducedMotion } from '@wordpress/compose'; */ export function useScaleCanvas( { scale, - frameSizeValue, + frameSize, iframeDocument, iframeWindowInnerHeight, contentHeight, @@ -33,7 +33,7 @@ export function useScaleCanvas( { const prefersReducedMotion = useReducedMotion(); const prevScaleRef = useRef( scale ); - const prevFrameSizeRef = useRef( frameSizeValue ); + const prevFrameSizeRef = useRef( frameSize ); const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); useEffect( () => { @@ -85,7 +85,7 @@ export function useScaleCanvas( { // visible area. scrollTopNext = ( scrollTopNext + clientHeight / 2 ) * scale + - frameSizeValue - + frameSize - clientHeight / 2; // Step 3: Handle an edge case so that you scroll to the top of the @@ -98,9 +98,7 @@ export function useScaleCanvas( { // iframe. We can't just let the browser handle it because we need to // animate the scaling. const maxScrollTop = - scrollHeight * ( scale / prevScale ) + - frameSizeValue * 2 - - clientHeight; + scrollHeight * ( scale / prevScale ) + frameSize * 2 - clientHeight; // Step 4: Clamp the scrollTopNext between the minimum and maximum // possible scrollTop positions. Round the value to avoid subpixel @@ -132,7 +130,7 @@ export function useScaleCanvas( { // Update previous values. prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSizeValue; + prevFrameSizeRef.current = frameSize; prevScaleRef.current = scale; // Set the final scroll position that was just animated to. @@ -172,7 +170,7 @@ export function useScaleCanvas( { ); } }; - }, [ iframeDocument, scale, frameSizeValue, prefersReducedMotion ] ); + }, [ iframeDocument, scale, frameSize, prefersReducedMotion ] ); // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect // that controls settings the CSS variables, but then we would need to do more work to ensure we're @@ -219,7 +217,7 @@ export function useScaleCanvas( { // frameSize has to be a px value for the scaling and frame size to be computed correctly. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-frame-size', - `${ frameSizeValue }px` + `${ frameSize }px` ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-content-height', @@ -262,7 +260,7 @@ export function useScaleCanvas( { }; }, [ scale, - frameSizeValue, + frameSize, iframeDocument, iframeWindowInnerHeight, contentHeight, From c8f2cc7df28dc0c25c4044f67a9de9db6f7658b2 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 11:34:27 -0600 Subject: [PATCH 35/64] Remove iframeWindowInnerHeight --- .../src/components/iframe/index.js | 20 ----------------- .../src/components/iframe/use-scale-canvas.js | 22 +++++++++---------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 1787b94dc71522..138a7d5c61b312 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -227,21 +227,6 @@ function Iframe( { }; }, [] ); - const [ iframeWindowInnerHeight, setIframeWindowInnerHeight ] = useState(); - - const iframeResizeRef = useRefEffect( ( node ) => { - const nodeWindow = node.ownerDocument.defaultView; - - setIframeWindowInnerHeight( nodeWindow.innerHeight ); - const onResize = () => { - setIframeWindowInnerHeight( nodeWindow.innerHeight ); - }; - nodeWindow.addEventListener( 'resize', onResize ); - return () => { - nodeWindow.removeEventListener( 'resize', onResize ); - }; - }, [] ); - const [ windowInnerWidth, setWindowInnerWidth ] = useState(); const windowResizeRef = useRefEffect( ( node ) => { @@ -281,7 +266,6 @@ function Iframe( { : scale, frameSize: parseInt( frameSize ), iframeDocument, - iframeWindowInnerHeight, contentHeight, containerWidth, windowInnerWidth, @@ -296,10 +280,6 @@ function Iframe( { clearerRef, writingFlowRef, disabledRef, - // Avoid resize listeners when not needed, these will trigger - // unnecessary re-renders when animating the iframe width, or when - // expanding preview iframes. - isZoomedOut ? iframeResizeRef : null, ] ); // Correct doctype is required to enable rendering in standards diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 63a71107f91fd7..9725fb44bcb583 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -9,21 +9,19 @@ import { useReducedMotion } from '@wordpress/compose'; * the states. * * @param {Object} root0 - * @param {number} root0.contentHeight The height of the content in the iframe. - * @param {number} root0.containerWidth The width of the container. - * @param {number} root0.frameSize The size of the frame around the content. - * @param {Document} root0.iframeDocument The document of the iframe. - * @param {number} root0.iframeWindowInnerHeight The height of the inner window - * @param {number} root0.windowInnerWidth The height of the inner window - * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. - * @param {number} root0.scale The scale of the canvas. - * @param {number} root0.scaleContainerWidth The width of the container at the scaled size. + * @param {number} root0.contentHeight The height of the content in the iframe. + * @param {number} root0.containerWidth The width of the container. + * @param {number} root0.frameSize The size of the frame around the content. + * @param {Document} root0.iframeDocument The document of the iframe. + * @param {number} root0.windowInnerWidth The height of the inner window + * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. + * @param {number} root0.scale The scale of the canvas. + * @param {number} root0.scaleContainerWidth The width of the container at the scaled size. */ export function useScaleCanvas( { scale, frameSize, iframeDocument, - iframeWindowInnerHeight, contentHeight, containerWidth, windowInnerWidth, @@ -225,8 +223,9 @@ export function useScaleCanvas( { ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-inner-height', - `${ iframeWindowInnerHeight }px` + `${ iframeDocument.documentElement.clientHeight }px` ); + iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-container-width', `${ containerWidth }px` @@ -262,7 +261,6 @@ export function useScaleCanvas( { scale, frameSize, iframeDocument, - iframeWindowInnerHeight, contentHeight, containerWidth, windowInnerWidth, From 7903702e62c36c09405c0ff1c45d3f71570f7af2 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 12:59:42 -0600 Subject: [PATCH 36/64] Remove unused window resize ref --- .../src/components/iframe/index.js | 18 +----------------- .../src/components/iframe/use-scale-canvas.js | 3 --- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 138a7d5c61b312..c8c49c4129e6cf 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -227,21 +227,6 @@ function Iframe( { }; }, [] ); - const [ windowInnerWidth, setWindowInnerWidth ] = useState(); - - const windowResizeRef = useRefEffect( ( node ) => { - const nodeWindow = node.ownerDocument.defaultView; - - setWindowInnerWidth( nodeWindow.innerWidth ); - const onResize = () => { - setWindowInnerWidth( nodeWindow.innerWidth ); - }; - nodeWindow.addEventListener( 'resize', onResize ); - return () => { - nodeWindow.removeEventListener( 'resize', onResize ); - }; - }, [] ); - const isZoomedOut = scale !== 1; useEffect( () => { @@ -268,7 +253,6 @@ function Iframe( { iframeDocument, contentHeight, containerWidth, - windowInnerWidth, isZoomedOut, scaleContainerWidth, } ); @@ -400,7 +384,7 @@ function Iframe( { ); return ( -
+
{ containerResizeListener }
Date: Wed, 13 Nov 2024 13:27:40 -0600 Subject: [PATCH 37/64] Refactor CSS --- .../src/components/iframe/content.scss | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index 5e390800719949..d1a5fea4e1a599 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -3,7 +3,16 @@ } .block-editor-iframe__html { + $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); + $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); + scale: $scale; transform-origin: top center; + // Add the top/bottom frame size. We use scaling to account for the left/right, as + // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling + // of the content. + padding-top: calc(#{$frame-size} / #{$scale}); + padding-bottom: calc(#{$frame-size} / #{$scale}); + // We don't want to animate the transform of the translateX because it is used // to "center" the canvas. Leaving it on causes the canvas to slide around in // odd ways. @@ -28,43 +37,35 @@ // and the doubled animations cause very odd animations. @include editor-canvas-resize-animation( transform 0s, top 0s, bottom 0s, right 0s, left 0s ); } -} -.block-editor-iframe__html.is-zoomed-out { - $scale: var(--wp-block-editor-iframe-zoom-out-scale); - $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); - $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); - $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); - $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); - $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); - // Apply an X translation to center the scaled content within the available space. - transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); - scale: #{$scale}; - background-color: $gray-300; - - // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, - // so we need to adjust the height of the content to match the scale by using negative margins. - $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); - $total-frame-height: calc(2 * #{$frame-size} / #{$scale}); - $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); - margin-bottom: calc(-1 * #{$total-height}); - // Add the top/bottom frame size. We use scaling to account for the left/right, as - // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling - // of the content. - padding-top: calc(#{$frame-size} / #{$scale}); - padding-bottom: calc(#{$frame-size} / #{$scale}); + &.is-zoomed-out { + $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); + $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); + $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); + $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); + // Apply an X translation to center the scaled content within the available space. + transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); + background-color: $gray-300; - body { - min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); + // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, + // so we need to adjust the height of the content to match the scale by using negative margins. + $extra-content-height: calc(#{$content-height} * (1 - #{$scale})); + $total-frame-height: calc(2 * #{$frame-size} / #{$scale}); + $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); + margin-bottom: calc(-1 * #{$total-height}); - > .is-root-container:not(.wp-block-post-content) { - flex: 1; - display: flex; - flex-direction: column; - height: 100%; + body { + min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); - > main { + > .is-root-container:not(.wp-block-post-content) { flex: 1; + display: flex; + flex-direction: column; + height: 100%; + + > main { + flex: 1; + } } } } From 53d8f6f1347a543371d02418fd746cb593992676 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 14:51:35 -0600 Subject: [PATCH 38/64] Move animation to state --- .../src/components/iframe/use-scale-canvas.js | 167 +++++++++--------- 1 file changed, 88 insertions(+), 79 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index c022c08876e25d..f32ecb9963e0fc 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { useReducedMotion } from '@wordpress/compose'; /** @@ -27,20 +27,24 @@ export function useScaleCanvas( { scaleContainerWidth, } ) { const prefersReducedMotion = useReducedMotion(); - + const [ isAnimatingZoomOut, setIsAnimatingZoomOut ] = useState( false ); + const transitionToRef = useRef( { + scale, + frameSize, + } ); const prevScaleRef = useRef( scale ); const prevFrameSizeRef = useRef( frameSize ); const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + // New state for isZoomedOut in the iframe component. That state is updated when the animation is completed, + // which causes the rerender to happen. Things that are in refs, now become state. ZoomOutAnimation is a state. + // ZoomOutAnimation being removed. useEffect( () => { - if ( - ! iframeDocument || - // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here - // instead of the dependency array to appease the linter. - ( scale === 1 ) === ( prevScaleRef.current === 1 ) - ) { + if ( ! iframeDocument || ! isAnimatingZoomOut ) { return; } + const nextScale = transitionToRef.current.scale; + const nextFrameSize = transitionToRef.current.frameSize; // Unscaled height of the current iframe container. const clientHeight = iframeDocument.documentElement.clientHeight; @@ -80,8 +84,8 @@ export function useScaleCanvas( { // Step 2: Apply the new scale and frame around the midpoint of the // visible area. scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * scale + - frameSize - + ( scrollTopNext + clientHeight / 2 ) * nextScale + + nextFrameSize - clientHeight / 2; // Step 3: Handle an edge case so that you scroll to the top of the @@ -94,7 +98,9 @@ export function useScaleCanvas( { // iframe. We can't just let the browser handle it because we need to // animate the scaling. const maxScrollTop = - scrollHeight * ( scale / prevScale ) + frameSize * 2 - clientHeight; + scrollHeight * ( nextScale / prevScale ) + + nextFrameSize * 2 - + clientHeight; // Step 4: Clamp the scrollTopNext between the minimum and maximum // possible scrollTop positions. Round the value to avoid subpixel @@ -117,28 +123,31 @@ export function useScaleCanvas( { ); iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ nextFrameSize }px` + ); - function onZoomOutTransitionEnd() { - // Remove the position fixed for the animation. - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); + // We can change the scale and frame size here, because that's what we want to animate. + // We don't want to update these values until the animation class has been added. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + nextScale + ); - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSize; - prevScaleRef.current = scale; + function onZoomOutTransitionEnd() { + if ( isAnimatingZoomOut ) { + setIsAnimatingZoomOut( false ); - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = nextFrameSize; + prevScaleRef.current = nextScale; + } } - let raf; if ( prefersReducedMotion ) { - // Hack: Wait for the window values to recalculate. - raf = iframeDocument.defaultView.requestAnimationFrame( - onZoomOutTransitionEnd - ); + onZoomOutTransitionEnd(); } else { iframeDocument.documentElement.addEventListener( 'transitionend', @@ -148,48 +157,25 @@ export function useScaleCanvas( { } return () => { + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = scrollTopNext; + iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scroll-top' ); iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scroll-top-next' ); - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - if ( prefersReducedMotion ) { - iframeDocument.defaultView.cancelAnimationFrame( raf ); - } else { - iframeDocument.documentElement.removeEventListener( - 'transitionend', - onZoomOutTransitionEnd - ); - } - }; - }, [ iframeDocument, scale, frameSize, prefersReducedMotion ] ); - - // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect - // that controls settings the CSS variables, but then we would need to do more work to ensure we're - // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large - // number of dependencies. - useEffect( () => { - if ( ! iframeDocument ) { - return; - } - if ( isZoomedOut ) { - iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); - } else { - // HACK: Since we can't remove this in the cleanup, we need to do it here. - iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); - } - - return () => { - // HACK: Skipping cleanup because it causes issues with the zoom out - // animation. More refactoring is needed to fix this properly. - // iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + iframeDocument.documentElement.removeEventListener( + 'transitionend', + onZoomOutTransitionEnd + ); }; - }, [ iframeDocument, isZoomedOut ] ); + }, [ iframeDocument, prefersReducedMotion, isAnimatingZoomOut ] ); // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { @@ -197,24 +183,6 @@ export function useScaleCanvas( { return; } - // Note: When we initialize the zoom out when the canvas is smaller (sidebars open), - // initialContainerWidth will be smaller than the full page, and reflow will happen - // when the canvas area becomes larger due to sidebars closing. This is a known but - // minor divergence for now. - - // This scaling calculation has to happen within the JS because CSS calc() can - // only divide and multiply by a unitless value. I.e. calc( 100px / 2 ) is valid - // but calc( 100px / 2px ) is not. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - scale - ); - - // frameSize has to be a px value for the scaling and frame size to be computed correctly. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ frameSize }px` - ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-content-height', `${ contentHeight }px` @@ -233,6 +201,11 @@ export function useScaleCanvas( { `${ scaleContainerWidth }px` ); + transitionToRef.current = { + scale, + frameSize, + }; + return () => { // HACK: Skipping cleanup because it causes issues with the zoom out // animation. More refactoring is needed to fix this properly. @@ -264,4 +237,40 @@ export function useScaleCanvas( { isZoomedOut, scaleContainerWidth, ] ); + + useEffect( () => { + if ( ! iframeDocument ) { + return; + } + + if ( isZoomedOut ) { + iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); + } + + setIsAnimatingZoomOut( true ); + + // Set the value that we want to animate from. + // We will update them after we add the animation class on next render. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + prevScaleRef.current + ); + + // frameSize has to be a px value for the scaling and frame size to be computed correctly. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ prevFrameSizeRef.current }px` + ); + + return () => { + iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-scale' + // ); + // iframeDocument.documentElement.style.removeProperty( + // '--wp-block-editor-iframe-zoom-out-frame-size' + // ); + }; + }, [ iframeDocument, isZoomedOut, setIsAnimatingZoomOut ] ); } From a25f7da49cc29636ba90a11bd9948466ac30cb79 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 13 Nov 2024 14:59:03 -0600 Subject: [PATCH 39/64] Fix scaling the canvas when sidebars open/close --- .../src/components/iframe/use-scale-canvas.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index f32ecb9963e0fc..89a7be6bc90f2b 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -183,6 +183,19 @@ export function useScaleCanvas( { return; } + // Set the value that we want to animate from. + // We will update them after we add the animation class on next render. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + scale + ); + + // frameSize has to be a px value for the scaling and frame size to be computed correctly. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ frameSize }px` + ); + iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-content-height', `${ contentHeight }px` From 5fcd9c1c17943c7cff282638290abd50e90489fb Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 14 Nov 2024 10:34:59 -0600 Subject: [PATCH 40/64] Recalculate scale before exiting scaled canvas to prevent snapping effect --- .../src/components/iframe/index.js | 9 +-- .../src/components/iframe/use-scale-canvas.js | 68 +++++++++++++++---- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index c8c49c4129e6cf..cb80efc7cfd61c 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -240,15 +240,8 @@ function Iframe( { containerWidth ); - const maxWidth = 750; - useScaleCanvas( { - scale: - scale === 'auto-scaled' - ? ( Math.min( containerWidth, maxWidth ) - - parseInt( frameSize ) * 2 ) / - scaleContainerWidth - : scale, + scale, frameSize: parseInt( frameSize ), iframeDocument, contentHeight, diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 89a7be6bc90f2b..2a96ce00316772 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -4,26 +4,40 @@ import { useEffect, useRef, useState } from '@wordpress/element'; import { useReducedMotion } from '@wordpress/compose'; +function calculateScale( { + frameSize, + containerWidth, + scaleContainerWidth, + maxContainerWidth, +} ) { + return ( + ( Math.min( containerWidth, maxContainerWidth ) - frameSize * 2 ) / + scaleContainerWidth + ); +} + /** * Handles scaling the canvas for the zoom out mode and animating between * the states. * - * @param {Object} root0 - * @param {number} root0.contentHeight The height of the content in the iframe. - * @param {number} root0.containerWidth The width of the container. - * @param {number} root0.frameSize The size of the frame around the content. - * @param {Document} root0.iframeDocument The document of the iframe. - * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. - * @param {number} root0.scale The scale of the canvas. - * @param {number} root0.scaleContainerWidth The width of the container at the scaled size. + * @param {Object} root0 + * @param {number} root0.containerWidth The width of the container. + * @param {number} root0.contentHeight The height of the content in the iframe. + * @param {number} root0.frameSize The size of the frame around the content. + * @param {Document} root0.iframeDocument The document of the iframe. + * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. + * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. + * @param {number|string} root0.scale The scale of the canvas. Default to 'auto-scaled'. + * @param {number} root0.scaleContainerWidth The width of the outer container used to calculate the scale. */ export function useScaleCanvas( { - scale, + containerWidth, + contentHeight, frameSize, iframeDocument, - contentHeight, - containerWidth, isZoomedOut, + maxContainerWidth = 750, + scale, scaleContainerWidth, } ) { const prefersReducedMotion = useReducedMotion(); @@ -35,6 +49,16 @@ export function useScaleCanvas( { const prevScaleRef = useRef( scale ); const prevFrameSizeRef = useRef( frameSize ); const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + const isAutoScaled = scale === 'auto-scaled'; + + const scaleValue = isAutoScaled + ? calculateScale( { + frameSize, + containerWidth, + maxContainerWidth, + scaleContainerWidth, + } ) + : scale; // New state for isZoomedOut in the iframe component. That state is updated when the animation is completed, // which causes the rerender to happen. Things that are in refs, now become state. ZoomOutAnimation is a state. @@ -187,9 +211,22 @@ export function useScaleCanvas( { // We will update them after we add the animation class on next render. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', - scale + scaleValue ); + if ( isAutoScaled && prevScaleRef.current !== 1 ) { + // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the + // original scale, we will snap to a much smaller scale due to the scale container changing size when exiting. + // We use containerWidth as the divisor, as scaleContainerWidth will always match the containerWidth when + // exiting. + prevScaleRef.current = calculateScale( { + containerWidth, + maxContainerWidth, + scaleContainerWidth: containerWidth, + frameSize: prevFrameSizeRef.current, + } ); + } + // frameSize has to be a px value for the scaling and frame size to be computed correctly. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-frame-size', @@ -215,7 +252,7 @@ export function useScaleCanvas( { ); transitionToRef.current = { - scale, + scale: scaleValue, frameSize, }; @@ -242,12 +279,13 @@ export function useScaleCanvas( { // ); }; }, [ - scale, + isAutoScaled, + scaleValue, frameSize, iframeDocument, contentHeight, containerWidth, - isZoomedOut, + maxContainerWidth, scaleContainerWidth, ] ); From 1c5d7d51c53830d6497fbb75cdf2dff952123593 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 14 Nov 2024 11:52:40 -0600 Subject: [PATCH 41/64] CSS linting --- packages/block-editor/src/components/iframe/content.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index d1a5fea4e1a599..fcd2aaa0210bbf 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -16,7 +16,7 @@ // We don't want to animate the transform of the translateX because it is used // to "center" the canvas. Leaving it on causes the canvas to slide around in // odd ways. - @include editor-canvas-resize-animation( transform 0s, scale 0s, padding 0s, translate 0s); + @include editor-canvas-resize-animation(transform 0s, scale 0s, padding 0s, translate 0s); &.zoom-out-animation { $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); @@ -35,7 +35,7 @@ // We only want to animate the scaling when entering zoom out. When sidebars // are toggled, the resizing of the iframe handles scaling the canvas as well, // and the doubled animations cause very odd animations. - @include editor-canvas-resize-animation( transform 0s, top 0s, bottom 0s, right 0s, left 0s ); + @include editor-canvas-resize-animation(transform 0s, top 0s, bottom 0s, right 0s, left 0s); } &.is-zoomed-out { From 96d62abbad3fd1727efb0f67bcb0e28ceed453da Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 14 Nov 2024 12:06:07 -0600 Subject: [PATCH 42/64] Move code into useScaleCanvas that isn't necessary in iframe index --- .../src/components/iframe/index.js | 19 +--------- .../src/components/iframe/use-scale-canvas.js | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index cb80efc7cfd61c..61f66ed0ff7b03 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -12,7 +12,6 @@ import { forwardRef, useMemo, useEffect, - useRef, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { @@ -124,7 +123,6 @@ function Iframe( { const { styles = '', scripts = '' } = resolvedAssets; /** @type {[Document, import('react').Dispatch]} */ const [ iframeDocument, setIframeDocument ] = useState(); - const initialContainerWidthRef = useRef( 0 ); const [ bodyClasses, setBodyClasses ] = useState( [] ); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); @@ -227,27 +225,12 @@ function Iframe( { }; }, [] ); - const isZoomedOut = scale !== 1; - - useEffect( () => { - if ( ! isZoomedOut ) { - initialContainerWidthRef.current = containerWidth; - } - }, [ containerWidth, isZoomedOut ] ); - - const scaleContainerWidth = Math.max( - initialContainerWidthRef.current, - containerWidth - ); - - useScaleCanvas( { + const { isZoomedOut, scaleContainerWidth } = useScaleCanvas( { scale, frameSize: parseInt( frameSize ), iframeDocument, contentHeight, containerWidth, - isZoomedOut, - scaleContainerWidth, } ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 2a96ce00316772..4bd5fc4923cd24 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -21,25 +21,35 @@ function calculateScale( { * the states. * * @param {Object} root0 - * @param {number} root0.containerWidth The width of the container. - * @param {number} root0.contentHeight The height of the content in the iframe. - * @param {number} root0.frameSize The size of the frame around the content. - * @param {Document} root0.iframeDocument The document of the iframe. - * @param {boolean} root0.isZoomedOut Whether the canvas is in zoom out mode. - * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. - * @param {number|string} root0.scale The scale of the canvas. Default to 'auto-scaled'. - * @param {number} root0.scaleContainerWidth The width of the outer container used to calculate the scale. + * @param {number} root0.containerWidth The width of the container. + * @param {number} root0.contentHeight The height of the content in the iframe. + * @param {number} root0.frameSize The size of the frame around the content. + * @param {Document} root0.iframeDocument The document of the iframe. + * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. + * @param {number|string} root0.scale The scale of the canvas. Can be an decimal between 0 and 1, 1, or 'auto-scaled'. */ export function useScaleCanvas( { containerWidth, contentHeight, frameSize, iframeDocument, - isZoomedOut, maxContainerWidth = 750, scale, - scaleContainerWidth, } ) { + const initialContainerWidthRef = useRef( 0 ); + const isZoomedOut = scale !== 1; + + useEffect( () => { + if ( ! isZoomedOut ) { + initialContainerWidthRef.current = containerWidth; + } + }, [ containerWidth, isZoomedOut ] ); + + const scaleContainerWidth = Math.max( + initialContainerWidthRef.current, + containerWidth + ); + const prefersReducedMotion = useReducedMotion(); const [ isAnimatingZoomOut, setIsAnimatingZoomOut ] = useState( false ); const transitionToRef = useRef( { @@ -324,4 +334,9 @@ export function useScaleCanvas( { // ); }; }, [ iframeDocument, isZoomedOut, setIsAnimatingZoomOut ] ); + + return { + isZoomedOut, + scaleContainerWidth, + }; } From ec37f2642f4007545308cb145ebd1e9dec46675e Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 14 Nov 2024 13:18:36 -0600 Subject: [PATCH 43/64] Move resize listeners into useScaledCanvas --- .../src/components/iframe/index.js | 20 +++++++------------ .../src/components/iframe/use-scale-canvas.js | 19 +++++++++++++----- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 61f66ed0ff7b03..751e940dd166cc 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -14,12 +14,7 @@ import { useEffect, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { - useResizeObserver, - useMergeRefs, - useRefEffect, - useDisabled, -} from '@wordpress/compose'; +import { useMergeRefs, useRefEffect, useDisabled } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -126,10 +121,6 @@ function Iframe( { const [ bodyClasses, setBodyClasses ] = useState( [] ); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); - const [ contentResizeListener, { height: contentHeight } ] = - useResizeObserver(); - const [ containerResizeListener, { width: containerWidth } ] = - useResizeObserver(); const setRef = useRefEffect( ( node ) => { node._load = () => { @@ -225,12 +216,15 @@ function Iframe( { }; }, [] ); - const { isZoomedOut, scaleContainerWidth } = useScaleCanvas( { + const { + contentResizeListener, + containerResizeListener, + isZoomedOut, + scaleContainerWidth, + } = useScaleCanvas( { scale, frameSize: parseInt( frameSize ), iframeDocument, - contentHeight, - containerWidth, } ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 4bd5fc4923cd24..22bab874290955 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useEffect, useRef, useState } from '@wordpress/element'; -import { useReducedMotion } from '@wordpress/compose'; +import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; function calculateScale( { frameSize, @@ -21,21 +21,28 @@ function calculateScale( { * the states. * * @param {Object} root0 - * @param {number} root0.containerWidth The width of the container. - * @param {number} root0.contentHeight The height of the content in the iframe. * @param {number} root0.frameSize The size of the frame around the content. * @param {Document} root0.iframeDocument The document of the iframe. * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. * @param {number|string} root0.scale The scale of the canvas. Can be an decimal between 0 and 1, 1, or 'auto-scaled'. + * + * @return {Object} An object containing the following properties: + * isZoomedOut: A boolean indicating if the canvas is zoomed out. + * scaleContainerWidth: The width of the container used to calculate the scale. + * contentResizeListener: A resize observer for the content. + * containerResizeListener: A resize observer for the container. */ export function useScaleCanvas( { - containerWidth, - contentHeight, frameSize, iframeDocument, maxContainerWidth = 750, scale, } ) { + const [ contentResizeListener, { height: contentHeight } ] = + useResizeObserver(); + const [ containerResizeListener, { width: containerWidth } ] = + useResizeObserver(); + const initialContainerWidthRef = useRef( 0 ); const isZoomedOut = scale !== 1; @@ -338,5 +345,7 @@ export function useScaleCanvas( { return { isZoomedOut, scaleContainerWidth, + contentResizeListener, + containerResizeListener, }; } From 28cff48cdfc3f54a77a86fe3e4d25ba10f2ccf65 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Fri, 15 Nov 2024 15:38:51 -0600 Subject: [PATCH 44/64] Combine into one useEffect --- .../src/components/iframe/content.scss | 2 +- .../src/components/iframe/use-scale-canvas.js | 356 ++++++++---------- 2 files changed, 167 insertions(+), 191 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index fcd2aaa0210bbf..870f0194bbb2e8 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -3,7 +3,7 @@ } .block-editor-iframe__html { - $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size); + $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size, 0); $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); scale: $scale; transform-origin: top center; diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 22bab874290955..dd97183e4aa5e6 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -45,6 +45,7 @@ export function useScaleCanvas( { const initialContainerWidthRef = useRef( 0 ); const isZoomedOut = scale !== 1; + const isAnimatingRef = useRef( false ); useEffect( () => { if ( ! isZoomedOut ) { @@ -59,13 +60,6 @@ export function useScaleCanvas( { const prefersReducedMotion = useReducedMotion(); const [ isAnimatingZoomOut, setIsAnimatingZoomOut ] = useState( false ); - const transitionToRef = useRef( { - scale, - frameSize, - } ); - const prevScaleRef = useRef( scale ); - const prevFrameSizeRef = useRef( frameSize ); - const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); const isAutoScaled = scale === 'auto-scaled'; const scaleValue = isAutoScaled @@ -77,146 +71,26 @@ export function useScaleCanvas( { } ) : scale; - // New state for isZoomedOut in the iframe component. That state is updated when the animation is completed, - // which causes the rerender to happen. Things that are in refs, now become state. ZoomOutAnimation is a state. - // ZoomOutAnimation being removed. + const prevScaleRef = useRef( scaleValue ); + const prevFrameSizeRef = useRef( frameSize ); + const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + useEffect( () => { - if ( ! iframeDocument || ! isAnimatingZoomOut ) { + if ( ! iframeDocument ) { return; } - const nextScale = transitionToRef.current.scale; - const nextFrameSize = transitionToRef.current.frameSize; - - // Unscaled height of the current iframe container. - const clientHeight = iframeDocument.documentElement.clientHeight; - - // Scaled height of the current iframe content. - const scrollHeight = iframeDocument.documentElement.scrollHeight; - - // Previous scale value. - const prevScale = prevScaleRef.current; - - // Unscaled size of the previous padding around the iframe content. - const prevFrameSize = prevFrameSizeRef.current; - - // Unscaled height of the previous iframe container. - const prevClientHeight = prevClientHeightRef.current ?? clientHeight; - - // We can't trust the set value from contentHeight, as it was measured - // before the zoom out mode was changed. After zoom out mode is changed, - // appenders may appear or disappear, so we need to get the height from - // the iframe at this point when we're about to animate the zoom out. - // The iframe scrollTop, scrollHeight, and clientHeight will all be - // accurate. The client height also does change when the zoom out mode - // is toggled, as the bottom bar about selecting the template is - // added/removed when toggling zoom out mode. - const scrollTop = iframeDocument.documentElement.scrollTop; - - // Step 0: Start with the current scrollTop. - let scrollTopNext = scrollTop; - - // Step 1: Undo the effects of the previous scale and frame around the - // midpoint of the visible area. - scrollTopNext = - ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / - prevScale - - prevClientHeight / 2; - - // Step 2: Apply the new scale and frame around the midpoint of the - // visible area. - scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * nextScale + - nextFrameSize - - clientHeight / 2; - - // Step 3: Handle an edge case so that you scroll to the top of the - // iframe if the top of the iframe content is visible in the container. - // The same edge case for the bottom is skipped because changing content - // makes calculating it impossible. - scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; - - // This is the scrollTop value if you are scrolled to the bottom of the - // iframe. We can't just let the browser handle it because we need to - // animate the scaling. - const maxScrollTop = - scrollHeight * ( nextScale / prevScale ) + - nextFrameSize * 2 - - clientHeight; - - // Step 4: Clamp the scrollTopNext between the minimum and maximum - // possible scrollTop positions. Round the value to avoid subpixel - // truncation by the browser which sometimes causes a 1px error. - scrollTopNext = Math.round( - Math.min( - Math.max( 0, scrollTopNext ), - Math.max( 0, maxScrollTop ) - ) - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); - - iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ nextFrameSize }px` - ); - - // We can change the scale and frame size here, because that's what we want to animate. - // We don't want to update these values until the animation class has been added. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - nextScale - ); - - function onZoomOutTransitionEnd() { - if ( isAnimatingZoomOut ) { - setIsAnimatingZoomOut( false ); - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = nextFrameSize; - prevScaleRef.current = nextScale; - } + if ( isZoomedOut ) { + iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); } - if ( prefersReducedMotion ) { - onZoomOutTransitionEnd(); - } else { - iframeDocument.documentElement.addEventListener( - 'transitionend', - onZoomOutTransitionEnd, - { once: true } - ); - } + setIsAnimatingZoomOut( true ); + isAnimatingRef.current = true; return () => { - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; - - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next' - ); - - iframeDocument.documentElement.removeEventListener( - 'transitionend', - onZoomOutTransitionEnd - ); + iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); }; - }, [ iframeDocument, prefersReducedMotion, isAnimatingZoomOut ] ); + }, [ iframeDocument, isZoomedOut, setIsAnimatingZoomOut ] ); // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { @@ -224,13 +98,6 @@ export function useScaleCanvas( { return; } - // Set the value that we want to animate from. - // We will update them after we add the animation class on next render. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - scaleValue - ); - if ( isAutoScaled && prevScaleRef.current !== 1 ) { // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the // original scale, we will snap to a much smaller scale due to the scale container changing size when exiting. @@ -244,11 +111,32 @@ export function useScaleCanvas( { } ); } - // frameSize has to be a px value for the scaling and frame size to be computed correctly. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ frameSize }px` - ); + if ( isAnimatingZoomOut || isAnimatingRef.current ) { + // Set the value that we want to animate from. + // We will update them after we add the animation class on next render. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + prevScaleRef.current + ); + + // frameSize has to be a px value for the scaling and frame size to be computed correctly. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ prevFrameSizeRef.current }px` + ); + } else { + // We will update them after we add the animation class on next render. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + scaleValue + ); + + // frameSize has to be a px value for the scaling and frame size to be computed correctly. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ frameSize }px` + ); + } iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-content-height', @@ -268,10 +156,133 @@ export function useScaleCanvas( { `${ scaleContainerWidth }px` ); - transitionToRef.current = { - scale: scaleValue, - frameSize, - }; + if ( isAnimatingZoomOut ) { + // Unscaled height of the current iframe container. + const clientHeight = iframeDocument.documentElement.clientHeight; + + // Scaled height of the current iframe content. + const scrollHeight = iframeDocument.documentElement.scrollHeight; + + // Previous scale value. + const prevScale = prevScaleRef.current; + + // Unscaled size of the previous padding around the iframe content. + const prevFrameSize = prevFrameSizeRef.current; + + // Unscaled height of the previous iframe container. + const prevClientHeight = + prevClientHeightRef.current ?? clientHeight; + + // We can't trust the set value from contentHeight, as it was measured + // before the zoom out mode was changed. After zoom out mode is changed, + // appenders may appear or disappear, so we need to get the height from + // the iframe at this point when we're about to animate the zoom out. + // The iframe scrollTop, scrollHeight, and clientHeight will all be + // accurate. The client height also does change when the zoom out mode + // is toggled, as the bottom bar about selecting the template is + // added/removed when toggling zoom out mode. + const scrollTop = iframeDocument.documentElement.scrollTop; + + // Step 0: Start with the current scrollTop. + let scrollTopNext = scrollTop; + + // Step 1: Undo the effects of the previous scale and frame around the + // midpoint of the visible area. + scrollTopNext = + ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / + prevScale - + prevClientHeight / 2; + + // Step 2: Apply the new scale and frame around the midpoint of the + // visible area. + scrollTopNext = + ( scrollTopNext + clientHeight / 2 ) * scaleValue + + frameSize - + clientHeight / 2; + + // Step 3: Handle an edge case so that you scroll to the top of the + // iframe if the top of the iframe content is visible in the container. + // The same edge case for the bottom is skipped because changing content + // makes calculating it impossible. + scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; + + // This is the scrollTop value if you are scrolled to the bottom of the + // iframe. We can't just let the browser handle it because we need to + // animate the scaling. + const maxScrollTop = + scrollHeight * ( scaleValue / prevScale ) + + frameSize * 2 - + clientHeight; + + // Step 4: Clamp the scrollTopNext between the minimum and maximum + // possible scrollTop positions. Round the value to avoid subpixel + // truncation by the browser which sometimes causes a 1px error. + scrollTopNext = Math.round( + Math.min( + Math.max( 0, scrollTopNext ), + Math.max( 0, maxScrollTop ) + ) + ); + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` + ); + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` + ); + + iframeDocument.documentElement.classList.add( + 'zoom-out-animation' + ); + // We can change the scale and frame size here, because that's what we want to animate. + // We don't want to update these values until the animation class has been added. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ frameSize }px` + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + scaleValue + ); + + function onZoomOutTransitionEnd() { + if ( isAnimatingZoomOut ) { + isAnimatingRef.current = false; + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + setIsAnimatingZoomOut( false ); + + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = frameSize; + prevScaleRef.current = scale; + + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = scrollTopNext; + + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next' + ); + } + } + + if ( prefersReducedMotion ) { + onZoomOutTransitionEnd(); + } else { + iframeDocument.documentElement.addEventListener( + 'transitionend', + onZoomOutTransitionEnd, + { once: true } + ); + } + } return () => { // HACK: Skipping cleanup because it causes issues with the zoom out @@ -296,6 +307,7 @@ export function useScaleCanvas( { // ); }; }, [ + isAnimatingZoomOut, isAutoScaled, scaleValue, frameSize, @@ -306,42 +318,6 @@ export function useScaleCanvas( { scaleContainerWidth, ] ); - useEffect( () => { - if ( ! iframeDocument ) { - return; - } - - if ( isZoomedOut ) { - iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); - } - - setIsAnimatingZoomOut( true ); - - // Set the value that we want to animate from. - // We will update them after we add the animation class on next render. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - prevScaleRef.current - ); - - // frameSize has to be a px value for the scaling and frame size to be computed correctly. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ prevFrameSizeRef.current }px` - ); - - return () => { - iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); - - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-frame-size' - // ); - }; - }, [ iframeDocument, isZoomedOut, setIsAnimatingZoomOut ] ); - return { isZoomedOut, scaleContainerWidth, From 28ded6a9e3692d81994280334135101f245632fc Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Fri, 15 Nov 2024 16:03:49 -0600 Subject: [PATCH 45/64] Add useEffect cleanup --- .../src/components/iframe/use-scale-canvas.js | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index dd97183e4aa5e6..ae4a1ede9639c5 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -156,6 +156,8 @@ export function useScaleCanvas( { `${ scaleContainerWidth }px` ); + let onZoomOutTransitionEnd = () => {}; + if ( isAnimatingZoomOut ) { // Unscaled height of the current iframe container. const clientHeight = iframeDocument.documentElement.clientHeight; @@ -248,30 +250,28 @@ export function useScaleCanvas( { scaleValue ); - function onZoomOutTransitionEnd() { - if ( isAnimatingZoomOut ) { - isAnimatingRef.current = false; - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - setIsAnimatingZoomOut( false ); - - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSize; - prevScaleRef.current = scale; - - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; - - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next' - ); - } - } + onZoomOutTransitionEnd = () => { + isAnimatingRef.current = false; + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + setIsAnimatingZoomOut( false ); + + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = frameSize; + prevScaleRef.current = scale; + + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = scrollTopNext; + + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next' + ); + }; if ( prefersReducedMotion ) { onZoomOutTransitionEnd(); @@ -285,26 +285,29 @@ export function useScaleCanvas( { } return () => { - // HACK: Skipping cleanup because it causes issues with the zoom out - // animation. More refactoring is needed to fix this properly. - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-frame-size' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-content-height' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-inner-height' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-container-width' - // ); - // iframeDocument.documentElement.style.removeProperty( - // '--wp-block-editor-iframe-zoom-out-scale-container-width' - // ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scale' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-frame-size' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-content-height' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-inner-height' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-container-width' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scale-container-width' + ); + + iframeDocument.documentElement.removeEventListener( + 'transitionend', + onZoomOutTransitionEnd + ); }; }, [ isAnimatingZoomOut, From 1dbdc8290b8e85fc1494306ad8721c3f4241f337 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 19 Nov 2024 16:12:34 -0600 Subject: [PATCH 46/64] Fix accidental setting prevScaleRef to scale instead of scaleValue --- packages/block-editor/src/components/iframe/use-scale-canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index ae4a1ede9639c5..656a54b14392cb 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -260,7 +260,7 @@ export function useScaleCanvas( { // Update previous values. prevClientHeightRef.current = clientHeight; prevFrameSizeRef.current = frameSize; - prevScaleRef.current = scale; + prevScaleRef.current = scaleValue; // Set the final scroll position that was just animated to. iframeDocument.documentElement.scrollTop = scrollTopNext; From cf8ffdec41c708833fdbd5413fe16617bad01fb5 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 19 Nov 2024 13:52:43 -0600 Subject: [PATCH 47/64] Switch to animations api --- packages/base-styles/_animations.scss | 5 ---- .../src/components/block-canvas/style.scss | 3 ++- .../src/components/iframe/content.scss | 13 ++------- .../src/components/iframe/use-scale-canvas.js | 27 +++++++++++++++---- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/base-styles/_animations.scss b/packages/base-styles/_animations.scss index ae5de9a803008c..e5bbf863757356 100644 --- a/packages/base-styles/_animations.scss +++ b/packages/base-styles/_animations.scss @@ -41,8 +41,3 @@ @warn "The `edit-post__fade-in-animation` mixin is deprecated. Use `animation__fade-in` instead."; @include animation__fade-in($speed, $delay); } - -@mixin editor-canvas-resize-animation($additional-transition-rules...) { - transition: all 400ms cubic-bezier(0.46, 0.03, 0.52, 0.96), $additional-transition-rules; - @include reduce-motion("transition"); -} diff --git a/packages/block-editor/src/components/block-canvas/style.scss b/packages/block-editor/src/components/block-canvas/style.scss index 8f6064de0b615c..ea54646e64a59a 100644 --- a/packages/block-editor/src/components/block-canvas/style.scss +++ b/packages/block-editor/src/components/block-canvas/style.scss @@ -4,6 +4,7 @@ iframe[name="editor-canvas"] { height: 100%; display: block; // Handles transitions between device previews - @include editor-canvas-resize-animation; + transition: all 400ms cubic-bezier(0.46, 0.03, 0.52, 0.96); + @include reduce-motion("transition"); background-color: $gray-300; } diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index 870f0194bbb2e8..bc820567220a6b 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -12,11 +12,8 @@ // of the content. padding-top: calc(#{$frame-size} / #{$scale}); padding-bottom: calc(#{$frame-size} / #{$scale}); - - // We don't want to animate the transform of the translateX because it is used - // to "center" the canvas. Leaving it on causes the canvas to slide around in - // odd ways. - @include editor-canvas-resize-animation(transform 0s, scale 0s, padding 0s, translate 0s); + // Prevents a flash of background color change when entering/exiting zoom out + transition: background-color 400ms; &.zoom-out-animation { $scroll-top: var(--wp-block-editor-iframe-zoom-out-scroll-top, 0); @@ -27,15 +24,9 @@ right: 0; top: calc(-1 * #{$scroll-top}); bottom: 0; - translate: 0 calc(#{$scroll-top} - #{$scroll-top-next}); // Force preserving a scrollbar gutter as scrollbar-gutter isn't supported in all browsers yet, // and removing the scrollbar causes the content to shift. overflow-y: scroll; - - // We only want to animate the scaling when entering zoom out. When sidebars - // are toggled, the resizing of the iframe handles scaling the canvas as well, - // and the doubled animations cause very odd animations. - @include editor-canvas-resize-animation(transform 0s, top 0s, bottom 0s, right 0s, left 0s); } &.is-zoomed-out { diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 656a54b14392cb..0781d3746d7bf7 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -250,6 +250,27 @@ export function useScaleCanvas( { scaleValue ); + const zoomOutAnimation = iframeDocument.documentElement.animate( + [ + { + translate: `0 0`, + scale: prevScale, + paddingTop: `${ prevFrameSize / prevScale }px`, + paddingBottom: `${ prevFrameSize / prevScale }px`, + }, + { + translate: `0 ${ scrollTop - scrollTopNext }px`, + scale: scaleValue, + paddingTop: `${ frameSize / scaleValue }px`, + paddingBottom: `${ frameSize / scaleValue }px`, + }, + ], + { + easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', + duration: 400, + } + ); + onZoomOutTransitionEnd = () => { isAnimatingRef.current = false; iframeDocument.documentElement.classList.remove( @@ -276,11 +297,7 @@ export function useScaleCanvas( { if ( prefersReducedMotion ) { onZoomOutTransitionEnd(); } else { - iframeDocument.documentElement.addEventListener( - 'transitionend', - onZoomOutTransitionEnd, - { once: true } - ); + zoomOutAnimation.onfinish = onZoomOutTransitionEnd; } } From f338821da98628cf8fbba646b81c649f76f94ed0 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 19 Nov 2024 16:41:46 -0600 Subject: [PATCH 48/64] Reduce rerenders by removing isAnimatingZoomOut state and relying just on the ref --- .../src/components/iframe/use-scale-canvas.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 0781d3746d7bf7..14145d25ab7463 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useEffect, useRef, useState } from '@wordpress/element'; +import { useEffect, useRef } from '@wordpress/element'; import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; function calculateScale( { @@ -59,7 +59,6 @@ export function useScaleCanvas( { ); const prefersReducedMotion = useReducedMotion(); - const [ isAnimatingZoomOut, setIsAnimatingZoomOut ] = useState( false ); const isAutoScaled = scale === 'auto-scaled'; const scaleValue = isAutoScaled @@ -84,13 +83,12 @@ export function useScaleCanvas( { iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); } - setIsAnimatingZoomOut( true ); isAnimatingRef.current = true; return () => { iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); }; - }, [ iframeDocument, isZoomedOut, setIsAnimatingZoomOut ] ); + }, [ iframeDocument, isZoomedOut ] ); // Calculate the scaling and CSS variables for the zoom out canvas useEffect( () => { @@ -111,7 +109,7 @@ export function useScaleCanvas( { } ); } - if ( isAnimatingZoomOut || isAnimatingRef.current ) { + if ( isAnimatingRef.current ) { // Set the value that we want to animate from. // We will update them after we add the animation class on next render. iframeDocument.documentElement.style.setProperty( @@ -158,7 +156,7 @@ export function useScaleCanvas( { let onZoomOutTransitionEnd = () => {}; - if ( isAnimatingZoomOut ) { + if ( isAnimatingRef.current ) { // Unscaled height of the current iframe container. const clientHeight = iframeDocument.documentElement.clientHeight; @@ -276,7 +274,6 @@ export function useScaleCanvas( { iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); - setIsAnimatingZoomOut( false ); // Update previous values. prevClientHeightRef.current = clientHeight; @@ -327,7 +324,6 @@ export function useScaleCanvas( { ); }; }, [ - isAnimatingZoomOut, isAutoScaled, scaleValue, frameSize, From 5c933995f98a81df1ff1f462cc3602eccf32869e Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 19 Nov 2024 16:55:45 -0600 Subject: [PATCH 49/64] Refactor to set scale and frame size on transition end It isn't necessary to set the previous values at the beginning of the useEffect, because that is handled by the starting frame of the web animations api. --- .../src/components/iframe/use-scale-canvas.js | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 14145d25ab7463..2c8aa7ade3aad4 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -109,26 +109,13 @@ export function useScaleCanvas( { } ); } - if ( isAnimatingRef.current ) { - // Set the value that we want to animate from. - // We will update them after we add the animation class on next render. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - prevScaleRef.current - ); - - // frameSize has to be a px value for the scaling and frame size to be computed correctly. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ prevFrameSizeRef.current }px` - ); - } else { - // We will update them after we add the animation class on next render. + // If we are not going to animate the transition set them directly. + // Example: Opening sidebars that reduce the scale of the canvas. + if ( ! isAnimatingRef.current ) { iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', scaleValue ); - // frameSize has to be a px value for the scaling and frame size to be computed correctly. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-frame-size', @@ -156,6 +143,9 @@ export function useScaleCanvas( { let onZoomOutTransitionEnd = () => {}; + /** + * Handle the zoom out animation. + */ if ( isAnimatingRef.current ) { // Unscaled height of the current iframe container. const clientHeight = iframeDocument.documentElement.clientHeight; @@ -237,16 +227,6 @@ export function useScaleCanvas( { iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - // We can change the scale and frame size here, because that's what we want to animate. - // We don't want to update these values until the animation class has been added. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ frameSize }px` - ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - scaleValue - ); const zoomOutAnimation = iframeDocument.documentElement.animate( [ @@ -271,6 +251,16 @@ export function useScaleCanvas( { onZoomOutTransitionEnd = () => { isAnimatingRef.current = false; + // Add our final scale and frame size now that the animation is done. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + scaleValue + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ frameSize }px` + ); + iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); From ff03e1ed66abd2c4cbeec201eb49a6d532c5a3b2 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 13:20:52 -0600 Subject: [PATCH 50/64] Add reverse animation if zoom state is toggled quickly --- .../src/components/iframe/use-scale-canvas.js | 303 ++++++++++-------- 1 file changed, 165 insertions(+), 138 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 2c8aa7ade3aad4..f3a2630201930d 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -45,7 +45,8 @@ export function useScaleCanvas( { const initialContainerWidthRef = useRef( 0 ); const isZoomedOut = scale !== 1; - const isAnimatingRef = useRef( false ); + const startAnimationRef = useRef( false ); + const animationRef = useRef( null ); useEffect( () => { if ( ! isZoomedOut ) { @@ -73,6 +74,7 @@ export function useScaleCanvas( { const prevScaleRef = useRef( scaleValue ); const prevFrameSizeRef = useRef( frameSize ); const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); + const prevScrollTop = useRef( 0 ); useEffect( () => { if ( ! iframeDocument ) { @@ -83,7 +85,7 @@ export function useScaleCanvas( { iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); } - isAnimatingRef.current = true; + startAnimationRef.current = true; return () => { iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); @@ -111,7 +113,7 @@ export function useScaleCanvas( { // If we are not going to animate the transition set them directly. // Example: Opening sidebars that reduce the scale of the canvas. - if ( ! isAnimatingRef.current ) { + if ( ! startAnimationRef.current ) { iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', scaleValue @@ -127,9 +129,11 @@ export function useScaleCanvas( { '--wp-block-editor-iframe-zoom-out-content-height', `${ contentHeight }px` ); + + let clientHeight = iframeDocument.documentElement.clientHeight; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-inner-height', - `${ iframeDocument.documentElement.clientHeight }px` + `${ clientHeight }px` ); iframeDocument.documentElement.style.setProperty( @@ -143,148 +147,176 @@ export function useScaleCanvas( { let onZoomOutTransitionEnd = () => {}; + let scrollTopNext; + /** - * Handle the zoom out animation. + * Handle the zoom out animation: + * - get the current scroll + * - calculate the same scroll position after scaling + * - apply fixed positioning to the canvas with a transform offset + * to keep the canvas centered + * - animate the scale and padding to the new scale and frame size + * - after the animation is complete, remove the fixed positioning + * and set the scroll position that keeps everything centered + * */ - if ( isAnimatingRef.current ) { - // Unscaled height of the current iframe container. - const clientHeight = iframeDocument.documentElement.clientHeight; - - // Scaled height of the current iframe content. - const scrollHeight = iframeDocument.documentElement.scrollHeight; - - // Previous scale value. - const prevScale = prevScaleRef.current; - - // Unscaled size of the previous padding around the iframe content. - const prevFrameSize = prevFrameSizeRef.current; - - // Unscaled height of the previous iframe container. - const prevClientHeight = - prevClientHeightRef.current ?? clientHeight; - - // We can't trust the set value from contentHeight, as it was measured - // before the zoom out mode was changed. After zoom out mode is changed, - // appenders may appear or disappear, so we need to get the height from - // the iframe at this point when we're about to animate the zoom out. - // The iframe scrollTop, scrollHeight, and clientHeight will all be - // accurate. The client height also does change when the zoom out mode - // is toggled, as the bottom bar about selecting the template is - // added/removed when toggling zoom out mode. - const scrollTop = iframeDocument.documentElement.scrollTop; - - // Step 0: Start with the current scrollTop. - let scrollTopNext = scrollTop; - - // Step 1: Undo the effects of the previous scale and frame around the - // midpoint of the visible area. - scrollTopNext = - ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / - prevScale - - prevClientHeight / 2; - - // Step 2: Apply the new scale and frame around the midpoint of the - // visible area. - scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * scaleValue + - frameSize - - clientHeight / 2; - - // Step 3: Handle an edge case so that you scroll to the top of the - // iframe if the top of the iframe content is visible in the container. - // The same edge case for the bottom is skipped because changing content - // makes calculating it impossible. - scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; - - // This is the scrollTop value if you are scrolled to the bottom of the - // iframe. We can't just let the browser handle it because we need to - // animate the scaling. - const maxScrollTop = - scrollHeight * ( scaleValue / prevScale ) + - frameSize * 2 - - clientHeight; - - // Step 4: Clamp the scrollTopNext between the minimum and maximum - // possible scrollTop positions. Round the value to avoid subpixel - // truncation by the browser which sometimes causes a 1px error. - scrollTopNext = Math.round( - Math.min( - Math.max( 0, scrollTopNext ), - Math.max( 0, maxScrollTop ) - ) - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); - - iframeDocument.documentElement.classList.add( - 'zoom-out-animation' - ); + if ( startAnimationRef.current ) { + // Don't allow a new transition to start again unless it was started by the zoom out mode changing. + startAnimationRef.current = false; + + if ( animationRef.current ) { + // When reversing the animation, we'll need to know: + // - The scroll position we started from before the animation. + // Otherwise, it will always report as 0 because of the fixed positioning. + // This can be used for the `scrollTopNext` value. + // - The previous client height, as it's used in the finishing state of the animation. + scrollTopNext = prevScrollTop.current; + clientHeight = prevClientHeightRef.current; + animationRef.current.reverse(); + } - const zoomOutAnimation = iframeDocument.documentElement.animate( - [ - { - translate: `0 0`, - scale: prevScale, - paddingTop: `${ prevFrameSize / prevScale }px`, - paddingBottom: `${ prevFrameSize / prevScale }px`, - }, - { - translate: `0 ${ scrollTop - scrollTopNext }px`, - scale: scaleValue, - paddingTop: `${ frameSize / scaleValue }px`, - paddingBottom: `${ frameSize / scaleValue }px`, - }, - ], - { - easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', - duration: 400, - } - ); + // Only start a new animation if the scale has changed. Otherwise we're just updating the CSS variables + // or reversing the animation. + else if ( scaleValue !== prevScaleRef.current ) { + // Scaled height of the current iframe content. + const scrollHeight = + iframeDocument.documentElement.scrollHeight; + + // Previous scale value. + const prevScale = prevScaleRef.current; + + // Unscaled size of the previous padding around the iframe content. + const prevFrameSize = prevFrameSizeRef.current; + + // Unscaled height of the previous iframe container. + const prevClientHeight = + prevClientHeightRef.current ?? clientHeight; + // We can't trust the set value from contentHeight, as it was measured + // before the zoom out mode was changed. After zoom out mode is changed, + // appenders may appear or disappear, so we need to get the height from + // the iframe at this point when we're about to animate the zoom out. + // The iframe scrollTop, scrollHeight, and clientHeight will all be + // accurate. The client height also does change when the zoom out mode + // is toggled, as the bottom bar about selecting the template is + // added/removed when toggling zoom out mode. + const scrollTop = iframeDocument.documentElement.scrollTop; + prevScrollTop.current = scrollTop; + // Step 0: Start with the current scrollTop. + scrollTopNext = scrollTop; + // Step 1: Undo the effects of the previous scale and frame around the + // midpoint of the visible area. + scrollTopNext = + ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / + prevScale - + prevClientHeight / 2; + + // Step 2: Apply the new scale and frame around the midpoint of the + // visible area. + scrollTopNext = + ( scrollTopNext + clientHeight / 2 ) * scaleValue + + frameSize - + clientHeight / 2; + + // Step 3: Handle an edge case so that you scroll to the top of the + // iframe if the top of the iframe content is visible in the container. + // The same edge case for the bottom is skipped because changing content + // makes calculating it impossible. + scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; + + // This is the scrollTop value if you are scrolled to the bottom of the + // iframe. We can't just let the browser handle it because we need to + // animate the scaling. + const maxScrollTop = + scrollHeight * ( scaleValue / prevScale ) + + frameSize * 2 - + clientHeight; + + // Step 4: Clamp the scrollTopNext between the minimum and maximum + // possible scrollTop positions. Round the value to avoid subpixel + // truncation by the browser which sometimes causes a 1px error. + scrollTopNext = Math.round( + Math.min( + Math.max( 0, scrollTopNext ), + Math.max( 0, maxScrollTop ) + ) + ); - onZoomOutTransitionEnd = () => { - isAnimatingRef.current = false; - // Add our final scale and frame size now that the animation is done. iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - scaleValue + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` ); + iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ frameSize }px` + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` ); - iframeDocument.documentElement.classList.remove( + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); - - // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSize; - prevScaleRef.current = scaleValue; - - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; - - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next' + animationRef.current = iframeDocument.documentElement.animate( + [ + { + translate: `0 0`, + scale: prevScale, + paddingTop: `${ prevFrameSize / prevScale }px`, + paddingBottom: `${ prevFrameSize / prevScale }px`, + }, + { + translate: `0 ${ scrollTop - scrollTopNext }px`, + scale: scaleValue, + paddingTop: `${ frameSize / scaleValue }px`, + paddingBottom: `${ frameSize / scaleValue }px`, + }, + ], + { + easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', + duration: 400, + } ); - }; + } - if ( prefersReducedMotion ) { - onZoomOutTransitionEnd(); - } else { - zoomOutAnimation.onfinish = onZoomOutTransitionEnd; + if ( animationRef.current ) { + // We need to define this outside of the initial animation, as it may end up + // getting reversed. + onZoomOutTransitionEnd = () => { + startAnimationRef.current = false; + animationRef.current = null; + // Add our final scale and frame size now that the animation is done. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + scaleValue + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ frameSize }px` + ); + + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + + // Update previous values. + prevClientHeightRef.current = clientHeight; + prevFrameSizeRef.current = frameSize; + prevScaleRef.current = scaleValue; + + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = scrollTopNext; + + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next' + ); + }; + + if ( prefersReducedMotion ) { + onZoomOutTransitionEnd(); + } else { + animationRef.current.onfinish = onZoomOutTransitionEnd; + } } } @@ -307,11 +339,6 @@ export function useScaleCanvas( { iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scale-container-width' ); - - iframeDocument.documentElement.removeEventListener( - 'transitionend', - onZoomOutTransitionEnd - ); }; }, [ isAutoScaled, From 65bd82eb4ef99b08c5e429bded7ab23d1fc8ff42 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 14:04:25 -0600 Subject: [PATCH 51/64] Refactor step 1 of reverse animation with transitionTo and transitionFrom refs --- .../src/components/iframe/use-scale-canvas.js | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index f3a2630201930d..26185df4f09552 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -75,6 +75,22 @@ export function useScaleCanvas( { const prevFrameSizeRef = useRef( frameSize ); const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); const prevScrollTop = useRef( 0 ); + const scrollTopNextRef = useRef( 0 ); + const clientHeightRef = useRef( 0 ); + + const transitionFrom = useRef( { + scaleValue, + frameSize, + clientHeight: 0, + scrollTopNext: 0, + } ); + + const transitionTo = useRef( { + scaleValue, + frameSize, + clientHeight: 0, + scrollTopNext: 0, + } ); useEffect( () => { if ( ! iframeDocument ) { @@ -130,7 +146,8 @@ export function useScaleCanvas( { `${ contentHeight }px` ); - let clientHeight = iframeDocument.documentElement.clientHeight; + const clientHeight = iframeDocument.documentElement.clientHeight; + clientHeightRef.current = clientHeight; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-inner-height', `${ clientHeight }px` @@ -145,10 +162,6 @@ export function useScaleCanvas( { `${ scaleContainerWidth }px` ); - let onZoomOutTransitionEnd = () => {}; - - let scrollTopNext; - /** * Handle the zoom out animation: * - get the current scroll @@ -158,7 +171,6 @@ export function useScaleCanvas( { * - animate the scale and padding to the new scale and frame size * - after the animation is complete, remove the fixed positioning * and set the scroll position that keeps everything centered - * */ if ( startAnimationRef.current ) { // Don't allow a new transition to start again unless it was started by the zoom out mode changing. @@ -170,9 +182,12 @@ export function useScaleCanvas( { // Otherwise, it will always report as 0 because of the fixed positioning. // This can be used for the `scrollTopNext` value. // - The previous client height, as it's used in the finishing state of the animation. - scrollTopNext = prevScrollTop.current; - clientHeight = prevClientHeightRef.current; animationRef.current.reverse(); + // Swap the transition values so that we can reverse the animation. + const tempTransitionFrom = transitionFrom.current; + const tempTransitionTo = transitionTo.current; + transitionFrom.current = tempTransitionTo; + transitionTo.current = tempTransitionFrom; } // Only start a new animation if the scale has changed. Otherwise we're just updating the CSS variables @@ -190,7 +205,7 @@ export function useScaleCanvas( { // Unscaled height of the previous iframe container. const prevClientHeight = - prevClientHeightRef.current ?? clientHeight; + prevClientHeightRef.current ?? clientHeightRef.current; // We can't trust the set value from contentHeight, as it was measured // before the zoom out mode was changed. After zoom out mode is changed, // appenders may appear or disappear, so we need to get the height from @@ -202,7 +217,7 @@ export function useScaleCanvas( { const scrollTop = iframeDocument.documentElement.scrollTop; prevScrollTop.current = scrollTop; // Step 0: Start with the current scrollTop. - scrollTopNext = scrollTop; + let scrollTopNext = scrollTop; // Step 1: Undo the effects of the previous scale and frame around the // midpoint of the visible area. scrollTopNext = @@ -240,6 +255,7 @@ export function useScaleCanvas( { Math.max( 0, maxScrollTop ) ) ); + scrollTopNextRef.current = scrollTopNext; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', @@ -274,22 +290,35 @@ export function useScaleCanvas( { duration: 400, } ); - } - if ( animationRef.current ) { + transitionFrom.current = { + scaleValue: prevScale, + frameSize: prevFrameSize, + clientHeight: prevClientHeight, + scrollTopNext: scrollTop, + }; + + transitionTo.current = { + scaleValue, + frameSize, + clientHeight, + scrollTopNext, + }; + // We need to define this outside of the initial animation, as it may end up // getting reversed. - onZoomOutTransitionEnd = () => { + const onZoomOutTransitionEnd = () => { startAnimationRef.current = false; animationRef.current = null; + // Add our final scale and frame size now that the animation is done. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', - scaleValue + transitionTo.current.scaleValue ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-frame-size', - `${ frameSize }px` + `${ transitionTo.current.frameSize }px` ); iframeDocument.documentElement.classList.remove( @@ -297,12 +326,14 @@ export function useScaleCanvas( { ); // Update previous values. - prevClientHeightRef.current = clientHeight; - prevFrameSizeRef.current = frameSize; - prevScaleRef.current = scaleValue; + prevClientHeightRef.current = + transitionTo.current.clientHeight; + prevFrameSizeRef.current = transitionTo.current.frameSize; + prevScaleRef.current = transitionTo.current.scaleValue; // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = scrollTopNext; + iframeDocument.documentElement.scrollTop = + transitionTo.current.scrollTopNext; iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scroll-top' From f236641bde338a9a24b2c011bb17eff16f6dd5b0 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 14:11:47 -0600 Subject: [PATCH 52/64] Finish refactor of transitionTo/From refs --- .../src/components/iframe/use-scale-canvas.js | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 26185df4f09552..878b1b282e150d 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -71,13 +71,6 @@ export function useScaleCanvas( { } ) : scale; - const prevScaleRef = useRef( scaleValue ); - const prevFrameSizeRef = useRef( frameSize ); - const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ ); - const prevScrollTop = useRef( 0 ); - const scrollTopNextRef = useRef( 0 ); - const clientHeightRef = useRef( 0 ); - const transitionFrom = useRef( { scaleValue, frameSize, @@ -114,16 +107,16 @@ export function useScaleCanvas( { return; } - if ( isAutoScaled && prevScaleRef.current !== 1 ) { + if ( isAutoScaled && transitionFrom.current.scaleValue !== 1 ) { // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the // original scale, we will snap to a much smaller scale due to the scale container changing size when exiting. // We use containerWidth as the divisor, as scaleContainerWidth will always match the containerWidth when // exiting. - prevScaleRef.current = calculateScale( { + transitionFrom.current.scaleValue = calculateScale( { containerWidth, maxContainerWidth, scaleContainerWidth: containerWidth, - frameSize: prevFrameSizeRef.current, + frameSize: transitionFrom.current.frameSize, } ); } @@ -147,7 +140,6 @@ export function useScaleCanvas( { ); const clientHeight = iframeDocument.documentElement.clientHeight; - clientHeightRef.current = clientHeight; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-inner-height', `${ clientHeight }px` @@ -172,6 +164,8 @@ export function useScaleCanvas( { * - after the animation is complete, remove the fixed positioning * and set the scroll position that keeps everything centered */ + + const prevScale = transitionFrom.current.scaleValue; if ( startAnimationRef.current ) { // Don't allow a new transition to start again unless it was started by the zoom out mode changing. startAnimationRef.current = false; @@ -192,20 +186,17 @@ export function useScaleCanvas( { // Only start a new animation if the scale has changed. Otherwise we're just updating the CSS variables // or reversing the animation. - else if ( scaleValue !== prevScaleRef.current ) { + else if ( scaleValue !== prevScale ) { // Scaled height of the current iframe content. const scrollHeight = iframeDocument.documentElement.scrollHeight; - // Previous scale value. - const prevScale = prevScaleRef.current; - // Unscaled size of the previous padding around the iframe content. - const prevFrameSize = prevFrameSizeRef.current; + const prevFrameSize = transitionFrom.current.frameSize; // Unscaled height of the previous iframe container. const prevClientHeight = - prevClientHeightRef.current ?? clientHeightRef.current; + transitionFrom.current.clientHeight ?? clientHeight; // We can't trust the set value from contentHeight, as it was measured // before the zoom out mode was changed. After zoom out mode is changed, // appenders may appear or disappear, so we need to get the height from @@ -215,7 +206,6 @@ export function useScaleCanvas( { // is toggled, as the bottom bar about selecting the template is // added/removed when toggling zoom out mode. const scrollTop = iframeDocument.documentElement.scrollTop; - prevScrollTop.current = scrollTop; // Step 0: Start with the current scrollTop. let scrollTopNext = scrollTop; // Step 1: Undo the effects of the previous scale and frame around the @@ -255,7 +245,6 @@ export function useScaleCanvas( { Math.max( 0, maxScrollTop ) ) ); - scrollTopNextRef.current = scrollTopNext; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', @@ -325,12 +314,6 @@ export function useScaleCanvas( { 'zoom-out-animation' ); - // Update previous values. - prevClientHeightRef.current = - transitionTo.current.clientHeight; - prevFrameSizeRef.current = transitionTo.current.frameSize; - prevScaleRef.current = transitionTo.current.scaleValue; - // Set the final scroll position that was just animated to. iframeDocument.documentElement.scrollTop = transitionTo.current.scrollTopNext; @@ -341,6 +324,9 @@ export function useScaleCanvas( { iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scroll-top-next' ); + + // Update previous values. + transitionFrom.current = transitionTo.current; }; if ( prefersReducedMotion ) { From 83ee4c5cf7aa9921f218ef236b86420b7b510d02 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 14:40:56 -0600 Subject: [PATCH 53/64] Refactor: computeScrollTopNext --- .../src/components/iframe/use-scale-canvas.js | 205 ++++++++++-------- 1 file changed, 113 insertions(+), 92 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 878b1b282e150d..448a2a9d83593a 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect, useRef, useCallback } from '@wordpress/element'; import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; function calculateScale( { @@ -16,6 +16,64 @@ function calculateScale( { ); } +/** + * Compute the next scrollTop position after scaling the iframe content. + * + * @param {number} scrollTop Current scrollTop position of the iframe + * @param {number} scrollHeight Scaled height of the current iframe content. + * @param {Object} transitionFrom The starting point of the transition + * @param {Object} transitionTo The ending state of the transition + * @return {number} The next scrollTop position after scaling the iframe content. + */ +function computeScrollTopNext( + scrollTop, + scrollHeight, + transitionFrom, + transitionTo +) { + const { + clientHeight: prevClientHeight, + frameSize: prevFrameSize, + scaleValue: prevScale, + } = transitionFrom; + const { clientHeight, frameSize, scaleValue } = transitionTo; + // Step 0: Start with the current scrollTop. + let scrollTopNext = scrollTop; + // Step 1: Undo the effects of the previous scale and frame around the + // midpoint of the visible area. + scrollTopNext = + ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / prevScale - + prevClientHeight / 2; + + // Step 2: Apply the new scale and frame around the midpoint of the + // visible area. + scrollTopNext = + ( scrollTopNext + clientHeight / 2 ) * scaleValue + + frameSize - + clientHeight / 2; + + // Step 3: Handle an edge case so that you scroll to the top of the + // iframe if the top of the iframe content is visible in the container. + // The same edge case for the bottom is skipped because changing content + // makes calculating it impossible. + scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; + + // This is the scrollTop value if you are scrolled to the bottom of the + // iframe. We can't just let the browser handle it because we need to + // animate the scaling. + const maxScrollTop = + scrollHeight * ( scaleValue / prevScale ) + + frameSize * 2 - + clientHeight; + + // Step 4: Clamp the scrollTopNext between the minimum and maximum + // possible scrollTop positions. Round the value to avoid subpixel + // truncation by the browser which sometimes causes a 1px error. + return Math.round( + Math.min( Math.max( 0, scrollTopNext ), Math.max( 0, maxScrollTop ) ) + ); +} + /** * Handles scaling the canvas for the zoom out mode and animating between * the states. @@ -85,6 +143,37 @@ export function useScaleCanvas( { scrollTopNext: 0, } ); + const onZoomOutTransitionEnd = useCallback( () => { + startAnimationRef.current = false; + animationRef.current = null; + + // Add our final scale and frame size now that the animation is done. + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scale', + transitionTo.current.scaleValue + ); + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-frame-size', + `${ transitionTo.current.frameSize }px` + ); + + iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); + + // Set the final scroll position that was just animated to. + iframeDocument.documentElement.scrollTop = + transitionTo.current.scrollTopNext; + + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top' + ); + iframeDocument.documentElement.style.removeProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next' + ); + + // Update previous values. + transitionFrom.current = transitionTo.current; + }, [ iframeDocument ] ); + useEffect( () => { if ( ! iframeDocument ) { return; @@ -187,16 +276,13 @@ export function useScaleCanvas( { // Only start a new animation if the scale has changed. Otherwise we're just updating the CSS variables // or reversing the animation. else if ( scaleValue !== prevScale ) { - // Scaled height of the current iframe content. - const scrollHeight = - iframeDocument.documentElement.scrollHeight; - // Unscaled size of the previous padding around the iframe content. const prevFrameSize = transitionFrom.current.frameSize; // Unscaled height of the previous iframe container. const prevClientHeight = transitionFrom.current.clientHeight ?? clientHeight; + // We can't trust the set value from contentHeight, as it was measured // before the zoom out mode was changed. After zoom out mode is changed, // appenders may appear or disappear, so we need to get the height from @@ -206,46 +292,29 @@ export function useScaleCanvas( { // is toggled, as the bottom bar about selecting the template is // added/removed when toggling zoom out mode. const scrollTop = iframeDocument.documentElement.scrollTop; - // Step 0: Start with the current scrollTop. - let scrollTopNext = scrollTop; - // Step 1: Undo the effects of the previous scale and frame around the - // midpoint of the visible area. - scrollTopNext = - ( scrollTopNext + prevClientHeight / 2 - prevFrameSize ) / - prevScale - - prevClientHeight / 2; - - // Step 2: Apply the new scale and frame around the midpoint of the - // visible area. - scrollTopNext = - ( scrollTopNext + clientHeight / 2 ) * scaleValue + - frameSize - - clientHeight / 2; - - // Step 3: Handle an edge case so that you scroll to the top of the - // iframe if the top of the iframe content is visible in the container. - // The same edge case for the bottom is skipped because changing content - // makes calculating it impossible. - scrollTopNext = scrollTop <= prevFrameSize ? 0 : scrollTopNext; - - // This is the scrollTop value if you are scrolled to the bottom of the - // iframe. We can't just let the browser handle it because we need to - // animate the scaling. - const maxScrollTop = - scrollHeight * ( scaleValue / prevScale ) + - frameSize * 2 - - clientHeight; - - // Step 4: Clamp the scrollTopNext between the minimum and maximum - // possible scrollTop positions. Round the value to avoid subpixel - // truncation by the browser which sometimes causes a 1px error. - scrollTopNext = Math.round( - Math.min( - Math.max( 0, scrollTopNext ), - Math.max( 0, maxScrollTop ) - ) + + transitionFrom.current = { + scaleValue: prevScale, + frameSize: prevFrameSize, + clientHeight: prevClientHeight, + scrollTopNext: scrollTop, + }; + + transitionTo.current = { + scaleValue, + frameSize, + clientHeight, + }; + + const scrollTopNext = computeScrollTopNext( + scrollTop, + iframeDocument.documentElement.scrollHeight, + transitionFrom.current, + transitionTo.current ); + transitionTo.current.scrollTopNext = scrollTopNext; + iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', `${ scrollTop }px` @@ -280,55 +349,6 @@ export function useScaleCanvas( { } ); - transitionFrom.current = { - scaleValue: prevScale, - frameSize: prevFrameSize, - clientHeight: prevClientHeight, - scrollTopNext: scrollTop, - }; - - transitionTo.current = { - scaleValue, - frameSize, - clientHeight, - scrollTopNext, - }; - - // We need to define this outside of the initial animation, as it may end up - // getting reversed. - const onZoomOutTransitionEnd = () => { - startAnimationRef.current = false; - animationRef.current = null; - - // Add our final scale and frame size now that the animation is done. - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scale', - transitionTo.current.scaleValue - ); - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-frame-size', - `${ transitionTo.current.frameSize }px` - ); - - iframeDocument.documentElement.classList.remove( - 'zoom-out-animation' - ); - - // Set the final scroll position that was just animated to. - iframeDocument.documentElement.scrollTop = - transitionTo.current.scrollTopNext; - - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top' - ); - iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next' - ); - - // Update previous values. - transitionFrom.current = transitionTo.current; - }; - if ( prefersReducedMotion ) { onZoomOutTransitionEnd(); } else { @@ -358,6 +378,7 @@ export function useScaleCanvas( { ); }; }, [ + onZoomOutTransitionEnd, isAutoScaled, scaleValue, frameSize, From 32b24e5b25f459a28ed86cf3ff611ba900625b73 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 14:51:19 -0600 Subject: [PATCH 54/64] Refactor: getAnimationKeyframes --- .../src/components/iframe/use-scale-canvas.js | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 448a2a9d83593a..7f88988d47ea64 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -74,6 +74,30 @@ function computeScrollTopNext( ); } +function getAnimationKeyframes( transitionFrom, transitionTo ) { + const { + scaleValue: prevScale, + frameSize: prevFrameSize, + scrollTopNext: scrollTop, + } = transitionFrom; + const { scaleValue, frameSize, scrollTopNext } = transitionTo; + + return [ + { + translate: `0 0`, + scale: prevScale, + paddingTop: `${ prevFrameSize / prevScale }px`, + paddingBottom: `${ prevFrameSize / prevScale }px`, + }, + { + translate: `0 ${ scrollTop - scrollTopNext }px`, + scale: scaleValue, + paddingTop: `${ frameSize / scaleValue }px`, + paddingBottom: `${ frameSize / scaleValue }px`, + }, + ]; +} + /** * Handles scaling the canvas for the zoom out mode and animating between * the states. @@ -329,20 +353,10 @@ export function useScaleCanvas( { 'zoom-out-animation' ); animationRef.current = iframeDocument.documentElement.animate( - [ - { - translate: `0 0`, - scale: prevScale, - paddingTop: `${ prevFrameSize / prevScale }px`, - paddingBottom: `${ prevFrameSize / prevScale }px`, - }, - { - translate: `0 ${ scrollTop - scrollTopNext }px`, - scale: scaleValue, - paddingTop: `${ frameSize / scaleValue }px`, - paddingBottom: `${ frameSize / scaleValue }px`, - }, - ], + getAnimationKeyframes( + transitionFrom.current, + transitionTo.current + ), { easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', duration: 400, From 1407f8f070bd00e001d50919ed1d804303600fe1 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 14:52:16 -0600 Subject: [PATCH 55/64] Add missing dependency --- packages/block-editor/src/components/iframe/use-scale-canvas.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 7f88988d47ea64..f8a3819a02e00e 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -393,6 +393,7 @@ export function useScaleCanvas( { }; }, [ onZoomOutTransitionEnd, + prefersReducedMotion, isAutoScaled, scaleValue, frameSize, From d5402bce4e621e5e7927ffed1654425c6f797d92 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 16:05:37 -0600 Subject: [PATCH 56/64] Refactor: create startZoomOutAnimation and rename scrollTopNext to transitionTo.scrollTop --- .../src/components/iframe/use-scale-canvas.js | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index f8a3819a02e00e..48bb90951dce89 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -19,22 +19,17 @@ function calculateScale( { /** * Compute the next scrollTop position after scaling the iframe content. * - * @param {number} scrollTop Current scrollTop position of the iframe * @param {number} scrollHeight Scaled height of the current iframe content. * @param {Object} transitionFrom The starting point of the transition * @param {Object} transitionTo The ending state of the transition * @return {number} The next scrollTop position after scaling the iframe content. */ -function computeScrollTopNext( - scrollTop, - scrollHeight, - transitionFrom, - transitionTo -) { +function computeScrollTopNext( scrollHeight, transitionFrom, transitionTo ) { const { clientHeight: prevClientHeight, frameSize: prevFrameSize, scaleValue: prevScale, + scrollTop: scrollTop, } = transitionFrom; const { clientHeight, frameSize, scaleValue } = transitionTo; // Step 0: Start with the current scrollTop. @@ -78,9 +73,9 @@ function getAnimationKeyframes( transitionFrom, transitionTo ) { const { scaleValue: prevScale, frameSize: prevFrameSize, - scrollTopNext: scrollTop, + scrollTop, } = transitionFrom; - const { scaleValue, frameSize, scrollTopNext } = transitionTo; + const { scaleValue, frameSize, scrollTop: scrollTopNext } = transitionTo; return [ { @@ -157,17 +152,45 @@ export function useScaleCanvas( { scaleValue, frameSize, clientHeight: 0, - scrollTopNext: 0, + scrollTop: 0, } ); const transitionTo = useRef( { scaleValue, frameSize, clientHeight: 0, - scrollTopNext: 0, + scrollTop: 0, } ); - const onZoomOutTransitionEnd = useCallback( () => { + const startZoomOutAnimation = useCallback( () => { + const { scrollTop } = transitionFrom.current; + const { scrollTop: scrollTopNext } = transitionTo.current; + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top', + `${ scrollTop }px` + ); + + iframeDocument.documentElement.style.setProperty( + '--wp-block-editor-iframe-zoom-out-scroll-top-next', + `${ scrollTopNext }px` + ); + + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + + return iframeDocument.documentElement.animate( + getAnimationKeyframes( + transitionFrom.current, + transitionTo.current + ), + { + easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', + duration: 400, + } + ); + }, [ iframeDocument ] ); + + const finishZoomOutAnimation = useCallback( () => { startAnimationRef.current = false; animationRef.current = null; @@ -185,7 +208,7 @@ export function useScaleCanvas( { // Set the final scroll position that was just animated to. iframeDocument.documentElement.scrollTop = - transitionTo.current.scrollTopNext; + transitionTo.current.scrollTop; iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scroll-top' @@ -315,13 +338,11 @@ export function useScaleCanvas( { // accurate. The client height also does change when the zoom out mode // is toggled, as the bottom bar about selecting the template is // added/removed when toggling zoom out mode. - const scrollTop = iframeDocument.documentElement.scrollTop; - transitionFrom.current = { scaleValue: prevScale, frameSize: prevFrameSize, clientHeight: prevClientHeight, - scrollTopNext: scrollTop, + scrollTop: iframeDocument.documentElement.scrollTop, }; transitionTo.current = { @@ -330,43 +351,18 @@ export function useScaleCanvas( { clientHeight, }; - const scrollTopNext = computeScrollTopNext( - scrollTop, + transitionTo.current.scrollTop = computeScrollTopNext( iframeDocument.documentElement.scrollHeight, transitionFrom.current, transitionTo.current ); - transitionTo.current.scrollTopNext = scrollTopNext; - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top', - `${ scrollTop }px` - ); - - iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-scroll-top-next', - `${ scrollTopNext }px` - ); - - iframeDocument.documentElement.classList.add( - 'zoom-out-animation' - ); - animationRef.current = iframeDocument.documentElement.animate( - getAnimationKeyframes( - transitionFrom.current, - transitionTo.current - ), - { - easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', - duration: 400, - } - ); + animationRef.current = startZoomOutAnimation(); if ( prefersReducedMotion ) { - onZoomOutTransitionEnd(); + finishZoomOutAnimation(); } else { - animationRef.current.onfinish = onZoomOutTransitionEnd; + animationRef.current.onfinish = finishZoomOutAnimation; } } } @@ -392,7 +388,8 @@ export function useScaleCanvas( { ); }; }, [ - onZoomOutTransitionEnd, + startZoomOutAnimation, + finishZoomOutAnimation, prefersReducedMotion, isAutoScaled, scaleValue, From fc6b4eeff4a30328faf554c09e943a6ad36fa64e Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 20 Nov 2024 16:25:52 -0600 Subject: [PATCH 57/64] Remove unneeded variables --- .../src/components/iframe/use-scale-canvas.js | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 48bb90951dce89..1f50366dc39732 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -221,6 +221,9 @@ export function useScaleCanvas( { transitionFrom.current = transitionTo.current; }, [ iframeDocument ] ); + /** + * Runs when zoom out mode is toggled, and sets the startAnimation flag. + */ useEffect( () => { if ( ! iframeDocument ) { return; @@ -237,15 +240,19 @@ export function useScaleCanvas( { }; }, [ iframeDocument, isZoomedOut ] ); - // Calculate the scaling and CSS variables for the zoom out canvas + /** + * This handles: + * 1. Setting the correct scale and vars of the canvas when zoomed out + * 2. The animation of zooming in/out + */ useEffect( () => { if ( ! iframeDocument ) { return; } + // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the + // original scale, we will snap to a much smaller scale due to the scale container changing size when exiting. if ( isAutoScaled && transitionFrom.current.scaleValue !== 1 ) { - // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the - // original scale, we will snap to a much smaller scale due to the scale container changing size when exiting. // We use containerWidth as the divisor, as scaleContainerWidth will always match the containerWidth when // exiting. transitionFrom.current.scaleValue = calculateScale( { @@ -256,7 +263,8 @@ export function useScaleCanvas( { } ); } - // If we are not going to animate the transition set them directly. + // If we are not going to animate the transition set them directly. If we are animating, + // these values will be set when the animation is finished. // Example: Opening sidebars that reduce the scale of the canvas. if ( ! startAnimationRef.current ) { iframeDocument.documentElement.style.setProperty( @@ -300,50 +308,37 @@ export function useScaleCanvas( { * - after the animation is complete, remove the fixed positioning * and set the scroll position that keeps everything centered */ - - const prevScale = transitionFrom.current.scaleValue; if ( startAnimationRef.current ) { // Don't allow a new transition to start again unless it was started by the zoom out mode changing. startAnimationRef.current = false; + /** + * Handle reversing the animation. If we already have an animation running, + * referse it. + */ if ( animationRef.current ) { - // When reversing the animation, we'll need to know: - // - The scroll position we started from before the animation. - // Otherwise, it will always report as 0 because of the fixed positioning. - // This can be used for the `scrollTopNext` value. - // - The previous client height, as it's used in the finishing state of the animation. animationRef.current.reverse(); - // Swap the transition values so that we can reverse the animation. + // Swap the transition to/from refs so that we set the correct values when + // finishZoomOutAnimation runs. const tempTransitionFrom = transitionFrom.current; const tempTransitionTo = transitionTo.current; transitionFrom.current = tempTransitionTo; transitionTo.current = tempTransitionFrom; - } - - // Only start a new animation if the scale has changed. Otherwise we're just updating the CSS variables - // or reversing the animation. - else if ( scaleValue !== prevScale ) { - // Unscaled size of the previous padding around the iframe content. - const prevFrameSize = transitionFrom.current.frameSize; - - // Unscaled height of the previous iframe container. - const prevClientHeight = - transitionFrom.current.clientHeight ?? clientHeight; + } else { + /** + * Start a new zoom animation. + */ // We can't trust the set value from contentHeight, as it was measured // before the zoom out mode was changed. After zoom out mode is changed, // appenders may appear or disappear, so we need to get the height from // the iframe at this point when we're about to animate the zoom out. // The iframe scrollTop, scrollHeight, and clientHeight will all be - // accurate. The client height also does change when the zoom out mode - // is toggled, as the bottom bar about selecting the template is - // added/removed when toggling zoom out mode. - transitionFrom.current = { - scaleValue: prevScale, - frameSize: prevFrameSize, - clientHeight: prevClientHeight, - scrollTop: iframeDocument.documentElement.scrollTop, - }; + // accurate. + transitionFrom.current.clientHeight = + transitionFrom.current.clientHeight ?? clientHeight; + transitionFrom.current.scrollTop = + iframeDocument.documentElement.scrollTop; transitionTo.current = { scaleValue, From 2325135a9d5313d0199b1b0dc8c97c53d30098f0 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 21 Nov 2024 10:55:15 -0600 Subject: [PATCH 58/64] Move CSS back to original location, as it no longer needs to be moved --- .../src/components/iframe/content.scss | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index bc820567220a6b..9b02716671de77 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -3,15 +3,7 @@ } .block-editor-iframe__html { - $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size, 0); - $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); - scale: $scale; transform-origin: top center; - // Add the top/bottom frame size. We use scaling to account for the left/right, as - // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling - // of the content. - padding-top: calc(#{$frame-size} / #{$scale}); - padding-bottom: calc(#{$frame-size} / #{$scale}); // Prevents a flash of background color change when entering/exiting zoom out transition: background-color 400ms; @@ -30,12 +22,15 @@ } &.is-zoomed-out { + $scale: var(--wp-block-editor-iframe-zoom-out-scale, 1); + $frame-size: var(--wp-block-editor-iframe-zoom-out-frame-size, 0); $inner-height: var(--wp-block-editor-iframe-zoom-out-inner-height); $content-height: var(--wp-block-editor-iframe-zoom-out-content-height); $scale-container-width: var(--wp-block-editor-iframe-zoom-out-scale-container-width); $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); // Apply an X translation to center the scaled content within the available space. transform: translateX(calc((#{$scale-container-width} - #{$container-width}) / 2 / #{$scale})); + scale: $scale; background-color: $gray-300; // Chrome seems to respect that transform scale shouldn't affect the layout size of the element, @@ -45,6 +40,12 @@ $total-height: calc(#{$extra-content-height} + #{$total-frame-height} + 2px); margin-bottom: calc(-1 * #{$total-height}); + // Add the top/bottom frame size. We use scaling to account for the left/right, as + // the padding left/right causes the contents to reflow, which breaks the 1:1 scaling + // of the content. + padding-top: calc(#{$frame-size} / #{$scale}); + padding-bottom: calc(#{$frame-size} / #{$scale}); + body { min-height: calc((#{$inner-height} - #{$total-frame-height}) / #{$scale}); From b41d282a4379b3bb192eb9174c6878988c064ad3 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 21 Nov 2024 11:24:55 -0600 Subject: [PATCH 59/64] Add comments --- .../src/components/iframe/use-scale-canvas.js | 88 ++++++++++++++----- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 1f50366dc39732..7f11960b12c518 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -4,11 +4,21 @@ import { useEffect, useRef, useCallback } from '@wordpress/element'; import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; +/** + * Calculate the scale of the canvas. + * + * @param {Object} root0 Object of options + * @param {number} root0.frameSize The size of the frame/offset around the canvas + * @param {number} root0.containerWidth Actual width of the canvas container + * @param {number} root0.maxContainerWidth Maximum width of the container to use for the scale calculation. This locks the canvas to a maximum width when zooming out. + * @param {number} root0.scaleContainerWidth Width the of the container wrapping the canvas container + * @return {number} scale value between 0 and/or equal to 1 + */ function calculateScale( { frameSize, containerWidth, - scaleContainerWidth, maxContainerWidth, + scaleContainerWidth, } ) { return ( ( Math.min( containerWidth, maxContainerWidth ) - frameSize * 2 ) / @@ -69,6 +79,13 @@ function computeScrollTopNext( scrollHeight, transitionFrom, transitionTo ) { ); } +/** + * Generate the keyframes to use for the zoom out animation. + * + * @param {Object} transitionFrom Object of the starting transition state + * @param {Object} transitionTo Object of the ending transition state + * @return {Object[]} An array of keyframes to use for the animation + */ function getAnimationKeyframes( transitionFrom, transitionTo ) { const { scaleValue: prevScale, @@ -100,9 +117,8 @@ function getAnimationKeyframes( transitionFrom, transitionTo ) { * @param {Object} root0 * @param {number} root0.frameSize The size of the frame around the content. * @param {Document} root0.iframeDocument The document of the iframe. - * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. + * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. Defaults to 750. * @param {number|string} root0.scale The scale of the canvas. Can be an decimal between 0 and 1, 1, or 'auto-scaled'. - * * @return {Object} An object containing the following properties: * isZoomedOut: A boolean indicating if the canvas is zoomed out. * scaleContainerWidth: The width of the container used to calculate the scale. @@ -122,7 +138,12 @@ export function useScaleCanvas( { const initialContainerWidthRef = useRef( 0 ); const isZoomedOut = scale !== 1; + const prefersReducedMotion = useReducedMotion(); + const isAutoScaled = scale === 'auto-scaled'; + // Track if the animation should start when the useEffect runs. const startAnimationRef = useRef( false ); + // Track the animation so we know if we have an animation running, + // and can cancel it, reverse it, call a finish event, etc. const animationRef = useRef( null ); useEffect( () => { @@ -136,9 +157,6 @@ export function useScaleCanvas( { containerWidth ); - const prefersReducedMotion = useReducedMotion(); - const isAutoScaled = scale === 'auto-scaled'; - const scaleValue = isAutoScaled ? calculateScale( { frameSize, @@ -148,6 +166,9 @@ export function useScaleCanvas( { } ) : scale; + /** + * The starting transition state for the zoom out animation. + */ const transitionFrom = useRef( { scaleValue, frameSize, @@ -155,6 +176,9 @@ export function useScaleCanvas( { scrollTop: 0, } ); + /** + * The ending transition state for the zoom out animation. + */ const transitionTo = useRef( { scaleValue, frameSize, @@ -162,6 +186,12 @@ export function useScaleCanvas( { scrollTop: 0, } ); + /** + * Start the zoom out animation. This sets the necessary CSS variables + * for animating the canvas and returns the Animation object. + * + * @return {Animation} The animation object for the zoom out animation. + */ const startZoomOutAnimation = useCallback( () => { const { scrollTop } = transitionFrom.current; const { scrollTop: scrollTopNext } = transitionTo.current; @@ -190,6 +220,15 @@ export function useScaleCanvas( { ); }, [ iframeDocument ] ); + /** + * Callback when the zoom out animation is finished. + * - Cleans up animations refs + * - Adds final CSS vars for scale and frame size to preserve the state + * - Removes the 'zoom-out-animation' class (which has the fixed positioning) + * - Sets the final scroll position after the canvas is no longer in fixed position + * - Removes CSS vars related to the animation + * - Sets the transitionFrom to the transitionTo state to be ready for the next animation + */ const finishZoomOutAnimation = useCallback( () => { startAnimationRef.current = false; animationRef.current = null; @@ -222,7 +261,10 @@ export function useScaleCanvas( { }, [ iframeDocument ] ); /** - * Runs when zoom out mode is toggled, and sets the startAnimation flag. + * Runs when zoom out mode is toggled, and sets the startAnimation flag + * so the animation will start when the next useEffect runs. We _only_ + * want to animate when the zoom out mode is toggled, not when the scale + * changes due to the container resizing. */ useEffect( () => { if ( ! iframeDocument ) { @@ -243,7 +285,7 @@ export function useScaleCanvas( { /** * This handles: * 1. Setting the correct scale and vars of the canvas when zoomed out - * 2. The animation of zooming in/out + * 2. If zoom out mode has been toggled, runs the animation of zooming in/out */ useEffect( () => { if ( ! iframeDocument ) { @@ -251,27 +293,27 @@ export function useScaleCanvas( { } // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the - // original scale, we will snap to a much smaller scale due to the scale container changing size when exiting. + // original scale, we will snap to a much smaller scale due to the scale container immediately changing sizes when exiting. if ( isAutoScaled && transitionFrom.current.scaleValue !== 1 ) { // We use containerWidth as the divisor, as scaleContainerWidth will always match the containerWidth when // exiting. transitionFrom.current.scaleValue = calculateScale( { + frameSize: transitionFrom.current.frameSize, containerWidth, maxContainerWidth, scaleContainerWidth: containerWidth, - frameSize: transitionFrom.current.frameSize, } ); } - // If we are not going to animate the transition set them directly. If we are animating, - // these values will be set when the animation is finished. - // Example: Opening sidebars that reduce the scale of the canvas. + // If we are not going to animate the transition, set the scale and frame size directly. + // If we are animating, these values will be set when the animation is finished. + // Example: Opening sidebars that reduce the scale of the canvas, but we don't want to + // animate the transition. if ( ! startAnimationRef.current ) { iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', scaleValue ); - // frameSize has to be a px value for the scaling and frame size to be computed correctly. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-frame-size', `${ frameSize }px` @@ -300,12 +342,13 @@ export function useScaleCanvas( { /** * Handle the zoom out animation: - * - get the current scroll - * - calculate the same scroll position after scaling - * - apply fixed positioning to the canvas with a transform offset + * + * - Get the current scrollTop position + * - Calculate where the same scroll position is after scaling + * - Apply fixed positioning to the canvas with a transform offset * to keep the canvas centered - * - animate the scale and padding to the new scale and frame size - * - after the animation is complete, remove the fixed positioning + * - Animate the scale and padding to the new scale and frame size + * - After the animation is complete, remove the fixed positioning * and set the scroll position that keeps everything centered */ if ( startAnimationRef.current ) { @@ -313,8 +356,7 @@ export function useScaleCanvas( { startAnimationRef.current = false; /** - * Handle reversing the animation. If we already have an animation running, - * referse it. + * If we already have an animation running, reverse it. */ if ( animationRef.current ) { animationRef.current.reverse(); @@ -334,7 +376,7 @@ export function useScaleCanvas( { // appenders may appear or disappear, so we need to get the height from // the iframe at this point when we're about to animate the zoom out. // The iframe scrollTop, scrollHeight, and clientHeight will all be - // accurate. + // the most accurate. transitionFrom.current.clientHeight = transitionFrom.current.clientHeight ?? clientHeight; transitionFrom.current.scrollTop = @@ -345,7 +387,6 @@ export function useScaleCanvas( { frameSize, clientHeight, }; - transitionTo.current.scrollTop = computeScrollTopNext( iframeDocument.documentElement.scrollHeight, transitionFrom.current, @@ -354,6 +395,7 @@ export function useScaleCanvas( { animationRef.current = startZoomOutAnimation(); + // If the user prefers reduced motion, finish the animation immediately and set the final state. if ( prefersReducedMotion ) { finishZoomOutAnimation(); } else { From 0ff92b5d041e07e184e784c22d23bc3ac63e0c68 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Mon, 25 Nov 2024 14:06:07 -0600 Subject: [PATCH 60/64] Fix eslint error --- .../block-editor/src/components/iframe/use-scale-canvas.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 7f11960b12c518..1e862332660998 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -246,6 +246,9 @@ export function useScaleCanvas( { iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); // Set the final scroll position that was just animated to. + // Disable reason: Eslint isn't smart enough to know that this is a + // DOM element. https://github.com/facebook/react/issues/31483 + // eslint-disable-next-line react-compiler/react-compiler iframeDocument.documentElement.scrollTop = transitionTo.current.scrollTop; From 92e0e52acb2eff8db88ba4822a0c252bcd36a57a Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Mon, 25 Nov 2024 14:09:06 -0600 Subject: [PATCH 61/64] Move scrollHeight to animateFrom state --- .../src/components/iframe/use-scale-canvas.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 1e862332660998..4d6853f17a584b 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -29,17 +29,17 @@ function calculateScale( { /** * Compute the next scrollTop position after scaling the iframe content. * - * @param {number} scrollHeight Scaled height of the current iframe content. * @param {Object} transitionFrom The starting point of the transition * @param {Object} transitionTo The ending state of the transition * @return {number} The next scrollTop position after scaling the iframe content. */ -function computeScrollTopNext( scrollHeight, transitionFrom, transitionTo ) { +function computeScrollTopNext( transitionFrom, transitionTo ) { const { clientHeight: prevClientHeight, frameSize: prevFrameSize, scaleValue: prevScale, - scrollTop: scrollTop, + scrollTop, + scrollHeight, } = transitionFrom; const { clientHeight, frameSize, scaleValue } = transitionTo; // Step 0: Start with the current scrollTop. @@ -174,6 +174,7 @@ export function useScaleCanvas( { frameSize, clientHeight: 0, scrollTop: 0, + scrollHeight: 0, } ); /** @@ -184,6 +185,7 @@ export function useScaleCanvas( { frameSize, clientHeight: 0, scrollTop: 0, + scrollHeight: 0, } ); /** @@ -384,6 +386,8 @@ export function useScaleCanvas( { transitionFrom.current.clientHeight ?? clientHeight; transitionFrom.current.scrollTop = iframeDocument.documentElement.scrollTop; + transitionFrom.current.scrollHeight = + iframeDocument.documentElement.scrollHeight; transitionTo.current = { scaleValue, @@ -391,7 +395,6 @@ export function useScaleCanvas( { clientHeight, }; transitionTo.current.scrollTop = computeScrollTopNext( - iframeDocument.documentElement.scrollHeight, transitionFrom.current, transitionTo.current ); From fb16c4ff88ca39f355aab98759013dcdbf3eaeb4 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Mon, 25 Nov 2024 14:32:12 -0600 Subject: [PATCH 62/64] Update JSDoc --- .../src/components/iframe/use-scale-canvas.js | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 4d6853f17a584b..7fc73f148f5b21 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -4,15 +4,24 @@ import { useEffect, useRef, useCallback } from '@wordpress/element'; import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; +/** + * @typedef {Object} TransitionState + * @property {number} scaleValue Scale of the canvas. + * @property {number} frameSize Size of the frame/offset around the canvas. + * @property {number} clientHeight ClientHeight of the iframe. + * @property {number} scrollTop ScrollTop of the iframe. + * @property {number} scrollHeight ScrollHeight of the iframe. + */ + /** * Calculate the scale of the canvas. * - * @param {Object} root0 Object of options - * @param {number} root0.frameSize The size of the frame/offset around the canvas - * @param {number} root0.containerWidth Actual width of the canvas container - * @param {number} root0.maxContainerWidth Maximum width of the container to use for the scale calculation. This locks the canvas to a maximum width when zooming out. - * @param {number} root0.scaleContainerWidth Width the of the container wrapping the canvas container - * @return {number} scale value between 0 and/or equal to 1 + * @param {Object} options Object of options + * @param {number} options.frameSize Size of the frame/offset around the canvas + * @param {number} options.containerWidth Actual width of the canvas container + * @param {number} options.maxContainerWidth Maximum width of the container to use for the scale calculation. This locks the canvas to a maximum width when zooming out. + * @param {number} options.scaleContainerWidth Width the of the container wrapping the canvas container + * @return {number} Scale value between 0 and/or equal to 1 */ function calculateScale( { frameSize, @@ -29,9 +38,9 @@ function calculateScale( { /** * Compute the next scrollTop position after scaling the iframe content. * - * @param {Object} transitionFrom The starting point of the transition - * @param {Object} transitionTo The ending state of the transition - * @return {number} The next scrollTop position after scaling the iframe content. + * @param {TransitionState} transitionFrom Starting point of the transition + * @param {TransitionState} transitionTo Ending state of the transition + * @return {number} Next scrollTop position after scaling the iframe content. */ function computeScrollTopNext( transitionFrom, transitionTo ) { const { @@ -82,9 +91,9 @@ function computeScrollTopNext( transitionFrom, transitionTo ) { /** * Generate the keyframes to use for the zoom out animation. * - * @param {Object} transitionFrom Object of the starting transition state - * @param {Object} transitionTo Object of the ending transition state - * @return {Object[]} An array of keyframes to use for the animation + * @param {TransitionState} transitionFrom Starting transition state. + * @param {TransitionState} transitionTo Ending transition state. + * @return {Object[]} An array of keyframes to use for the animation. */ function getAnimationKeyframes( transitionFrom, transitionTo ) { const { @@ -110,20 +119,24 @@ function getAnimationKeyframes( transitionFrom, transitionTo ) { ]; } +/** + * @typedef {Object} ScaleCanvasResult + * @property {boolean} isZoomedOut A boolean indicating if the canvas is zoomed out. + * @property {number} scaleContainerWidth The width of the container used to calculate the scale. + * @property {Object} contentResizeListener A resize observer for the content. + * @property {Object} containerResizeListener A resize observer for the container. + */ + /** * Handles scaling the canvas for the zoom out mode and animating between * the states. * - * @param {Object} root0 - * @param {number} root0.frameSize The size of the frame around the content. - * @param {Document} root0.iframeDocument The document of the iframe. - * @param {number} root0.maxContainerWidth The max width of the canvas to use as the starting scale point. Defaults to 750. - * @param {number|string} root0.scale The scale of the canvas. Can be an decimal between 0 and 1, 1, or 'auto-scaled'. - * @return {Object} An object containing the following properties: - * isZoomedOut: A boolean indicating if the canvas is zoomed out. - * scaleContainerWidth: The width of the container used to calculate the scale. - * contentResizeListener: A resize observer for the content. - * containerResizeListener: A resize observer for the container. + * @param {Object} options Object of options. + * @param {number} options.frameSize Size of the frame around the content. + * @param {Document} options.iframeDocument Document of the iframe. + * @param {number} options.maxContainerWidth Max width of the canvas to use as the starting scale point. Defaults to 750. + * @param {number|string} options.scale Scale of the canvas. Can be an decimal between 0 and 1, 1, or 'auto-scaled'. + * @return {ScaleCanvasResult} Properties of the result. */ export function useScaleCanvas( { frameSize, @@ -168,6 +181,7 @@ export function useScaleCanvas( { /** * The starting transition state for the zoom out animation. + * @type {import('react').RefObject} */ const transitionFrom = useRef( { scaleValue, @@ -179,6 +193,7 @@ export function useScaleCanvas( { /** * The ending transition state for the zoom out animation. + * @type {import('react').RefObject} */ const transitionTo = useRef( { scaleValue, From 4076a7b5050c20d2aaaa5683bd00dac085ea5797 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 26 Nov 2024 13:51:29 -0600 Subject: [PATCH 63/64] Rename ref variables to use "Ref" suffix --- .../src/components/iframe/use-scale-canvas.js | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index 7fc73f148f5b21..e1e90f5a50b9bc 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -183,7 +183,7 @@ export function useScaleCanvas( { * The starting transition state for the zoom out animation. * @type {import('react').RefObject} */ - const transitionFrom = useRef( { + const transitionFromRef = useRef( { scaleValue, frameSize, clientHeight: 0, @@ -195,7 +195,7 @@ export function useScaleCanvas( { * The ending transition state for the zoom out animation. * @type {import('react').RefObject} */ - const transitionTo = useRef( { + const transitionToRef = useRef( { scaleValue, frameSize, clientHeight: 0, @@ -210,8 +210,8 @@ export function useScaleCanvas( { * @return {Animation} The animation object for the zoom out animation. */ const startZoomOutAnimation = useCallback( () => { - const { scrollTop } = transitionFrom.current; - const { scrollTop: scrollTopNext } = transitionTo.current; + const { scrollTop } = transitionFromRef.current; + const { scrollTop: scrollTopNext } = transitionToRef.current; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scroll-top', @@ -227,8 +227,8 @@ export function useScaleCanvas( { return iframeDocument.documentElement.animate( getAnimationKeyframes( - transitionFrom.current, - transitionTo.current + transitionFromRef.current, + transitionToRef.current ), { easing: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)', @@ -253,11 +253,11 @@ export function useScaleCanvas( { // Add our final scale and frame size now that the animation is done. iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', - transitionTo.current.scaleValue + transitionToRef.current.scaleValue ); iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-frame-size', - `${ transitionTo.current.frameSize }px` + `${ transitionToRef.current.frameSize }px` ); iframeDocument.documentElement.classList.remove( 'zoom-out-animation' ); @@ -267,7 +267,7 @@ export function useScaleCanvas( { // DOM element. https://github.com/facebook/react/issues/31483 // eslint-disable-next-line react-compiler/react-compiler iframeDocument.documentElement.scrollTop = - transitionTo.current.scrollTop; + transitionToRef.current.scrollTop; iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scroll-top' @@ -277,7 +277,7 @@ export function useScaleCanvas( { ); // Update previous values. - transitionFrom.current = transitionTo.current; + transitionFromRef.current = transitionToRef.current; }, [ iframeDocument ] ); /** @@ -314,11 +314,11 @@ export function useScaleCanvas( { // We need to update the appropriate scale to exit from. If sidebars have been opened since setting the // original scale, we will snap to a much smaller scale due to the scale container immediately changing sizes when exiting. - if ( isAutoScaled && transitionFrom.current.scaleValue !== 1 ) { + if ( isAutoScaled && transitionFromRef.current.scaleValue !== 1 ) { // We use containerWidth as the divisor, as scaleContainerWidth will always match the containerWidth when // exiting. - transitionFrom.current.scaleValue = calculateScale( { - frameSize: transitionFrom.current.frameSize, + transitionFromRef.current.scaleValue = calculateScale( { + frameSize: transitionFromRef.current.frameSize, containerWidth, maxContainerWidth, scaleContainerWidth: containerWidth, @@ -382,10 +382,10 @@ export function useScaleCanvas( { animationRef.current.reverse(); // Swap the transition to/from refs so that we set the correct values when // finishZoomOutAnimation runs. - const tempTransitionFrom = transitionFrom.current; - const tempTransitionTo = transitionTo.current; - transitionFrom.current = tempTransitionTo; - transitionTo.current = tempTransitionFrom; + const tempTransitionFrom = transitionFromRef.current; + const tempTransitionTo = transitionToRef.current; + transitionFromRef.current = tempTransitionTo; + transitionToRef.current = tempTransitionFrom; } else { /** * Start a new zoom animation. @@ -397,21 +397,21 @@ export function useScaleCanvas( { // the iframe at this point when we're about to animate the zoom out. // The iframe scrollTop, scrollHeight, and clientHeight will all be // the most accurate. - transitionFrom.current.clientHeight = - transitionFrom.current.clientHeight ?? clientHeight; - transitionFrom.current.scrollTop = + transitionFromRef.current.clientHeight = + transitionFromRef.current.clientHeight ?? clientHeight; + transitionFromRef.current.scrollTop = iframeDocument.documentElement.scrollTop; - transitionFrom.current.scrollHeight = + transitionFromRef.current.scrollHeight = iframeDocument.documentElement.scrollHeight; - transitionTo.current = { + transitionToRef.current = { scaleValue, frameSize, clientHeight, }; - transitionTo.current.scrollTop = computeScrollTopNext( - transitionFrom.current, - transitionTo.current + transitionToRef.current.scrollTop = computeScrollTopNext( + transitionFromRef.current, + transitionToRef.current ); animationRef.current = startZoomOutAnimation(); From 798549e13e4b66af35bcfe0851c794e05095710a Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 26 Nov 2024 10:54:13 -0600 Subject: [PATCH 64/64] Comment style guide --- .../src/components/iframe/use-scale-canvas.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index e1e90f5a50b9bc..c72266e82e2b0a 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -239,12 +239,12 @@ export function useScaleCanvas( { /** * Callback when the zoom out animation is finished. - * - Cleans up animations refs - * - Adds final CSS vars for scale and frame size to preserve the state - * - Removes the 'zoom-out-animation' class (which has the fixed positioning) - * - Sets the final scroll position after the canvas is no longer in fixed position - * - Removes CSS vars related to the animation - * - Sets the transitionFrom to the transitionTo state to be ready for the next animation + * - Cleans up animations refs. + * - Adds final CSS vars for scale and frame size to preserve the state. + * - Removes the 'zoom-out-animation' class (which has the fixed positioning). + * - Sets the final scroll position after the canvas is no longer in fixed position. + * - Removes CSS vars related to the animation. + * - Sets the transitionFrom to the transitionTo state to be ready for the next animation. */ const finishZoomOutAnimation = useCallback( () => { startAnimationRef.current = false; @@ -363,13 +363,13 @@ export function useScaleCanvas( { /** * Handle the zoom out animation: * - * - Get the current scrollTop position - * - Calculate where the same scroll position is after scaling + * - Get the current scrollTop position. + * - Calculate where the same scroll position is after scaling. * - Apply fixed positioning to the canvas with a transform offset - * to keep the canvas centered - * - Animate the scale and padding to the new scale and frame size + * to keep the canvas centered. + * - Animate the scale and padding to the new scale and frame size. * - After the animation is complete, remove the fixed positioning - * and set the scroll position that keeps everything centered + * and set the scroll position that keeps everything centered. */ if ( startAnimationRef.current ) { // Don't allow a new transition to start again unless it was started by the zoom out mode changing.