Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom in/out to correct location #67126

Merged
merged 32 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4192156
Midway commit. Scaling out is broken
jeryj Oct 30, 2024
5bb30a1
Fix zoom in animation
jeryj Oct 30, 2024
22186f2
Fix scaling by preserving CSS properites
jeryj Oct 30, 2024
afd8ca3
Prevent reflow from removing and re adding scrollbar in animation
jeryj Oct 30, 2024
123d647
Only rerun zoom out use effects if zoom out has changed.
jeryj Oct 31, 2024
fd15912
Allow all CSS vars to update when scale changes
jeryj Oct 31, 2024
3a3e1ad
Midway commit. Math is wrong for addressing top/bottom exceptions
jeryj Oct 31, 2024
e16a2a8
Remove usePrevious usage
ajlende Oct 31, 2024
e4bf2dd
Add prefers-reduced-motion to setTimeout delay
ajlende Nov 5, 2024
8d767b4
WIP Working zoom without frame size
ajlende Nov 5, 2024
f37547d
Account for changes to client height when determining edge threshold
jeryj Nov 6, 2024
5cce3b2
Zoom to center unless it will reveal top or bottom
jeryj Nov 6, 2024
8b9c693
Account for a top threshold when zooming in and out
jeryj Nov 6, 2024
adea71c
Clean up math and add comments
ajlende Nov 6, 2024
fcc49f4
Add event listener instead of timeout
ajlende Nov 6, 2024
08247f0
Fix reduced motion
ajlende Nov 6, 2024
ec73741
Remove timeout ref
ajlende Nov 6, 2024
6996c1b
Refactor callback as separate effect
ajlende Nov 6, 2024
c79718e
Fix JSDoc comment
ajlende Nov 6, 2024
77559a8
Try to add back useEffect cleanups
ajlende Nov 6, 2024
9fa40e2
Initialize prevClientHeight in the useEffect
ajlende Nov 7, 2024
f0fe6ab
use useReducedMotion
jeryj Nov 7, 2024
0ccd9e5
Add test for zoom in/out location
jeryj Nov 7, 2024
e88ee83
Hack to fix reduced motion
ajlende Nov 8, 2024
14b9f63
Clean up the frameSizeValue and scaleValue calculations
ajlende Nov 8, 2024
f9ab519
Replace TODO comments with HACK comments
ajlende Nov 8, 2024
c5d3445
Add cleanup for raf
ajlende Nov 8, 2024
534485a
Simplify CSS diff for 6.7 review
ajlende Nov 8, 2024
8b912e4
Add one more HACK comment
ajlende Nov 8, 2024
32ae736
Do not allow scrollTopNext to be smaller than 0
jeryj Nov 11, 2024
73e032e
Fix zoom-out.spec.js
ajlende Nov 19, 2024
857ca64
Disable errant react-compiler eslint rule
ajlende Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading