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

[6.7] Zoom in/out to correct location #66618

Merged
merged 30 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b0d073a
Midway commit. Scaling out is broken
jeryj Oct 30, 2024
122790d
Fix zoom in animation
jeryj Oct 30, 2024
1dedf7a
Fix scaling by preserving CSS properites
jeryj Oct 30, 2024
114e23b
Prevent reflow from removing and re adding scrollbar in animation
jeryj Oct 30, 2024
39eb8dd
Only rerun zoom out use effects if zoom out has changed.
jeryj Oct 31, 2024
d08a6f6
Allow all CSS vars to update when scale changes
jeryj Oct 31, 2024
046902a
Midway commit. Math is wrong for addressing top/bottom exceptions
jeryj Oct 31, 2024
ab88dfa
Remove usePrevious usage
ajlende Oct 31, 2024
be9ade9
Add prefers-reduced-motion to setTimeout delay
ajlende Nov 5, 2024
9f2d384
WIP Working zoom without frame size
ajlende Nov 5, 2024
c4f97ba
Account for changes to client height when determining edge threshold
jeryj Nov 6, 2024
8bc12b2
Zoom to center unless it will reveal top or bottom
jeryj Nov 6, 2024
fffc48d
Account for a top threshold when zooming in and out
jeryj Nov 6, 2024
42dd030
Clean up math and add comments
ajlende Nov 6, 2024
5fff8f9
Add event listener instead of timeout
ajlende Nov 6, 2024
c182b70
Fix reduced motion
ajlende Nov 6, 2024
a9211e7
Remove timeout ref
ajlende Nov 6, 2024
6c91090
Refactor callback as separate effect
ajlende Nov 6, 2024
615f2b3
Fix JSDoc comment
ajlende Nov 6, 2024
907dde4
Try to add back useEffect cleanups
ajlende Nov 6, 2024
fe8fd4e
Initialize prevClientHeight in the useEffect
ajlende Nov 7, 2024
624be01
use useReducedMotion
jeryj Nov 7, 2024
b9eb0d2
Add test for zoom in/out location
jeryj Nov 7, 2024
d51db38
Hack to fix reduced motion
ajlende Nov 8, 2024
1e7f1ee
Clean up the frameSizeValue and scaleValue calculations
ajlende Nov 8, 2024
740cd9a
Replace TODO comments with HACK comments
ajlende Nov 8, 2024
37111fa
Add cleanup for raf
ajlende Nov 8, 2024
ce8665d
Simplify CSS diff for 6.7 review
ajlende Nov 8, 2024
ab31e44
Add one more HACK comment
ajlende Nov 8, 2024
0b4d86d
Do not allow scrollTopNext to be smaller than 0
jeryj Nov 11, 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you comment on why fixed positioning is needed on the html element to make this work?

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
228 changes: 184 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 initialContainerWidth = 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 @@ -252,6 +255,19 @@ function Iframe( {
containerWidth
);

const frameSizeValue = parseInt( frameSize );

const maxWidth = 750;
const scaleValue =
scale === 'default'
? ( 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 @@ -303,47 +319,173 @@ 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.
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 }
ajlende marked this conversation as resolved.
Show resolved Hide resolved
);
}

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),
// 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
Expand All @@ -354,11 +496,7 @@ function Iframe( {
// but calc( 100px / 2px ) is not.
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-scale',
scale === 'default'
? ( 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 @@ -384,27 +522,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'
// );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine. So we always want to calculate these variables I guess. Unmounting is not needed then because when the iframe unmounts these attributes are automatically removed too.

};
}, [
scale,
scaleValue,
frameSize,
iframeDocument,
iframeWindowInnerHeight,
Expand Down
Loading
Loading