Skip to content

Commit

Permalink
Zoom in/out to correct location (#67126)
Browse files Browse the repository at this point in the history
Co-authored-by: jeryj <[email protected]>
Co-authored-by: ajlende <[email protected]>
Co-authored-by: stokesman <[email protected]>
Co-authored-by: ndiego <[email protected]>
Co-authored-by: richtabor <[email protected]>
Co-authored-by: draganescu <[email protected]>
Co-authored-by: colorful-tones <[email protected]>
Co-authored-by: getdave <[email protected]>
Co-authored-by: priethor <[email protected]>
Co-authored-by: annezazu <[email protected]>
Co-authored-by: ellatrix <[email protected]>
Co-authored-by: cbravobernal <[email protected]>
  • Loading branch information
13 people authored Nov 25, 2024
1 parent b76d9f0 commit 2215a04
Show file tree
Hide file tree
Showing 3 changed files with 371 additions and 47 deletions.
19 changes: 16 additions & 3 deletions packages/block-editor/src/components/iframe/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@
// 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);
@include editor-canvas-resize-animation( transform 0s, scale 0s, padding 0s, translate 0s);

&.zoom-out-animation {
// we only want to animate the scaling when entering zoom out. When sidebars
$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
// 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);
@include editor-canvas-resize-animation( transform 0s, top 0s, bottom 0s, right 0s, left 0s );
}
}

Expand Down
231 changes: 187 additions & 44 deletions packages/block-editor/src/components/iframe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
useMergeRefs,
useRefEffect,
useDisabled,
useReducedMotion,
} from '@wordpress/compose';
import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
Expand Down Expand Up @@ -121,6 +122,7 @@ function Iframe( {
};
}, [] );
const { styles = '', scripts = '' } = resolvedAssets;
/** @type {[Document, import('react').Dispatch<Document>]} */
const [ iframeDocument, setIframeDocument ] = useState();
const initialContainerWidthRef = useRef( 0 );
const [ bodyClasses, setBodyClasses ] = useState( [] );
Expand All @@ -130,6 +132,7 @@ function Iframe( {
useResizeObserver();
const [ containerResizeListener, { width: containerWidth } ] =
useResizeObserver();
const prefersReducedMotion = useReducedMotion();

const setRef = useRefEffect( ( node ) => {
node._load = () => {
Expand Down Expand Up @@ -268,6 +271,19 @@ function Iframe( {
containerWidth
);

const frameSizeValue = parseInt( frameSize );

const maxWidth = 750;
const scaleValue =
scale === 'auto-scaled'
? ( Math.min( containerWidth, maxWidth ) - frameSizeValue * 2 ) /
scaleContainerWidth
: scale;

const prevScaleRef = useRef( scaleValue );
const prevFrameSizeRef = useRef( frameSizeValue );
const prevClientHeightRef = useRef( /* Initialized in the useEffect. */ );

const disabledRef = useDisabled( { isDisabled: ! readonly } );
const bodyRef = useMergeRefs( [
useBubbleEvents( iframeDocument ),
Expand Down Expand Up @@ -320,47 +336,176 @@ function Iframe( {

useEffect( () => cleanup, [ cleanup ] );

const zoomOutAnimationClassnameRef = 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 || ! isZoomedOut ) {
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;
}

const handleZoomOutAnimationClassname = () => {
clearTimeout( zoomOutAnimationClassnameRef.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 +
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.classList.add(
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'
);

zoomOutAnimationClassnameRef.current = setTimeout( () => {
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.
// 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
);
} 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
);
}, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss
}
};
}, [ iframeDocument, scaleValue, frameSizeValue, prefersReducedMotion ] );

handleZoomOutAnimationClassname();
iframeDocument.documentElement.classList.add( 'is-zoomed-out' );
// 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;
}

return () => {
handleZoomOutAnimationClassname();
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 || ! isZoomedOut ) {
if ( ! iframeDocument ) {
return;
}

const maxWidth = 750;
// 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
// when the canvas area becomes larger due to sidebars closing. This is a known but
Expand All @@ -371,11 +516,7 @@ function Iframe( {
// but calc( 100px / 2px ) is not.
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-scale',
scale === 'auto-scaled'
? ( Math.min( containerWidth, maxWidth ) -
parseInt( frameSize ) * 2 ) /
scaleContainerWidth
: scale
scaleValue
);

// frameSize has to be a px value for the scaling and frame size to be computed correctly.
Expand All @@ -401,27 +542,29 @@ function Iframe( {
);

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'
);
// 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'
// );
};
}, [
scale,
scaleValue,
frameSize,
iframeDocument,
iframeWindowInnerHeight,
Expand Down
Loading

0 comments on commit 2215a04

Please sign in to comment.